Initial Commit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# Migrate `suaveplan-audio-fingerprint` (Python tier) into genesis
|
||||
|
||||
**Package**: `@suaveplan/audio-fingerprint`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package (migration from upstream Python tier)
|
||||
|
||||
- **Design**: ./design.md
|
||||
- **Spec delta**: ./specs/audio-fingerprint/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/react-audio-fingerprint` already ships a browser-tier TypeScript implementation of Shazam-style audio fingerprinting (peak detection on a magnitude spectrogram). For corpus-scale identification, recognised broadcast monitoring, and noise-robust matching of compressed or modified audio, the browser implementation is intentionally lightweight. Upstream `/mnt/s/web/suaveplan/packages/python/audio-fingerprint/` carries the high-accuracy backend: a librosa + scipy chromagram pipeline wrapped in a FastAPI service, plus a typed HTTP client (`PythonAudioService`) that browser code calls.
|
||||
|
||||
The React sibling already reserves a `python-backend` subpath for this service. Without the Python tier the high-accuracy path is unreachable. This change migrates the Python service plus the TypeScript HTTP client to `packages/media/audio/audio-fingerprint/` so the React package can stop deferring the path and downstream apps can run a real fingerprint backend behind their browser surface.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `@suaveplan/audio-fingerprint` package at `packages/media/audio/audio-fingerprint/` (Python tier + TS HTTP client bridge).
|
||||
- ADDED: `src/fingerprint.py` — `AudioFingerprinter` class (chromagram + 2D peak detection + constellation hashing + Jaccard matching + median time-offset estimation).
|
||||
- ADDED: `src/api/app.py` — FastAPI application exposing `GET /`, `POST /fingerprint`, `POST /match` with CORS for `localhost:3000` + `localhost:5173`.
|
||||
- ADDED: `src/client/python-audio-service.ts` — typed `PythonAudioService` HTTP client (`healthCheck` / `fingerprint` / `match`) with `AbortController`-based timeouts.
|
||||
- ADDED: `src/types/audio-fingerprint.types.ts` — `HealthCheckResponse`, `AudioFingerprint`, `FingerprintRequest` / `Response`, `MatchRequest` / `Response`, `ErrorResponse`, `AudioFingerprintConfig`.
|
||||
- ADDED: `pyproject.toml` + `environment.yml` for the Python runtime (librosa, scipy, numpy, fastapi, uvicorn, soundfile, python-multipart; dev: pytest, pytest-cov, pytest-asyncio, httpx, ruff, mypy).
|
||||
- ADDED: `pytest --cov --cov-fail-under=100` Python test suite at `tests/test_fingerprint.py` + `tests/test_api.py`.
|
||||
- ADDED: TS bridge vitest suite at `tests/python-audio-service.test.ts` using a real `fetch` mock against an in-memory ASGI surface emulator and `AbortController` for timeout semantics.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Production deployment manifests (Dockerfile, Helm chart) — handled per-app deploy, not in the library.
|
||||
- Wiring the React sibling's `python-backend` subpath to consume this client — separate follow-up so the React change has its own spec delta.
|
||||
- ChromaPrint / AcoustID interoperability — different algorithm; would be a peer package, not a feature here.
|
||||
- Native (non-FastAPI) RPC transport — the upstream uses HTTP; we preserve parity. A gRPC adapter is a future change.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `/mnt/s/web/suaveplan/packages/python/audio-fingerprint/`. There is no `enterprise/audio-fingerprint.md` audit on main (the upstream is a Python-tier service rather than a TS library), so the source mapping references the upstream source tree directly.
|
||||
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Chromagram fingerprinting | `src/fingerprint.py` L67-L90 | verbatim algorithm, repackaged |
|
||||
| 2D peak detection | `src/fingerprint.py` L92-L132 | verbatim (`scipy.ndimage.maximum_filter` (3,5)) |
|
||||
| Constellation hashing | `src/fingerprint.py` L134-L171 | verbatim fan-pairing |
|
||||
| Full-file pipeline | `src/fingerprint.py` L173-L213 | verbatim |
|
||||
| Jaccard matching + time offset | `src/fingerprint.py` L215-L267 | verbatim |
|
||||
| FastAPI `/` + `/fingerprint` + `/match` | `src/api/app.py` L31-L171 | verbatim endpoints, CORS preserved |
|
||||
| TS HTTP client | `src/PythonAudioService.ts` L45-L175 | renamed file to kebab-case for genesis convention; surface preserved |
|
||||
| Response / request types | `types/index.ts` L7-L111 | preserved field-for-field |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| `client.timeout` propagates `AbortController` reasons | Upstream throws on timeout but the message tells callers nothing actionable; genesis surfaces `"Request timeout after Nms: <endpoint>"` already-present in the upstream client (kept verbatim, listed here because it appears as its own spec scenario). |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- Python: `pytest --cov=src --cov-fail-under=100` from the package directory; covers `fingerprint.py` and `api/app.py` end-to-end including FastAPI `TestClient` and `httpx.ASGITransport` async paths.
|
||||
- TS: vitest suite at `tests/python-audio-service.test.ts` using runner-neutral imports from `@suaveplan/testing/runner`, real `fetch` stubbed via a script-injected handler (no `vi.mock`), pinning every public method, the timeout path, the error-response parsing path, and the unknown-error fallback path.
|
||||
- Both gates exit with 100% line + branch coverage on the touched modules.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/audio-fingerprint` to enumerate. The package is new; the only logical dependent is `@suaveplan/react-audio-fingerprint`, whose `python-backend` subpath is intentionally not wired in this change.
|
||||
|
||||
- `@suaveplan/react-audio-fingerprint` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: the `python-backend` subpath wiring is deferred to its own change; this migration is additive and does not modify the React surface)
|
||||
@@ -0,0 +1,268 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: audio-fingerprint (enterprise-audio-fingerprint-python)
|
||||
|
||||
**Package**: `@suaveplan/audio-fingerprint`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Chromagram-based fingerprinter
|
||||
|
||||
The package SHALL expose a Python class `AudioFingerprinter` whose constructor accepts `sample_rate`, `n_fft`, `hop_length`, and `n_chroma` and persists each as a public attribute. The instance MUST extract chromagrams from monaural audio using librosa's `chroma_stft` over a `librosa.stft` magnitude spectrogram, returning an `(n_chroma, n_frames)` float matrix whose minimum value is non-negative.
|
||||
|
||||
#### Scenario: default construction exposes documented defaults
|
||||
|
||||
- **WHEN** `AudioFingerprinter()` is instantiated with no arguments
|
||||
- **THEN** `sample_rate == 22050`, `n_fft == 2048`, `hop_length == 512`, `n_chroma == 12`
|
||||
|
||||
#### Scenario: chromagram has correct shape and non-negative values
|
||||
|
||||
- **WHEN** `extract_chromagram` is called on a 2-second 440 Hz sine wave
|
||||
- **THEN** the returned array has shape `(n_chroma, frames>0)` and `min() >= 0`
|
||||
|
||||
**Verified by:** `tests/test_fingerprint.py::test_fingerprinter_initialization` AND `tests/test_fingerprint.py::test_fingerprinter_custom_parameters` AND `tests/test_fingerprint.py::test_extract_chromagram`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Audio loading from disk
|
||||
|
||||
`AudioFingerprinter.load_audio` SHALL load an audio file from disk via librosa, downmix to mono, resample to the configured `sample_rate`, and return a `float32` ndarray. It MUST raise `FileNotFoundError` with the message `"Audio file not found: <path>"` when the path does not exist and MUST raise `RuntimeError` wrapping the underlying cause when librosa itself fails.
|
||||
|
||||
#### Scenario: loads a real WAV
|
||||
|
||||
- **WHEN** `load_audio` is called on a 2-second WAV
|
||||
- **THEN** the result is a `float32` ndarray with `len > 0` whose values lie in `[-1.0, 1.0]`
|
||||
|
||||
#### Scenario: missing file raises FileNotFoundError
|
||||
|
||||
- **WHEN** `load_audio("/nonexistent/file.wav")` is called
|
||||
- **THEN** `FileNotFoundError` is raised matching `Audio file not found`
|
||||
|
||||
**Verified by:** `tests/test_fingerprint.py::test_load_audio` AND `tests/test_fingerprint.py::test_load_audio_nonexistent_file`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 2D spectral peak detection
|
||||
|
||||
`AudioFingerprinter.find_peaks` SHALL detect local maxima on the normalised chromagram using `scipy.ndimage.maximum_filter` with a `(3, 5)` neighborhood. A coordinate `(t, f)` SHALL be returned only when its normalised amplitude equals the local maximum AND is greater-than-or-equal-to the caller-supplied `threshold`. When the chromagram's maximum is zero the function MUST return an empty list. Returned peaks MUST be `list[tuple[int, int, float]]` of `(time_frame, chroma_bin, amplitude)`.
|
||||
|
||||
#### Scenario: higher threshold filters out smaller peaks
|
||||
|
||||
- **WHEN** `find_peaks` is called on the same chromagram with thresholds 0.3 and 0.7
|
||||
- **THEN** `len(peaks_low) >= len(peaks_high)`
|
||||
|
||||
#### Scenario: zero-magnitude chromagram returns empty list
|
||||
|
||||
- **WHEN** `find_peaks` is called on a chromagram whose max is 0
|
||||
- **THEN** an empty list is returned
|
||||
|
||||
**Verified by:** `tests/test_fingerprint.py::test_find_peaks` AND `tests/test_fingerprint.py::test_find_peaks_threshold` AND `tests/test_fingerprint.py::test_find_peaks_zero_magnitude`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Constellation-map fingerprint hashing
|
||||
|
||||
`AudioFingerprinter.generate_fingerprint` SHALL pair each peak with the next `fan_value` peaks in time-sorted order and emit `(hash, time_offset, time_delta)` tuples where `hash` is the deterministic hash of `(frequency1, frequency2, time_delta)`. The returned `set` MUST contain only non-negative `time_offset` and `time_delta` values, and MUST grow monotonically as `fan_value` increases.
|
||||
|
||||
#### Scenario: higher fan value produces at least as many fingerprints
|
||||
|
||||
- **WHEN** `generate_fingerprint` is called with `fan_value=5` and `fan_value=20` on the same peak list
|
||||
- **THEN** `len(fps_large) >= len(fps_small)`
|
||||
|
||||
**Verified by:** `tests/test_fingerprint.py::test_generate_fingerprint` AND `tests/test_fingerprint.py::test_generate_fingerprint_fan_value`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Full-pipeline file fingerprinting
|
||||
|
||||
`AudioFingerprinter.fingerprint_file` SHALL run the full pipeline (`load_audio` → `extract_chromagram` → `find_peaks` → `generate_fingerprint`) and return a dict with `fingerprints` (set), `duration` (seconds, float), `peaks_count` (int), and `fingerprints_count` (int) where `fingerprints_count == len(fingerprints)`.
|
||||
|
||||
#### Scenario: pipeline yields consistent counts
|
||||
|
||||
- **WHEN** `fingerprint_file` is called on a 2-second WAV
|
||||
- **THEN** the result contains all four keys and `fingerprints_count == len(fingerprints) > 0`
|
||||
|
||||
**Verified by:** `tests/test_fingerprint.py::test_fingerprint_file`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Jaccard-based fingerprint matching with time offset
|
||||
|
||||
`AudioFingerprinter.match_fingerprints` SHALL compare two fingerprint sets by hash, return Jaccard similarity (`|intersection| / |union|`) as `confidence` (zero when both sides are empty), report `matched = (match_count >= min_matches)`, and estimate `time_offset` as the median per-hash `(reference_time - query_time)` only when `match_count >= min_matches`. When `match_count < min_matches`, `time_offset` MUST default to `0`.
|
||||
|
||||
#### Scenario: identical inputs yield high confidence and near-zero offset
|
||||
|
||||
- **WHEN** the same audio is fingerprinted twice and `match_fingerprints` is called
|
||||
- **THEN** `matched is True`, `confidence > 0.9`, `abs(time_offset) <= 5`
|
||||
|
||||
#### Scenario: impossible threshold forces matched = False
|
||||
|
||||
- **WHEN** `min_matches=999999` on identical fingerprint sets
|
||||
- **THEN** `matched is False`
|
||||
|
||||
#### Scenario: empty-on-empty input yields zero confidence and zero offset
|
||||
|
||||
- **WHEN** `match_fingerprints(set(), set())` is called
|
||||
- **THEN** `match_count == 0`, `confidence == 0`, `time_offset == 0`, `matched is False`
|
||||
|
||||
**Verified by:** `tests/test_fingerprint.py::test_match_fingerprints_identical` AND `tests/test_fingerprint.py::test_match_fingerprints_min_matches` AND `tests/test_fingerprint.py::test_match_fingerprints_empty`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: FastAPI health endpoint
|
||||
|
||||
The FastAPI application SHALL expose `GET /` returning `{"status": "ok", "service": "audio-fingerprinting"}` with HTTP 200.
|
||||
|
||||
#### Scenario: health check returns the documented payload
|
||||
|
||||
- **WHEN** the FastAPI TestClient calls `GET /`
|
||||
- **THEN** status is 200 and the JSON body equals `{"status": "ok", "service": "audio-fingerprinting"}`
|
||||
|
||||
**Verified by:** `tests/test_api.py::test_root_endpoint`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: FastAPI fingerprint endpoint
|
||||
|
||||
The FastAPI application SHALL expose `POST /fingerprint` accepting a multipart `file` upload with optional `peak_threshold` (float) and `fan_value` (int) form fields. The response body MUST include `filename`, `duration`, `peaks_count`, `fingerprints_count`, and `fingerprints` (array of `{hash, time, delta}`). The handler MUST persist the upload to a `NamedTemporaryFile`, process it, and unlink the file in a `finally` block. The handler MUST raise HTTP 500 with detail `"Failed to process audio file: <message>"` when the underlying pipeline raises. Posting without a file MUST return HTTP 422.
|
||||
|
||||
#### Scenario: happy-path returns fingerprints
|
||||
|
||||
- **WHEN** a WAV file is posted to `/fingerprint`
|
||||
- **THEN** status is 200, `fingerprints_count > 0`, and each `fingerprints[i]` contains `hash`, `time`, `delta`
|
||||
|
||||
#### Scenario: custom parameters propagate
|
||||
|
||||
- **WHEN** form data `peak_threshold=0.6 fan_value=20` is posted with a WAV
|
||||
- **THEN** the handler returns 200 with `fingerprints_count > 0`
|
||||
|
||||
#### Scenario: missing file returns 422
|
||||
|
||||
- **WHEN** `POST /fingerprint` is called with no body
|
||||
- **THEN** status is 422
|
||||
|
||||
#### Scenario: pipeline failure surfaces as HTTP 500
|
||||
|
||||
- **WHEN** a non-audio file is posted to `/fingerprint`
|
||||
- **THEN** status is 500 with `detail` starting with `Failed to process audio file:`
|
||||
|
||||
**Verified by:** `tests/test_api.py::test_fingerprint_endpoint` AND `tests/test_api.py::test_fingerprint_endpoint_with_parameters` AND `tests/test_api.py::test_fingerprint_endpoint_no_file` AND `tests/test_api.py::test_fingerprint_endpoint_invalid_audio` AND `tests/test_api.py::test_fingerprint_endpoint_async`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: FastAPI match endpoint
|
||||
|
||||
The FastAPI application SHALL expose `POST /match` accepting `query_file` and `reference_file` multipart uploads with optional `peak_threshold`, `fan_value`, `min_matches` form fields. The response body MUST include `query_file`, `reference_file`, `query_duration`, `reference_duration`, `matched`, `match_count`, `confidence`, and `time_offset`. The handler MUST persist both uploads to `NamedTemporaryFile`s and unlink them in `finally`. The handler MUST raise HTTP 500 with detail `"Failed to match audio files: <message>"` on pipeline failure. Posting without both files MUST return HTTP 422.
|
||||
|
||||
#### Scenario: matching identical files returns matched=True with high confidence
|
||||
|
||||
- **WHEN** the same WAV is posted as `query_file` and `reference_file`
|
||||
- **THEN** status is 200, `matched is True`, `confidence > 0.9`, `abs(time_offset) <= 5`
|
||||
|
||||
#### Scenario: custom parameters propagate
|
||||
|
||||
- **WHEN** `peak_threshold=0.6 fan_value=20 min_matches=5` is posted with identical query and reference WAVs
|
||||
- **THEN** status is 200 and `matched is True`
|
||||
|
||||
#### Scenario: missing files return 422
|
||||
|
||||
- **WHEN** `POST /match` is called with no body
|
||||
- **THEN** status is 422
|
||||
|
||||
#### Scenario: pipeline failure surfaces as HTTP 500
|
||||
|
||||
- **WHEN** a non-audio file is posted as one side of `/match`
|
||||
- **THEN** status is 500 with `detail` starting with `Failed to match audio files:`
|
||||
|
||||
**Verified by:** `tests/test_api.py::test_match_endpoint` AND `tests/test_api.py::test_match_endpoint_with_parameters` AND `tests/test_api.py::test_match_endpoint_different_files` AND `tests/test_api.py::test_match_endpoint_missing_files` AND `tests/test_api.py::test_match_endpoint_invalid_audio` AND `tests/test_api.py::test_match_endpoint_async`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CORS configured for local dev
|
||||
|
||||
The FastAPI application SHALL respond to preflight `OPTIONS` requests from `http://localhost:3000` and `http://localhost:5173` with `Access-Control-Allow-Origin` echoed back.
|
||||
|
||||
#### Scenario: preflight from localhost:3000
|
||||
|
||||
- **WHEN** the TestClient sends `OPTIONS /fingerprint` with `Origin: http://localhost:3000`
|
||||
- **THEN** the response carries an `access-control-allow-origin` header
|
||||
|
||||
**Verified by:** `tests/test_api.py::test_cors_headers`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: TypeScript HTTP client surface
|
||||
|
||||
The package SHALL export a `PythonAudioService` class (TS) whose constructor accepts an optional `AudioFingerprintConfig` (`baseUrl`, `timeout`) defaulting to `"http://localhost:8000"` and `30000` ms respectively. The class MUST expose `healthCheck()`, `fingerprint(req)`, and `match(req)` methods returning the documented response shapes. Requests MUST be cancelled via `AbortController` after `timeout` ms, raising `Error("Request timeout after <Nms>: <endpoint>")`. Error responses MUST be parsed via `await response.json()` and the `detail` field used as the thrown message; if parsing fails the client MUST fall back to `HTTP <status>: <statusText>`. Non-`Error` thrown values MUST surface as `Error("Unknown error occurred: <stringified>")`.
|
||||
|
||||
#### Scenario: healthCheck returns the parsed body
|
||||
|
||||
- **WHEN** `client.healthCheck()` is invoked against a stub that returns `{ status: "ok", service: "audio-fingerprinting" }`
|
||||
- **THEN** the resolved value equals that object
|
||||
|
||||
#### Scenario: fingerprint posts the file and optional params via FormData
|
||||
|
||||
- **WHEN** `client.fingerprint({ file, peak_threshold: 0.6, fan_value: 20 })` is invoked
|
||||
- **THEN** the request is `POST /fingerprint` with a multipart body whose `file` entry equals the input and whose `peak_threshold` / `fan_value` entries equal `"0.6"` / `"20"`
|
||||
|
||||
#### Scenario: match posts both files and all optional params
|
||||
|
||||
- **WHEN** `client.match({ query_file, reference_file, peak_threshold, fan_value, min_matches })` is invoked
|
||||
- **THEN** the request is `POST /match` and every supplied option appears as a string FormData entry
|
||||
|
||||
#### Scenario: timeout aborts the request
|
||||
|
||||
- **WHEN** the server fails to respond within `timeout` ms
|
||||
- **THEN** the client rejects with `Error("Request timeout after Nms: <endpoint>")`
|
||||
|
||||
#### Scenario: error body without parseable JSON falls back to status text
|
||||
|
||||
- **WHEN** the server returns HTTP 500 with a body that is not valid JSON
|
||||
- **THEN** the client rejects with `Error("HTTP 500: <statusText>")`
|
||||
|
||||
#### Scenario: error body with `detail` field surfaces the detail
|
||||
|
||||
- **WHEN** the server returns HTTP 500 with `{ "detail": "Failed to process audio file: bad" }`
|
||||
- **THEN** the client rejects with `Error("Failed to process audio file: bad")`
|
||||
|
||||
#### Scenario: non-Error thrown by fetch surfaces as Unknown error
|
||||
|
||||
- **WHEN** the underlying fetch throws a string
|
||||
- **THEN** the client rejects with `Error("Unknown error occurred: <string>")`
|
||||
|
||||
**Verified by:** `tests/python-audio-service.test.ts::"healthCheck returns the parsed body"` AND `tests/python-audio-service.test.ts::"fingerprint posts the file and optional params via FormData"` AND `tests/python-audio-service.test.ts::"match posts both files and all optional params"` AND `tests/python-audio-service.test.ts::"timeout aborts the request"` AND `tests/python-audio-service.test.ts::"non-json error body falls back to status text"` AND `tests/python-audio-service.test.ts::"json error body surfaces the detail field"` AND `tests/python-audio-service.test.ts::"non-Error thrown by fetch surfaces as Unknown error"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: TypeScript types match Python wire format
|
||||
|
||||
The package SHALL export TypeScript types `HealthCheckResponse`, `AudioFingerprint`, `FingerprintRequest`, `FingerprintResponse`, `MatchRequest`, `MatchResponse`, `ErrorResponse`, and `AudioFingerprintConfig` whose shapes match the JSON the Python service emits and consumes. The package SHALL provide subpath exports `./client` and `./types` so consumers can pick the wire types without pulling in the client implementation.
|
||||
|
||||
#### Scenario: subpath imports resolve
|
||||
|
||||
- **WHEN** a consumer imports `PythonAudioService` from `@suaveplan/audio-fingerprint/client` and types from `@suaveplan/audio-fingerprint/types`
|
||||
- **THEN** both resolve and pass the package's own typecheck pass
|
||||
|
||||
**Verified by:** `tests/exports.test.ts::"barrel re-exports the client and types"` AND `tests/exports.test.ts::"client subpath re-exports PythonAudioService"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Python coverage and TS coverage at 100%
|
||||
|
||||
The package SHALL ship with a `pyproject.toml` configuring `pytest --cov=src --cov-branch --cov-fail-under=100` and a `vitest.config.ts` enforcing 100/100/100/100 istanbul thresholds on the TS surface. CI invocations of `pytest` and `bunx turbo test:coverage --filter=@suaveplan/audio-fingerprint` MUST hard-fail below 100%.
|
||||
|
||||
#### Scenario: pytest enforces 100% line+branch coverage
|
||||
|
||||
- **WHEN** the test command `micromamba run -n suaveplan-audio-fingerprint pytest --cov=src --cov-branch --cov-fail-under=100` is run from the package directory
|
||||
- **THEN** it exits 0 only when total coverage is 100%
|
||||
|
||||
#### Scenario: vitest enforces 100% line+branch coverage on the TS bridge
|
||||
|
||||
- **WHEN** `bunx turbo test:coverage --filter=@suaveplan/audio-fingerprint` is run
|
||||
- **THEN** it exits 0 only when statements, branches, functions, and lines all reach 100%
|
||||
|
||||
**Verified by:** `pyproject.toml::[tool.coverage.report]` AND `vitest.config.ts::thresholds`
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,64 @@
|
||||
# Tasks — Migrate `suaveplan-audio-fingerprint` (Python tier) into genesis
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 Confirm clean working tree (`git status` reports nothing) and worktrees accounted for
|
||||
- [x] 0.2 No stale `.claude/worktrees/` to prune (single agent worktree, this one)
|
||||
- [x] 0.3 MANIFEST.md regenerated after package landing (step 7.3)
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read upstream source `/mnt/s/web/suaveplan/packages/python/audio-fingerprint/` end-to-end
|
||||
- [x] 1.2 No existing `openspec/specs/audio-fingerprint/spec.md` to read
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` sections 0–14
|
||||
- [x] 1.4 Draft `proposal.md`
|
||||
- [x] 1.5 Fill in `## Source mapping` table
|
||||
- [x] 1.6 Draft `specs/audio-fingerprint/spec.md`
|
||||
- [x] 1.7 Every requirement carries `**Verified by:**`
|
||||
|
||||
## 2. Implementation (Python)
|
||||
|
||||
- [x] 2.1 `src/fingerprint.py` — `AudioFingerprinter` (chromagram, peak detection, constellation hashing, file pipeline, Jaccard matching)
|
||||
- [x] 2.2 `src/__init__.py` — package marker
|
||||
- [x] 2.3 `src/api/__init__.py` — re-exports `app`
|
||||
- [x] 2.4 `src/api/app.py` — FastAPI app with `/`, `/fingerprint`, `/match`, CORS for localhost dev origins
|
||||
|
||||
## 3. Implementation (TypeScript bridge)
|
||||
|
||||
- [x] 3.1 `src/types/audio-fingerprint.types.ts` — all request/response/config interfaces
|
||||
- [x] 3.2 `src/client/python-audio-service.ts` — `PythonAudioService` HTTP client
|
||||
- [x] 3.3 `src/client/index.ts` — explicit named re-export of the client class
|
||||
- [x] 3.4 `src/types/index.ts` — explicit named type re-exports
|
||||
- [x] 3.5 `src/index.ts` — package root barrel with explicit named re-exports
|
||||
|
||||
## 4. Tests
|
||||
|
||||
- [x] 4.1 `tests/test_fingerprint.py` — covers every scenario in the spec delta
|
||||
- [x] 4.2 `tests/test_api.py` — covers every FastAPI scenario including failure paths
|
||||
- [x] 4.3 `tests/python-audio-service.test.ts` — covers every TS client scenario
|
||||
- [x] 4.4 `tests/exports.test.ts` — pins barrel + subpath re-export contract
|
||||
- [x] 4.5 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for genuinely async)
|
||||
|
||||
## 5. Documentation
|
||||
|
||||
- [x] 5.1 Sibling `.md` for every `.ts` source file (`fingerprint-types.md`, `python-audio-service.md`)
|
||||
- [x] 5.2 `README.md` linking every co-located `.md` under `## Modules`
|
||||
- [x] 5.3 Sibling `.md` for the two Python implementation files (`fingerprint.md`, `app.md`)
|
||||
|
||||
## 6. Pre-archive verification
|
||||
|
||||
- [x] 6.1 `pyproject.toml` configures `pytest --cov=src --cov-branch --cov-fail-under=100`
|
||||
- [x] 6.2 `bunx turbo lint typecheck test --filter=@suaveplan/audio-fingerprint` — green
|
||||
- [x] 6.3 `bunx turbo test:coverage --filter=@suaveplan/audio-fingerprint` — 100/100/100/100
|
||||
- [x] 6.4 `micromamba run -n suaveplan-audio-fingerprint pytest --cov=src --cov-branch --cov-fail-under=100` — exits 0
|
||||
- [x] 6.5 No `// biome-ignore`, no `console.log`, no `any`, no `.skip`, no TODO/FIXME in TS source
|
||||
- [x] 6.6 No direct `from "vitest"` / `from "bun:test"` imports in tests
|
||||
- [x] 6.7 Python turbo task not currently wired — documented as follow-up in the migration report; `pytest` is invoked standalone via micromamba
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold `specs/audio-fingerprint/spec.md` into `openspec/specs/audio-fingerprint/spec.md`
|
||||
- [x] 7.2 Move `openspec/changes/enterprise-audio-fingerprint-python/` → `openspec/archive/2026-05-23-enterprise-audio-fingerprint-python/`
|
||||
- [x] 7.3 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user