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
28 changes: 26 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,32 @@ npm run test:smoke -- tests/smoke/upload-phase1.spec.ts
TEST_FLAC_PATH=/path/to/track.flac GEMINI_API_KEY=… VITE_ENABLE_PHASE2_GEMINI=true ./scripts/test-e2e.sh # Full live Gemini run
```

### Scripts at a glance

Operational and one-shot scripts live in two places. They are not on the request-path; reach for them when the situation calls for it.

`scripts/` (repo root):
1. `dev.sh` — full-stack dev launcher (covered above).
2. `test-e2e.sh` / `test-e2e-integration.sh` — e2e harnesses (covered above).
3. `calibrate_confidence.py` — threshold-sweep harness for the pitch / chord / sidechain detectors. Research-only.

`apps/backend/scripts/`:
1. `bootstrap.sh` — recreate the Python 3.11 venv (covered above).
2. `render_upload_limit_contract.py` — regenerate the operator-facing upload-limit contract text from `upload_limits.py`. Run after changing the canonical limits.
3. `evaluate_phase1.py`, `evaluate_polyphonic.py`, `evaluate_structure_sweep.py` — offline evaluation harnesses for the Phase 1 detector battery, the research polyphonic transcriber, and structure-segmentation parameter sweeps. Wired to `phase1_evaluation.py` / `polyphonic_evaluation.py`.
4. `audit_pass1.py`, `genre_check.py`, `replay_catalog_validation.py` — corpus auditing and Live 12 device-catalog validation. `genre_corpus.md` is the corpus manifest.

## Architecture

### Repo Layout — what's on the product path

`apps/backend/`, `apps/ui/`, and `scripts/` are the product. The remaining top-level directories are off-path and safe to skip unless the task explicitly names them:

1. `advisory/` — auditor notes and one-shot reviews (e.g. `phase1_audit/`). Not imported by either app.
2. `experiments/` — sandbox scratch space (e.g. `crepe_test/` carries only a venv). Throwaway by intent; do not wire production code here.
3. `docs/` — long-form rationale (`ARCHITECTURE_STRATEGY.md`, `history/`). Read these *before* structural changes; do not treat them as living API docs.
4. `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

ASA's hybrid architecture splits work into three layers. Read `docs/ARCHITECTURE_STRATEGY.md` before proposing changes to this structure — it records *why* the design is shaped this way.
Expand Down Expand Up @@ -112,7 +136,7 @@ The backend supports staged execution via `analysis_runtime.py`, which persists

Run-oriented endpoints (`/api/analysis-runs*`) are the canonical interface for staged execution. Legacy `POST /api/analyze`, `POST /api/analyze/estimate`, and `POST /api/phase2` remain only as temporary compatibility wrappers — do not build new functionality on them.

Phase 3 audition-sample generation (`POST/GET /api/analysis-runs/{run_id}/samples`) is **on-demand**, not a queue stage — the UI explicitly requests it after interpretation completes. See `server_samples.py` and the `sample_*.py` modules.
Phase 3 audition-sample generation (`POST/GET /api/analysis-runs/{run_id}/samples`) is **on-demand**, not a queue stage — the UI explicitly requests it after interpretation completes. See `server_samples.py` and the `sample_*.py` modules. On the frontend, `apps/ui/src/components/SamplePlayback.tsx` is the entry point — mounted from `AnalysisResults.tsx` and driven by `src/services/sampleGenerationClient.ts` (`generateSamples` posts, `fetchSamples` retrieves).

Frontend polling: `src/services/analysisRunsClient.ts` creates runs and polls stage snapshots. `src/services/analyzer.ts` orchestrates the create-run + poll loop and projects display payloads.

Expand Down Expand Up @@ -140,7 +164,7 @@ Artifact access goes through `artifact_storage.py` rather than direct disk paths
9. **`upload_limits.py`**: Canonical 100 MiB raw-audio / 101 MiB request-envelope limits. Regenerate the operator contract via `scripts/render_upload_limit_contract.py` if numbers change.
10. **`spectral_viz.py`**: Librosa-based spectrogram/time-series artifacts. Called after measurement; failures are non-critical.
11. **`url_ingest.py`**: SSRF-guarded URL-mode ingestion for `POST /api/analysis-runs` — fetches a public `http`/`https` audio URL and feeds it through the same downstream pipeline as a multipart upload.
12. **`csv_export.py`**: CSV exporters for Phase 1 time-series fields, backing `GET /api/analysis-runs/{run_id}/export/csv/{field_path}`. Keeps the route handler a thin lookup-and-serve.
12. **`csv_export.py`**: CSV exporters for Phase 1 time-series fields, backing `GET /api/analysis-runs/{run_id}/export/csv/{field_path}`. Keeps the route handler a thin lookup-and-serve. Field paths are an explicit allowlist (`csv_export.list_supported_fields()` — currently `lufsCurve.shortTerm`, `lufsCurve.momentary`, `rhythmDetail.tempoCurve`, `spectralBalanceTimeSeries`); arbitrary descent into the payload is not supported. Example: `curl http://127.0.0.1:8100/api/analysis-runs/<run_id>/export/csv/rhythmDetail.tempoCurve -o tempo.csv`.
13. **`stage_status.py`**: Collapses the eight internal stage statuses into the additive client-facing `publicStatus` field on every stage snapshot.
14. **`server_samples.py` + `sample_generation.py`, `sample_theory.py`, `sample_synthesis.py`, `sample_drums.py`**: Phase 3 audition-sample generation. `sample_theory.py` builds the PyTheory musical plan, `sample_synthesis.py` renders audio (FluidSynth with sine-additive fallback), `sample_drums.py` synthesizes drum one-shots, `sample_generation.py` orchestrates and emits the citation manifest. On-demand only.
15. **`dsp_bandbank.py` + `dsp_utils.py`**: Shared DSP primitives — `BatchedBandpass` (4th-order Butterworth bandpass bank) and cross-module utility functions.
Expand Down
59 changes: 33 additions & 26 deletions apps/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ import {
loadPhase2RequestedPreference,
savePhase2RequestedPreference,
} from './utils/phase2Preference';
import { getAppViewHref } from './utils/appView';
// Audit quick-hit: `getAppViewHref` import retired — its only consumer
// was the Dense DAW Lab header link, which was removed in this PR. The
// helper is still exported for any future re-introduction (e.g. behind a
// settings menu) and from `main.tsx` for the active-view router.
import { startRenderBenchmarkCycle } from './utils/renderBenchmark';

// Note: gemini-3.1-flash-preview is intentionally omitted — three live test runs
Expand Down Expand Up @@ -963,32 +966,36 @@ export default function App() {
</div>

<div className="flex items-center gap-2 sm:gap-4">
{/* Audit #7: de-emphasized the Dense DAW Lab link. The accent-orange
chip competed with the brand mark and the model selector for the
user's eye on every page. It's still discoverable, just quieter. */}
<a
href={getAppViewHref('daw-concept')}
className="hidden sm:inline-flex items-center text-[10px] font-mono uppercase tracking-[0.18em] text-text-secondary transition-colors hover:text-accent"
>
Dense DAW Lab →
</a>
{/* Audit quick-hit: removed the Dense DAW Lab link from the
header. A prior pass had already demoted it to muted text, but
the audit re-flagged it for sitting in the primary flow's
header without context for what it is. The route stays
accessible via direct URL (?view=daw-concept) — see
`getAppViewHref('daw-concept')` and `main.tsx`. */}
<div className="hidden sm:flex items-center space-x-4">
<div className="flex items-center space-x-2">
<label className="text-[10px] font-mono text-text-secondary uppercase">Interpretation Model</label>
<select
data-testid="phase2-model-desktop"
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
disabled={phase2ModelSelectorDisabled}
className="appearance-none bg-bg-card border border-border text-text-primary text-[10px] font-mono py-1 pl-2 pr-6 rounded-sm focus:outline-none focus:border-accent cursor-pointer disabled:opacity-50"
>
{MODELS.map((model) => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</select>
</div>
{/* Audit quick-hit: dropped the "Interpretation Model" label
and shrank the select to a discreet text-secondary dropdown.
The audit flagged the prior styling as foregrounding an
AI-model choice an intermediate producer has no basis to
make. Selector stays accessible (smoke spec requires it
visible at desktop viewport) but now reads as background
metadata, not a primary control. Title attribute carries
the long-form context. */}
<select
data-testid="phase2-model-desktop"
aria-label="Interpretation model"
title="AI model used for Phase 2 interpretation (advanced)"
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
disabled={phase2ModelSelectorDisabled}
className="appearance-none bg-transparent border-none text-text-secondary/70 hover:text-text-primary text-[10px] font-mono py-0 pl-0 pr-4 rounded-sm focus:outline-none focus:text-text-primary cursor-pointer disabled:opacity-50 transition-colors"
>
{MODELS.map((model) => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</select>
{phase2StatusBadge && (
<span
data-testid="phase2-status-badge"
Expand Down
Loading