Skip to content

Re-evaluate Session Musician: stacked stem-note draft + melody contour with honest confidence bands - #22

Merged
slittycode merged 1 commit into
mainfrom
claude/thirsty-easley-faeaea
May 13, 2026
Merged

Re-evaluate Session Musician: stacked stem-note draft + melody contour with honest confidence bands#22
slittycode merged 1 commit into
mainfrom
claude/thirsty-easley-faeaea

Conversation

@slittycode

Copy link
Copy Markdown
Owner

Summary

Re-shapes the Session Musician panel so its pitch/note output is honest about what the field can and can't do. The architecture docs are clear: polyphonic full-mix audio-to-MIDI for dense electronic music is unsolved, and what we ship today is best-effort monophonic stem extraction + a Gemini stem listening read. The UI now reflects that without paternalism — the producer gets every signal that exists, each labelled with what it is and what to do with it.

What changed

One panel, two stacked blocks. The old PITCH/NOTE ↔ MELODY toggle is gone. Block A (Stem note draft) and Block B (Melody contour) both render simultaneously when their data exists. Each block has its own Preview and Download with distinct filenames (track-analysis-stems.mid, track-analysis-melody.mid) so Ableton drag-and-drop is unambiguous.

One shared preview controller. usePreviewController() is called exactly once in SessionMusicianPanel; the returned PreviewController is passed down to both <MidiControlsRow> instances. Starting a preview on Block B automatically stops Block A — one remote control, not two.

Confidence becomes plain-language guidance. A four-band ladder replaces the bare percentage:

Band Range Label What the producer should do
Solid ≥ 80% Solid scaffold "Notes look reliable. Expect light cleanup in Ableton's piano roll."
Workable 50–79% Workable draft "Most notes are in the right ballpark. Plan to redraw the trickiest bars."
Rough 25–49% Rough sketch "Pitch tracking caught the general shape. Treat this as a rhythm grid more than note truth."
Unreliable < 25% Unreliable "The pitch detector wasn't confident. Use the pitch range and most-played notes as scale hints — don't trust specific notes."

Pill text format: Workable draft · 72% — label tells you what to do, number lets you audit why.

Block A render-state precedence (load-bearing). Defined in services/sessionMusician/renderState.ts:

  1. pitchNoteMode === 'off'absent (stale cached notes can't sneak back onto the screen)
  2. requested but transcriptionDetail missing → requested-but-unavailable
  3. missing payload, not requested → absent
  4. notes.length === 0ran-with-no-result
  5. non-torchcrepe method → legacy (wins over fallback — re-analyze is the actionable fix)
  6. fullMixFallback === truefull-mix-fallback
  7. otherwise → stem-aware

Each non-nominal state replaces the band pill with a stronger notice and hides controls that don't apply (stem filter, confidence slider).

"What stays useful" footer. Even at Unreliable, three signals carry: pitch range (tells you MIDI track range / synth tuning), top-played notes (scale hint), average note length (rhythmic feel). Rendered as chips below the piano roll across all data-bearing states.

Stem listening notes (Gemini) moves to its own component. StemListeningNotesPanel lifts the JSX out of AnalysisResults and renders adjacent to Session Musician inside a local space-y-4 wrapper so the two panels read as a paired suite. hasStemListeningNotesContent now powers the StickyNav inclusion, the render gate, and the off-state cross-link copy in one place. Renamed in the nav from "Stem Summary" to "Stem Notes."

Off-state has three variants based on data availability — melody present, no melody + listening notes present, or neither. All variants qualify melody as "when available" because melodyDetail is None on three real code paths (analyze.py:1189, analyze_fast.py:141, analyze_rhythm.py:438).

App toggle copy tightened to match the new panel structure without overpromising what stays available when stem mode is off.

File map

Pure helpers (apps/ui/src/services/sessionMusician/):

  • confidenceBand.ts — band ladder + pill formatter
  • noteConversion.ts — transcription/melody → display-note converters
  • stemListeningNotes.tshasStemListeningNotesContent helper used in three places
  • renderState.tsderiveNoteDraftRenderState + isLegacyTranscriptionMethod

Shared React pieces (apps/ui/src/components/sessionMusician/):

  • PianoRollCanvas.tsx, QuantizeControls.tsx, ConfidenceBandBadge.tsx, MidiControlsRow.tsx, usePreviewController.ts

Block components in the same directory:

  • NoteDraftBlock.tsx (Block A), MelodyContourBlock.tsx (Block B)

Heavy edits:

  • SessionMusicianPanel.tsx — drops the toggle / sourceMode state, orchestrates the two blocks, owns the preview controller and off-state banner
  • AnalysisResults.tsx — wraps the two panels in section-musician-suite, replaces the inline stem-summary JSX with <StemListeningNotesPanel>, renames the nav label

Light edits:

  • App.tsx — toggle description copy
  • Three e2e specs — assert the new track-analysis-stems.mid filename for the Block A download

Tests

  • 331 unit tests pass. Rewrote sessionMusicianPanel.test.ts (dropped the brittle useState call-count mocking pattern). Added new suites: confidenceBand, noteConversion, stemListeningNotes, renderState, stemListeningNotesPanel.
  • 46 smoke tests pass (1 skipped — live Gemini). Extended upload-phase1-midi.spec.ts with shared-preview mutual-exclusion, per-block downloads with distinct filenames, and slider-filtered-to-zero hiding the Download.
  • No backend changes — every field used here already existed in the contract.

Verify

cd apps/ui
npm run verify

Plan

Full design rationale, render-state matrix, UI copy library, and decision history live in .claude/plans/re-evaluate-asa-s-session-musician-sharded-balloon.md.

🤖 Generated with Claude Code

…lody contour with honest confidence bands

Replaces the per-mode toggle with two vertically stacked blocks rendered
simultaneously: a "Stem note draft" (Block A — torchcrepe on Demucs stems) and
a "Melody contour" (Block B — Essentia full-mix melody). Each block owns its
own Preview / Download with distinct filenames so Ableton drag-and-drop is
unambiguous. A single shared usePreviewController prevents the two previews
from playing at once.

Confidence renders as a "label · NN%" pill plus a one-sentence plain-language
guidance line (Solid scaffold / Workable draft / Rough sketch / Unreliable).
The pill replaces the bare percentage and tells the producer what to do with
the output at each tier without hiding the raw number.

Block A's render state is precedence-driven (renderState.ts):
  1. pitchNoteMode === 'off' → absent (stale cached notes can't sneak back in)
  2. requested but transcriptionDetail missing → requested-but-unavailable
  3. transcriptionDetail missing, not requested → absent
  4. transcriptionDetail.notes empty → ran-with-no-result
  5. non-torchcrepe method → legacy (wins over full-mix fallback)
  6. fullMixFallback === true → full-mix-fallback
  7. otherwise → stem-aware

Each non-nominal state replaces the band pill with a stronger notice and hides
controls that don't apply (stem filter / confidence slider). The "What stays
useful" footer surfaces pitch range, top-played notes, and average note
duration even when individual notes are unreliable.

The Gemini stem listening notes section moves into a new
StemListeningNotesPanel and renders adjacent to Session Musician inside a
local space-y-4 wrapper so the two panels read as a paired suite.
hasStemListeningNotesContent now powers the StickyNav inclusion, the render
gate, and the off-state cross-link copy in one place. The
section-stem-summary StickyNav anchor stays put; only the label changes
("Stem Summary" → "Stem Notes").

Off-state banner has three variants depending on what's available: melody
present, no melody + listening notes present, or neither. All variants
qualify melody as "when available" because analyze.py, analyze_fast.py, and
analyze_rhythm.py all clear melodyDetail in real code paths.

App toggle copy tightened to match the new panel structure without
overpromising.

Pure helpers extracted to services/sessionMusician/: confidenceBand,
noteConversion, stemListeningNotes, renderState. Shared React pieces in
components/sessionMusician/: PianoRollCanvas, QuantizeControls,
ConfidenceBandBadge, MidiControlsRow, usePreviewController, plus the two
blocks. Existing SessionMusicianPanel helper exports
(filterNotesByConfidence, formatFilteredNoteCount, formatVibratoConfidence,
deriveTranscriptionProvenance, MIDI_DOWNLOAD_FILE_NAME) remain for backward
compatibility.

Tests: rewrote sessionMusicianPanel.test.ts (dropped the brittle useState
call-count mocking pattern) and added confidenceBand, noteConversion,
stemListeningNotes, renderState, and stemListeningNotesPanel unit suites.
Extended upload-phase1-midi.spec.ts with shared-preview mutual-exclusion,
per-block downloads, and slider-filtered-to-zero. Three e2e tests now assert
track-analysis-stems.mid (Block A's per-block filename).

No backend changes — every field used here already existed in the contract.

Plan: .claude/plans/re-evaluate-asa-s-session-musician-sharded-balloon.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@slittycode slittycode left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE (posted as COMMENT — can't approve own PR)

Summary

Decomposes a 700+ line monolith into focused block components, replaces the per-mode toggle with two simultaneously rendered blocks, and adds an honest confidence ladder with plain-language guidance. Phase 1 data is read-only throughout. All 331 unit tests pass.

Findings

Worth considering (neither is blocking):

MelodyContourBlock.tsx:100–102 — Melody pitch range renders as raw MIDI integers ("MIDI range: 48 – 79"). Block A shows note names ("Range: C3 – G5") via pitchRange.minName/maxName. MelodyDetail.pitchRange has no name fields, so the fix is calling midiToNoteName() (already available via noteConversion.ts). The label correctly says "MIDI range:" so it isn't lying, just inconsistent with Block A.

QuantizeControls.tsx:30 — Grid buttons are missing type="button". Every other interactive button in the component tree (MidiControlsRow, SessionMusicianPanel, NoteDraftBlock stem filter) has it. Won't cause real problems in this SPA but breaks internal consistency.

Test results

331 passed / 0 failed. New suites: renderState (16), confidenceBand (18), noteConversion (16), stemListeningNotes (7), stemListeningNotesPanel (7). sessionMusicianPanel.test.ts covers the six Block A render states, Block B melody states, off-state banner variants, and simultaneous rendering. Smoke suite extended with shared-preview mutual-exclusion, per-block filenames, and slider-filtered-to-zero.

Phase boundary check

Clean. types.ts is not in this diff. Phase 1 fields are read by NoteDraftBlock, MelodyContourBlock, and the renderState/noteConversion helpers but never written. Confidence band and display note conversions produce new local objects; they don't mutate phase1. hasStemListeningNotesContent reads the Gemini envelope only to gate UI visibility, not to override any measurement.


Generated by Claude Code

@slittycode
slittycode merged commit bab6483 into main May 13, 2026
2 checks passed
@slittycode
slittycode deleted the claude/thirsty-easley-faeaea branch May 13, 2026 00:39
slittycode added a commit that referenced this pull request May 13, 2026
PR #22 shipped a four-band confidence ladder (Solid scaffold / Workable draft
/ Rough sketch / Unreliable) but the PILL_CLASSES map collapsed them into
only two tones — accent for solid + workable, warning for rough + unreliable.
A producer couldn't visually distinguish Solid from Workable, or Rough from
Unreliable, at a glance.

Map the four bands to a green → orange → amber → red traffic-light ladder so
the visual hierarchy matches the producer's mental model ("best to worst at
a glance"):

  Solid scaffold   (≥80%)  → success (#00ff9d green)
  Workable draft   (50-79%) → accent  (#ff8800 orange)
  Rough sketch     (25-49%) → warning (#ffb800 amber)
  Unreliable       (<25%)  → error   (#ff3333 red)

All four CSS variables already exist in apps/ui/src/index.css and are the
canonical severity vocabulary across MeasurementPrimitives, DiagnosticLog,
and FileUpload (success = ready, accent = active, warning = caution,
error = critical). No theme additions; no new shades.

Opacity tuning unified at /30 border, /10 bg, text-{color} to match
MeasurementPrimitives.BADGE_TONE_CLASSES. Color carries the differentiation
now — the previous per-band opacity wobble didn't communicate anything.

NoteDraftBlock's overrideTone="rough" path (used for full-mix-fallback and
legacy render states) now renders amber, which is the right tone for "treat
with caution, not unreliable junk." No wiring change needed.

Tests: new tests/services/confidenceBandBadge.test.ts renders all four
confidence values and asserts each emits the expected color tokens. A
pairwise-distinct assertion survives future opacity tweaks. Two override-path
tests lock in the full-mix-fallback / legacy visual (high-confidence input
with overrideTone="rough" produces warning tokens, not green; low-confidence
input stays warning, not red). No existing tests touched — confidenceBand
asserts id/label/percent (not classes); panel + smoke tests assert on text.

Targets PR #22 (claude/thirsty-easley-faeaea) as the base. Plan in
~/.claude/plans/re-evaluate-asa-s-session-musician-sharded-balloon.md
("Follow-up plan #1" section).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
slittycode added a commit that referenced this pull request May 13, 2026
…27)

PR #22 shipped a four-band confidence ladder (Solid scaffold / Workable draft
/ Rough sketch / Unreliable) but the PILL_CLASSES map collapsed them into
only two tones — accent for solid + workable, warning for rough + unreliable.
A producer couldn't visually distinguish Solid from Workable, or Rough from
Unreliable, at a glance.

Map the four bands to a green → orange → amber → red traffic-light ladder so
the visual hierarchy matches the producer's mental model ("best to worst at
a glance"):

  Solid scaffold   (≥80%)  → success (#00ff9d green)
  Workable draft   (50-79%) → accent  (#ff8800 orange)
  Rough sketch     (25-49%) → warning (#ffb800 amber)
  Unreliable       (<25%)  → error   (#ff3333 red)

All four CSS variables already exist in apps/ui/src/index.css and are the
canonical severity vocabulary across MeasurementPrimitives, DiagnosticLog,
and FileUpload (success = ready, accent = active, warning = caution,
error = critical). No theme additions; no new shades.

Opacity tuning unified at /30 border, /10 bg, text-{color} to match
MeasurementPrimitives.BADGE_TONE_CLASSES. Color carries the differentiation
now — the previous per-band opacity wobble didn't communicate anything.

NoteDraftBlock's overrideTone="rough" path (used for full-mix-fallback and
legacy render states) now renders amber, which is the right tone for "treat
with caution, not unreliable junk." No wiring change needed.

Tests: new tests/services/confidenceBandBadge.test.ts renders all four
confidence values and asserts each emits the expected color tokens. A
pairwise-distinct assertion survives future opacity tweaks. Two override-path
tests lock in the full-mix-fallback / legacy visual (high-confidence input
with overrideTone="rough" produces warning tokens, not green; low-confidence
input stays warning, not red). No existing tests touched — confidenceBand
asserts id/label/percent (not classes); panel + smoke tests assert on text.

Targets PR #22 (claude/thirsty-easley-faeaea) as the base. Plan in
~/.claude/plans/re-evaluate-asa-s-session-musician-sharded-balloon.md
("Follow-up plan #1" section).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
slittycode added a commit that referenced this pull request May 13, 2026
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 added a commit that referenced this pull request May 13, 2026
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 added a commit that referenced this pull request May 13, 2026
When a cached run shows up with `transcriptionMethod: 'basic-pitch'` (or any
non-torchcrepe method), the Stem note draft block enters its `legacy` render
state and shows the existing "Re-analyze for current stem-aware quality"
notice. PR #22 left the notice as just words — this adds the actual button.

Behaviour
- Block A renders a "Re-analyze with stem-aware pipeline" button only in the
  `legacy` render state AND only when App passes a callback (i.e. when a
  source File is loaded and no analysis is currently in flight).
- Clicking the button calls back up through SessionMusicianPanel →
  AnalysisResults → App, which fires `handleStartAnalysis` with an explicit
  `pitchNoteRequested: true` override. The File state (`audioFile`) is
  retained in App across analyses, so no re-upload is required; the existing
  bytes are re-posted to `POST /api/analysis-runs` with stem-aware mode on.
- The UI toggle "STEM PITCH/NOTE TRANSLATION" is also updated to reflect the
  override so subsequent manual Run Analysis clicks behave consistently.

Investigation
- `analysis_runtime.create_run` stores the artifact bytes anew on every run —
  there's no artifact-reuse API today. But because `audioFile: File` is
  retained in App state after analysis completes, re-running just means
  re-POSTing the same File, which is what the producer expects. No backend
  changes needed.

Wiring
- New optional `onReanalyzeWithStemAware?: () => void` prop on
  `SessionMusicianPanel` and `NoteDraftBlock`. AnalysisResults threads it
  through. App constructs the handler as
  `audioFile && !isAnalyzing ? () => handleStartAnalysis({ pitchNoteRequested: true }) : undefined`,
  so the button is only rendered (and clickable) when a re-run is actually
  viable.
- `handleStartAnalysis` gains a typed `overrides` param. The override is
  read in the call to `analyzeAudio`, and a matching
  `setPitchNoteTranslationRequested` keeps the toggle visually in sync.

Tests
- Vitest (`sessionMusicianPanel.test.ts`): added five static-markup cases
  covering button visibility — legacy + callback → button renders, legacy +
  no callback → no button, stem-aware → no button, full-mix-fallback → no
  button, ran-with-no-result / requested-but-unavailable → no button.
- Playwright smoke (`upload-phase1-midi.spec.ts`): stubs a run that returns
  `transcriptionMethod: 'basic-pitch'`. After the page renders the legacy
  state, asserts the button is visible, clicks it, and confirms a second
  POST to `/api/analysis-runs` fires with `pitch_note_mode=stem_notes`.

Targets PR #22 (claude/thirsty-easley-faeaea) as the base; will retarget to
main once 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 added a commit that referenced this pull request May 13, 2026
When a cached run shows up with `transcriptionMethod: 'basic-pitch'` (or any
non-torchcrepe method), the Stem note draft block enters its `legacy` render
state and shows the existing "Re-analyze for current stem-aware quality"
notice. PR #22 left the notice as just words — this adds the actual button.

Behaviour
- Block A renders a "Re-analyze with stem-aware pipeline" button only in the
  `legacy` render state AND only when App passes a callback (i.e. when a
  source File is loaded and no analysis is currently in flight).
- Clicking the button calls back up through SessionMusicianPanel →
  AnalysisResults → App, which fires `handleStartAnalysis` with an explicit
  `pitchNoteRequested: true` override. The File state (`audioFile`) is
  retained in App across analyses, so no re-upload is required; the existing
  bytes are re-posted to `POST /api/analysis-runs` with stem-aware mode on.
- The UI toggle "STEM PITCH/NOTE TRANSLATION" is also updated to reflect the
  override so subsequent manual Run Analysis clicks behave consistently.

Investigation
- `analysis_runtime.create_run` stores the artifact bytes anew on every run —
  there's no artifact-reuse API today. But because `audioFile: File` is
  retained in App state after analysis completes, re-running just means
  re-POSTing the same File, which is what the producer expects. No backend
  changes needed.

Wiring
- New optional `onReanalyzeWithStemAware?: () => void` prop on
  `SessionMusicianPanel` and `NoteDraftBlock`. AnalysisResults threads it
  through. App constructs the handler as
  `audioFile && !isAnalyzing ? () => handleStartAnalysis({ pitchNoteRequested: true }) : undefined`,
  so the button is only rendered (and clickable) when a re-run is actually
  viable.
- `handleStartAnalysis` gains a typed `overrides` param. The override is
  read in the call to `analyzeAudio`, and a matching
  `setPitchNoteTranslationRequested` keeps the toggle visually in sync.

Tests
- Vitest (`sessionMusicianPanel.test.ts`): added five static-markup cases
  covering button visibility — legacy + callback → button renders, legacy +
  no callback → no button, stem-aware → no button, full-mix-fallback → no
  button, ran-with-no-result / requested-but-unavailable → no button.
- Playwright smoke (`upload-phase1-midi.spec.ts`): stubs a run that returns
  `transcriptionMethod: 'basic-pitch'`. After the page renders the legacy
  state, asserts the button is visible, clicks it, and confirms a second
  POST to `/api/analysis-runs` fires with `pitch_note_mode=stem_notes`.

Targets PR #22 (claude/thirsty-easley-faeaea) as the base; will retarget to
main once 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant