diff --git a/apps/backend/prompts/phase2_system.txt b/apps/backend/prompts/phase2_system.txt index 3a21c045..b60b3863 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–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. 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..105a4bd7 100644 --- a/apps/backend/tests/test_phase2_prompt_catalog.py +++ b/apps/backend/tests/test_phase2_prompt_catalog.py @@ -50,6 +50,29 @@ 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, 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) + if __name__ == "__main__": unittest.main()