test: Wired POST /engine/trace endpoint with preprocess + vectorize pip…
- "engine/api/routes.py" - "engine/tests/test_api.py" - "engine/main.py" GSD-Task: S01/T05
This commit is contained in:
parent
ae74228fb0
commit
291a810605
11 changed files with 492 additions and 172 deletions
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
52
.gsd/milestones/M001/slices/S01/S01-SUMMARY.md
Normal file
52
.gsd/milestones/M001/slices/S01/S01-SUMMARY.md
Normal file
|
|
@ -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. ✅
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
77
.gsd/milestones/M001/slices/S01/tasks/T05-SUMMARY.md
Normal file
77
.gsd/milestones/M001/slices/S01/tasks/T05-SUMMARY.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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: '<svg>...', 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
105
engine/api/routes.py
Normal file
105
engine/api/routes.py
Normal file
|
|
@ -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'<path\b[^>]*\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,
|
||||
}
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
178
engine/tests/test_api.py
Normal file
178
engine/tests/test_api.py
Normal file
|
|
@ -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 "<svg" in body["output"]
|
||||
assert "metadata" in body
|
||||
|
||||
def test_metadata_shape(self, test_png):
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("test.png", test_png, "image/png")},
|
||||
data={"mode": "potrace"},
|
||||
)
|
||||
body = resp.json()
|
||||
meta = body["metadata"]
|
||||
assert "path_count" in meta
|
||||
assert "node_count_total" in meta
|
||||
assert "open_paths" in meta
|
||||
assert "warnings" in meta
|
||||
assert "processing_ms" in meta
|
||||
assert isinstance(meta["path_count"], int)
|
||||
assert isinstance(meta["processing_ms"], float)
|
||||
|
||||
def test_svg_has_paths(self, test_png):
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("test.png", test_png, "image/png")},
|
||||
data={"mode": "potrace"},
|
||||
)
|
||||
body = resp.json()
|
||||
assert body["metadata"]["path_count"] >= 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 "<svg" in resp.json()["output"]
|
||||
|
||||
|
||||
class TestTraceEndpointVtracer:
|
||||
"""Tests for /engine/trace with mode=vtracer."""
|
||||
|
||||
def test_basic_trace(self, test_png):
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("test.png", test_png, "image/png")},
|
||||
data={"mode": "vtracer"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["format"] == "svg"
|
||||
assert "<svg" in body["output"].lower() or "svg" in body["output"]
|
||||
|
||||
def test_metadata_present(self, test_png):
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("test.png", test_png, "image/png")},
|
||||
data={"mode": "vtracer"},
|
||||
)
|
||||
body = resp.json()
|
||||
meta = body["metadata"]
|
||||
assert isinstance(meta["path_count"], int)
|
||||
assert isinstance(meta["processing_ms"], float)
|
||||
|
||||
def test_custom_params(self, test_png):
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("test.png", test_png, "image/png")},
|
||||
data={
|
||||
"mode": "vtracer",
|
||||
"params": json.dumps({"filter_speckle": 10, "corner_threshold": 90}),
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
class TestTraceEndpointValidation:
|
||||
"""Tests for input validation on /engine/trace."""
|
||||
|
||||
def test_invalid_mode(self, test_png):
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("test.png", test_png, "image/png")},
|
||||
data={"mode": "invalid"},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_unsupported_output_format(self, test_png):
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("test.png", test_png, "image/png")},
|
||||
data={"mode": "potrace", "output_format": "pdf"},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_invalid_params_json(self, test_png):
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("test.png", test_png, "image/png")},
|
||||
data={"mode": "potrace", "params": "not-json"},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_empty_file(self):
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("empty.png", b"", "image/png")},
|
||||
data={"mode": "potrace"},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_corrupt_image(self):
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("bad.png", b"not an image", "image/png")},
|
||||
data={"mode": "potrace"},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_defaults_to_potrace(self, test_png):
|
||||
"""Mode defaults to potrace when not specified."""
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("test.png", test_png, "image/png")},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert "<svg" in resp.json()["output"]
|
||||
|
||||
def test_preset_ignored(self, test_png):
|
||||
"""Preset param is accepted but ignored for now."""
|
||||
resp = client.post(
|
||||
"/engine/trace",
|
||||
files={"file": ("test.png", test_png, "image/png")},
|
||||
data={"mode": "potrace", "preset": "logo"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
Loading…
Add table
Reference in a new issue