From 1f32fca7d8f80afb7b512c507d0c6ffd0da752b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 02:31:29 +0000 Subject: [PATCH 1/2] =?UTF-8?q?WS2:=20Phase=202=20prompt=20=E2=80=94=20v2?= =?UTF-8?q?=20loudness=20units,=20PLR=20mastering=20rules,=20bpm=20hedge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on the Phase 1 v2 branch (#128), the prompt's loudness guidance now matches what the backend actually emits under v2: - truePeak framing corrected from the v1 "linear amplitude proxy; >1.0 is an over" to dBTP ">0.0 dBTP is an inter-sample over; null for silence" — in both the citation hint table and the load-bearing MUST loudness-defect rule. Under v1 these would now tell Gemini the wrong over-threshold. - New PLR-driven mastering decision bands (plr = truePeak_dBTP − lufsIntegrated): PLR <=8 already-limited (don't pile on), 8-12 transparent limiter, >=14 dynamic (glue into a limiter). Plus a worked Glue Compressor card citing plr + lufsIntegrated. plr added to the Saturator/Limiter citation row. - bpmConfidence hedge at the v2 normalized 0.4 threshold (matches the frontend validator's NORMALIZED_LOW_CONFIDENCE_THRESHOLD): hedge tempo-locked advice and name the half/double-time alternative below 0.4. All cited paths (plr, truePeak, bpmConfidence, lufsIntegrated, crestFactor) are real top-level Phase1Result fields, so the citation-existence validator accepts them. test_phase2_prompt_catalog gains a regression case pinning the v2 units + new rules; the PLR worked card validates against the Live 12 catalog. Descriptor-hooks expansion (snare/hihat/reverb in _build_descriptor_hooks) is deferred — those fields are already in the citable catalog and the hook work carries the documented invented-citation hazard; a focused follow-up is safer. https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q --- apps/backend/prompts/phase2_system.txt | 12 +++++++++-- .../tests/test_phase2_prompt_catalog.py | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/backend/prompts/phase2_system.txt b/apps/backend/prompts/phase2_system.txt index 3a21c045..c4f81dca 100644 --- a/apps/backend/prompts/phase2_system.txt +++ b/apps/backend/prompts/phase2_system.txt @@ -123,7 +123,7 @@ Scan this table once before drafting a recommendation. Cite the MOST SPECIFIC ap | Sidechain Glue / Compressor | `sidechainDetail.pumpingRate`, `sidechainDetail.pumpingStrength`, `sidechainDetail.pumpingConfidence`, `sidechainDetail.envelopeShape32` (preferred for attack/release), `sidechainDetail.envelopeShape` | | Reverb device | `reverbDetail.rt60`, `reverbDetail.tailEnergyRatio`, `reverbDetail.perBandRt60.{low|lowMids|highMids|highs}`, `reverbDetail.preDelayMs`, `reverbDetail.isWet`; per-stem: `stemAnalysis.{stem}.reverbDetail.*` | | Stereo width / Utility | `stereoDetail.stereoWidth`, `stereoDetail.stereoCorrelation`, `stereoDetail.subBassCorrelation`, `stereoDetail.bandCorrelations.{band}`, `stereoDetail.correlationCurve`; per-stem: `stemAnalysis.{stem}.stereoDetail.*` | -| Saturator / Limiter ceiling | `truePeak` (linear amplitude proxy; >1.0 is an over), `saturationDetail.clippedSampleCount`, `saturationDetail.clippedSamplePercent`, `saturationDetail.peakRatio95to50`, `saturationDetail.rmsToPeakRatioDb`, `saturationDetail.saturationLikely` | +| Saturator / Limiter ceiling | `truePeak` (dBTP; 0.0 dBTP is full scale, so >0.0 is an inter-sample over; `null` for silence), `plr` (peak-to-loudness ratio, LU), `saturationDetail.clippedSampleCount`, `saturationDetail.clippedSamplePercent`, `saturationDetail.peakRatio95to50`, `saturationDetail.rmsToPeakRatioDb`, `saturationDetail.saturationLikely` | | Groove / MIDI quantize / humanize | `bpm`, `grooveDetail.perDrumSwing.{kick|snare|hihat}`, `grooveDetail.kickAccent`, `grooveDetail.hihatAccent`, `rhythmDetail.tempoCurve` | | Drum-bus character / hat-snare moves | `kickDetail.*`, `snareDetail.*`, `hihatDetail.*`, `transientDensityDetail.{band}.onsetRatePerSecond` / `.eventCount` | | Synthesis / Sound design | `kickDetail.*`, `bassDetail.*`, `supersawDetail.*`, `acidDetail.*`, `synthesisCharacter.oddToEvenRatio`, `synthesisCharacter.inharmonicity`, `textureCharacter.textureScore`, `textureCharacter.lowBandFlatness`, `textureCharacter.midBandFlatness`, `textureCharacter.highBandFlatness`, `melodyDetail.*` | @@ -141,6 +141,7 @@ Recent measurement additions are easy to miss; cite them when relevant rather th - lufsCurve.shortTerm / lufsCurve.momentary — per-frame EBU R128 loudness over time. Cite for section-relative dynamics ("the short-term LUFS rises from -16 LU at 1:23 to -8 LU at 1:30"), breakdown-vs-drop contrast, automation hints. - spectralBalanceTimeSeries — per-frame 7-band balance ([{t, subBass, lowBass, lowMids, mids, upperMids, highs, brilliance}, ...]). Cite for filter-sweep claims, build-up brightness opening, time-localized spectral motion that the spectralBalance scalars conflate away. - rhythmDetail.tempoCurve — instantaneous-BPM curve smoothed across beats. Cite when discussing ritardando/accelerando, DJ-tool tempo blends, or any tempo-modulated section. If the curve is flat, the single mean BPM is enough. +- bpmConfidence (0–1, normalized) — confidence in the detected bpm. When bpmConfidence < 0.4 the tempo reading is shaky (commonly a half-/double-time ambiguity): hedge any tempo-locked recommendation, name the likely alternative ("≈128 BPM, but confidence is low — it may be half-time at 64"), and avoid hard tempo-sync values. When ≥ 0.4 you may commit to the bpm. Cite bpm with bpmConfidence. - rhythmDetail.downbeats — measured bar-1 (downbeat) timestamps, phase-resolved from the kick-accent pattern within the detected meter (no longer a fixed every-4th-beat assumption). Cite for bar-aligned moves: where a drop/section should land, scene/clip boundaries on bar 1, arrangement locators. ALWAYS pair with rhythmDetail.downbeatConfidence — when it is below ~0.4 (typical of four-on-the-floor where every beat carries a kick), the beat grid is solid but which beat is "1" is uncertain, so hedge the bar-1 claim accordingly. - stereoDetail.correlationCurve — 1-second windowed L/R correlation, full and sub-band. Cite for Utility-width automation, mono-collapsing the drop, stereo motion the global stereoCorrelation misses. - arrangementDetail.noveltyCurve — higher-resolution (256-point) arrangement-change timeline. Cite for build-up ramps, transition density, sections-within-sections detail. arrangementDetail.noveltyPeaks remains the peak summary. @@ -157,9 +158,14 @@ STEREO IMAGING RULES (Utility / EQ Eight M/S) - Frequency-selective width: when stereoDetail.bandCorrelations.{band} shows one band anomalously narrow or wide, reach for EQ Eight Mode = M/S to treat that band's side energy — rule-only guidance, do not template a separate card for it. SATURATION / CLIPPING HINTS (Phase 1.C #5) -- MUST address measured loudness defects: if `saturationDetail.clippedSampleCount` > 0 (digital clipping) OR `truePeak` is above 1.0 (truePeak is a LINEAR amplitude proxy — 1.0 is 0 dBFS full scale, so values above 1.0 are inter-sample overs), you MUST include at least one card in `abletonRecommendations` or `mixAndMasterChain` with category MASTERING or DYNAMICS (e.g. a Limiter with Ceiling -0.3 dB, or a pre-master Saturator) whose `phase1Fields` cite the triggering measurement (`saturationDetail.clippedSampleCount` and/or `truePeak`). This is the one loudness move you may not omit — it corrects an objective defect, not a stylistic choice. Choosing an overall loudness target ("too loud / too quiet") remains your judgement; this rule fires only on measured clipping/overs. (A consistency check flags a missing action as MISSING_LOUDNESS_ACTION.) +- MUST address measured loudness defects: if `saturationDetail.clippedSampleCount` > 0 (digital clipping) OR `truePeak` is above 0.0 dBTP (truePeak is in dBTP — 0.0 dBTP is 0 dBFS full scale, so values above 0.0 are inter-sample overs; `null` means silence and does not trigger this rule), you MUST include at least one card in `abletonRecommendations` or `mixAndMasterChain` with category MASTERING or DYNAMICS (e.g. a Limiter with Ceiling -0.3 dB, or a pre-master Saturator) whose `phase1Fields` cite the triggering measurement (`saturationDetail.clippedSampleCount` and/or `truePeak`). This is the one loudness move you may not omit — it corrects an objective defect, not a stylistic choice. Choosing an overall loudness target ("too loud / too quiet") remains your judgement; this rule fires only on measured clipping/overs. (A consistency check flags a missing action as MISSING_LOUDNESS_ACTION.) - saturationDetail.clippedSamplePercent / saturationDetail.clippedSampleCount — any non-zero clipping is a real signal. Cite when recommending Limiter ceilings or Saturator pre-master moves. - saturationDetail.peakRatio95to50 / saturationDetail.rmsToPeakRatioDb — proxy for compression/limiting depth. Low peakRatio (<1.7) + low rmsToPeakRatioDb (<7 dB) ≈ heavy mastering. Cite for makeup-gain / limiter recommendations. +- plr (peak-to-loudness ratio, LU) = truePeak(dBTP) − lufsIntegrated. It is the master's dynamic headroom and should drive the mastering move (cite it on the card). The absolute loudness *target* stays your judgement; these bands shape *how* you get there: + - PLR ≤ 8 LU → already loud and limited. Do NOT pile on more limiting; if more loudness is wanted, prefer gentle parallel/clipping moves and warn that further limiting squashes transients. Cite plr (and crestFactor). + - PLR 8–12 LU → typical mastered range; a transparent Limiter (Ceiling -0.3 to -1.0 dB) with modest gain fits. Cite plr with truePeak. + - PLR ≥ 14 LU → dynamic, peaky master with headroom; bus glue compression into a limiter can raise perceived loudness without clipping. Cite plr with lufsIntegrated. + When plr is null (silence or undefined truePeak), do not make a PLR-based claim. - saturationDetail.saturationLikely is hint-only. Treat as low-confidence; hedge ("saturation may be present" rather than "saturation is present") and ground the claim in the specific scalar that triggered it. SIDECHAIN ENVELOPE (Phase 1.C #6) @@ -483,6 +489,8 @@ abletonRecommendations - {"device":"Operator","deviceFamily":"NATIVE","trackContext":"Acid Bass","workflowStage":"SOUND_DESIGN","category":"SYNTHESIS","parameter":"Amp Envelope Decay","value":"320 ms","reason":"The measured bass sustain is compact and percussive.","phase1Fields":["bassDetail.averageDecayMs","bassDetail.transientRatio"],"advancedTip":"Add a second oscillator one octave down only if the measured sub range still feels thin."} - Valid mastering example card (use this shape whenever a loudness defect is measured — see the MUST rule in SATURATION / CLIPPING HINTS): - {"device":"Limiter","deviceFamily":"NATIVE","trackContext":"Master","workflowStage":"MASTER","category":"MASTERING","parameter":"Ceiling","value":"-0.3 dB","reason":"The master shows measured digital clipping, so a true-peak limiter at -0.3 dB restores headroom before the clipped peaks.","phase1Fields":["saturationDetail.clippedSampleCount","saturationDetail.clippedSamplePercent"],"advancedTip":"Also enable the Limiter's True Peak parameter (set it On) so inter-sample overs are caught on lossy export — Ceiling alone limits sample peaks, not true peaks. Slow the Release to 120-180 ms to avoid audible pumping."} +- Valid PLR-driven mastering card (no clipping measured, but the master's dynamics suggest a move — see the plr bands in SATURATION / CLIPPING HINTS): + - {"device":"Glue Compressor","deviceFamily":"NATIVE","trackContext":"Master","workflowStage":"MASTER","category":"MASTERING","parameter":"Makeup","value":"2 dB","reason":"A PLR of 15 LU shows a dynamic, peaky master with headroom, so gentle bus glue plus makeup lifts perceived loudness before the limiter without crushing transients.","phase1Fields":["plr","lufsIntegrated"],"advancedTip":"Slow Attack (10-30 ms) so transients pass, Ratio 2:1-4:1, aim for 1-2 dB of gain reduction, then follow with a Limiter at -0.3 dB Ceiling."} - Valid sidechain example card (emit when pumping is measured — see SIDECHAIN ENVELOPE; omit when pumpingConfidence < 0.2): - {"device":"Compressor","deviceFamily":"NATIVE","trackContext":"Bass","workflowStage":"MIX","category":"DYNAMICS","parameter":"Sidechain","value":"On (Kick trigger); Threshold -22 dB, Ratio 6:1","reason":"Recreates the measured quarter-note ducking by routing the kick to the Compressor sidechain, with attack and release read from the envelope.","phase1Fields":["sidechainDetail.pumpingStrength","sidechainDetail.pumpingConfidence","sidechainDetail.envelopeShape32"],"advancedTip":"Set Release so gain returns by the recovery point shown in envelopeShape32 — a return by step 6 of 32 at the measured BPM is a short release; switch to the Glue Compressor for quarter/eighth glue."} - Valid stereo example card (emit when sub-bass is not mono — omit when subBassMono is already true): diff --git a/apps/backend/tests/test_phase2_prompt_catalog.py b/apps/backend/tests/test_phase2_prompt_catalog.py index a8b3e042..84526978 100644 --- a/apps/backend/tests/test_phase2_prompt_catalog.py +++ b/apps/backend/tests/test_phase2_prompt_catalog.py @@ -50,6 +50,27 @@ def test_prompt_blocks_old_invalid_parameter_strings(self): self.assertIn('"parameter":"Amp Envelope Decay"', prompt) self.assertIn("Reverb device Predelay parameter", prompt) + def test_prompt_loudness_units_are_v2_dbtp_with_plr_and_bpm_hedge(self): + """WS2: the prompt must teach Phase 1 v2 loudness semantics. + + truePeak is dBTP (not a linear proxy), the mastering move is driven by + the now-correct plr, and the tempo hedge uses the v2 normalized 0.4 + bpmConfidence threshold (matching phase2Validator's shared threshold). + """ + prompt = _load_prompt() + + # v2: truePeak is dBTP. The old linear framing (1.0 == full scale, + # >1.0 == over) would mislead Gemini now that >0.0 dBTP is the over. + self.assertNotIn("linear amplitude proxy", prompt) + self.assertIn("above 0.0 dBTP", prompt) + + # PLR-driven mastering decision bands surface the now-correct plr. + self.assertIn("peak-to-loudness ratio", prompt) + self.assertIn("PLR ≤ 8 LU", prompt) + + # bpmConfidence hedge at the v2 normalized 0.4 threshold. + self.assertIn("bpmConfidence < 0.4", prompt) + if __name__ == "__main__": unittest.main() From d1b3c9f405156df996e78dc2333dd04beb15bc11 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 02:37:25 +0000 Subject: [PATCH 2/2] =?UTF-8?q?WS2=20review:=20close=20the=20PLR=20band=20?= =?UTF-8?q?gap=20(extend=20middle=20band=20to=208=E2=80=9314=20LU)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PLR mastering bands were ≤8 / 8–12 / ≥14, leaving 12–14 LU with no instruction (a PLR-13 master had nothing to anchor to). Extend the middle band to 8–14 LU so the bands are contiguous. Regression test pins it. https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q --- apps/backend/prompts/phase2_system.txt | 2 +- apps/backend/tests/test_phase2_prompt_catalog.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/backend/prompts/phase2_system.txt b/apps/backend/prompts/phase2_system.txt index c4f81dca..b60b3863 100644 --- a/apps/backend/prompts/phase2_system.txt +++ b/apps/backend/prompts/phase2_system.txt @@ -163,7 +163,7 @@ SATURATION / CLIPPING HINTS (Phase 1.C #5) - saturationDetail.peakRatio95to50 / saturationDetail.rmsToPeakRatioDb — proxy for compression/limiting depth. Low peakRatio (<1.7) + low rmsToPeakRatioDb (<7 dB) ≈ heavy mastering. Cite for makeup-gain / limiter recommendations. - plr (peak-to-loudness ratio, LU) = truePeak(dBTP) − lufsIntegrated. It is the master's dynamic headroom and should drive the mastering move (cite it on the card). The absolute loudness *target* stays your judgement; these bands shape *how* you get there: - PLR ≤ 8 LU → already loud and limited. Do NOT pile on more limiting; if more loudness is wanted, prefer gentle parallel/clipping moves and warn that further limiting squashes transients. Cite plr (and crestFactor). - - PLR 8–12 LU → typical mastered range; a transparent Limiter (Ceiling -0.3 to -1.0 dB) with modest gain fits. Cite plr with truePeak. + - PLR 8–14 LU → typical mastered range; a transparent Limiter (Ceiling -0.3 to -1.0 dB) with modest gain fits. Cite plr with truePeak. - PLR ≥ 14 LU → dynamic, peaky master with headroom; bus glue compression into a limiter can raise perceived loudness without clipping. Cite plr with lufsIntegrated. When plr is null (silence or undefined truePeak), do not make a PLR-based claim. - saturationDetail.saturationLikely is hint-only. Treat as low-confidence; hedge ("saturation may be present" rather than "saturation is present") and ground the claim in the specific scalar that triggered it. diff --git a/apps/backend/tests/test_phase2_prompt_catalog.py b/apps/backend/tests/test_phase2_prompt_catalog.py index 84526978..105a4bd7 100644 --- a/apps/backend/tests/test_phase2_prompt_catalog.py +++ b/apps/backend/tests/test_phase2_prompt_catalog.py @@ -64,9 +64,11 @@ def test_prompt_loudness_units_are_v2_dbtp_with_plr_and_bpm_hedge(self): self.assertNotIn("linear amplitude proxy", prompt) self.assertIn("above 0.0 dBTP", prompt) - # PLR-driven mastering decision bands surface the now-correct plr. + # PLR-driven mastering decision bands surface the now-correct plr, and + # the bands are contiguous (no 12–14 LU gap — review follow-up). self.assertIn("peak-to-loudness ratio", prompt) self.assertIn("PLR ≤ 8 LU", prompt) + self.assertIn("PLR 8–14 LU", prompt) # bpmConfidence hedge at the v2 normalized 0.4 threshold. self.assertIn("bpmConfidence < 0.4", prompt)