The Mark/Kimi_Agent_Brother_10_Digitizing_App workspace now sends the user's Underlay toggle so the front-end control has a real effect the moment the engine adopts it. Same forward-compatible pattern as fabric and target_stitch_count: accepted on the wire, ignored by _translate_settings until the engine wires it in. Test: test_digitize_accepts_underlay_field — verifies on/off payloads both 200 and reach the runner with the right value. Pytest: 159/159 pass. Ruff clean. |
||
|---|---|---|
| .forgejo/workflows | ||
| .gsd | ||
| data | ||
| docs | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| .webhook-ping | ||
| app.py | ||
| config.py | ||
| Dockerfile | ||
| models.py | ||
| pipeline_runner.py | ||
| pyproject.toml | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
| thread_resolver.py | ||
easystitch-service
HTTP service that wraps the easystitch embroidery engine for browser/TS clients.
The first client is ThreadCraft — a TypeScript/React embroidery design app. ThreadCraft needs a CAM engine; rather than re-implementing embroidery algorithms in the browser (scipy, scikit-learn, KDTree don't have JS equivalents), it calls this service over HTTP and renders the stitch results on Fabric.js.
The service is the deployment surface for the algorithms that already exist in Mark/easystitch. This repo is the wrapper, the contract, and the deployment artifact — not the algorithms themselves.
Documentation
| Doc | What's in it |
|---|---|
docs/api.md |
Full API contract — endpoints, request/response shapes, error codes, CORS |
docs/deployment.md |
Build, registry, node_hub registration, scaling, rollback |
docs/operations.md |
On-call runbook — triage, common alerts, escalation |
docs/observability.md |
Logs, events, request_id, manual metrics, alerts |
docs/capacity.md |
Sizing, vertical/horizontal scaling, SAAS capacity planning |
docs/codegen.md |
OpenAPI → TS codegen plan (v2) |
docs/walk-plan.md |
Phased walk to a functional stack — issue ordering, dependencies, parallelization, risks |
Who this is for
SMB-pro embroidery shops — print-on-demand houses, promotional product shops, in-house corporate design teams, boutique apparel brands, sports team decorators. Operators running 1–10 embroidery machines who currently pay $2–8k/seat for Wilcom/Hatch, or $15–75/design to Fiverr digitizers.
The SAAS pitch is simple: replace a $4k/year Wilcom seat plus $500/month Fiverr spend with a service that costs $50–200/month and digitizes in seconds. Privacy-sensitive (customer logos, brand assets) — handled by per-tenant storage isolation and a "your designs never train our models" guarantee.
Pricing tiers reflect job volume:
- Small — $49/mo, 50 designs
- Pro — $149/mo, 200 designs
- Shop — $399/mo, unlimited + API access
- Wholesale — per-design at print-on-demand scale
Architecture
┌─────────────────┐ HTTPS ┌─────────────────────┐
│ ThreadCraft │ ──────────▶ │ easystitch-service │
│ (TS, browser) │ ◀────────── │ (Python, FastAPI) │
│ │ JSON+bin │ │
└─────────────────┘ └─────────────────────┘
│
▼
┌──────────────────┐
│ easystitch │
│ EmbroideryPipeline │
└──────────────────┘
Four endpoints, JSON-in / mixed-out:
POST /v1/digitize— raster image (multipart PNG/JPG ≤ 4 MB) → classified regions + stitch objectsPOST /v1/export— stitch objects → embroidery file bytes (PES/DST/EXP/JEF)POST /v1/threads/resolve— color list → ΔE-2000-matched Brother PR-10 thread catalog entriesGET /v1/threads/catalog— full Brother PR-10 thread catalog
Plus:
GET /healthz— liveness/readiness probe for the container orchestratorGET /openapi.json— FastAPI-generated OpenAPI 3.1 schema (used for TS type generation in v2; v1 keeps types manually synced)
CORS is locked to known ThreadCraft origins. Auth is a single shared household bearer token — v1 uses API_KEY set in the service env and Authorization: Bearer <token> sent from the frontend. GET /healthz is exempt so container orchestrators can probe it.
API contract
Full request/response shapes, error codes, and CORS policy are in docs/api.md. The pydantic models in models.py and the TS types in Mark/Kimi_Agent_Brother_10_Digitizing_App/src/lib/easystitch-client.ts must match this exactly. v2 will codegen the TS types from the OpenAPI schema — see docs/codegen.md.
POST /v1/digitize
multipart/form-data with two parts:
| Part | Type | Content |
|---|---|---|
image |
file | binary PNG/JPG, decoded size ≤ MAX_UPLOAD_MB (default 4 MB) |
settings |
string | JSON-encoded settings object (below) |
Settings JSON:
{
"stitchPreference": "balanced|quality|speed",
"fillDensityMm": 0.4,
"satinStitchSpacingMm": 0.4,
"runningStitchSpacingMm": 0.4,
"minStitchLengthMm": 1.0,
"maxStitchLengthMm": 8.0,
"hoopSizeMm": { "w": 100, "h": 100 },
"maxColors": 10,
"machineProfile": "brother-pr10",
"fabric": "cotton",
"targetStitchCount": 1500
}
fabric and targetStitchCount are optional and accepted for forward compatibility — the engine does not consume them yet. hoopSizeMm also accepts the long width/height spelling. Settings keys outside this list are rejected with 422 extra_forbidden.
Response:
{
"regions": [
{ "id": "r0", "polygon": [[x, y], ...], "color": "#aabbcc", "stitchType": "satin" }
],
"stitchObjects": [
{
"id": "s0",
"type": "satin",
"threadColor": "#aabbcc",
"paletteIndex": 12,
"stitches": [{ "x": 1.2, "y": 3.4 }, ...]
}
],
"stats": {
"totalStitches": 1842,
"colorCount": 4,
"processingMs": 612
}
}
The ThreadCraft frontend renders regions[] as overlay polygons on Fabric.js, and stitchObjects[] as the actual stitches (satin as parallel stitch pairs, fill as alternating-direction rows, running as point-to-point lines).
Development
Prerequisites
- Python 3.11+
Mark/easystitchcloned as a sibling repo at../easystitch- Node.js 18+ (only for the TS client codegen tooling)
uv(recommended) orpip+ venv
Setup
# From the easystitch-service repo root
uv venv
source .venv/bin/activate
uv pip install -e ../easystitch # sibling-repo install
uv pip install -r requirements.txt
Run locally
# Requires easystitch repo as a sibling
uvicorn app:app --reload --host 0.0.0.0 --port 8080
Smoke test:
curl -s http://localhost:8080/healthz | jq
# {"status":"ok","version":"0.1.0"}
# Test /v1/digitize with a fixture image
python tests/manual/smoke_digitize.py tests/fixtures/logo.png
Run tests
pytest # unit + integration
pytest tests/unit/ # fast, no pipeline
pytest tests/integration/ # hits the real easystitch pipeline
Configuration
All config is 12-factor env vars. Defaults in app.py are dev-safe.
| Env var | Default | Description |
|---|---|---|
LOG_LEVEL |
info |
Structured JSON log level |
MAX_UPLOAD_MB |
4 |
Reject larger uploads with 413 |
MAX_QUEUE_DEPTH |
8 |
Per-replica concurrent in-flight requests before 503 |
THREAD_POOL_SIZE |
0 (auto = CPU count) |
Executor pool for CPU-bound pipeline |
THREAD_PALETTE_PATH |
auto-resolved (see below) |
Catalog file for /v1/threads/resolve and /v1/threads/catalog. Resolution order: (1) THREAD_PALETTE_PATH env var if set; (2) /data/brother_pr10.json inside the container (Dockerfile stages data/brother_pr10.json here); (3) <repo>/data/brother_pr10.json for local uvicorn runs. Shipped catalog lives at data/brother_pr10.json in this repo. |
ALLOWED_ORIGINS |
http://localhost:3000 |
Comma-separated CORS allowlist. Dev default is http://localhost:3000; production deployments must include the ThreadCraft origin (e.g. https://threadcraft.markkeers.com). |
REQUEST_TIMEOUT_S |
30 |
Per-request pipeline budget before 504 |
API_KEY |
'' (empty) |
Shared bearer token; when non-empty, /v1/* requires it |
Thread palette resolution
THREAD_PALETTE_PATH is resolved at startup in this order:
- The
THREAD_PALETTE_PATHenv var, if set (always wins — use this to point at a custom catalog). /data/brother_pr10.json— the path inside the published Docker image. TheDockerfilecopies the in-repodata/brother_pr10.jsonto this location at build time.<repo>/data/brother_pr10.json— the in-repo file, used for localuvicornruns against the source tree.
A fresh uvicorn app:app --reload run with no env overrides loads data/brother_pr10.json from the repo root — no manual editing required. The Dockerfile copies the same file to /data/brother_pr10.json at image build time, so both default paths point at identical content.
To see which path the current process would use:
python -c "from config import settings; print(settings.thread_palette_path)"
Deployment
Deployed via the project's node_hub instance. Single-container deployment today; horizontal scaling is a v2 concern (see Capacity & scaling).
Build & push
docker build -t easystitch-service:latest .
# Push to whatever registry node_hub reads from
node_hub registration
The service is registered as a managed app:
- Port: 8080
- Healthcheck:
GET /healthzevery 10s - Env vars: see table above
- Replicas: 1 (start; scale on queue depth)
- URL:
http://easystitch-service.tail018ac4.ts.net(tailnet-internal)
Dockerfile is multi-stage — the easystitch base image is ~1.5GB (scipy, scikit-learn, scikit-image); the service layer adds <100MB.
Full deployment details in docs/deployment.md.
Capacity & scaling
This is a CPU-bound service. Embroidery digitizing isn't GPU-friendly (scipy/scikit-learn KDTree and flood-fill are CPU-bound, not matrix-multiply heavy).
Baseline: one replica on a 4-core/8GB box handles ~5 concurrent digitizes at ~3s each. Tail latency for POST /v1/digitize on a 1024×1024 PNG is ~600ms in Python + network; budget for spikes to 5s.
Queue depth: MAX_QUEUE_DEPTH=8 per replica — beyond that, requests get 503. The frontend treats 503 as retryable with exponential backoff.
Scaling triggers:
- Queue depth > 6 sustained for 5 minutes → +1 replica
- 95th percentile
processingMs> 3000 sustained for 10 minutes → +1 replica - 5xx rate > 1% over 5 minutes → page on-call (alerting)
v1 scaling: vertical. v2: horizontal with shared-nothing replicas (pipeline is stateless). The catalog (THREAD_PALETTE_PATH) needs to be readable from each replica — vendor it into the image or use a shared volume.
Observability
Logs are structured JSON to stdout, one line per event. Per-request fields:
request_id— UUID generated per request, propagated to all log lines within that requestendpoint—/v1/digitize,/v1/export, etc.processing_ms— wall-clock time inside the pipelinetotal_stitches,color_count— output stats for/v1/digitizestatus_code— final response code
node_hub ships logs to the project's log sink. To investigate a request:
# Find all events for a request_id
journalctl ... | jq 'select(.request_id == "abc-123")'
For v1 there's no metrics endpoint. v2: Prometheus /metrics with request count, latency histogram, queue depth gauge, in-flight gauge.
Versioning
easystitch pin: the service pins to a specific git tag of Mark/easystitch (e.g. v0.4.2). Bumping the pin is a deliberate change — tested in a branch before merging to main.
Service semver: follows the response-shape contract, not easystitch internals:
- Major bump — breaking change to
/v1/*request or response schema (any client must update) - Minor bump — additive (new optional fields, new endpoints, new stitch types)
- Patch bump — bug fixes, performance, internal refactors
The frontend (ThreadCraft) pins to a known service version via the X-Service-Version response header on /healthz.
Deprecation policy: when a major bump happens, the old version stays live for at least 30 days on a /v1-legacy/* route (or in a separate node_hub instance) so clients can migrate.
Project layout
easystitch-service/
├── app.py # FastAPI app, endpoint handlers
├── models.py # Pydantic request/response models
├── pipeline_runner.py # Thread-pool wrapper around EmbroideryPipeline
├── config.py # Env-var-driven configuration
├── docs/
│ ├── api.md # Full API contract
│ ├── deployment.md # node_hub setup
│ └── operations.md # On-call runbook
├── tests/
│ ├── unit/ # Pydantic, error paths, executor
│ ├── integration/ # Real pipeline end-to-end
│ ├── fixtures/ # Sample PNGs + expected outputs
│ └── manual/ # Smoke-test scripts
├── Dockerfile
├── .dockerignore
├── requirements.txt
├── pyproject.toml
└── README.md # You are here
Contributing
- Branch from
main:git checkout -b feat/<short-description> - Make changes. Add tests for new behavior.
- Run
pytestandruff checkbefore pushing. - Push the branch and open a PR against
main. - CI runs unit tests + lint + Dockerfile build. Integration tests run nightly.
When changing the API contract — update docs/api.md, the pydantic models in models.py, and coordinate with the frontend (ThreadCraft). API changes are major-version events.
When bumping the easystitch pin — note it in the PR description; test against representative fixtures before merging.
Dependencies
Mark/easystitch— the engine. Sibling repo. Pinned via git tag.Mark/Kimi_Agent_Brother_10_Digitizing_App— primary client (ThreadCraft).Mark/infrastructure— node_hub, DNS, registry config.
Related repos
Mark/easystitch— Python embroidery CAM engineMark/Kimi_Agent_Brother_10_Digitizing_App— ThreadCraft frontend (TS/React)Mark/infrastructure— node_hub + tailnet + deployment infrastructure
License
TBD. easystitch is the underlying engine; this wrapper inherits whatever license applies there.