Run catalog_metadata on saved verified audio with explicit coverage
Summary
Runs the existing Music Flamingo catalog_metadata preset against the verified full-song audio already saved as an audio_source document, and persists the validated JSON as a separate catalog_metadata interpretation. Before this, only the preview-URL path (callContextMusicPreset) could run this preset, and nothing on the Context path kept the endpoint response, the validated record and the coverage labels separate.
This slice adds:
resolveSongEvidenceCoverage: labels the evidencefull,preview,unverified,wrong_recordingormissing, each with a schema-valid extent, identity and range. A caller-supplied language label is trimmed and passed through, never guessed; one too long to be a label is dropped rather than throwing.parseCatalogMetadataResponse: turns raw text or an object into a validated record, or intoinvalidwith a reason. It never throws, and it enforces the limits the preset prompt defines.analyzeSavedContextCatalogMetadataandContextStructuredOutputInvalid: invalid structured output fails the attempt instead of becoming an accepted document.- Two shared helpers, now used by both the existing summary/lyric analyzer and the new module:
loadAcceptedContextAudioSource(looks up the accepted audio and checks the workspace storage key) andrequestSavedContextAudioAnalysis(re-checks checksum and duration, signs a 15-minute URL, makes one production call). scripts/analyzeContextCatalogMetadata.ts: an operator runner that mirrors the lyric runner. It makes a paid call and was not run.
The trace records the local preset prompt and params, a provenance note, the media manifest (label, range, checksum, no URL), the full endpoint response body, validation status, timing and a billing note. The signed URL and API key never enter rpc payloads.
Refs recoupable/app#2127 (child of epic recoupable/app#2116).
Design notes (checked against the code)
- The production
/api/songs/analyzeendpoint already applies the preset'sparseJsonLike. Forcatalog_metadatait returns an object, or the raw string when parsing fails (processAnalyzeMusicRequest). So the parser acceptsunknown, and the response schema is a loose object that keeps every key the endpoint returns, includingpreset. - The validator enforces the preset's own contract: a positive integer
tempo_bpm;energy_levelanddanceabilityfrom 1 to 10; at most 3subgenres, 5mood, 3lyrical_themesand 3similar_artists; non-empty trimmed strings. Every field is optional. Output that breaks these limits fails the attempt. - Audio without waveform verification gets the label
unverified(identityuncertain). That keeps it separate fromwrong_recording, which requires positive evidence: a duration disagreement greater than 2 s (identitymismatch). - The analyzer refuses anything that isn't
fullevidence before it claims an attempt or calls the provider, so no paid call is made on unverified audio. A subject with no accepted audio is refused the same way. presetVersionis the sha256 of the local preset prompt and params, and a test pins the formula. A changed recipe therefore makes a new paid call, while a compatible accepted result is reused.- Moving
analyzeSavedContextAudio.tsonto the shared loader and request helper changes no behaviour: its existing tests, the fulllib/sitessuite (its only other caller is the Sites analyzer) and the lyric path all pass unchanged. No open api PR touches these files.
Merge sequencing
api only. No docs change: the OpenAPI topics enum already lists catalog_metadata, and no new action is exposed. No database change: claim_context_enrichment has accepted topic catalog_metadata since migrations 20260923070000 and 20260927090000. No app change. This PR depends on no open PR, and no other open api PR touches analyzeSavedContextAudio.ts, the new files or scripts/analyzeContext*.
Verification
All commands were run in the PR worktree at commit 3520bc4c (base origin/main c72887a).
- Ran:
pnpm exec vitest run lib/context: 71 files passed, 8 skipped; 355 tests passed, 8 skipped. The baseline on origin/main was 326 passed / 8 skipped, so this PR adds 29 tests with no regressions. - Ran:
pnpm exec vitest run lib/sites(covers the Sites caller ofanalyzeSavedContextAudio): 50 files passed, 3 skipped; 226 tests passed, 4 skipped. - Ran:
pnpm exec tsc --noEmit: 199 pre-existingerror TSlines across 66 files, none in any file this PR touches and none inlib/context. An independent reviewer checked a copy without this PR's files and got the same 199. - Ran:
pnpm exec eslinton the 12 changed or added.tsfiles: clean.pnpm exec prettier --checkon those files plusAUDIO-SOURCE.md: all formatted. - Red → green: all three original test files first failed with
Cannot find module, then passed 22/22. The review follow-up was also written test-first. These tests failed before the fix: out-of-limit values were accepted (energy 99, danceability -4, tempo -20, 7 moods, empty genre); the response was not trimmed; unverified audio was labeledwrong_recording; a 101-character language label threw; the trace dropped endpoint keys; the shared request helper did not exist. All pass now. - Behaviour covered by fixtures:
- Invalid structured output calls
fail_context_enrichmentand nevercomplete_context_enrichment. - Reuse makes no provider, signing or verification call.
- Changed bytes or duration are rejected before signing or fetching.
- Unverified audio is refused before any claim.
- The signed URL and API key never appear in any rpc payload.
- Invalid structured output calls
- Not verified: hosted deployment; the live Music Flamingo / production analyze endpoint (no paid call was made, every provider response is a fixture, and the operator script was not run); production or any hosted database (no migration, no RPC executed); the hosted Sites path.
Out of scope / known gaps
-
Nothing dispatches this recipe over HTTP, MCP or a workflow, because paid dispatch is gated by #2123 and #2125. The module is server-only. Its only entry point is the operator script, which mirrors
scripts/analyzeContextLyrics.ts. -
Other parts of the ticket are not built here:
- the versioned neutral song-summary topic (the existing
song_summary/saved-audio-summary-themes-v3path is unchanged) - audio and lyrics acquisition adapters (resolving accessible audio or lyrics through providers)
- lyrics source, version and language attribution
resolveSongEvidenceCoveragealready provides thepreview,missingand language outputs, but no acquisition path calls it yet; the analyzer only runs on saved full audio. - the versioned neutral song-summary topic (the existing
-
CE07 cost references:
costUsd: nullandcostStatus: "unknown", exactly as in the sibling modules. -
Failed-attempt traces are not persisted, including the endpoint response of an attempt whose output failed validation, because
fail_context_enrichment(p_owner, p_attempt)accepts no reason. The thrownContextStructuredOutputInvalidcarriesreasonandrawLengthfor logs. Persisting the trace needs a database migration. -
The existing shared
parseJsonLikehelper replaces every'with", so a Python-style dict whose text contains an apostrophe (for exampledon't) fails to parse and that attempt fails. This behaviour is pre-existing inlib/flamingo/presets/parseJsonLike.ts, which production also uses. It is not changed here. -
The Sites hosted analyzer rejects non-string provider responses, so it cannot run JSON presets through this module as-is.
-
No Records data and no new dependencies.
🤖 Generated with Claude Code