feat(editor): multitrack stem ingest — import, rename, reorder, and pair studio tracks with chart tracks - #283
Merged
Merged
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (14)
📝 WalkthroughWalkthroughAdds multitrack audio-stem import and management, chart-track pairing, persistence in sloppaks, editor commands, stem-mixer soloing, and frontend/backend tests. ChangesMultitrack stem management
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant Editor as stem-tracks.js
participant API as routes.py
participant Storage as manifest or session
Editor->>API: Import audio files with stem_links
API->>Storage: Store stems and apply pairings
Storage-->>API: Return authoritative stem state
API-->>Editor: Return stems, stem_links, and persistence status
Suggested reviewers: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
ChrisBeWithYou
pushed a commit
that referenced
this pull request
Jul 15, 2026
…ource gating + isolation (review #283 items 15-18) Item 15: every stem op (pairing sync via new op 'links', rename, reorder, delete, import) ships the CURRENT S.stemLinks, the backend session seeds from the manifest on load and honors the submitted snapshot, and pairings ride the same manifest write as the op — so the authoritative response can never resurrect stale links over an unsaved pairing. Failed POSTs half-apply nothing. Item 16: stem responses carry a persisted verdict (dir-form sloppak = durable now; zip/create = Save/Build) and _adopt marks the session dirty on non-durable changes so the lifecycle guard can't silently discard them. Create-mode Build now ships stem_links too (the third save path). Item 17: editorSoloMyStem is gated on a real host.stemMixChanged consumer (stemMixerAvailable); with none wired it reports unavailable instead of flipping state nothing reads, and the menu greys it via needs:'stemMixer'. Item 18: solo-my-source is an exclusive isolate — enabling clears every other stem's solo, toggle-off restores the no-solo state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…air studio tracks with chart tracks Christian's ask: more than demucs's five — take N tracks from an actual studio session, ingest, rename, rearrange, and transcribe/chart against each as the user wishes. Backend (routes.py): - POST /import-stems (multipart, any count): filename-derived ids (_stem_safe_id: sanitized, deduped, 60-char cap), files into the sloppak session's stems/ + manifest stems list (zip-form persists on Save — the replace-audio rule) or, for create-mode GP/MIDI imports, into the session's stem_files which Build packs into the new pak (staging/stems/ + manifest entries in _write_sloppak_pak). - POST /stem-op rename|reorder|delete: pure list ops (_stems_rename_pure refuses collisions; _stems_reorder_pure demands a full permutation — partial orders get no authority to drop entries; _stems_manifest_list preserves unknown entry fields so another tool's stem metadata survives our edits); renames MOVE the file and follow the links; deletes drop their links. - editor_stem_links manifest extension key (chart-track key → stem id, the audio_shift absent/empty/garbage contract) at all three save paths + /load; /load's stems gate relaxed >=2 → >=1 (a lone imported track must reach the manager; the mixer strips keep their own gate). Frontend (src/stem-tracks.js + modal): - File ▸ Audio tracks…: import (multi-file), rename (click the name), reorder (▴▾ — the manifest is order-authoritative), delete, and a per-track pairing select over the chart tracks. Chart keys use the _partViewKeyPure id-or-name rule — never a bare index, so links survive part reordering. - 'Solo my source track' (registry + Transport menu): solos the current track's paired stem via S.stemMix — the transcription move; audible once the stem-mixer strips (#275) land, honest status either way. tests/stem_tracks.test.mjs (4, fails on main): row model + key rule, one-stem-per-track pairing, solo verb exactness, persistence wire. tests/test_stem_tracks.py (5): id sanitize/dedupe, rename/reorder pure hard-edges, unknown-field preservation, the links contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…ource gating + isolation (review #283 items 15-18) Item 15: every stem op (pairing sync via new op 'links', rename, reorder, delete, import) ships the CURRENT S.stemLinks, the backend session seeds from the manifest on load and honors the submitted snapshot, and pairings ride the same manifest write as the op — so the authoritative response can never resurrect stale links over an unsaved pairing. Failed POSTs half-apply nothing. Item 16: stem responses carry a persisted verdict (dir-form sloppak = durable now; zip/create = Save/Build) and _adopt marks the session dirty on non-durable changes so the lifecycle guard can't silently discard them. Create-mode Build now ships stem_links too (the third save path). Item 17: editorSoloMyStem is gated on a real host.stemMixChanged consumer (stemMixerAvailable); with none wired it reports unavailable instead of flipping state nothing reads, and the menu greys it via needs:'stemMixer'. Item 18: solo-my-source is an exclusive isolate — enabling clears every other stem's solo, toggle-off restores the no-solo state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
… mix The stem manager hides the `full` combined-mix entry (both /load and _stem_state_payload skip id=="full"), so S.stems — and the `order` the UI sends on a reorder — never contains it. But the stem-op reorder validated that order against the WHOLE manifest stems list via _stems_reorder_pure, which still includes `full`, so `sorted(order) != sorted(ids)` and every reorder on a real (stem-separated / replace-audio'd) sloppak returned 400 "order must be a full permutation". Root cause: the permutation was checked against the full manifest list instead of the surfaced subset. Fix: _reorder_with_hidden reorders only the entries the manager surfaced (the ids `order` names), keeps hidden entries (`full`, plus any payload-dropped missing-file/path-escape stems) at the front, and still fails closed on a foreign or duplicate id. Wired into both the sloppak and create-mode reorder branches. Regression: test_reorder_with_hidden_keeps_full_and_reorders_managed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byrongamatos
force-pushed
the
feat/editor-stem-tracks
branch
from
July 15, 2026 20:43
7b667de to
f2819f8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Bring in a whole studio session. Christian's ask: more than demucs's five — take N tracks from an actual session, ingest, rename, rearrange, and transcribe/chart against each as the user wishes.
editor_stem_linksmanifest extension key (theaudio_shiftretention contract, third use now) and key chart tracks by the id-or-name rule — never a bare index, so links survive part reordering. Renames follow the link; deletes drop it.S.stemMixcontract — the transcription move. Audible the moment feat(editor): stem mixer — isolate any instrument while editing #275's strips land; the pairing itself is stored, shown, and shipped today.stems/+ manifest; zip-form persists on Save — the replace-audio rule) and fresh GP/MIDI imports (session-held, packed by Build into the new feedpak'sstems/+ manifest)./load's stems gate relaxed≥2 → ≥1(a lone imported track must reach the manager; the mixer strips keep their own ≥2 gate).Backend hard edges (all pinned)
_stems_reorder_puredemands a full permutation — a partial order gets no authority to drop entries._stems_rename_purerefuses collisions and never mutates its input._stems_manifest_listpreserves unknown entry fields — another tool's stem metadata survives our edits. Path-traversal guards mirror_safe_stem_pathon every file op.Testing
tests/stem_tracks.test.mjs(4, fails on main) +tests/test_stem_tracks.py(5). Full suites green: 162 JS / 0 fail, pytest 264, lint at baseline. Live-verified end-to-end on the testbed: imported two synthesized "studio tracks" (Guitar L (DI).wav,Bass DI.wav) through the real endpoint → renamed → reordered → paired with Lead → saved → confirmedstems/GuitarMain.wav+stems/Bass_DI.wavandeditor_stem_links: {lead: GuitarMain}inside the pak's manifest on disk, alongside the untouched demucs stems → then deleted/reordered/unlinked and confirmed the pak restored exactly. Zero page errors.🤖 Generated with Claude Code
https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Summary by CodeRabbit