Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ Operational and one-shot scripts live in two places. They are not on the request
2. `packages/loudness-spectro-wasm/` — browser-first WebAssembly DSP (ITU-R BS.1770-5 / EBU R128 loudness, A-weighted spectrum, and spectral-reassignment spectrogram). Rust lifted from [openmeters](https://github.com/httpsworldview/openmeters) and compiled via `wasm-bindgen`, GPL-3.0-or-later. **Phase 1: standalone, not yet imported by `apps/ui` or `apps/backend`.** Has its own Cargo workspace, `npm`/`cargo` build, and EBU/ebur128/pyloudnorm validation layers — see [`packages/loudness-spectro-wasm/README.md`](packages/loudness-spectro-wasm/README.md). When integration lands, the canonical Phase 1 LUFS contract still comes from the Essentia path until this is wired in and proven at parity. (The reassigned spectrogram on the product path today comes from librosa via the `reassigned` spectral-enhancement endpoint, not this package.)
3. `audits/` — dated, automated audit reports (e.g. `nightly-2026-05-19.md`, `phase2-recommendation-surface-2026-05-24.md`, `full-review-2026-05-30.md`). Past-tense paper trail; not imported by either app. (Older one-shot advisory deliverables like the phase 1 audit have been archived under `docs/history/phase1-audit/`.)
4. `incorporations/` — planning docs for incorporating upstream projects (e.g. `forking-plans-2026-05-14.md`, `beat-this-measurement-gate-2026-05-20.md`, `msst-separation-licence-gate-2026-06-05.md`). Planning notes only; not code.
5. `docs/` — long-form rationale and architectural records. Key contents: `ARCHITECTURE_STRATEGY.md` (three-layer design rationale), `SETUP.md` (first-run setup), `LAYER2_EVALUATION.md` and `POLYPHONIC_TRANSCRIPTION_SPIKE.md` (Layer 2 research), `SAMPLE_GENERATION.md` (Phase 3 design), `PHASE2_PROVIDER.md` (Phase 2 provider selection — Gemini vs MOSS sidecar, licence status, provider contract), `adr/` (Architecture Decision Records — `0001-phase1-json-schema-v1.md` declares the v1 schema stability contract; `0002-phase1-loudness-units-v2.md` bumped `truePeak` to dBTP and `bpmConfidence` to normalized 0–1, schema now at `phase1.v2`; `0003-recommendations-contract-v1.md` freezes the Phase 2 `recommendations` contract (`recommendations.v1`) — a normalized, schema-validated, citation-gated projection of the device cards), and `history/` (completed plans, one-shot audits, and deliverables — past-tense). Read the relevant doc *before* structural changes; do not treat them as living API docs.
5. `docs/` — long-form rationale and architectural records. Key contents: `ARCHITECTURE_STRATEGY.md` (three-layer design rationale), `SETUP.md` (first-run setup), `LAYER2_EVALUATION.md` and `POLYPHONIC_TRANSCRIPTION_SPIKE.md` (Layer 2 research), `SAMPLE_GENERATION.md` (Phase 3 design), `PHASE2_PROVIDER.md` (Phase 2 provider selection — Gemini vs MOSS sidecar, licence status, provider contract), `ASA_ABLETON_BOUNDARY.md` (the file-coupled contract between ASA and the sibling `asa-ableton` `.als` generator — the `phase2-export.v1` handoff envelope), `adr/` (Architecture Decision Records — `0001-phase1-json-schema-v1.md` declares the v1 schema stability contract; `0002-phase1-loudness-units-v2.md` bumped `truePeak` to dBTP and `bpmConfidence` to normalized 0–1, schema now at `phase1.v2`; `0003-recommendations-contract-v1.md` freezes the Phase 2 `recommendations` contract (`recommendations.v1`) — a normalized, schema-validated, citation-gated projection of the device cards), and `history/` (completed plans, one-shot audits, and deliverables — past-tense). Read the relevant doc *before* structural changes; do not treat them as living API docs.
6. `tests/ground_truth/` — labeled-corpus fixtures consumed by `scripts/calibrate_confidence.py` (see `tests/ground_truth/README.md`). Not a test suite — each app owns its own (`apps/backend/tests/`, `apps/ui/tests/`).

### Three-Layer Model
Expand Down Expand Up @@ -298,6 +298,8 @@ Artifact access goes through `artifact_storage.py` rather than direct disk paths

24. **`phase2_provider.py` + `moss_sidecar/`**: Selectable Phase 2 interpretation provider (default-off experiment). `ASA_PHASE2_PROVIDER=moss` routes the producer_summary interpretation to a self-hosted OpenMOSS MOSS-Audio FastAPI sidecar instead of Gemini; `ASA_PHASE2_PROVIDER=claude` routes it to `ClaudeCliProvider` — a text-only interpreter that runs the local Claude Code CLI headless (sandboxed: `--safe-mode`, `--tools ""`, `--no-session-persistence`, response schema enforced via `--json-schema`), grounds purely on the prompt's embedded Phase 1 JSON, and needs no `GEMINI_API_KEY`. All paths flow through the identical parse/citation/catalogue validators. STEP ONE licence gate: MOSS-Audio *weights* are Apache-2.0 but the *modeling code* has no effective licence, so the sidecar ships no OpenMOSS code and the real-model path is a 501 stub — research-only, not promotable. `moss_sidecar/app.py` is the FastAPI sidecar; `moss_sidecar/mock_interpreter.py` generates deterministic schema-valid Phase-1-grounded output for the default `ASA_MOSS_SIDECAR_MODE=mock` path. See `docs/PHASE2_PROVIDER.md` for the full design rationale and licence analysis.

25. **`phase2_export.py`**: Versioned Phase 2 handoff envelope (`phase2-export.v1`) for downstream consumers — the sibling `asa-ableton` repo (turns ASA recommendations into an openable Live 12 `.als` starter set) and `scripts/evaluate_recommendations.py --phase2`. Backs `GET /api/analysis-runs/{run_id}/export/phase2`: the stored `producer_summary` interpretation result verbatim (including the frozen `recommendations.v1` projection), the authoritative Phase 1 payload its citations resolve against, the full `validationWarnings` trail, and provenance — one curl, one self-contained file, no snapshot surgery. Same thin lookup-and-serve pattern as `csv_export.py`; key set frozen by `tests/test_phase2_export.py`. Cross-repo contract documented in `docs/ASA_ABLETON_BOUNDARY.md`.

The subprocess isolation means `analyze.py` works as a standalone CLI. Check `apps/backend/JSON_SCHEMA.md` before adding new analyzer output fields. Check `apps/backend/ARCHITECTURE.md` for the full HTTP flow and contract details.

**Phase 2 (`POST /api/phase2`, legacy compat):** Uploads audio to Gemini inline if ≤100 MiB, or via the Gemini Files API if larger. Phase 1 JSON is appended to the system prompt from `prompts/phase2_system.txt`. Also relevant: `prompts/stem_summary_system.txt` and `prompts/live12_device_catalog.json` (the *prompt-injected* device catalogue — distinct from the runtime-validation `data/live12_catalogue.json` consumed by `phase2_catalogue_gates.py`). Backend defense-in-depth: `server_phase2.py`'s `_validate_phase2_citation_paths` mirrors the frontend citation-existence check and emits `validationWarnings` when a recommendation cites a Phase 1 path that doesn't exist — it flags invented citations rather than failing, since Phase 1 stays authoritative (invariant #1). `phase2_catalogue_gates.apply_live12_catalogue_gates` runs immediately after for source-catalogue checks (see backend file #20 above). Additional regression gates: `tests/test_phase2_grammar_fix.py` locks in the gerund-fix post-process in `server_phase2.py`; `tests/test_phase2_prompt_catalog.py` pins prompt examples against the Live 12 catalogue.
Expand Down
1 change: 1 addition & 0 deletions apps/backend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ python3.11 -m venv venv
- `url_ingest.py`: SSRF-guarded URL-mode ingestion for `POST /api/analysis-runs`. Fetches a public `http`/`https` audio file and streams the bytes through the same downstream pipeline as a multipart upload, enforcing the shared 100 MiB cap.
- `audio_mime.py`: Canonical, host-independent filename→MIME map for ingested audio (`canonical_audio_mime`). Mirrors `apps/ui/src/services/audioFile.ts` so a `.flac` resolves to `audio/flac` on every OS (stdlib `mimetypes` is host-dependent). Imported directly by `server_phase2.py` and `url_ingest.py`, and reached by `server.py` via `server_phase2._get_audio_mime_type` — keep the two maps in sync.
- `csv_export.py`: CSV exporters for Phase 1 time-series fields; backs `GET /api/analysis-runs/{run_id}/export/csv/{field_path}`.
- `phase2_export.py`: Versioned `phase2-export.v1` handoff envelope for downstream consumers (the sibling `asa-ableton` `.als` generator; `scripts/evaluate_recommendations.py --phase2`); backs `GET /api/analysis-runs/{run_id}/export/phase2`. Same thin lookup-and-serve pattern as `csv_export.py`. Cross-repo contract documented in `docs/ASA_ABLETON_BOUNDARY.md`.
- `stage_status.py`: Collapses the eight internal stage statuses into the additive client-facing `publicStatus` field carried on every stage in the run snapshot.
- `sample_generation.py`, `sample_theory.py`, `sample_synthesis.py`, `sample_drums.py`: Phase 3 audition-sample generation — PyTheory plan, FluidSynth/sine-additive render, NumPy drum one-shots, citation manifest. On-demand only.
- `dsp_bandbank.py`, `dsp_utils.py`: Shared DSP primitives — `BatchedBandpass` Butterworth bank and cross-module utilities.
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
| `url_ingest.py` | SSRF-guarded URL-mode ingestion for `POST /api/analysis-runs`. Fetches a public `http`/`https` audio file and streams the bytes through the same downstream pipeline as a multipart upload, enforcing the shared 100 MiB cap. |
| `audio_mime.py` | Canonical, host-independent filename→MIME resolution for ingested audio (`canonical_audio_mime`). Mirrors the frontend map in `apps/ui/src/services/audioFile.ts` so a `.flac` resolves to `audio/flac` on every OS — stdlib `mimetypes` is host-dependent (`audio/x-flac` on macOS vs `audio/flac` on Linux), which failed the test gate and could mislabel a FLAC handed to Gemini. Imported directly by `server_phase2.py` and `url_ingest.py`, and reached by `server.py` via `server_phase2._get_audio_mime_type`. |
| `csv_export.py` | CSV exporters for Phase 1 time-series fields, keyed by dotted JSON path. Backs `GET /api/analysis-runs/{run_id}/export/csv/{field_path}` and keeps the route handler a thin lookup-and-serve. |
| `phase2_export.py` | Versioned Phase 2 handoff envelope (`phase2-export.v1`) for downstream consumers — the sibling `asa-ableton` `.als` generator and `scripts/evaluate_recommendations.py --phase2`. Backs `GET /api/analysis-runs/{run_id}/export/phase2`; same thin lookup-and-serve pattern as `csv_export.py`. See [`docs/ASA_ABLETON_BOUNDARY.md`](../../docs/ASA_ABLETON_BOUNDARY.md). |
| `transcription_pianoroll.py` | Renders the pitch-note translation stage's `transcriptionDetail` as a velocity-encoded `(pitch, time)` uint8 matrix via `symusic`. Backs `GET /api/analysis-runs/{run_id}/transcription/pianoroll`. Derived view — Phase 1 stays authoritative; the response cites the Phase 1 `bpm` and `timeSignature` so chain of custody (PURPOSE invariant #2) is preserved. |
| `stage_status.py` | Collapses the eight internal stage statuses into the additive client-facing `publicStatus` field carried on every stage in the run snapshot. |
| `server_samples.py` + `sample_generation.py` / `sample_theory.py` / `sample_synthesis.py` / `sample_drums.py` | Phase 3 audition-sample generation. PyTheory musical plan, FluidSynth (sine-additive fallback) audio render, NumPy drum one-shots, and a citation manifest tying every clip back to a Phase 1 field. On-demand only — not part of the staged-execution queue. See [`docs/SAMPLE_GENERATION.md`](../../docs/SAMPLE_GENERATION.md). |
Expand Down Expand Up @@ -93,6 +94,7 @@ Custom routes:
- `GET /api/analysis-runs/{run_id}/artifacts` and `…/artifacts/{artifact_id}`
- `GET /api/analysis-runs/{run_id}/source-audio` — re-serves the original ingested audio for the run. Owner-only (no admin bypass). Saves a round-trip vs looking up the source artifact id first.
- `GET /api/analysis-runs/{run_id}/export/csv/{field_path}` — CSV export of a Phase 1 time-series field. See [`docs/adr/0001-phase1-json-schema-v1.md`](../../docs/adr/0001-phase1-json-schema-v1.md) and the registry in [`csv_export.py`](csv_export.py).
- `GET /api/analysis-runs/{run_id}/export/phase2` — single-file `phase2-export.v1` handoff envelope: the stored `producer_summary` interpretation result verbatim (including the frozen `recommendations.v1` projection), the authoritative Phase 1 payload its citations resolve against, the full `validationWarnings` trail, and provenance. The cross-repo contract for the sibling `asa-ableton` `.als` generator and the input format `scripts/evaluate_recommendations.py --phase2` accepts. Status codes: 200 (attachment), 404 (`RUN_NOT_FOUND`/`PHASE2_EXPORT_NOT_AVAILABLE`). Implementation in [`phase2_export.py`](phase2_export.py); contract doc at [`docs/ASA_ABLETON_BOUNDARY.md`](../../docs/ASA_ABLETON_BOUNDARY.md).
- `GET /api/analysis-runs/{run_id}/transcription/pianoroll` — velocity-encoded pianoroll matrix derived from the pitch-note translation stage's `transcriptionDetail`. Query params: `mode` (`frame`|`onset`, default `frame`), `pitchLow` (default 21), `pitchHigh` (default 109, exclusive), `tpq` (default 4). Response cites the Phase 1 `bpm` and `timeSignature` so every cell traces back to a measurement. Status codes: 200 (payload), 400 (`INVALID_MODE`/`INVALID_PITCH_RANGE`/`INVALID_TPQ`), 404 (`RUN_NOT_FOUND`/`TRANSCRIPTION_NOT_REQUESTED`/`TRANSCRIPTION_NOT_AVAILABLE`), 409 (`MEASUREMENT_NOT_COMPLETED`/`TRANSCRIPTION_NOT_COMPLETED`). Implementation in [`transcription_pianoroll.py`](transcription_pianoroll.py).
- `POST /api/analysis-runs/{run_id}/spectral-enhancements/{kind}` — on-demand spectral artifacts. `kind` is one of `cqt`, `hpss`, `onset`, `chroma_interactive`, or `reassigned` (sharper transient/frequency localization via `librosa.reassigned_spectrogram`).
- `POST /api/analysis-runs/{run_id}/pitch-note-translations`
Expand Down
121 changes: 121 additions & 0 deletions apps/backend/phase2_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Phase 2 handoff export — versioned envelope for downstream consumers.

Backs ``GET /api/analysis-runs/{run_id}/export/phase2`` and keeps the route
handler a thin lookup-and-serve (same pattern as ``csv_export.py``).

ASA's Phase 2 interpretation result is the input to downstream tooling that
lives *outside* this repo — concretely the sibling ``asa-ableton`` project
(turns the device cards into an openable Live 12 ``.als`` starter set) and the
in-repo recommendation-proof harness (``scripts/evaluate_recommendations.py
--phase2``). Until now that JSON had no export path: it exists only embedded in
the full run snapshot, with the validation warnings living in a different
subtree (attempt ``diagnostics``) and the grounding provenance in a third.
Consumers hand-extracted it, which is exactly the kind of unversioned, fragile
handoff ADR 0001 warns about.

This module freezes a single-file envelope (``phase2-export.v1``):

- ``phase2`` — the stored ``producer_summary`` interpretation result,
verbatim, exactly as the validator tail left it (including the additive
``recommendations`` field — the frozen ``recommendations.v1`` contract,
ADR 0003).
- ``phase1`` — the authoritative measurement payload the interpretation was
grounded on, included so a consumer can independently verify that every
cited ``phase1Fields`` path resolves (PURPOSE.md invariant #2) without a
second request.
- ``validationWarnings`` — the full warn-and-keep trail (citation-existence,
semantic, catalogue-gate) from the attempt diagnostics, so a non-browser
consumer sees the same chain-of-custody flags the UI renders.
- ``provenance`` — the stored attempt provenance (interpretation schema
version, profile, model, prompt version, grounding ids), verbatim.

Derived and read-only: the export projects stored run state and never
re-estimates or mutates anything (invariant #1). Only the ``producer_summary``
profile is exportable — it is the profile that carries device cards;
``stem_summary`` has no downstream device consumer.

See ``docs/ASA_ABLETON_BOUNDARY.md`` for the cross-repo contract this backs.
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Mapping

EXPORT_SCHEMA_VERSION = "phase2-export.v1"

# The device-card profile. stem_summary results carry no device cards and are
# deliberately not exportable through this envelope.
EXPORTABLE_PROFILE_ID = "producer_summary"


def _utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()


def _producer_summary_attempt(snapshot: Mapping[str, Any]) -> Mapping[str, Any] | None:
"""The completed producer_summary attempt from a run snapshot, or ``None``.

Reads the per-profile map rather than the stage-level ``result`` because
the stage's preferred attempt can be a different profile (e.g.
``stem_summary`` finishing first).
"""
stages = snapshot.get("stages")
if not isinstance(stages, Mapping):
return None
interpretation = stages.get("interpretation")
if not isinstance(interpretation, Mapping):
return None
profiles = interpretation.get("profiles")
if not isinstance(profiles, Mapping):
return None
attempt = profiles.get(EXPORTABLE_PROFILE_ID)
if not isinstance(attempt, Mapping):
return None
if attempt.get("status") != "completed":
return None
if not isinstance(attempt.get("result"), Mapping):
# A completed attempt can carry a null result (skip path) — there is
# nothing to hand off in that case.
return None
return attempt


def _validation_warnings(attempt: Mapping[str, Any]) -> list[dict[str, Any]]:
diagnostics = attempt.get("diagnostics")
if not isinstance(diagnostics, Mapping):
return []
warnings = diagnostics.get("validationWarnings")
if not isinstance(warnings, list):
return []
return [warning for warning in warnings if isinstance(warning, dict)]


def build_phase2_export(
snapshot: Mapping[str, Any],
*,
exported_at: str | None = None,
) -> dict[str, Any] | None:
"""Project a run snapshot into a ``phase2-export.v1`` envelope.

Returns ``None`` when the run has no completed ``producer_summary``
interpretation result to hand off (not requested, still running, failed,
or completed-with-skip). Deterministic and side-effect-free.
"""
attempt = _producer_summary_attempt(snapshot)
if attempt is None:
return None
measurement = snapshot.get("stages", {}).get("measurement", {})
measurement_result = (
measurement.get("result") if isinstance(measurement, Mapping) else None
)
provenance = attempt.get("provenance")
return {
"schemaVersion": EXPORT_SCHEMA_VERSION,
"runId": snapshot.get("runId"),
"exportedAt": exported_at if exported_at is not None else _utc_now_iso(),
"provenance": provenance if isinstance(provenance, Mapping) else None,
"validationWarnings": _validation_warnings(attempt),
"phase1": measurement_result if isinstance(measurement_result, Mapping) else None,
"phase2": attempt["result"],
}
21 changes: 21 additions & 0 deletions apps/backend/recommendation_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,27 @@ def infer_domain(track_context: str | None, category: str | None) -> str:
return UNKNOWN_DOMAIN


def coerce_phase2_payload(raw: Mapping[str, Any]) -> Mapping[str, Any]:
"""Accept either a bare ``Phase2Result`` or a ``phase2-export.v1`` envelope.

The backend's ``GET /api/analysis-runs/{run_id}/export/phase2`` route
(``phase2_export.py``) wraps the interpretation result in a versioned
handoff envelope ``{schemaVersion: "phase2-export.v1", ..., phase2: {...}}``
so one downloaded file feeds both this harness and the sibling
``asa-ableton`` repo. Unwrap it here so ``--phase2`` (and a fixture-dir
``phase2.json``) can be that file directly; a bare result passes through
unchanged.
"""
schema_version = raw.get("schemaVersion")
if (
isinstance(schema_version, str)
and schema_version.startswith("phase2-export.")
and isinstance(raw.get("phase2"), Mapping)
):
return raw["phase2"]
return raw


def normalize_phase2(phase2: Mapping[str, Any]) -> list[NormalizedRec]:
"""Adapter: ``Phase2Result`` JSON -> normalized recs.

Expand Down
Loading
Loading