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
12 changes: 10 additions & 2 deletions apps/backend/prompts/phase2_system.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.*` |
Expand All @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
23 changes: 23 additions & 0 deletions apps/backend/tests/test_phase2_prompt_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()