Skip to content

Track Store for Sessions#150

Open
BuffMcBigHuge wants to merge 10 commits into
mainfrom
marco/feat/session-mods
Open

Track Store for Sessions#150
BuffMcBigHuge wants to merge 10 commits into
mainfrom
marco/feat/session-mods

Conversation

@BuffMcBigHuge
Copy link
Copy Markdown
Collaborator

@BuffMcBigHuge BuffMcBigHuge commented May 26, 2026

Summary

This PR adds browser-local saved sessions to the realtime motion graph performance UI. A performer can save the current session, reopen it later from the same browser, rename/delete saved sessions, and see an always-visible dirty/save status pill while performing.

The saved session snapshot captures the active runtime config, selected fixture, uploaded custom track metadata, stem overlay state, and local audio assets needed to restore the performance state without requiring a fresh upload.

What Changed

  • Added a local saved sessions controller that manages save, save-copy, open, rename, delete, active-session tracking, dirty-state detection, and user-facing errors.
  • Added SavePill and SessionsTile UI surfaces so saved sessions are available directly from the performance screen and drawer.
  • Added IndexedDB-backed persistence for session records, decoded upload PCM, original upload files, and cached stem assets.
  • Added session snapshot capture/restore helpers that serialize performance state and hydrate custom tracks back into the live store.
  • Extended custom track metadata with stable asset IDs, trim windows, original filenames, stem asset IDs, and a restored-session flag.
  • Added skip_stem_extraction through the frontend protocol and backend swap/start paths so restored sessions with cached stems do not re-run the backend stem extraction path.

Notes

Saved sessions are intentionally local to the browser/device. The audio assets live in IndexedDB, while a legacy localStorage read path keeps older lightweight saved session records from disappearing. Saved custom-track stems must be ready before saving a non-full source mode, because restore depends on the cached stem PCM.

Deleting a session removes the session record but leaves shared cached audio assets untouched, which avoids breaking other saved sessions that may reference the same uploaded audio.

Test Plan

No automated checks are currently reported on this PR branch.

Recommended manual validation:

  • Upload and trim a custom track, choose full, save the session, reload the page, and reopen it from Local Saved Sessions.
  • Upload a custom track, choose vocals or instruments, wait for stem extraction, save, reload, and confirm restore uses cached stem audio without triggering backend extraction again.
  • Verify Save, Save copy, Rename, Delete, and Open flows from the drawer.
  • Verify the SavePill transitions between unsaved, saving, and saved states after control changes.
  • Exercise IndexedDB unavailable/quota failure paths enough to confirm the UI surfaces an error instead of silently losing the session.

Signed-off-by: BuffMcBigHuge <marco@bymar.co>
Signed-off-by: BuffMcBigHuge <marco@bymar.co>
@BuffMcBigHuge BuffMcBigHuge marked this pull request as ready for review May 26, 2026 20:37
@BuffMcBigHuge
Copy link
Copy Markdown
Collaborator Author

Review: Track Store for Sessions (#150)

Full review of purpose, correctness, and complexity. TL;DR: the feature is well-scoped and the backend wiring is clean, but the branch does not type-check (5 tsc errors), so next build/CI will fail — that has to be fixed before merge. There's also meaningful duplicate/dead code and a storage-doubling issue worth trimming while we're here.


Purpose (as I understand it)

Browser-local saved sessions for the realtime motion-graph performance UI: save / open / rename / delete sessions persisted in IndexedDB. A snapshot captures runtime config + selected fixture + custom-track metadata + stem-overlay state, plus the decoded PCM, original upload files, and cached MelBand stems needed to restore without a re-upload. A new skip_stem_extraction flag is threaded frontend→backend so a restored session with cached stems doesn't re-rip stems. UI is a SavePill (live dirty/saved status) and a SessionsTile in the existing drawer.

This is a reasonable, self-contained feature and the snapshot/restore split is sound in principle.


Blocking — the branch is broken (fails tsc --noEmit)

Running tsc --noEmit in demos/realtime_motion_graph_web/web produces 5 errors, all introduced by this PR:

  1. pending state vs PendingTrackUpload mismatchAudioSourceCrate.tsx:269, LiteTrackCarousel.tsx:153, TrackPicker.tsx:139.
    The local pending useState declares trimStartS: number; trimEndS: number (required), but commitUploadedTrack's PendingTrackUpload declares them optional (trimStartS?). Passing setPending into commitUploadedTrack({ setPending }) is therefore type-incompatible.
    Fix: make the three local states trimStartS?: number; trimEndS?: number to match PendingTrackUpload (or make them required in PendingTrackUpload — pick one).

  2. Unguarded optional decodeduseLocalSavedSessions.ts:149 and sessionSnapshot.ts:168.
    CustomTrack.decoded is DecodedFixture | undefined, but both spots pass track.decoded into a required DecodedFixture parameter without a guard. collectStoredSession already checks if (!track) throw — it just needs if (!track.decoded) throw too.

These would have been caught by running the existing npm run typecheck. Please add that to whatever runs on the PR.


Over-engineering / complexity to cut

  1. Dead, duplicated persistence path in sessionSnapshot.ts.
    persistSessionSnapshotAssets, relinkSessionAudioAsset, the RestoreProgress/onProgress machinery, and the deleteSessionAudioAsset / deleteSessionUploadFile helpers are never called anywhere. persistSessionSnapshotAssets in particular is a near-line-for-line duplicate of collectStoredSession + seedSessionAssets in the hook. There are two parallel implementations of "iterate custom tracks, persist PCM/file/stems, validate stems-ready." Keep one. Deleting the dead exports removes a large slice of this 383-line file and the second source of truth.

  2. PCM is stored twice in IndexedDB.
    On save, collectStoredSession embeds the full decoded PCM (audioAssets with Float32Array) and the original Files inside the session record written to the sessions store, while seedSessionAssets also writes the identical PCM/files to the audio/files stores. The embedded copies are only consumed to re-seed on open — but those assets are already present (delete never removes them). Net effect: every saved session persists its (potentially many-MB) audio twice, doubling quota pressure.
    On top of that, readSessions()listLocalSavedSessionRecords() does getAll() on the sessions store at app load, pulling every session's embedded PCM into memory just to map(asSummary) and throw it away.
    Fix: the session record should hold only metadata + asset IDs (the LocalSavedSessionRecord shape). Assets live solely in the audio/files stores. This also removes the redundant re-seed on open.

  3. Redundant asset passes on open.
    open() runs seedSessionAssetsapplySessionSnapshotcheckSessionCompleteness (reads each asset back from IDB) → hydrateCustomTracks (reads them again). Three async passes over the same assets. With trt: dynamic LoRA library with register/enable/disable lifecycle #4 applied, the seed step disappears and the completeness check can fold into hydrate (hydrate already reports missing assets).

  4. add()'s union-overload signature is awkward.
    metadataOrPersisted?: CustomTrackAssetMetadata | boolean followed by a trailing persisted arg, with a runtime typeof === "boolean" branch, exists only for back-compat with the two old add(..., true) call sites. A single options object (or a separate addWithMetadata) would be clearer than positional polymorphism.


Correctness / perf (non-blocking but worth it)

  1. syncDirty serializes the whole snapshot on every store tick.
    It subscribes to the performance/lora/curve/customTracks/stemOverlay stores and, on each emission, calls captureSessionSnapshot() (builds config + iterates all custom tracks) then JSON.stringify via sessionSnapshotSignature. During a live performance, param/MIDI/automation updates fire these stores at high frequency, so this stringifies the full config on every tick. Debounce it (rAF or a trailing timeout).

  2. dirty defaults to true so the SavePill reads "Unsaved changes" on a brand-new, untouched session. Minor UX noise; fine to leave, but worth a thought.

  3. No tests. The PR body confirms none. The signature round-trip (capture → signature stability), snapshot validation, and the skip_stem_extraction gating in session.py are all cheap to unit-test and would have caught the build break.


What's good / direct

  • Backend skip_stem_extraction thread is clean and minimalconfig.pystate.pysession.py (both swap and start paths) → ws_adapter.py → protocol types, consistently gating resolve_upload_stem_source_mode. This is the to-the-point part of the PR.
  • resolveBackendSourceMode / shouldSkipStemExtraction split is a tidy way to keep the overlay sourceMode while telling the backend not to re-rip.
  • sessionAudioAssets.ts is a small, sensible IndexedDB wrapper (versioned, onupgradeneeded, per-store helpers).
  • Reusing the existing AdvancedDrawer savedTab/unsavedDot extension points (already on main) avoids UI churn, and mobile is covered via MobileFullSheet passthrough.

Suggested order of work

  1. Fix the 5 type errors (item 1–2) and wire npm run typecheck into PR checks.
  2. Collapse to a single persistence path; stop embedding PCM in the session record (items 3–5).
  3. Debounce syncDirty (item 7).
  4. Add a couple of unit tests for snapshot signature + skip_stem_extraction gating (item 9).

The feature is worth landing; it mainly needs the build fixed and the duplicate/double-stored persistence trimmed down.

Signed-off-by: BuffMcBigHuge <marco@bymar.co>
Signed-off-by: BuffMcBigHuge <marco@bymar.co>
@BuffMcBigHuge BuffMcBigHuge mentioned this pull request Jun 3, 2026
8 tasks
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