AI-powered text-to-handwriting synthesis engine.
Convert typed text into realistic handwritten documents with customizable imperfections, layout, and style.
HandWrite is a full-stack application that transforms typed text into realistic handwritten documents. It uses a Graves RNN neural network to generate handwriting strokes as SVG paths, then composites them onto paper backgrounds with configurable imperfections β pressure variation, ink smudging, wobble, and more β producing output that closely mimics genuine handwriting.
- AI Handwriting Generation β Graves RNN model generates natural stroke patterns from arbitrary text input
- Multi-page Document Support β Automatically paginates long text across multiple pages with consistent sizing
- Custom Paper Backgrounds β Upload your own ruled/blank paper scans as backgrounds
- Ink Color Control β Choose any ink color for the generated handwriting
- Layout Customization β Configurable margins, line spacing (0.1Γβ3.0Γ), word spacing, and page rotation
- Natural Variation System β Randomize line start positions, word spacing, line spacing, line inclination, and per-word inclination for realistic imperfection
- Imperfections Engine β Stroke wobble, pen pressure variation, slant inconsistency, random strikethroughs, ink smudging, and bleed-through effects
- Mixed Character Rendering β Unsupported characters automatically rendered with a handwriting-style font fallback (Caveat)
- Export Options β Download as individual PNGs, ZIP archive, or multi-page PDF
- Real-time Progress β WebSocket-based live generation progress updates
- Session Management β Isolated sessions with automatic cleanup (24-hour TTL for inactive sessions; completed sessions persist until deleted)
HandWrite/
βββ backend/ # FastAPI Python backend
β βββ app/
β β βββ main.py # FastAPI app entry point & CORS setup
β β βββ config.py # Pydantic settings (env-based config)
β β βββ api/ # REST & WebSocket endpoints
β β β βββ session.py # Session creation
β β β βββ upload.py # Paper & style image upload
β β β βββ generation.py # Page generation orchestration
β β β βββ download.py # PNG/ZIP/PDF export
β β β βββ pageset.py # Page set save/load (ZIP + manifest)
β β β βββ queue.py # Background generation queue API
β β β βββ websocket.py # Real-time progress broadcast
β β βββ models/ # AI model implementations
β β β βββ base.py # Abstract HandwritingModel base class
β β β βββ graves_rnn.py # Graves RNN (primary, fully implemented)
β β β βββ hwt.py # HWT GAN (stub)
β β β βββ diffusion_pen.py# DiffusionPen (stub)
β β β βββ one_dm.py # One-DM (stub)
β β β βββ registry.py # Lazy-loading model registry
β β βββ models_src/ # Third-party model source code
β β β βββ handwriting-synthesis-master/ # Graves RNN implementation
β β βββ compositor/ # Image composition pipeline
β β β βββ layout.py # Line positioning, spacing, inclination
β β β βββ renderer.py # Page rendering & word inclination
β β β βββ imperfections.py# Wobble, pressure, slant, smudge, bleed-through
β β β βββ baseline.py # Baseline utilities
β β βββ schemas/
β β β βββ models.py # Pydantic request/response models
β β βββ services/
β β βββ session_manager.py # Session lifecycle & cleanup
β β βββ queue_worker.py # Background queue processing
β β βββ image_processor.py # Image processing utilities
β β βββ strikethrough.py # Strikethrough effect generation
β βββ scripts/
β β βββ test_graves_rnn.py # Model integration test
β βββ requirements.txt # Python dependencies
β βββ Dockerfile # Backend container definition
β
βββ frontend/ # React + Vite frontend
β βββ src/
β β βββ App.jsx # Main application shell
β β βββ main.jsx # React entry point
β β βββ index.css # Global styles & design tokens
β β βββ api/
β β β βββ client.js # Axios API client
β β βββ store/
β β β βββ appStore.js # Zustand global state
β β βββ hooks/
β β β βββ useWebSocket.js # WebSocket hook for live progress
β β βββ components/
β β βββ Layout/ # Header, Sidebar (page previews)
β β βββ Controls/ # LayoutControls, ImperfectionsPanel
β β βββ Upload/ # PaperUpload, StyleUpload, TextAreaSelector
β β βββ Generation/ # GenerationProgress overlay
β β βββ Session/ # SessionNameModal, SessionsPanel
β β βββ common/ # Slider, Button, Card
β βββ package.json
β βββ vite.config.js
β βββ index.html
β
βββ docker-compose.yml # Full-stack Docker orchestration
βββ README.md
| Requirement | Version | Notes |
|---|---|---|
| Python | 3.10+ | 3.10 recommended (TensorFlow compatibility) |
| Node.js | 18+ | LTS recommended |
| npm | 9+ | Comes with Node.js |
| Git | Any | To clone the repository |
| Cairo | System | Required by cairosvg for SVGβPNG conversion |
# Install Cairo via GTK (required for cairosvg)
# Download GTK3 runtime from: https://github.com/niconiahi/gtkwin32/releases
# Or install via Chocolatey:
choco install gtk-runtime
# Alternatively, install MSYS2 and use:
pacman -S mingw-w64-x86_64-cairosudo apt-get update && sudo apt-get install -y \
build-essential libcairo2-dev libglib2.0-0 \
fonts-dejavu wget
# Optional: Install Caveat handwriting font for better fallback rendering
mkdir -p ~/.local/share/fonts
wget -O ~/.local/share/fonts/Caveat-Regular.ttf \
"https://github.com/google/fonts/raw/main/ofl/caveat/Caveat%5Bwght%5D.ttf"
fc-cache -f -vbrew install cairo pango gdk-pixbuf libffigit clone https://github.com/yourusername/HandWrite.git
cd HandWrite# Create and activate a virtual environment
cd backend
python -m venv venv
# Windows
.\venv\Scripts\activate
# Linux/macOS
source venv/bin/activate
# Install Python dependencies
pip install -r requirements.txt
β οΈ TensorFlow Note: The project ships with GPU-capable TensorFlow (tensorflow[and-cuda]) β it automatically runs on CPU when no GPU is present. The Graves RNN model depends ontf-kerasfor backward compatibility with TF1-style code, soTF_USE_LEGACY_KERASmust be set before running:$env:TF_USE_LEGACY_KERAS = "1"On Linux/macOS:
export TF_USE_LEGACY_KERAS=1On machines without a GPU you can substitute
tensorflow-cpuinbackend/requirements.txtto skip the CUDA libraries.
# From the backend/ directory, with venv activated
cd backend
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reloadThe API will be available at http://localhost:8000. Verify with:
curl http://localhost:8000/health
# Expected: {"status":"ok"}The first request that triggers generation will take longer as the Graves RNN model loads its weights (~40MB) into memory. Subsequent requests will be fast.
Open a new terminal window:
cd frontend
npm installnpm run devThe UI will be available at http://localhost:3000.
π‘ API URL: The frontend defaults to
http://localhost:8000for the backend. If your backend runs on a different host/port, set the environment variable before starting:# Linux/macOS VITE_API_URL=http://your-backend:8000 npm run dev # Windows PowerShell $env:VITE_API_URL = "http://your-backend:8000"; npm run dev
# From the project root
docker-compose up --build| Service | URL |
|---|---|
| Frontend | http://localhost:3000 |
| Backend | http://localhost:8000 |
Docker volumes:
model_weightsβ Persists downloaded model weights (MODEL_WEIGHTS_DIR) between container restartssession_dataβ Persists session data (TEMP_DIR) between container restarts./backend/appβ Mounted for live code reload during development./frontend/srcβ Mounted for hot module replacement
To run in the background:
docker-compose up -d --buildTo stop:
docker-compose downThe backend is configured to use your NVIDIA GPU if available, which dramatically speeds up generation. To enable this, you must prepare your host machine:
- Install NVIDIA Drivers: Ensure the latest NVIDIA drivers are installed on your Windows host.
- Enable WSL 2: Ensure WSL 2 is installed and enabled on your system.
- Configure Docker Desktop: Open Docker Desktop settings, navigate to General, and ensure Use the WSL 2 based engine is checked. Docker Desktop automatically handles the NVIDIA Container Toolkit integration.
- Install NVIDIA Drivers: Install the proprietary NVIDIA drivers for your distribution.
- Install NVIDIA Container Toolkit:
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \ && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \ sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt-get update sudo apt-get install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker
- Once configured, running
docker-compose up --buildwill automatically utilize the GPU.
- Open the app at
http://localhost:3000 - Upload paper (optional) β Upload a scanned paper image as the background. If skipped, a plain white A4 page is used.
- Select model β Choose from the dropdown in the header. Currently, Graves RNN is the only fully implemented model.
- Type your text in the central editor panel
- Adjust layout settings in the left sidebar:
- Margins, ink color
- Line & word spacing
- Natural variation controls
- Configure imperfections for realism:
- Stroke wobble, pressure variation, slant
- Random strikethroughs
- Ink smudge & bleed-through
- Click "Generate" β The system will:
- Split text into lines
- Generate handwriting strokes via the AI model
- Apply imperfections
- Composite onto the paper background
- Automatically paginate if text overflows
- Preview & download pages from the right sidebar. Export as PNG, ZIP, or PDF.
| Control | Range | Description |
|---|---|---|
| Line Spacing | 0.1β3.0 | Vertical space multiplier between lines |
| Word Spacing | 5β50 px | Horizontal gap between words |
| Line Start Randomness | 0β50 px | Random horizontal offset for each line start |
| Word Spacing Randomness | 0β100% | Random jitter on word spacing |
| Line Spacing Randomness | 0β100% | Random variation in vertical spacing between lines |
| Line Inclination Randomness | 0β3Β° | Random tilt applied to entire lines |
| Word Inclination Randomness | 0β5Β° | Random tilt applied to individual words |
| Page Rotation | Β±15Β° | Global rotation of all handwriting on the page |
| Control | Range | Description |
|---|---|---|
| Stroke Wobble | 0.0β1.0 | Per-scanline displacement simulating hand tremor |
| Pressure Variation | 0.0β1.0 | Alpha modulation simulating pen pressure changes |
| Slant Inconsistency | 0.0β1.0 | Affine shear to simulate natural handwriting slant |
| Strikethrough Probability | 0.0β0.3 | Chance of adding a strikethrough to any word |
| Ink Smudge | 0.0β1.0 | Localized Gaussian blur to simulate ink smudging |
| Bleed-through | 0.0β1.0 | Simulated ink bleed from reverse side |
Base URL: http://localhost:8000
GET /health
β {"status": "ok"}
POST /session/new
β {"session_id": "uuid-string"}
POST /session/{session_id}/upload-paper
Body: multipart/form-data (files[])
β {"message": "...", "files": ["/path/to/paper_....png"]}
POST /session/{session_id}/upload-style
Body: multipart/form-data (files[])
β {"message": "...", "files": ["/path/to/style_....png"]}
POST /session/{session_id}/estimate-layout
Body: {"text": "...", "layout_config": {...}}
β {"estimated_pages": N, "chars_per_page": N}
POST /session/{session_id}/generate-page
Body: {
"page_index": 0,
"text_slice": "Your text here",
"layout": { ... LayoutConfig ... },
"imperfections": { ... ImperfectionConfig ... },
"model_name": "graves_rnn"
}
β {"message": "...", "page_url": "...", "lines_placed": N, "total_lines": N, "chars_consumed": N}
GET /session/{session_id}/page/{page_index}
β PNG image
GET /session/{session_id}/download/page/{page_index}
β PNG file download
GET /session/{session_id}/download/all?format=zip
β ZIP archive of all pages
GET /session/{session_id}/download/all?format=pdf
β Multi-page PDF document
WS /session/{session_id}/ws
Messages received (single-page generation):
{"type": "generation_start", "page_index": N}
{"type": "model_loading", "page_index": N}
{"type": "model_loaded", "page_index": N}
{"type": "line_progress", "line_index": N, "total_lines": N, "page_index": N}
{"type": "post_processing", "current": N, "total": N, "page_index": N}
{"type": "page_complete", "page_index": N}
Messages received (queue-based generation):
{"type": "queue_update", "sessions": [...]}
{"type": "queue_progress", "session_id": ..., "phase": ..., "line_index": N, "total_lines": N, ...}
{"type": "queue_page_complete", "session_id": ..., "page_index": N, "pages_generated": N}
{"type": "queue_session_complete", "session_id": ..., "pages_generated": N}
Based on Alex Graves' 2013 paper "Generating Sequences With Recurrent Neural Networks". This model generates handwriting as a sequence of pen strokes using a mixture density network conditioned on character input.
- Input: Text string
- Output: SVG stroke paths β rasterized to PNG
- No style reference images required (uses 13 built-in handwriting styles)
- Supported characters: A-Z (except Q, X, Z β mapped to alternatives), a-z, 0-9, common punctuation
- Unsupported characters: Rendered with Caveat handwriting font fallback
| Model | Type | Style Refs | Status |
|---|---|---|---|
| HWT | GAN | 15 images | Stub |
| DiffusionPen | Diffusion | 5 images | Stub |
| One-DM | Diffusion | 1 image | Stub |
| Variable | Default | Description |
|---|---|---|
TF_USE_LEGACY_KERAS |
1 |
Required. Use legacy Keras API |
PYTHONPATH |
/app (Docker) |
Python module resolution path |
MODEL_WEIGHTS_DIR |
weights |
Directory for model weight files (Graves RNN checkpoints) |
TEMP_DIR |
temp/handwrite_sessions |
Session data storage directory |
SESSION_TTL_HOURS |
24 |
Inactive sessions are auto-cleaned after this many hours |
VITE_API_URL |
http://localhost:8000 |
Backend API URL for the frontend |
Cairo native libraries must be installed system-wide. Install GTK3 runtime or use MSYS2. See System Dependencies.
Make sure TF_USE_LEGACY_KERAS=1 is set. The Graves RNN model depends on tf-keras for backward compatibility with TF1-style code.
# Windows PowerShell
$env:TF_USE_LEGACY_KERAS = "1"The Graves RNN model weights (~40MB) are loaded lazily on first generation request. This is normal β subsequent requests reuse the loaded model.
The backend auto-cleans inactive sessions after 24 hours (SESSION_TTL_HOURS). If you leave the tab idle, the session may expire. Refresh the page to create a new session.
- Backend default:
8000β change withuvicorn app.main:app --port XXXX - Frontend default:
3000β change infrontend/vite.config.js(server.port)
This project is for personal / educational use. The Graves RNN model source is from sjvasquez/handwriting-synthesis.