HTTP service wrapper around easystitch (Mark/easystitch) for the ThreadCraft frontend. Three endpoints: /v1/digitize, /v1/export, /v1/threads/resolve. FastAPI + uvicorn. Deployed via node_hub, tailnet-trusted, no auth.
Find a file
Mark Whaite e452c59ccc
All checks were successful
CI / test (push) Successful in 2m38s
CI / docker-build (push) Has been skipped
fix(#115): DigitizeSettings accepts underlay as forward-compatible wire field
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.
2026-09-29 18:52:27 -07:00
.forgejo/workflows ci: gate docker-build job to manual dispatch only 2026-09-10 22:52:02 -07:00
.gsd feat(#29): implement GET /v1/threads/catalog route 2026-07-22 09:22:17 -07:00
data Fix #23: README THREAD_PALETTE_PATH documents auto-resolution 2026-09-08 14:40:57 -07:00
docs fix(#31): add fabric/targetStitchCount to DigitizeSettings, keep extra=forbid 2026-09-09 21:20:59 -07:00
tests fix(#115): DigitizeSettings accepts underlay as forward-compatible wire field 2026-09-29 18:52:27 -07:00
.dockerignore Bootstrap easystitch-service: scaffold app, config, models, pipeline runner, tests 2026-06-23 14:29:51 -07:00
.env.example feat(easystitch-service): allow-list ThreadCraft origins in CORS middleware (Mark/Kimi_Agent_Brother_10_Digitizing_App#13) 2026-06-27 04:11:44 -07:00
.gitignore chore: gitignore .kilo/ working notes (#26) 2026-09-09 08:47:07 -07:00
.pre-commit-config.yaml chore: add pre-commit hooks to catch lint/type drift before push 2026-09-11 19:57:24 -07:00
.webhook-ping webhook ping 2026-08-27 18:14:36 +00:00
app.py fix(#31): add fabric/targetStitchCount to DigitizeSettings, keep extra=forbid 2026-09-09 21:20:59 -07:00
config.py fix(config): resolve THREAD_PALETTE_PATH relative to project in local dev 2026-07-14 17:51:51 -07:00
Dockerfile fix(deploy): include thread palette catalog in image, add smoke fixture 2026-06-24 18:31:38 -07:00
models.py fix(#115): DigitizeSettings accepts underlay as forward-compatible wire field 2026-09-29 18:52:27 -07:00
pipeline_runner.py fix(#37): rewrite corrupt-PNG 422 detail to drop SVG-parser mislabel 2026-09-18 06:30:31 -07:00
pyproject.toml feat(threads-resolve): vectorize resolver with numpy + pre-computed Lab matrix 2026-07-14 18:10:30 -07:00
README.md fix(#31): add fabric/targetStitchCount to DigitizeSettings, keep extra=forbid 2026-09-09 21:20:59 -07:00
requirements-dev.txt Bootstrap easystitch-service: scaffold app, config, models, pipeline runner, tests 2026-06-23 14:29:51 -07:00
requirements.txt feat(threads-resolve): vectorize resolver with numpy + pre-computed Lab matrix 2026-07-14 18:10:30 -07:00
thread_resolver.py fix(#30): add alternativesLimit to /v1/threads/resolve; correct algorithm doc 2026-09-09 20:59:04 -07:00

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 objects
  • POST /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 entries
  • GET /v1/threads/catalog — full Brother PR-10 thread catalog

Plus:

  • GET /healthz — liveness/readiness probe for the container orchestrator
  • GET /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/easystitch cloned as a sibling repo at ../easystitch
  • Node.js 18+ (only for the TS client codegen tooling)
  • uv (recommended) or pip + 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:

  1. The THREAD_PALETTE_PATH env var, if set (always wins — use this to point at a custom catalog).
  2. /data/brother_pr10.json — the path inside the published Docker image. The Dockerfile copies the in-repo data/brother_pr10.json to this location at build time.
  3. <repo>/data/brother_pr10.json — the in-repo file, used for local uvicorn runs 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 /healthz every 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 request
  • endpoint — /v1/digitize, /v1/export, etc.
  • processing_ms — wall-clock time inside the pipeline
  • total_stitches, color_count — output stats for /v1/digitize
  • status_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

  1. Branch from main: git checkout -b feat/<short-description>
  2. Make changes. Add tests for new behavior.
  3. Run pytest and ruff check before pushing.
  4. Push the branch and open a PR against main.
  5. 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.
  • Mark/easystitch — Python embroidery CAM engine
  • Mark/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.