These guidelines are for AI coding assistants (GitHub Copilot, Cursor, Cline, Claude, etc.) working on the Inkforge codebase. Follow these conventions strictly.
Inkforge is a human-like handwriting synthesis engine powered by a stroke-level generative ML model (LSTM + Mixture Density Network). It is not a font renderer. The system generates handwriting as sequences of pen strokes with learned distributions over pressure, velocity, slant, and spacing.
React Frontend → FastAPI Backend → PyTorch Inference Engine
↓
Celery + Redis (async task queue)
↓
CairoSVG + Pillow (rendering)
- Paper: Graves (2013) — "Generating Sequences with Recurrent Neural Networks" (arXiv:1308.0850)
- Dataset: IAM On-Line Handwriting Database (13,049 texts, 221 writers)
All handwriting is represented as sequences of 5-tuples:
(Δx, Δy, p₁, p₂, p₃)
Δx, Δy = relative pen displacements from previous position
p₁ = pen-down (actively drawing)
p₂ = pen-up (moving without drawing)
p₃ = end-of-sequence sentinel
Rules:
- Exactly one of
p₁, p₂, p₃is 1 at any timestep; the others are 0 Δx, Δyare relative (delta) coordinates, NOT absolute- When converting to absolute for rendering, accumulate deltas
- Stroke sequences are variable-length; pad/truncate to
max_seq_len=700for training
Do NOT change these values without explicit approval — they are baked into the PRD:
| Parameter | Value | Location |
|---|---|---|
| Character embedding dim | d=256 | model.py |
| Style latent dim | z ∈ ℝ¹²⁸ | model.py, style_encoder.py |
| LSTM hidden dim | 512 | model.py |
| LSTM layers | 3 | model.py |
| Dropout | 0.2 | model.py |
| MDN mixtures (M) | 20 | model.py |
| MDN params per mixture | 6 (π, μx, μy, σx, σy, ρ) | model.py |
| Pen state outputs | 3 (p₁, p₂, p₃) | model.py |
These 7 parameters are exposed to users via UI sliders. They are NOT post-processing — they operate at the model/latent level:
| Parameter | Default | Range | Implementation |
|---|---|---|---|
| Stroke Width Variation | 0.5 | 0.0–1.0 | Derived from pen velocity |
| Character Inconsistency | 0.4 | 0.0–1.0 | Noise in style vector z |
| Slant Angle | 5° | -30° to +30° | Global bias + per-word variance |
| Baseline Drift | 0.3 | 0.0–1.0 | Sinusoidal y-axis noise |
| Ligature Formation | Enabled | On/Off | Contextual stroke connections |
| Fatigue Simulation | Disabled | On/Off | Increasing latent noise over position |
| Ink Bleed | 0.2 | 0.0–1.0 | Post-render Gaussian diffusion |
- Python 3.10+ — use modern type hints (
list[str],dict[str, int],X | None) - PEP 8 — enforced via
ruff - Line length: 100 characters max
- Imports: sorted with
isort(ruff handles this)
# ✅ Good — all args and returns typed
def generate(self, text: str, style_z: torch.Tensor, temperature: float = 0.4) -> list[tuple]:
...
# ❌ Bad — missing types
def generate(self, text, style_z, temperature=0.4):
...def compute_mdn_loss(
mdn_params: torch.Tensor,
target: torch.Tensor,
) -> torch.Tensor:
"""
Compute MDN negative log-likelihood loss.
Args:
mdn_params: Predicted mixture parameters [batch, seq, M*6].
target: Ground truth strokes [batch, seq, 2].
Returns:
Scalar loss tensor.
"""- Use
pydantic.BaseModelfor all API schemas - Use
Field(...)with descriptions for all fields - Use enums for fixed choice sets
- Validate constraints with
ge,le,min_length,max_length
- Use
APIRouterper domain (generate, export, styles, health) - All route functions must be
async - Use dependency injection for services
- Return proper HTTP status codes (202 for async jobs, 404 for not found)
- React 18 with functional components and hooks only (no class components)
- Zustand for state management (no Redux)
- Tailwind CSS for styling (utility-first)
- Use
constby default;letonly when reassignment is needed - Destructure props and state
- File naming:
PascalCase.jsxfor components,camelCase.jsfor utils/hooks/stores
// 1. Imports
import { useState, useEffect } from "react";
// 2. Component
function TextInputPanel({ onTextChange, maxLength = 2000 }) {
const [text, setText] = useState("");
// 3. Handlers
const handleChange = (e) => {
// ...
};
// 4. Render
return (
<div>...</div>
);
}
// 5. Export
export default TextInputPanel;backend/
app/
api/routes/ → One file per endpoint group
models/ → Pydantic schemas only (NOT ML models)
services/ → Business logic (inference, rendering)
ml/ → PyTorch model definitions and training code
tests/ → Mirror app/ structure with test_ prefix
frontend/
src/
components/ → React components (PascalCase.jsx)
hooks/ → Custom hooks (useXxx.js)
stores/ → Zustand stores (xxxStore.js)
utils/ → Helper functions (camelCase.js)
assets/ → Static assets (images, icons)
Rules:
- Never put ML model code in
models/(that's for Pydantic schemas) - ML code goes in
app/ml/ - One React component per file
- Keep components under 200 lines; extract sub-components if longer
| Method | Path | Purpose |
|---|---|---|
| POST | /generate |
Submit async generation job |
| GET | /job/{job_id} |
Poll job status |
| POST | /export |
Render to PNG/PDF/SVG |
| GET | /styles |
List style presets |
| GET | /health |
Service health check |
- Always return JSON
- Use
202 Acceptedfor async jobs (not 200) - Include
job_idin generation responses - Error responses must include
detailfield
feat/— new featuresfix/— bug fixesrefactor/— code restructuringdocs/— documentationml/— ML model changes
feat(api): add WebSocket stroke streaming endpoint
fix(ml): correct MDN loss gradient computation
docs: update README with training instructions
refactor(frontend): extract CanvasPreview component
- Backend: pytest with
pytest-asynciofor async endpoints - ML: Test model instantiation, output shapes, and MDN sampling
- API: Use
TestClientfrom FastAPI - All new features must include tests
- Maintain >80% coverage on core modules
- DO NOT use absolute coordinates for strokes — always use deltas
(Δx, Δy) - DO NOT treat this as a font rendering system — strokes are generated, not looked up
- DO NOT put ML model Python code in
app/models/— that's for Pydantic schemas - DO NOT use
anytype in TypeScript/JavaScript — use proper types - DO NOT commit model checkpoints (
.pt,.pth) — they are gitignored - DO NOT commit
.envfiles — only.env.example - DO NOT hardcode model hyperparameters — use config YAML files
- DO NOT use synchronous inference in API routes — always queue via Celery
- DO NOT mix pen states — exactly one of
(p₁, p₂, p₃)must be 1 at each timestep - DO NOT use class-based React components — only functional + hooks
- Never generate content that simulates signatures
- Include watermark metadata in all exports
- Sanitize all user text input before processing
- Rate-limit generation endpoints (future: API key auth)
- No PII stored in generation artifacts