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
18 changes: 3 additions & 15 deletions apps/ui/src/components/analysisResultsViewModel.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { AbletonRecommendation, Phase1Result, Phase2Result } from "../types";
import {
type ConfidenceBand,
getConfidenceBand,
toConfidenceBand,
} from "../services/sessionMusician/confidenceBand";

// Audit Finding #4: the three-level `ConfidenceLevel` vocabulary stays alive
// for the melody-insights key/value detail rows in Sonic Element cards
// (rendered as plain text like "High (78%)", not as a colored pill). The
// confidence-badge pill path migrated to the canonical four-band ladder
// via `ConfidenceBand` below. Follow-up: migrate the melody rows too once
// producers see the new bands in context.
export type ConfidenceLevel = "High" | "Moderate" | "Low";

export interface ConfidenceBadgeViewModel {
label: string;
/**
Expand Down Expand Up @@ -213,7 +206,6 @@ export interface MelodyInsightsViewModel {
dominantNotes: string[];
rangeLabel: string;
confidence: number;
confidenceLabel: ConfidenceLevel;
isDraft: boolean;
source: MelodyInsightsSource;
}
Expand Down Expand Up @@ -473,14 +465,12 @@ export function buildMelodyInsights(phase1: Phase1Result): MelodyInsightsViewMod
? `${transcriptionDetail.pitchRange.minName as string} - ${transcriptionDetail.pitchRange.maxName as string}`
: "n/a";
const confidence = Math.max(0, Math.min(1, transcriptionDetail.averageConfidence ?? 0));
const confidenceLabel: ConfidenceLevel = confidence >= 0.8 ? "High" : confidence >= 0.5 ? "Moderate" : "Low";

return {
noteCount,
dominantNotes,
rangeLabel,
confidence,
confidenceLabel,
isDraft: confidence < LOW_TRANSCRIPTION_CONFIDENCE_THRESHOLD,
source: "transcription",
};
Expand All @@ -496,14 +486,12 @@ export function buildMelodyInsights(phase1: Phase1Result): MelodyInsightsViewMod
? `${midiToNoteName(detail.pitchRange.min as number)} - ${midiToNoteName(detail.pitchRange.max as number)}`
: "n/a";
const confidence = Math.max(0, Math.min(1, detail.pitchConfidence ?? 0));
const confidenceLabel: ConfidenceLevel = confidence >= 0.8 ? "High" : confidence >= 0.5 ? "Moderate" : "Low";

return {
noteCount,
dominantNotes,
rangeLabel,
confidence,
confidenceLabel,
isDraft: confidence < LOW_MELODY_CONFIDENCE_THRESHOLD,
source: "melody",
};
Expand Down Expand Up @@ -581,7 +569,7 @@ function getSonicMeasurements(
melodyInsights.source === "transcription"
? "Transcription"
: "Melody Confidence",
value: `${melodyInsights.confidenceLabel} (${Math.round(melodyInsights.confidence * 100)}%)`,
value: `${getConfidenceBand(melodyInsights.confidence).label} (${Math.round(melodyInsights.confidence * 100)}%)`,
},
]
: []),
Expand Down Expand Up @@ -1109,7 +1097,7 @@ function buildMelodyPatchParameters(insights: MelodyInsightsViewModel): ChainPar
{ label: noteLabel, value: `${insights.noteCount}` },
{ label: "Note Range", value: insights.rangeLabel },
{ label: "Dominant Notes", value: insights.dominantNotes.slice(0, 3).join(", ") || "n/a" },
{ label: confidenceLabel, value: `${Math.round(insights.confidence * 100)}% (${insights.confidenceLabel})` },
{ label: confidenceLabel, value: `${Math.round(insights.confidence * 100)}% (${getConfidenceBand(insights.confidence).label})` },
];
}

Expand Down
10 changes: 10 additions & 0 deletions apps/ui/src/services/mixDoctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ export function resolveMixDoctorGenreId(phase1: Phase1Result): string {
return DEFAULT_GENRE_ID;
}

// Audit 2026-05-16 §4 (boundary review): the truePeak − lufsIntegrated branch
// below is *not* a competing source of truth with Phase 1. The backend's
// analyze_plr (apps/backend/analyze_core.py) computes
// `round(true_peak − lufs_integrated, 2)` — byte-identical to the fallback
// here — and emits `plr` in both --fast (analyze.py:1385) and full
// (analyze.py:1775) paths. The fallback only fires when `phase1.plr` is
// null/undefined, which today means: a legacy snapshot replayed from runtime
// SQLite that predates the field. When it fires it lands on the same number
// the backend would have. Result is consumed locally for advisory scoring;
// never written back into the Phase 1 snapshot.
function estimatePlr(phase1: Phase1Result): number | null {
const explicit = asFiniteNumber(phase1.plr);
if (explicit !== null) return roundTo(explicit, 2);
Expand Down
39 changes: 38 additions & 1 deletion apps/ui/tests/services/analysisResultsViewModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
truncateAtSentenceBoundary,
truncateBySentenceCount,
} from '../../src/components/analysisResultsViewModel';
import { getConfidenceBand } from '../../src/services/sessionMusician/confidenceBand';
import { MeasurementResult, Phase2Result, TranscriptionDetail } from '../../src/types';

const measurement: MeasurementResult = {
Expand Down Expand Up @@ -747,7 +748,43 @@ describe('analysisResultsViewModel helpers', () => {
expect(insights?.noteCount).toBe(6);
expect(insights?.rangeLabel).toBe('C3 - G5');
expect(insights?.dominantNotes).toEqual(['C3', 'G3']);
expect(insights?.confidenceLabel).toBe('High');
expect(insights?.isDraft).toBe(false);
expect(insights?.confidence).toBeCloseTo(0.83);
expect(getConfidenceBand(insights!.confidence).label).toBe('Solid scaffold');
});

// PR #60: cover the two render sites that interpolate the canonical band
// label, not just the upstream getConfidenceBand mapping. A regression in
// either string-interpolation site would otherwise hide until manual QA.
it('renders the canonical band label inside the Sonic Element melody row (transcription path)', () => {
const sonicCards = buildSonicElementCards(
{ ...measurement, transcriptionDetail: pitchNote },
{
kick: 'Kick.',
bass: 'Bass.',
melodicArp: 'Arp.',
grooveAndTiming: 'Groove.',
effectsAndTexture: 'Fx.',
widthAndStereo: 'Width.',
harmonicContent: 'Harmony.',
},
);
const melodic = sonicCards.find((card) => card.id === 'melodicArp');
const confidenceRow = melodic?.measurements.find((m) => m.label === 'Transcription');
expect(confidenceRow?.value).toBe('Solid scaffold (83%)');
});

it('renders the canonical band label inside the MIDI Clip Guide patch parameters (melody fallback path)', () => {
// Phase 2 with no MIDI-focused recommendations forces the synthetic
// "MIDI Clip Guide" card, which calls buildMelodyPatchParameters with
// the melodyDetail-derived insights (pitchConfidence: 0.72 → workable).
const phase2 = {
mixAndMasterChain: [],
synthAndDrumPatches: [],
} as unknown as Phase2Result;
const cards = buildPatchCards(measurement, phase2);
const guide = cards.find((c) => c.device === 'MIDI Clip Guide');
const confidenceRow = guide?.parameters.find((p) => p.label === 'Melody Confidence');
expect(confidenceRow?.value).toBe('72% (Workable draft)');
});
});
10 changes: 10 additions & 0 deletions audits/nightly-2026-05-16.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@ and the safer-but-stricter reading would still flag this.
emit `plr`, or keep the fallback and document it as an intentional derived
display rather than a Phase 1 override.

**Resolution (post-merge follow-up):** keep the fallback, document why.
Verified `analyze_plr` in [apps/backend/analyze_core.py:624-635](../apps/backend/analyze_core.py) computes
`round(true_peak − lufs_integrated, 2)` — byte-identical to the UI fallback —
and that `plr` is emitted in both `--fast` ([analyze.py:1385](../apps/backend/analyze.py))
and full ([analyze.py:1775](../apps/backend/analyze.py)) paths. The fallback fires only
when `phase1.plr` is null/undefined (legacy snapshots replayed from runtime
SQLite that predate the field) and lands on the same number the backend would
have. Documented inline above `estimatePlr` so the next audit doesn't re-flag
this. See [apps/ui/src/services/mixDoctor.ts:102](../apps/ui/src/services/mixDoctor.ts).

---

### What was *not* found
Expand Down