feat: per-stem confidence in Session Musician note draft - #28
Conversation
Bass and lead stems often have very different transcription confidence — bass
might be Solid (0.85) while other is Rough (0.4). Collapsing them into one
combined band hides that. When the producer toggles the BASS / OTHER button
in Block A, the confidence pill now reflects that stem's mean confidence; the
"All" state keeps showing the overall.
Backend
- analyze_transcription.py: new pure helper `_per_stem_average_confidence`
computes the mean confidence per `stemSource` over the surviving
post-dedup, post-cap notes. `TorchcrepeBackend.transcribe` emits the new
`perStemAverageConfidence: Record<string, number>` field on both the
populated and empty result paths. Returns `{}` when `fullMixFallback` is
true so the UI doesn't show a per-stem band where there's no meaningful
separation to report.
- JSON_SCHEMA.md: documents the new field.
- analyze.py: re-exports the helper so test_analyze can reach it via the
same module-loading pattern as the other transcription helpers.
- No changes to server.py or analysis_runtime — the field flows through the
existing transcriptionDetail passthrough automatically (confirmed by the
new round-trip server test).
Frontend
- measurement.ts: adds optional `perStemAverageConfidence` to
TranscriptionDetail. Optional because legacy snapshots and the empty-notes
path may omit it.
- renderState.ts: new pure helper `selectNoteDraftBandConfidence` picks the
right confidence value based on (transcriptionDetail, stemFilter,
renderState). Falls back to the overall when stem filter is null, the
per-stem field is missing, the selected stem key isn't keyed, or the value
is non-finite. Returns the overall in full-mix-fallback / legacy render
states since those replace the band copy entirely.
- NoteDraftBlock.tsx: uses the helper to drive the band confidence prop.
- analysisRunsClient.ts and backendPhase1Client.ts: both rebuild
transcriptionDetail from an explicit field list and were silently dropping
the new field. Added `parsePerStemAverageConfidence` to each — clamps to
0-1 in the legacy path and skips non-finite entries in both.
Tests
- test_analyze.py: pure-helper coverage for `_per_stem_average_confidence`
(groups means by stem, handles empty notes, skips missing/empty/null
stemSource, tolerates non-numeric confidence values).
- test_server.py: end-to-end round-trip — subprocess emits
perStemAverageConfidence, runtime persists it, run snapshot returns it
unchanged. Catches any future whitelisting in the worker path.
- renderState.test.ts: `selectNoteDraftBandConfidence` matrix (overall fallback,
bass stem, other stem, missing field, missing key, non-finite value, plus
the two override render states).
- upload-phase1-midi.spec.ts: stubs `perStemAverageConfidence: {bass: 0.92,
other: 0.32}` and clicks BASS → OTHER → All, asserting the pill text
changes "Solid scaffold · 92%" → "Rough sketch · 32%" → "Solid scaffold ·
83%" (the overall).
Targets PR #22 (claude/thirsty-easley-faeaea) as the base; will retarget to
main after that lands. Plan in
~/.claude/plans/re-evaluate-asa-s-session-musician-sharded-balloon.md
("Out-of-scope follow-ups" section).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
slittycode
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Summary
Additive Phase 1 schema extension: perStemAverageConfidence is computed at transcription time from surviving post-dedup notes, emitted in both the populated and empty result paths, documented in JSON_SCHEMA.md, and propagated read-only through both client parsers to a pure selector that drives the confidence pill in NoteDraftBlock. The PR also fixes the silent field-dropping bug in both client parsers. Test coverage is solid: 4 backend unit tests, 1 server round-trip, 7 frontend unit tests, and a smoke test that clicks through the BASS → OTHER → All toggle sequence. (Could not run test suites locally — venv and npm packages not installed in this workspace; PR description reports 364 backend + 46 smoke passing.)
Findings
Worth considering
-
clamp01asymmetry between parsers (analysisRunsClient.tsvsbackendPhase1Client.ts):backendPhase1Client.tsclamps per-stem values to [0, 1] viaclamp01()for defense against backend payload bugs;analysisRunsClient.tsdoes not. Both parse the same backend output, so it's harmless today, but if the backend ever emits an out-of-range float through the runs path, the two display paths will diverge. Worth making them consistent. -
NaN/Infinity not guarded in
_per_stem_average_confidence(analyze_transcription.py): Thetry/except (TypeError, ValueError)block correctly drops non-numeric strings andNone, but a Pythonfloat('nan')orfloat('inf')silently passes through and would propagate into the JSON vianp.mean. torchcrepe output is bounded to[0, 1]so this is a remote edge case, not a current risk. Amath.isfinite(conf)guard after the cast would close it cheaply.
Test results
Could not execute locally (venv/npm not installed in this workspace). PR-reported: 364 backend, 46 smoke — all passing.
Phase boundary check
Clean. perStemAverageConfidence is computed inside TorchcrepeBackend.transcribe (Phase 1), flows through both client parsers as read-only data, and is consumed by a pure selector in the UI. No Phase 2 involvement, no Phase 1 mutation downstream.
Generated by Claude Code
Why
Bass and lead stems often have very different transcription confidence — bass might be Solid (0.85) while the lead is Rough (0.4). PR #22 collapses both into a single combined band, which hides that. When a producer toggles BASS or OTHER in the Stem note draft block, the confidence pill should reflect that stem's mean confidence; the "All" state keeps showing the overall.
What changed
Backend
_per_stem_average_confidenceinanalyze_transcription.pycomputes mean confidence perstemSource.TorchcrepeBackend.transcribeemitsperStemAverageConfidence: Record<string, number>on both populated and empty result paths. Returns{}whenfullMixFallbackis true.JSON_SCHEMA.mddocuments the new field.Frontend
perStemAverageConfidenceonTranscriptionDetailintypes/measurement.ts.selectNoteDraftBandConfidenceinrenderState.tspicks the right value based on(transcriptionDetail, stemFilter, renderState).NoteDraftBlock.tsxuses the helper to drive the band's confidence prop.analysisRunsClient.tsandbackendPhase1Client.tsrebuildtranscriptionDetailfrom an explicit field list and were silently dropping any new fields. Added matchingparsePerStemAverageConfidencein each.Tests
test_analyze.py— pure-helper coverage (4 tests).test_server.py— end-to-end round-trip catching any future whitelist trimming.renderState.test.ts—selectNoteDraftBandConfidencematrix (7 tests).upload-phase1-midi.spec.ts— clicks BASS → OTHER → All and asserts the pill text changes.364 backend tests + 46 smoke tests pass.
🤖 Generated with Claude Code