Skip to content

Live 12 catalogue + Phase 2 output validator (warn-and-keep), symusic MIDI migration - #119

Merged
slittycode merged 8 commits into
mainfrom
claude/priceless-williams-d36dd2
May 29, 2026
Merged

Live 12 catalogue + Phase 2 output validator (warn-and-keep), symusic MIDI migration#119
slittycode merged 8 commits into
mainfrom
claude/priceless-williams-d36dd2

Conversation

@slittycode

Copy link
Copy Markdown
Owner

Summary

Two related backend efforts on this branch:

1. Live 12 catalogue + Phase 2 output validator (advisory). Extracts the upstream AbletonLive12_MIDIRemoteScripts into a checked-in catalogue of (device, parameter) tuples (data/live12_catalogue.json + schema, built by scripts/build_live12_catalogue.py), then inspects every Phase 2 Gemini recommendation against it. Devices that aren't in the catalogue, parameters not on the device, out-of-range values, or missing phase1Fields citations surface as RECOMMENDATION_UNVERIFIED advisory warnings on the existing validationWarnings channel. This closes the failure mode where Gemini named a device/parameter the user couldn't actually find in Live.

2. symusic MIDI migration. Migrates the backend's MIDI write/parse paths (sample_synthesis.write_midi, the research polyphonic/ground-truth parsers) off pretty_midi onto symusic, making symusic the single canonical MIDI library on the backend. pretty_midi is retained only as the deliberate parity reader in test_sample_synthesis. Pins symusic==0.6.0 in requirements.txt.

Review-driven redesign: drop/rewrite → warn-and-keep

The validator was originally a mutating gate that dropped recommendations and rewrote parameters. Two independent Opus reviews flagged this as a critical, ship-blocking defect:

  1. Confidently-wrong rewrites. difflib fuzzy resolution rewrote a producer's "Band 1 Frequency" to "1 Frequency B" on EQ Eight — wrong band and wrong A/B curve — and shipped it with full authority. A rewrite to the wrong control is worse than the gap it tried to close.
  2. Silent dropping of valid advice. The statically-extracted (and therefore incomplete) catalogue couldn't confirm legitimate controls like EQ Eight "Band 1 Gain", so it deleted them.
  3. Gate exception → 502. The gate ran inside the try block wrapping the Gemini call, so a catalogue load/parse error would convert an otherwise-successful interpretation into a GEMINI_GENERATE_FAILED 502.

Both defects violate the product's measurement-cited specificity invariant (PURPOSE.md #3). The fix converts apply_live12_catalogue_gates to advisory warn-and-keep, matching the sibling _validate_phase2_citation_paths:

  • Never drops a recommendation; never rewrites a parameter. phase2_result is no longer mutated.
  • fuzzy_resolve is demoted to a warning-suppressor: a close-but-inexact parameter is kept silently (no wrong-band mutation); only a parameter with no close match warns. The fuzzy target is never trusted to rewrite.
  • The new RECOMMENDATION_UNVERIFIED code is intentionally not in the frontend SALVAGE_WARNING_CODES set — nothing is salvaged/dropped, so it renders as a plain advisory warning via the existing graceful fallback rather than tripping the "Gemini output was repaired" decision gate. No frontend change required.
  • The gate call in server.py is wrapped in its own try/except → a catalogue error degrades to a CATALOGUE_CHECK_UNAVAILABLE warning instead of a 502.

A regression test asserts EQ-band phrasings ("Band 1 Frequency", "Frequency 1", "Band 1 Gain", …) are never rewritten against the real shipped catalogue, plus coverage that no record is ever dropped.

Commits

  1. feat(backend): Live 12 catalogue + Phase 2 output validator
  2. refactor(backend): migrate sample_synthesis.write_midi to symusic
  3. refactor(backend): migrate research MIDI parsers off pretty_midi to symusic
  4. docs(backend): drop PR-E (symusic-beat 4th method) — out of scope
  5. fix(backend): pin missing symusic dependency after sample_synthesis migration
  6. fix(backend): downgrade Live 12 catalogue gate to warn-and-keep (the review fix)

Test plan

  • Full backend suite green: 951 pass, 1 skip (./venv/bin/python -m unittest discover -s tests)
  • New regression test: EQ-band parameter phrasings are never rewritten against the real shipped catalogue
  • New coverage: no recommendation is ever dropped under any failing check
  • test_server.py salvage test (COERCED_ENUM_VALUE) still passes — the _valid_phase2_result() fixture produces zero catalogue warnings under warn-and-keep
  • Live Gemini smoke on a known track (RUN_GEMINI_LIVE_SMOKE=true) — recommended before merge to confirm advisory warnings render cleanly on the results surface

🤖 Generated with Claude Code

slittycode and others added 6 commits May 28, 2026 23:26
Drops pretty_midi from the sample synthesis MIDI-write path; symusic
becomes the single canonical MIDI library on the backend. The .mid file
is a spec-conformant Standard MIDI file that DAWs parse identically, so
the change is non-audible — the existing parity gate in
tests/test_sample_synthesis.WriteMidiTests reads the symusic output back
via pretty_midi and confirms the notes survive the round trip.

Module docstring updated to call out the new writer + the parity
contract for the next maintainer.

919 / 919 backend unittests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ymusic

Research-only path: polyphonic_evaluation.summarize_midi_file and
scripts/import_midi_to_ground_truth.py now parse MIDI via
symusic.Score(path).to("Second") instead of pretty_midi.PrettyMIDI.
Test fixture writers in test_polyphonic_evaluation.py and
test_phase1_evaluation_transcription.py likewise build their tiny .mid
inputs through symusic.

Drops three pretty_midi importers; only test_sample_synthesis.py keeps
pretty_midi as the deliberate parity reader for write_midi's output.

919 / 919 backend unittests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The consolidation plan called for adding symusic-beat as an observational
fourth method in beat_evaluation.py. Probing symusic's API shows
get_beats / get_downbeats operate on symbolic Score tempo maps, not on
audio — symusic is not an audio-analysis library and has nothing to
offer this harness, which takes audio paths.

Recording the rationale in the file's header docstring so a future
maintainer doesn't re-do the same investigation. The pre-registered
beat_this vs kick_accent gate (ADOPT_MARGIN = 0.10) is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract gluon/AbletonLive12_MIDIRemoteScripts via stdlib `ast` into a
checked-in catalogue of (device, parameter) tuples, then gate every Phase 2
Gemini recommendation against it. Recommendations whose device is unknown,
whose parameter is not on the device (after a difflib fuzzy_resolve attempt),
or whose phase1Fields citation is missing/empty get dropped from
`abletonRecommendations` / `mixAndMasterChain` / `secretSauce.workflowSteps`,
with structured `RECOMMENDATION_REJECTED` events emitted onto the existing
`validationWarnings` channel. Fuzzy rewrites emit a `PARAMETER_REWRITTEN`
event carrying original + resolved + requestId so nothing is rewritten
silently.

Why: Phase 2 was emitting Live 12 device/parameter recommendations with no
machine-checkable proof the target was real. The upstream MIDI Remote
Scripts are the canonical map of what Live 12 exposes; turning that into
data closes the failure mode where Gemini hallucinated device names,
abbreviated parameter names, or out-of-range values that the user only
discovered when trying to apply them in Live.

Notable details:
* The numeric range gate is wired but inert when min/max are absent.
  Static-source extraction does not carry ranges (they live on runtime
  `Live.DeviceParameter` objects); reserved for future enrichment.
* The source catalogue covers NATIVE Live 12 only; eight MAX_FOR_LIVE
  devices in the curated prompt catalog are explicitly out of scope per
  GOAL and fall through to the existing `_validate_phase2_catalog_entry`
  warning-only check.
* Hand-mapped `displayName` + `category` per class lets the validator
  accept both `Eq8` (canonical Live class) and `EQ Eight` (Ableton UI
  label) without forcing the caller to pick one. A regression test asserts
  every native device in `prompts/live12_device_catalog.json` is recognized
  by `Live12Catalogue.has_device()`.
* The gate code lives in a sibling `phase2_catalogue_gates.py` module
  re-exported by `server_phase2.py` -- keeps it stdlib-only so unit tests
  run without the fastapi/essentia import chain.

50 new tests, all green. The existing 39 fastapi-dependent tests the
catalogue work could plausibly touch (test_phase2_citation_paths,
test_phase2_grammar_fix, test_phase2_prompt_catalog, test_server_phase2)
also pass post-refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…igration

Commit `029fb92e` ("refactor(backend): migrate sample_synthesis.write_midi
to symusic") introduced `from symusic import Note, Score, Tempo, Track` at
`apps/backend/sample_synthesis.py:31` but never added `symusic` to
`apps/backend/requirements.txt`. After a fresh
`./apps/backend/scripts/bootstrap.sh`, eight test modules failed to
import — every cascade traced back to that single import:
test_sample_synthesis (direct), test_sample_generation,
test_sample_audio_content, test_server, test_server_samples (via
server → server_samples → sample_generation → sample_synthesis),
test_cleanup, test_phase1_evaluation_transcription, and
test_polyphonic_evaluation.

Adds `symusic==0.6.0` to requirements.txt — matches the existing `==`
pin style, alphabetically between `submitit` and `sympy`, supports
Python 3.11 (the backend's required interpreter), and resolves cleanly
against the rest of the pin set.

Also updates `_valid_phase2_result()` in `tests/test_server.py` to
include `phase1Fields` on the three recommendation entries
(abletonRecommendations[0], mixAndMasterChain[0],
secretSauce.workflowSteps[0]). The fixture predates the catalogue gates'
citation-existence requirement; with symusic now installed the previously-
masked `test_run_interpretation_request_with_profile_config_surfaces_salvage_warnings_in_normal_diagnostics`
runs and exposed the gap. Fixture-only update; no contract change.

948 / 948 backend unittests pass (1 skipped, unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Independent review found the catalogue gate produced confidently-wrong
output and silently dropped legitimate advice:

  1. Fuzzy parameter resolution rewrote a producer's "Band 1 Frequency"
     to "1 Frequency B" on EQ Eight (wrong band AND wrong A/B curve) and
     shipped it with full authority via a PARAMETER_REWRITTEN event.
  2. Unknown-device / unknown-parameter checks DROPPED recommendations the
     statically-extracted (and therefore incomplete) catalogue couldn't
     confirm — deleting valid advice like EQ Eight "Band 1 Gain".

Both violate the product's measurement-cited specificity invariant, and a
rewrite to the wrong control is worse than the gap it tried to close.

Convert apply_live12_catalogue_gates to advisory warn-and-keep, matching the
sibling _validate_phase2_citation_paths:

  - Never drops a recommendation; never rewrites a parameter. phase2_result
    is no longer mutated.
  - Emits RECOMMENDATION_UNVERIFIED warnings (reason discriminator:
    device_unknown / parameter_unknown / value_out_of_range /
    citation_missing) on the existing validationWarnings channel.
  - fuzzy_resolve is demoted to a warning-suppressor: a close-but-inexact
    parameter is kept silently (no wrong-band mutation); only a parameter
    with no close match warns. The fuzzy target is never trusted to rewrite.
  - The new code is intentionally NOT in the frontend SALVAGE_WARNING_CODES
    set — nothing is salvaged/dropped, so it renders as a plain advisory
    warning via the graceful fallback rather than tripping the "Gemini
    output was repaired" decision gate.

Also wrap the gate call in server.py in its own try/except so a catalogue
load/parse error degrades to a CATALOGUE_CHECK_UNAVAILABLE warning instead
of converting an otherwise-successful Gemini interpretation into a 502.

Add a regression test asserting EQ-band phrasings ("Band 1 Frequency",
"Frequency 1", "Band 1 Gain", ...) are never rewritten against the real
shipped catalogue, plus coverage that no record is ever dropped. Full
backend suite: 951 pass, 1 skip.

Co-Authored-By: Claude Opus 4.7 (1M context) <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

Two coherent backend changes: a Live 12 source-catalogue validator that annotates Phase 2 recommendations advisory-only (warn-and-keep, never mutates), and a migration of all backend MIDI write/parse paths from pretty_midi to symusic. Phase boundary is clean throughout. The warn-and-keep redesign correctly eliminates the specific failure mode (confident-but-wrong fuzzy rewrites on multi-band EQ parameters) that motivated the change. The PR claims 951 pass / 1 skip; I cannot run the suite in this environment (no venv on this remote checkout), but the test files are present and the parity contract for write_midi is already wired via the pre-existing WriteMidiTests.test_write_midi_emits_expected_note (reads symusic output back with pretty_midi).

Findings

Worth considering

DeviceEntry is dataclass(frozen=True) with a dict field (_parameters_by_name: dict[str, ParamSpec]). frozen=True synthesises __hash__, which would raise TypeError: unhashable type: 'dict' if a DeviceEntry is ever used as a dict key or in a set. It isn't today, so this is latent, not current. MappingProxyType would fix it cleanly, but it's not a ship-blocker.

_FUZZY_MATCH_CUTOFF = 0.7 at live12_catalogue.py:37 is a bare constant with no calibration reference. The comment in phase2_catalogue_gates.py explains the intent (warning suppressor, never rewriter), and the EQ-band regression test in test_live12_catalogue.py asserts the critical non-rewrite invariant. That's sufficient for now, but document the cutoff origin somewhere before raising it — a 0.8 cutoff could miss the "Band 1 Frequency" → near-match suppression that the redesign relies on.

Score(480, ttype="Second") in sample_synthesis.write_midi (sample_synthesis.py:124): the 480 PPQ is stored but its only function when ttype="Second" is as a hint for MIDI serialisation. The WriteMidiTests parity read via pretty_midi confirms the output is spec-conformant. No action needed, just noting what the 480 is doing.

Not flagging

The re-export chain phase2_catalogue_gates → server_phase2 → server is intentional and documented (keeps the gates module stdlib-only for unit-test isolation). Valid pattern.

require_citation=True is hardwired at every call site in phase2_catalogue_gates.py. The parameter exists for future flexibility; not a bug.

pretty_midi remaining in requirements.txt after the migration is correct — it's retained exclusively as the parity reader in WriteMidiTests.

Test results

Suite not runnable (no venv in this environment). PR states 951 pass / 1 skip. The new test files are present and structurally sound: test_live12_catalogue.py (343 lines, covers API surface + generator + curated-catalogue regression), test_phase2_validator_catalogue.py (532 lines, covers all four warning codes + the explicit no-rewrite assertion). test_server.py fixture updated with phase1Fields citations so the warn-and-keep validator doesn't fire false positives against the existing salvage test.

Phase boundary check

Clean. apply_live12_catalogue_gates reads phase2_result without mutating it. The function is explicitly documented and tested for this invariant. server.py wraps the call defensively so a catalogue parse error produces a CATALOGUE_CHECK_UNAVAILABLE advisory warning rather than failing the Gemini interpretation. Phase 1 measurements are not read, modified, or re-derived anywhere in the new code.


Generated by Claude Code

slittycode and others added 2 commits May 29, 2026 17:44
Resolves the PR #119 conflict with #117 (transcription pianoroll) and
#118 (docs refresh).

Sole conflict: apps/backend/requirements.txt — both branches added the
identical symusic==0.6.0 pin; only the preceding comment differed. Kept
main's explanatory comment and broadened it to name both consumers:
#117's transcription_pianoroll.py and this branch's sample_synthesis.py /
research MIDI parsers, which made symusic the canonical backend MIDI lib.

server.py and test_server.py auto-merged cleanly — #117's pianoroll
endpoint (~line 2584) and this branch's Live 12 catalogue gate (~line
1636) occupy disjoint regions. Verified both survive: catalogue-gate
import/call/error-handler and pianoroll import/endpoint all present.

Full backend suite on the merged tree: 999 pass, 1 skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the PR #119 conflict with #120 (symusic consolidation +
pianoroll UI + route 404 fix), which landed a superset of this branch's
symusic work.

Sole conflict: apps/backend/sample_synthesis.py. This branch carried the
initial write_midi→symusic migration (029fb92); #120 incorporated that
and built the symusic.Synthesizer backend, _resolve_backend precedence,
and _build_score_from_plan extraction on top. All three conflict regions
were a strict superset on #120's side (HEAD's third region contributed
nothing), so the file was resolved by taking origin/main wholesale —
no unique content lost.

Everything else auto-merged. Two post-merge corrections to keep this PR
scoped to its real payload:
  * requirements.txt reset to match origin/main exactly. The auto-merge
    correctly dropped pretty_midi + mido (per #120) and kept the single
    symusic==0.6.0 pin; a leftover broadened comment from the earlier
    #117 merge round was reverted to #120's wording, since this branch
    adds no dependency of its own (the catalogue/validator is stdlib-only).
  * server.py combines #120's pianoroll-route 404 fix (unwrapped
    transcriptionDetail read, server.py:2735) with this branch's Live 12
    catalogue gate — verified the unwrapped read is primary, so #117's
    nested-key bug did not survive.

This branch's remaining unique payload is the Live 12 catalogue + Phase 2
output validator (1a2b5ad + the warn-and-keep fix a2a7b97); the symusic
commits are now redundant and collapse to zero net diff against main.

Full backend suite on the merged tree: 1011 pass, 2 skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@slittycode
slittycode merged commit a8f9976 into main May 29, 2026
3 checks passed
@slittycode
slittycode deleted the claude/priceless-williams-d36dd2 branch May 29, 2026 14:07
slittycode added a commit that referenced this pull request May 30, 2026
* docs: catch up agent/architecture docs to current code

CLAUDE.md, ARCHITECTURE.md, READMEs, and AGENTS.md files had drifted past
several recent shipped changes. This commit refreshes them so an agent
reading the docs sees what the code actually contains:

- Document `live12_catalogue.py` and `phase2_catalogue_gates.py` (the
  warn-and-keep Live 12 source-catalogue gate added in #119) across
  CLAUDE.md, apps/backend/ARCHITECTURE.md, apps/backend/AGENTS.md, and
  apps/backend/README.md. Clarify how the new `data/live12_catalogue.json`
  differs from the prompt-injected `prompts/live12_device_catalog.json`.
- Add the `data/` directory to the CLAUDE.md repo-layout map and root
  README; surface the recommendation-proof campaign docs (GOAL.md,
  NEEDS.md, RECOMMENDATION_VERDICT.md) in the doc tables and precedence
  order.
- Document the missing operator scripts: top-level
  `scripts/build_live12_catalogue.py` and
  `apps/backend/scripts/parity_probe_synth_backends.py`. Add
  `evaluate_recommendations.py` and `emit_deterministic_recs.ts` to the
  backend AGENTS.md script list.
- UI: document `RecommendationVerificationBadge`,
  `Phase2ConsistencyReport`, and the transcription pianoroll
  client/components in `apps/ui/README.md` and `apps/ui/AGENTS.md`.

https://claude.ai/code/session_01KKfCTxJ3Bbadbd2naTagSb

* docs: fix CLAUDE.md repo-layout intro contradiction

The Repo Layout intro declared `data/` on the product path but then
opened the numbered inventory with "The remaining top-level directories
are off-path and safe to skip" — contradicting itself for `data/` (and
arguably `packages/`).

Reword the intro to frame the list as the full top-level layout (with
product-path entries called out per item) instead of an off-path
inventory. No content moves; only the framing sentence changes.

Surfaced by PR #121 review.

https://claude.ai/code/session_01KKfCTxJ3Bbadbd2naTagSb

---------

Co-authored-by: Claude <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