diff --git a/.gsd/event-log.jsonl b/.gsd/event-log.jsonl index 619c5a5..04b2621 100644 --- a/.gsd/event-log.jsonl +++ b/.gsd/event-log.jsonl @@ -5,3 +5,4 @@ {"cmd":"plan-slice","params":{"milestoneId":"M001","sliceId":"S02"},"ts":"2026-03-26T03:55:23.088Z","actor":"agent","hash":"7990d7932192bfde","session_id":"5b5a1848-fcbc-4200-aa49-5260215f4e78"} {"cmd":"plan-slice","params":{"milestoneId":"M001","sliceId":"S03"},"ts":"2026-03-26T03:55:42.360Z","actor":"agent","hash":"2d5b99521edd6ccd","session_id":"5b5a1848-fcbc-4200-aa49-5260215f4e78"} {"cmd":"complete-task","params":{"milestoneId":"M001","sliceId":"S01","taskId":"T01"},"ts":"2026-03-26T04:07:03.179Z","actor":"agent","hash":"5a804380eb33710e","session_id":"f5306801-4a7b-4a78-9c7a-c96e61e0b90b"} +{"cmd":"complete-task","params":{"milestoneId":"M001","sliceId":"S01","taskId":"T05"},"ts":"2026-03-26T04:21:26.263Z","actor":"agent","hash":"6869e9cc311e2c24","session_id":"ff7a7bf7-7e3f-4ef0-8897-40b2141eba83"} diff --git a/.gsd/milestones/M001/slices/S01/S01-PLAN.md b/.gsd/milestones/M001/slices/S01/S01-PLAN.md index d43ecc0..2bf7bc1 100644 --- a/.gsd/milestones/M001/slices/S01/S01-PLAN.md +++ b/.gsd/milestones/M001/slices/S01/S01-PLAN.md @@ -28,7 +28,7 @@ - Estimate: 45min - Files: engine/pipeline/__init__.py, engine/pipeline/preprocessing.py, engine/tests/test_preprocessing.py - Verify: cd engine && python -m pytest tests/test_preprocessing.py -v -- [ ] **T03: Potrace vectorization (Mode A)** — 1. Create engine/pipeline/vectorize.py +- [x] **T03: Potrace vectorization (Mode A)** — 1. Create engine/pipeline/vectorize.py 2. Implement potrace_trace() function: - Accepts preprocessed numpy array (binary image) - Calls pypotrace with tunable params: turdsize, alphamax, opticurve, opttolerance @@ -39,7 +39,7 @@ - Estimate: 45min - Files: engine/pipeline/vectorize.py, engine/tests/test_vectorize.py, engine/tests/fixtures/ - Verify: cd engine && python -m pytest tests/test_vectorize.py -v -k potrace -- [ ] **T04: VTracer vectorization (Mode B)** — 1. Add VTracer tracing to engine/pipeline/vectorize.py +- [x] **T04: VTracer vectorization (Mode B)** — 1. Add VTracer tracing to engine/pipeline/vectorize.py 2. Implement vtracer_trace() function: - Accepts preprocessed image (can be color for VTracer) - Calls vtracer with tunable params: colormode, hierarchical, filter_speckle, color_precision, layer_difference, corner_threshold, length_threshold, splice_threshold @@ -50,7 +50,7 @@ - Estimate: 45min - Files: engine/pipeline/vectorize.py, engine/tests/test_vectorize.py - Verify: cd engine && python -m pytest tests/test_vectorize.py -v -k vtracer -- [ ] **T05: Wire up /engine/trace endpoint** — 1. Create engine/api/routes.py with trace endpoint +- [x] **T05: Wired POST /engine/trace endpoint with preprocess + vectorize pipeline, SVG metadata, and 14 integration tests** — 1. Create engine/api/routes.py with trace endpoint 2. POST /engine/trace accepts multipart/form-data: - file (image), mode (potrace|vtracer), output_format (svg only for now), preset (ignored for now), params (JSON override) 3. Wire pipeline: receive file → preprocess → vectorize (mode-dependent) → return SVG + metadata diff --git a/.gsd/milestones/M001/slices/S01/S01-SUMMARY.md b/.gsd/milestones/M001/slices/S01/S01-SUMMARY.md new file mode 100644 index 0000000..ec5f78d --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/S01-SUMMARY.md @@ -0,0 +1,52 @@ +--- +id: S01 +milestone: M001 +status: complete +tasks_completed: 5 +tasks_total: 5 +completed_at: 2026-03-26T04:30:00.000Z +--- + +# S01: Core Pipeline — Preprocessing + Vectorization + +## Goal +Stand up FastAPI server with OpenCV preprocessing pipeline and dual-mode vectorization (Potrace + VTracer). Establish the repo structure, dependencies, and core /engine/trace endpoint. + +## Outcome +All 5 tasks completed successfully. The engine accepts PNG uploads via POST /engine/trace and returns SVG output with metadata using either Potrace or VTracer mode. Full test suite: 76 tests passing. + +## What Was Built + +### T01: Project Scaffold +Created engine/ Python project with FastAPI skeleton, pyproject.toml, all dependencies (fastapi, uvicorn, opencv-python-headless, pypotrace, vtracer, python-multipart, Pillow). Verified uvicorn starts cleanly. + +### T02: OpenCV Preprocessing Pipeline +Implemented `engine/pipeline/preprocessing.py` with full pipeline: grayscale conversion, bilateral filter denoise, CLAHE contrast enhancement, Otsu thresholding, optional Canny edge detection, morphological operations. All stages tunable via parameters dict. + +### T03: Potrace Vectorization (Mode A) +Implemented `potrace_trace()` in `engine/pipeline/vectorize.py`. Converts preprocessed binary images to SVG via pypotrace with tunable params (turdsize, alphamax, opticurve, opttolerance). + +### T04: VTracer Vectorization (Mode B) +Implemented `vtracer_trace()` in `engine/pipeline/vectorize.py`. Converts grayscale or color images to SVG via vtracer Python bindings with tunable params (colormode, filter_speckle, color_precision, etc.). + +### T05: API Endpoint + Integration Tests +Created `engine/api/routes.py` with POST /engine/trace endpoint. Accepts multipart form-data (file, mode, output_format, preset, params). Returns JSON with SVG output and metadata (path_count, node_count_total, open_paths, processing_ms, warnings). 14 integration tests covering both modes, metadata shape, custom params, and validation errors. + +## Key Files +- `engine/main.py` — FastAPI app entry point +- `engine/api/routes.py` — /engine/trace endpoint +- `engine/pipeline/preprocessing.py` — OpenCV preprocessing pipeline +- `engine/pipeline/vectorize.py` — Potrace + VTracer vectorization +- `engine/tests/` — 76 tests (unit + integration) + +## Key Decisions +- SVG metadata extraction via regex (lightweight, no XML parser dependency) +- Vectorize params filtered by known param names per mode +- Bilateral filter chosen over Gaussian for edge-preserving denoise +- CLAHE for contrast rather than simple histogram equalization + +## Verification +Full test suite: `cd engine && .venv/bin/python -m pytest tests/ -v` — 76 passed + +## Demo +POST /engine/trace with a PNG logo returns valid SVG using both Potrace and VTracer modes. ✅ diff --git a/.gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md b/.gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md index f90500f..75e72ef 100644 --- a/.gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md +++ b/.gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md @@ -2,62 +2,21 @@ id: T02 parent: S01 milestone: M001 -provides: - - preprocessing pipeline for raster-to-vector conversion -key_files: - - engine/pipeline/preprocessing.py - - engine/tests/test_preprocessing.py +provides: [] +requires: [] +affects: [] +key_files: [] key_decisions: [] -patterns_established: - - pipeline stages as pure functions accepting numpy arrays with tunable params dict +patterns_established: [] +drill_down_paths: [] observability_surfaces: [] duration: "" -verification_result: passed -completed_at: 2026-03-26T04:30:00.000Z +verification_result: "" +completed_at: blocker_discovered: false --- -# T02: OpenCV preprocessing pipeline with six tunable stages and 24 unit tests - -**Implemented full OpenCV preprocessing pipeline (grayscale, bilateral denoise, CLAHE contrast, Otsu/manual threshold, optional Canny edge detection, morphological close) with all stages tunable via params dict — 24 tests passing** +# T02: OpenCV preprocessing pipeline ## What Happened - -Created `engine/pipeline/preprocessing.py` with the complete preprocessing pipeline. Each stage is a standalone function accepting a numpy array and returning a numpy array, with tunable parameters. The `preprocess()` orchestrator accepts raw image bytes and a params dict, runs the full pipeline, and returns a clean binary image ready for vectorization. - -Stages in order: decode → grayscale → bilateral denoise → CLAHE contrast → Otsu threshold → (optional Canny edge detection) → morphological dilate/erode. - -Created `engine/tests/test_preprocessing.py` with 24 tests covering every stage individually plus end-to-end pipeline tests with default params, edge detection mode, custom params, and invalid input rejection. - -## Verification - -``` -cd engine && .venv/bin/python -m pytest tests/test_preprocessing.py -v -# 24 passed in 0.14s -``` - -`pip install -e .` verified working inside the venv. - -## Verification Evidence - -| # | Command | Exit Code | Verdict | Duration | -|---|---------|-----------|---------|----------| -| 1 | `.venv/bin/python -m pytest tests/test_preprocessing.py -v` | 0 | ✅ pass | 140ms | -| 2 | `.venv/bin/pip install -e .` | 0 | ✅ pass | 3000ms | - -## Diagnostics - -Import and call individual stage functions for inspection: `from pipeline.preprocessing import denoise, threshold` etc. Each returns a numpy array that can be visualized with `cv2.imwrite()`. - -## Deviations - -None. - -## Known Issues - -The verification gate's `pip install -e .` must run inside the venv (`.venv/bin/pip`), not system python, due to PEP 668 externally-managed-environment restriction on Ubuntu 24.04. - -## Files Created/Modified - -- `engine/pipeline/preprocessing.py` — Full preprocessing pipeline with six tunable stages -- `engine/tests/test_preprocessing.py` — 24 unit tests covering all stages and end-to-end pipeline +No summary recorded. diff --git a/.gsd/milestones/M001/slices/S01/tasks/T03-SUMMARY.md b/.gsd/milestones/M001/slices/S01/tasks/T03-SUMMARY.md index e60384e..8667317 100644 --- a/.gsd/milestones/M001/slices/S01/tasks/T03-SUMMARY.md +++ b/.gsd/milestones/M001/slices/S01/tasks/T03-SUMMARY.md @@ -2,63 +2,21 @@ id: T03 parent: S01 milestone: M001 -provides: - - Potrace vectorization function for converting binary images to SVG -key_files: - - engine/pipeline/vectorize.py - - engine/tests/test_vectorize.py +provides: [] +requires: [] +affects: [] +key_files: [] key_decisions: [] -patterns_established: - - vectorize module exposes per-backend trace functions (potrace_trace) returning SVG strings +patterns_established: [] +drill_down_paths: [] observability_surfaces: [] duration: "" -verification_result: passed -completed_at: 2026-03-26T05:00:00.000Z +verification_result: "" +completed_at: blocker_discovered: false --- # T03: Potrace vectorization (Mode A) -**Implemented potrace_trace() function that converts preprocessed binary numpy arrays to well-formed SVG via pypotrace, with tunable turdsize/alphamax/opticurve/opttolerance params — 19 tests passing** - ## What Happened - -Created `engine/pipeline/vectorize.py` with the `potrace_trace()` function. It accepts a 2D binary numpy array (output of the preprocessing pipeline), converts nonzero pixels to a uint32 bitmap for pypotrace, traces paths with configurable parameters (turdsize, alphamax, opticurve, opttolerance), and converts the resulting path curves into SVG path data (M/L/C/Z commands). The output is a complete, well-formed SVG string with correct dimensions and viewBox. - -The internal `_path_to_svg()` helper handles both corner segments (L commands) and cubic bezier segments (C commands with c1/c2 control points) from the potrace output. - -Created 19 tests in `engine/tests/test_vectorize.py` covering: basic SVG output validation (well-formed XML, correct dimensions, path element), shape tracing (squares produce corners, circles produce bezier curves), parameter tuning (turdsize despeckle, opticurve on/off, alphamax polygon mode), edge cases (all-black, all-white, non-square images, different dtypes, 3D rejection), and integration with the preprocessing pipeline. - -## Verification - -``` -cd engine && .venv/bin/python -m pytest tests/test_vectorize.py -v -k potrace -# 19 passed in 0.11s - -.venv/bin/python -m pytest tests/ -v -# 43 passed in 0.16s (24 preprocessing + 19 vectorize) -``` - -## Verification Evidence - -| # | Command | Exit Code | Verdict | Duration | -|---|---------|-----------|---------|----------| -| 1 | `.venv/bin/python -m pytest tests/test_vectorize.py -v -k potrace` | 0 | ✅ pass | 110ms | -| 2 | `.venv/bin/python -m pytest tests/ -v` | 0 | ✅ pass | 160ms | - -## Diagnostics - -Import and test directly: `from pipeline.vectorize import potrace_trace`. Pass any 2D numpy array of 0/nonzero values. The returned SVG string can be written to a file and opened in a browser for visual inspection. - -## Deviations - -None. - -## Known Issues - -pypotrace's Bitmap requires uint32 data and interprets values as zero/nonzero (not 0/255). The `potrace_trace()` function handles this conversion internally via `(binary_img > 0).astype(np.uint32)`. - -## Files Created/Modified - -- `engine/pipeline/vectorize.py` — Potrace vectorization with potrace_trace() and SVG path generation -- `engine/tests/test_vectorize.py` — 19 tests covering output format, shapes, params, edge cases, and preprocessing integration +No summary recorded. diff --git a/.gsd/milestones/M001/slices/S01/tasks/T04-SUMMARY.md b/.gsd/milestones/M001/slices/S01/tasks/T04-SUMMARY.md index dc974ed..6df140d 100644 --- a/.gsd/milestones/M001/slices/S01/tasks/T04-SUMMARY.md +++ b/.gsd/milestones/M001/slices/S01/tasks/T04-SUMMARY.md @@ -2,64 +2,21 @@ id: T04 parent: S01 milestone: M001 -provides: - - VTracer vectorization function for converting images (grayscale, BGR, BGRA) to SVG -key_files: - - engine/pipeline/vectorize.py - - engine/tests/test_vectorize.py -key_decisions: - - Used convert_raw_image_to_svg with PNG encoding instead of convert_pixels_to_svg — avoids slow Python-side pixel tuple construction -patterns_established: - - vtracer_trace() accepts 2D or 3D numpy arrays (unlike potrace_trace which requires 2D binary), encodes to PNG internally +provides: [] +requires: [] +affects: [] +key_files: [] +key_decisions: [] +patterns_established: [] +drill_down_paths: [] observability_surfaces: [] duration: "" -verification_result: passed -completed_at: 2026-03-26T06:00:00.000Z +verification_result: "" +completed_at: blocker_discovered: false --- # T04: VTracer vectorization (Mode B) -**Implemented vtracer_trace() function that converts grayscale or color numpy arrays to SVG via VTracer's Rust backend, with tunable colormode/filter_speckle/mode/corner_threshold params — 19 tests passing, 62 total** - ## What Happened - -Added `vtracer_trace()` to `engine/pipeline/vectorize.py`. The function accepts 2D (grayscale) or 3D (BGR/BGRA) numpy arrays, encodes them as PNG via OpenCV, and passes the raw bytes to VTracer's `convert_raw_image_to_svg()` Rust binding. This avoids the slow path of constructing Python tuples for every pixel. - -Tunable parameters: colormode (binary/color), hierarchical (stacked/cutout), filter_speckle, color_precision, layer_difference, corner_threshold, length_threshold, splice_threshold, mode (spline/polygon/none), path_precision, max_iterations. - -Added 19 VTracer tests covering: basic SVG output validation (well-formed XML, correct dimensions, path elements), input type acceptance (2D grayscale, 3D BGR, 4-channel BGRA, rejection of 4D), parameter effects (color mode, filter_speckle noise removal, polygon vs spline mode), edge cases (all-black, all-white), preprocessing integration, and Mode A vs Mode B comparison. - -## Verification - -``` -cd engine && .venv/bin/python -m pytest tests/test_vectorize.py -v -k vtracer -# 19 passed in 0.12s - -.venv/bin/python -m pytest tests/ -v -# 62 passed in 0.20s (24 preprocessing + 19 potrace + 19 vtracer) -``` - -## Verification Evidence - -| # | Command | Exit Code | Verdict | Duration | -|---|---------|-----------|---------|----------| -| 1 | `.venv/bin/python -m pytest tests/test_vectorize.py -v -k vtracer` | 0 | ✅ pass | 120ms | -| 2 | `.venv/bin/python -m pytest tests/ -v` | 0 | ✅ pass | 200ms | - -## Diagnostics - -Import and test directly: `from pipeline.vectorize import vtracer_trace`. Pass any 2D or 3D numpy array. The returned SVG string includes an XML declaration and can be written to a file for browser inspection. Unlike potrace_trace, vtracer_trace accepts color images and produces multi-path color SVGs when colormode='color'. - -## Deviations - -Used `convert_raw_image_to_svg` with PNG encoding instead of `convert_pixels_to_svg` — the pixel-tuple API requires constructing a Python list of (R,G,B,A) tuples which is slow for large images. The PNG path delegates all pixel handling to the Rust side. - -## Known Issues - -None. - -## Files Created/Modified - -- `engine/pipeline/vectorize.py` — Added vtracer_trace() function with PNG-encoding path to VTracer Rust backend -- `engine/tests/test_vectorize.py` — Added 19 VTracer tests (input types, params, edge cases, preprocessing integration, Mode A vs B comparison) +No summary recorded. diff --git a/.gsd/milestones/M001/slices/S01/tasks/T05-SUMMARY.md b/.gsd/milestones/M001/slices/S01/tasks/T05-SUMMARY.md new file mode 100644 index 0000000..03c73d7 --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/tasks/T05-SUMMARY.md @@ -0,0 +1,77 @@ +--- +id: T05 +parent: S01 +milestone: M001 +provides: [] +requires: [] +affects: [] +key_files: ["engine/api/routes.py", "engine/tests/test_api.py", "engine/main.py"] +key_decisions: ["SVG metadata via regex - lightweight, no XML parser needed", "Vectorize params filtered by known param names per mode"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "cd engine && .venv/bin/python -m pytest tests/test_api.py -v (14 passed), full suite 76 passed" +completed_at: 2026-03-26T04:21:26.227Z +blocker_discovered: false +--- + +# T05: Wired POST /engine/trace endpoint with preprocess + vectorize pipeline, SVG metadata, and 14 integration tests + +> Wired POST /engine/trace endpoint with preprocess + vectorize pipeline, SVG metadata, and 14 integration tests + +## What Happened +--- +id: T05 +parent: S01 +milestone: M001 +key_files: + - engine/api/routes.py + - engine/tests/test_api.py + - engine/main.py +key_decisions: + - SVG metadata via regex - lightweight, no XML parser needed + - Vectorize params filtered by known param names per mode +duration: "" +verification_result: passed +completed_at: 2026-03-26T04:21:26.233Z +blocker_discovered: false +--- + +# T05: Wired POST /engine/trace endpoint with preprocess + vectorize pipeline, SVG metadata, and 14 integration tests + +**Wired POST /engine/trace endpoint with preprocess + vectorize pipeline, SVG metadata, and 14 integration tests** + +## What Happened + +Created engine/api/routes.py with POST /engine/trace endpoint accepting multipart form-data (file, mode, output_format, preset, params). Wires full pipeline: decode → preprocess → vectorize (potrace or vtracer) → extract SVG metadata → return JSON. Updated main.py to include the router. Created 14 integration tests covering both modes, metadata shape, custom params, and validation errors. + +## Verification + +cd engine && .venv/bin/python -m pytest tests/test_api.py -v (14 passed), full suite 76 passed + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd engine && .venv/bin/python -m pytest tests/test_api.py -v` | 0 | ✅ pass | 430ms | +| 2 | `cd engine && .venv/bin/python -m pytest tests/ -v` | 0 | ✅ pass | 470ms | + + +## Deviations + +None. + +## Known Issues + +None. + +## Files Created/Modified + +- `engine/api/routes.py` +- `engine/tests/test_api.py` +- `engine/main.py` + + +## Deviations +None. diff --git a/.gsd/state-manifest.json b/.gsd/state-manifest.json index bd32a4a..a0805c1 100644 --- a/.gsd/state-manifest.json +++ b/.gsd/state-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "exported_at": "2026-03-26T04:07:03.179Z", + "exported_at": "2026-03-26T04:21:26.263Z", "milestones": [ { "id": "M001", @@ -448,7 +448,7 @@ "slice_id": "S01", "id": "T02", "title": "OpenCV preprocessing pipeline", - "status": "pending", + "status": "complete", "one_liner": "", "narrative": "", "verification_result": "", @@ -484,7 +484,7 @@ "slice_id": "S01", "id": "T03", "title": "Potrace vectorization (Mode A)", - "status": "pending", + "status": "complete", "one_liner": "", "narrative": "", "verification_result": "", @@ -520,7 +520,7 @@ "slice_id": "S01", "id": "T04", "title": "VTracer vectorization (Mode B)", - "status": "pending", + "status": "complete", "one_liner": "", "narrative": "", "verification_result": "", @@ -553,19 +553,26 @@ "milestone_id": "M001", "slice_id": "S01", "id": "T05", - "title": "Wire up /engine/trace endpoint", - "status": "pending", - "one_liner": "", - "narrative": "", - "verification_result": "", + "title": "Wired POST /engine/trace endpoint with preprocess + vectorize pipeline, SVG metadata, and 14 integration tests", + "status": "complete", + "one_liner": "Wired POST /engine/trace endpoint with preprocess + vectorize pipeline, SVG metadata, and 14 integration tests", + "narrative": "Created engine/api/routes.py with POST /engine/trace endpoint accepting multipart form-data (file, mode, output_format, preset, params). Wires full pipeline: decode → preprocess → vectorize (potrace or vtracer) → extract SVG metadata → return JSON. Updated main.py to include the router. Created 14 integration tests covering both modes, metadata shape, custom params, and validation errors.", + "verification_result": "cd engine && .venv/bin/python -m pytest tests/test_api.py -v (14 passed), full suite 76 passed", "duration": "", - "completed_at": null, + "completed_at": "2026-03-26T04:21:26.227Z", "blocker_discovered": false, - "deviations": "", + "deviations": "None.", "known_issues": "", - "key_files": [], - "key_decisions": [], - "full_summary_md": "", + "key_files": [ + "engine/api/routes.py", + "engine/tests/test_api.py", + "engine/main.py" + ], + "key_decisions": [ + "SVG metadata via regex - lightweight, no XML parser needed", + "Vectorize params filtered by known param names per mode" + ], + "full_summary_md": "---\nid: T05\nparent: S01\nmilestone: M001\nkey_files:\n - engine/api/routes.py\n - engine/tests/test_api.py\n - engine/main.py\nkey_decisions:\n - SVG metadata via regex - lightweight, no XML parser needed\n - Vectorize params filtered by known param names per mode\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-03-26T04:21:26.233Z\nblocker_discovered: false\n---\n\n# T05: Wired POST /engine/trace endpoint with preprocess + vectorize pipeline, SVG metadata, and 14 integration tests\n\n**Wired POST /engine/trace endpoint with preprocess + vectorize pipeline, SVG metadata, and 14 integration tests**\n\n## What Happened\n\nCreated engine/api/routes.py with POST /engine/trace endpoint accepting multipart form-data (file, mode, output_format, preset, params). Wires full pipeline: decode → preprocess → vectorize (potrace or vtracer) → extract SVG metadata → return JSON. Updated main.py to include the router. Created 14 integration tests covering both modes, metadata shape, custom params, and validation errors.\n\n## Verification\n\ncd engine && .venv/bin/python -m pytest tests/test_api.py -v (14 passed), full suite 76 passed\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `cd engine && .venv/bin/python -m pytest tests/test_api.py -v` | 0 | ✅ pass | 430ms |\n| 2 | `cd engine && .venv/bin/python -m pytest tests/ -v` | 0 | ✅ pass | 470ms |\n\n\n## Deviations\n\nNone.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `engine/api/routes.py`\n- `engine/tests/test_api.py`\n- `engine/main.py`\n", "description": "1. Create engine/api/routes.py with trace endpoint\n2. POST /engine/trace accepts multipart/form-data:\n - file (image), mode (potrace|vtracer), output_format (svg only for now), preset (ignored for now), params (JSON override)\n3. Wire pipeline: receive file → preprocess → vectorize (mode-dependent) → return SVG + metadata\n4. Response shape: {output: '...', format: 'svg', metadata: {path_count, node_count_total, open_paths, warnings, processing_ms}}\n5. Add basic metadata extraction from SVG output\n6. Integration test: upload PNG → get SVG back", "estimate": "45min", "files": [ @@ -836,6 +843,28 @@ "verdict": "pass", "duration_ms": 200, "created_at": "2026-03-26T04:07:03.124Z" + }, + { + "id": 4, + "task_id": "T05", + "slice_id": "S01", + "milestone_id": "M001", + "command": "cd engine && .venv/bin/python -m pytest tests/test_api.py -v", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 430, + "created_at": "2026-03-26T04:21:26.227Z" + }, + { + "id": 5, + "task_id": "T05", + "slice_id": "S01", + "milestone_id": "M001", + "command": "cd engine && .venv/bin/python -m pytest tests/ -v", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 470, + "created_at": "2026-03-26T04:21:26.227Z" } ] } \ No newline at end of file diff --git a/engine/api/routes.py b/engine/api/routes.py new file mode 100644 index 0000000..5e249e2 --- /dev/null +++ b/engine/api/routes.py @@ -0,0 +1,105 @@ +"""API routes for the Kerf Engine trace endpoint.""" + +import json +import re +import time + +from fastapi import APIRouter, File, Form, HTTPException, UploadFile + +from pipeline.preprocessing import preprocess +from pipeline.vectorize import potrace_trace, vtracer_trace + +router = APIRouter() + +VALID_MODES = {"potrace", "vtracer"} + + +def _extract_svg_metadata(svg: str) -> dict: + """Extract basic metadata from an SVG string.""" + path_matches = re.findall(r']*\bd="([^"]*)"', svg) + path_count = len(path_matches) + + node_count_total = 0 + open_paths = 0 + for d_attr in path_matches: + # Count SVG path commands (M, L, C, Q, A, Z, etc.) + commands = re.findall(r"[MLHVCSQTAZ]", d_attr, re.IGNORECASE) + node_count_total += len(commands) + # A path is "open" if it doesn't end with Z + if not d_attr.rstrip().upper().endswith("Z"): + open_paths += 1 + + return { + "path_count": path_count, + "node_count_total": node_count_total, + "open_paths": open_paths, + } + + +@router.post("/engine/trace") +async def trace( + file: UploadFile = File(...), + mode: str = Form("potrace"), + output_format: str = Form("svg"), + preset: str = Form("default"), + params: str = Form("{}"), +): + """Convert a raster image to SVG via the preprocessing + vectorization pipeline.""" + if mode not in VALID_MODES: + raise HTTPException( + status_code=422, + detail=f"Invalid mode '{mode}'. Must be one of: {', '.join(sorted(VALID_MODES))}", + ) + + if output_format != "svg": + raise HTTPException( + status_code=422, + detail=f"Unsupported output_format '{output_format}'. Only 'svg' is supported.", + ) + + try: + user_params = json.loads(params) + except json.JSONDecodeError as exc: + raise HTTPException(status_code=422, detail=f"Invalid params JSON: {exc}") + + raw_bytes = await file.read() + if not raw_bytes: + raise HTTPException(status_code=422, detail="Uploaded file is empty") + + warnings: list[str] = [] + start = time.perf_counter() + + try: + preprocessed = preprocess(raw_bytes, params=user_params) + except ValueError as exc: + raise HTTPException(status_code=422, detail=f"Preprocessing failed: {exc}") + + try: + if mode == "potrace": + svg_output = potrace_trace(preprocessed, **{ + k: v for k, v in user_params.items() + if k in ("turdsize", "alphamax", "opticurve", "opttolerance") + }) + else: + svg_output = vtracer_trace(preprocessed, **{ + k: v for k, v in user_params.items() + if k in ( + "colormode", "hierarchical", "filter_speckle", "color_precision", + "layer_difference", "corner_threshold", "length_threshold", + "splice_threshold", "mode", "path_precision", "max_iterations", + ) + }) + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Vectorization failed: {exc}") + + processing_ms = round((time.perf_counter() - start) * 1000, 2) + + metadata = _extract_svg_metadata(svg_output) + metadata["warnings"] = warnings + metadata["processing_ms"] = processing_ms + + return { + "output": svg_output, + "format": "svg", + "metadata": metadata, + } diff --git a/engine/main.py b/engine/main.py index 8ba9053..ca71981 100644 --- a/engine/main.py +++ b/engine/main.py @@ -2,12 +2,16 @@ from fastapi import FastAPI +from api.routes import router + app = FastAPI( title="Kerf Engine", description="Raster-to-vector conversion pipeline with Potrace and VTracer modes", version="0.1.0", ) +app.include_router(router) + @app.get("/health") async def health(): diff --git a/engine/tests/test_api.py b/engine/tests/test_api.py new file mode 100644 index 0000000..a7717b2 --- /dev/null +++ b/engine/tests/test_api.py @@ -0,0 +1,178 @@ +"""Integration tests for the /engine/trace endpoint.""" + +import json + +import cv2 +import numpy as np +import pytest +from fastapi.testclient import TestClient + +from main import app + +client = TestClient(app) + + +def _make_test_png(width: int = 100, height: int = 100) -> bytes: + """Create a simple test PNG with a white rectangle on black background.""" + img = np.zeros((height, width, 3), dtype=np.uint8) + cv2.rectangle(img, (20, 20), (80, 80), (255, 255, 255), -1) + ok, buf = cv2.imencode(".png", img) + assert ok + return buf.tobytes() + + +@pytest.fixture +def test_png() -> bytes: + return _make_test_png() + + +class TestTraceEndpointPotrace: + """Tests for /engine/trace with mode=potrace.""" + + def test_basic_trace(self, test_png): + resp = client.post( + "/engine/trace", + files={"file": ("test.png", test_png, "image/png")}, + data={"mode": "potrace"}, + ) + assert resp.status_code == 200 + body = resp.json() + assert body["format"] == "svg" + assert "= 1 + + def test_custom_params(self, test_png): + resp = client.post( + "/engine/trace", + files={"file": ("test.png", test_png, "image/png")}, + data={ + "mode": "potrace", + "params": json.dumps({"turdsize": 5, "alphamax": 0.5}), + }, + ) + assert resp.status_code == 200 + assert "