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:
jlightner 2026-03-26 04:22:39 +00:00
parent ae74228fb0
commit 291a810605
11 changed files with 492 additions and 172 deletions

View file

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

View file

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

View 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. ✅

View file

@ -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.

View file

@ -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.

View file

@ -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.

View 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.

View file

@ -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
View 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,
}

View file

@ -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
View 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