Stand up EasyStitch as an HTTP service for ThreadCraft #11

Closed
opened 2026-06-22 16:00:02 +00:00 by Mark · 2 comments
Owner

Stand up EasyStitch as an HTTP service for ThreadCraft

Context

ThreadCraft (this repo) is a TypeScript/React app. The auto-digitize pipeline (#6–#10)
and the PES export (#2) need an embroidery CAM engine to do the actual work.

A complete, mature Python implementation already exists: EasyStitch at
https://forgejo.tail018ac4.ts.net/Mark/easystitch (local clone at
~/Projects/easystitch/). It implements:

  • Raster → vector preprocessing (src/raster/)
  • Semantic SVG analysis (src/svg_annotation/)
  • Stitch classification — running / satin / fill (src/embroidery_converter/classifier.py)
  • Satin column generation (src/embroidery_converter/satin_stitch_generator.py)
  • Tatami fill with offset (src/embroidery_converter/fill_stitch_generator.py)
  • Underlay (src/embroidery_converter/underlay_generator.py)
  • ΔE-based thread palette mapping (src/embroidery_converter/thread_manager.py)
  • Output generators for PES, DST, EXP, JEF (src/output_generators/)
  • Path optimization and pull compensation (src/post_processing/, src/post_processor/)

The algorithms are perf-tuned (recent KDTree + scipy commits, June 2026), have a
Dockerfile, and ship a 5-stage EmbroideryPipeline orchestrator in
src/main_pipeline.py. What EasyStitch does not have is an HTTP layer — it's
CLI only today.

Goal

Expose EasyStitch's pipeline as an HTTP service the ThreadCraft frontend can call.
No algorithm ports — the Python code stays Python. Frontend issues HTTP requests,
receives JSON stitch objects, renders them on the Fabric.js canvas.

Architecture

┌─────────────────┐     HTTPS    ┌─────────────────────┐
│  ThreadCraft    │ ──────────▶  │  EasyStitch service │
│  (TS, browser)  │ ◀──────────  │  (Python, FastAPI)  │
│                 │   JSON+bin   │  wrapped Dockerfile │
└─────────────────┘              └─────────────────────┘
                                        │
                                        ▼
                                  ┌──────────────┐
                                  │  EasyStitch  │
                                  │  EmbroideryPipeline │
                                  └──────────────┘

API surface (v1)

Three endpoints, all POST, JSON-in / mixed-out:

POST /v1/digitize

Input raster → classified regions + stitch objects.

Request:

{
  "image_b64": "<base64 PNG/JPG, ≤ 4MB encoded>",
  "settings": {
    "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"
  }
}

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
  }
}

Backend: wraps src/main_pipeline.py EmbroideryPipeline after the raster
ingest stage.

POST /v1/export

Stitch objects (already computed) → embroidery file bytes.

Request:

{
  "stitchObjects": [ ...same as /digitize response... ],
  "format": "pes|dst|exp|jef",
  "machineProfile": "brother-pr10"
}

Response: Content-Type: application/octet-stream with the raw embroidery
file body. Headers include X-Stitch-Count, X-Color-Count.

Backend: wraps the relevant src/output_generators/<fmt>_generator.py plus
src/post_processing/ pull-compensation.

POST /v1/threads/resolve

ΔE-based thread palette lookup (used by the Workspace for live color previews
during manual editing).

Request:

{ "colors": ["#aabbcc", "#112233"] }

Response:

{
  "matches": [
    { "src": "#aabbcc", "best": { "code": "907", "name": "Pale Plum", "hex": "#a89bb1" }, "deltaE": 1.2 }
  ]
}

Backend: wraps src/embroidery_converter/thread_manager.py ΔE-2000 distance
against the embedded Brother PR-10 thread catalog.

Implementation

  1. New service directory: easystitch-service/ at the repo root (or separate
    repo — TBD based on monorepo vs sibling-service decision).
  2. Wrapper layer: app.py FastAPI app that imports src.main_pipeline and
    the relevant generator modules, exposes the 3 endpoints above. Validate inputs
    with pydantic (already a dependency).
  3. Service config: environment-driven (port, log level, max upload size,
    machine profile catalog path). 12-factor friendly.
  4. Docker: extend the existing Dockerfile in ~/Projects/easystitch/ to
    CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]. Or a
    thin Dockerfile in the service dir that FROM easystitch:latest and copies
    the FastAPI layer.
  5. Deployment via node_hub: register as a managed app with the right env
    vars, expose via the existing /proxy/:instanceId reverse proxy. ThreadCraft
    frontend talks to it at the tailnet-local URL — no public exposure needed.
  6. CORS: allow ThreadCraft's dev (http://localhost:3000) and prod origins
    only. Tailnet is trusted; no auth required between them (matches node_hub's
    NO AUTH policy).
  7. Healthcheck: GET /healthz returns {"status": "ok", "version": "..."}
    for node_hub's liveness probe.
  8. Concurrency: FastAPI sync endpoints on a thread pool (the pipeline is
    CPU-bound; use run_in_executor or a worker pool of N=CPU cores). Reject
    requests beyond a queue depth with 503.

Files touched

  • New: easystitch-service/app.py, easystitch-service/Dockerfile,
    easystitch-service/requirements.txt, easystitch-service/README.md
  • Modified: ~/Projects/easystitch/Dockerfile (add CMD uvicorn) OR copy
    the base image and override.
  • Modified: ThreadCraft package.json — add EASYSTITCH_API_URL env var
    and a src/lib/easystitch-client.ts typed wrapper.
  • Modified: ThreadCraft vite.config.ts — proxy /api/easystitch to the
    service in dev.

Acceptance

  • POST /v1/digitize on a 1024×1024 PNG returns stitch objects within 5 seconds
    on the local dev box (CPU-only is fine; GPU not required for v1).
  • POST /v1/export for format=pes returns a file that loads in a real
    Brother PR-10 (manual smoke test, no machine in CI).
  • TypeScript wrapper has typed responses; tsc --noEmit clean.
  • node_hub launches the service, /healthz returns 200.
  • Round-trip test: a hand-drawn SVG → /digitize → render in Fabric.js → /export
    PES → file opens in a viewer (e.g. https://embroiderviewer.com or Ink/Stitch).

Dependencies

This ticket blocks the following ThreadCraft issues, which currently frame
their work as "implement from scratch" and need to be re-scoped to "call the
service and render the result":

  • #2 Implement real PES file export → becomes "wire /v1/export to the
    ExportDialog and write the binary to a download"
  • #6 P1 Image Ingestion & Color Quantization → becomes "call /v1/digitize
    with the uploaded raster; render returned regions[] on the canvas"
  • #8 P3 Satin Column Stitch Generation → becomes "render returned
    stitchObjects[].type=='satin' as parallel stitch pairs on the canvas"
  • #9 P4 Tatami Fill Stitch Generation → becomes "render returned
    stitchObjects[].type=='fill' as alternating-direction rows on the canvas"
  • #10 P5 Thread Palette Mapping & Engine Integration → becomes "store the
    paletteIndex from /v1/digitize and surface in the Editor's color picker"

This ticket does not block #7 (region classification), which operates on
canvas state and should port the rules from
~/Projects/easystitch/src/embroidery_converter/classifier.py to TypeScript.

Out of scope (v1)

  • Authentication between ThreadCraft and the service (tailnet-trusted).
  • Multi-user design persistence — covered by #4 (separate backend, separate
    service).
  • Real Brother PR-10 hardware smoke test — manual, not CI.
  • Streaming / chunked responses — uploads are small (≤ 4MB), single-shot is fine.
  • GPU acceleration — v1 is CPU-only.

Risks

  • Latency: a full /digitize round-trip is ~600ms in Python + network. May
    need a progress UI (covered in #10 integration notes).
  • Memory: raster → vector on a 4MB PNG can spike. Cap input size; reject
    oversized uploads with 413.
  • Service downtime: if the Python service is down, the Workspace's
    auto-digitize button should fail gracefully (show error toast, fall back to
    manual mode). Do not crash the React app.
  • Python ↔ TS contract drift: pydantic models on the server, generated TS
    types on the client. Keep them in lockstep; consider codegen from OpenAPI
    schema in v2.
# Stand up EasyStitch as an HTTP service for ThreadCraft ## Context ThreadCraft (this repo) is a TypeScript/React app. The auto-digitize pipeline (#6–#10) and the PES export (#2) need an embroidery CAM engine to do the actual work. A complete, mature Python implementation already exists: **EasyStitch** at https://forgejo.tail018ac4.ts.net/Mark/easystitch (local clone at `~/Projects/easystitch/`). It implements: - Raster → vector preprocessing (`src/raster/`) - Semantic SVG analysis (`src/svg_annotation/`) - Stitch classification — running / satin / fill (`src/embroidery_converter/classifier.py`) - Satin column generation (`src/embroidery_converter/satin_stitch_generator.py`) - Tatami fill with offset (`src/embroidery_converter/fill_stitch_generator.py`) - Underlay (`src/embroidery_converter/underlay_generator.py`) - ΔE-based thread palette mapping (`src/embroidery_converter/thread_manager.py`) - Output generators for **PES, DST, EXP, JEF** (`src/output_generators/`) - Path optimization and pull compensation (`src/post_processing/`, `src/post_processor/`) The algorithms are perf-tuned (recent KDTree + scipy commits, June 2026), have a Dockerfile, and ship a 5-stage `EmbroideryPipeline` orchestrator in `src/main_pipeline.py`. What EasyStitch **does not** have is an HTTP layer — it's CLI only today. ## Goal Expose EasyStitch's pipeline as an HTTP service the ThreadCraft frontend can call. No algorithm ports — the Python code stays Python. Frontend issues HTTP requests, receives JSON stitch objects, renders them on the Fabric.js canvas. ## Architecture ``` ┌─────────────────┐ HTTPS ┌─────────────────────┐ │ ThreadCraft │ ──────────▶ │ EasyStitch service │ │ (TS, browser) │ ◀────────── │ (Python, FastAPI) │ │ │ JSON+bin │ wrapped Dockerfile │ └─────────────────┘ └─────────────────────┘ │ ▼ ┌──────────────┐ │ EasyStitch │ │ EmbroideryPipeline │ └──────────────┘ ``` ## API surface (v1) Three endpoints, all `POST`, JSON-in / mixed-out: ### `POST /v1/digitize` Input raster → classified regions + stitch objects. **Request:** ```json { "image_b64": "<base64 PNG/JPG, ≤ 4MB encoded>", "settings": { "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" } } ``` **Response:** ```json { "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 } } ``` **Backend:** wraps `src/main_pipeline.py` `EmbroideryPipeline` after the raster ingest stage. ### `POST /v1/export` Stitch objects (already computed) → embroidery file bytes. **Request:** ```json { "stitchObjects": [ ...same as /digitize response... ], "format": "pes|dst|exp|jef", "machineProfile": "brother-pr10" } ``` **Response:** `Content-Type: application/octet-stream` with the raw embroidery file body. Headers include `X-Stitch-Count`, `X-Color-Count`. **Backend:** wraps the relevant `src/output_generators/<fmt>_generator.py` plus `src/post_processing/` pull-compensation. ### `POST /v1/threads/resolve` ΔE-based thread palette lookup (used by the Workspace for live color previews during manual editing). **Request:** ```json { "colors": ["#aabbcc", "#112233"] } ``` **Response:** ```json { "matches": [ { "src": "#aabbcc", "best": { "code": "907", "name": "Pale Plum", "hex": "#a89bb1" }, "deltaE": 1.2 } ] } ``` **Backend:** wraps `src/embroidery_converter/thread_manager.py` ΔE-2000 distance against the embedded Brother PR-10 thread catalog. ## Implementation 1. **New service directory:** `easystitch-service/` at the repo root (or separate repo — TBD based on monorepo vs sibling-service decision). 2. **Wrapper layer:** `app.py` FastAPI app that imports `src.main_pipeline` and the relevant generator modules, exposes the 3 endpoints above. Validate inputs with pydantic (already a dependency). 3. **Service config:** environment-driven (port, log level, max upload size, machine profile catalog path). 12-factor friendly. 4. **Docker:** extend the existing `Dockerfile` in `~/Projects/easystitch/` to `CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]`. Or a thin `Dockerfile` in the service dir that `FROM easystitch:latest` and copies the FastAPI layer. 5. **Deployment via node_hub:** register as a managed app with the right env vars, expose via the existing `/proxy/:instanceId` reverse proxy. ThreadCraft frontend talks to it at the tailnet-local URL — no public exposure needed. 6. **CORS:** allow ThreadCraft's dev (`http://localhost:3000`) and prod origins only. Tailnet is trusted; no auth required between them (matches node_hub's `NO AUTH` policy). 7. **Healthcheck:** `GET /healthz` returns `{"status": "ok", "version": "..."}` for node_hub's liveness probe. 8. **Concurrency:** FastAPI sync endpoints on a thread pool (the pipeline is CPU-bound; use `run_in_executor` or a worker pool of N=CPU cores). Reject requests beyond a queue depth with `503`. ## Files touched - **New:** `easystitch-service/app.py`, `easystitch-service/Dockerfile`, `easystitch-service/requirements.txt`, `easystitch-service/README.md` - **Modified:** `~/Projects/easystitch/Dockerfile` (add `CMD uvicorn`) OR copy the base image and override. - **Modified:** ThreadCraft `package.json` — add `EASYSTITCH_API_URL` env var and a `src/lib/easystitch-client.ts` typed wrapper. - **Modified:** ThreadCraft `vite.config.ts` — proxy `/api/easystitch` to the service in dev. ## Acceptance - `POST /v1/digitize` on a 1024×1024 PNG returns stitch objects within 5 seconds on the local dev box (CPU-only is fine; GPU not required for v1). - `POST /v1/export` for `format=pes` returns a file that loads in a real Brother PR-10 (manual smoke test, no machine in CI). - TypeScript wrapper has typed responses; `tsc --noEmit` clean. - node_hub launches the service, `/healthz` returns 200. - Round-trip test: a hand-drawn SVG → /digitize → render in Fabric.js → /export PES → file opens in a viewer (e.g. `https://embroiderviewer.com` or Ink/Stitch). ## Dependencies This ticket **blocks** the following ThreadCraft issues, which currently frame their work as "implement from scratch" and need to be re-scoped to "call the service and render the result": - **#2** Implement real PES file export → becomes "wire `/v1/export` to the ExportDialog and write the binary to a download" - **#6** P1 Image Ingestion & Color Quantization → becomes "call `/v1/digitize` with the uploaded raster; render returned `regions[]` on the canvas" - **#8** P3 Satin Column Stitch Generation → becomes "render returned `stitchObjects[].type=='satin'` as parallel stitch pairs on the canvas" - **#9** P4 Tatami Fill Stitch Generation → becomes "render returned `stitchObjects[].type=='fill'` as alternating-direction rows on the canvas" - **#10** P5 Thread Palette Mapping & Engine Integration → becomes "store the `paletteIndex` from `/v1/digitize` and surface in the Editor's color picker" This ticket **does not block** #7 (region classification), which operates on canvas state and should port the rules from `~/Projects/easystitch/src/embroidery_converter/classifier.py` to TypeScript. ## Out of scope (v1) - Authentication between ThreadCraft and the service (tailnet-trusted). - Multi-user design persistence — covered by #4 (separate backend, separate service). - Real Brother PR-10 hardware smoke test — manual, not CI. - Streaming / chunked responses — uploads are small (≤ 4MB), single-shot is fine. - GPU acceleration — v1 is CPU-only. ## Risks - **Latency:** a full /digitize round-trip is ~600ms in Python + network. May need a progress UI (covered in #10 integration notes). - **Memory:** raster → vector on a 4MB PNG can spike. Cap input size; reject oversized uploads with `413`. - **Service downtime:** if the Python service is down, the Workspace's auto-digitize button should fail gracefully (show error toast, fall back to manual mode). Do not crash the React app. - **Python ↔ TS contract drift:** pydantic models on the server, generated TS types on the client. Keep them in lockstep; consider codegen from OpenAPI schema in v2.
Author
Owner

Dependents

The following ThreadCraft issues are blocked on this service:

Each has been re-scoped to a thin-client pattern: call the service, render
the result, do not port the algorithms. Once /v1/digitize and /v1/export
are live, the dependents become renderers and frontend plumbing.

Note: #7 (Phase 2: Region Classification) is not a service client —
it ports the classification rule set from
~/Projects/easystitch/src/embroidery_converter/classifier.py to TypeScript
because classification runs over canvas state and must be synchronous for
live editor previews. #7 can proceed in parallel with #11.

## Dependents The following ThreadCraft issues are blocked on this service: - #2 - #6 - #7 - #8 - #9 - #10 Each has been re-scoped to a thin-client pattern: call the service, render the result, do not port the algorithms. Once `/v1/digitize` and `/v1/export` are live, the dependents become renderers and frontend plumbing. Note: **#7 (Phase 2: Region Classification)** is **not** a service client — it ports the classification rule set from `~/Projects/easystitch/src/embroidery_converter/classifier.py` to TypeScript because classification runs over canvas state and must be synchronous for live editor previews. #7 can proceed in parallel with #11.
Author
Owner

⚠️ Re-scoped — decomposition in progress

This issue is being decomposed into sub-issues filed against the new Mark/easystitch-service repo (created 2026-06-23).

Repository: https://forgejo.tail018ac4.ts.net/Mark/easystitch-service

The HTTP service lives in its own repo (per integration-shape decision). The three endpoints specified here (/v1/digitize, /v1/export, /v1/threads/resolve) and the deployment via node_hub remain the spec — see the new sub-issues for execution.

This parent issue will stay open as the integration contract / API spec. Sub-issues track the actual build.

## ⚠️ Re-scoped — decomposition in progress This issue is being decomposed into sub-issues filed against the new **`Mark/easystitch-service`** repo (created 2026-06-23). Repository: https://forgejo.tail018ac4.ts.net/Mark/easystitch-service The HTTP service lives in its own repo (per integration-shape decision). The three endpoints specified here (`/v1/digitize`, `/v1/export`, `/v1/threads/resolve`) and the deployment via node_hub remain the spec — see the new sub-issues for execution. This parent issue will stay open as the integration contract / API spec. Sub-issues track the actual build.
Mark 2026-06-27 09:54:15 +00:00
Sign in to join this conversation.
No description provided.