Add manual lyrics editor: tap-to-time and waveform drag-editing - #9
Conversation
Both existing lyrics plugins were generate-only (Whisper align, save) with no way to fix the ~10% alignment gets wrong or author lyrics for a song with no vocals stem. Adds a hand-editing surface: tap-to-time, drag/resize syllable blocks over a client-decoded waveform, text seeding with manual syllable splits, and loading/correcting a song's existing lyrics via a new GET /lyrics endpoint. Saves go through a new POST /save-lyrics, validated server-side the same way the sloppak loader filters lyrics.json on read. Also fixes a real bug in the existing align-based /save: it wrote lyrics.json into the extraction cache for zip-form sloppaks instead of the distributable .sloppak file, so edits were silently lost whenever the cache was invalidated. Both save paths now funnel through one _persist_lyrics helper (ported from lyrics_karaoke's mature zip-repack pattern) that rewrites directory-form packs in place and re-zips zip-form packs with a one-time .bak. Hardens _find_vocals_stem against a manifest-controlled path escaping the pack directory, matching the containment check already applied to request-supplied filenames.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Important
Two flows can silently write or discard the wrong data, and are worth addressing before merge: an alignment result isn't bound to the song it was computed for, so "Edit Manually"/"Save to Song" can persist the previous song's alignment into the currently selected one; and re-opening the editor (or leaving the screen) discards unsaved edits with no warning. Both are detailed inline, alongside two minor notes.
Reviewed changes
- Backend persistence rework — new
_persist_lyrics/_rezip_sloppak/_atomic_write_json; both/saveand the new/save-lyricsfunnel through it, so zip-form.sloppakedits now land in the distributable archive (with a one-time.bak) instead of the extraction cache. - New endpoints + manifest hardening —
GET /lyrics(existing lyrics, stem URLs, duration) andPOST /save-lyrics(validated; empty list clears the track);_safe_source_pathrefuses manifest-declared paths that escape the pack directory, and_find_vocals_stemnow routes through it. - Manual editor (screen.html/screen.js) — tap-to-time, waveform drag/resize editing, seed-from-text, A–B loop, playback speed, undo/redo, inspector; waveform peaks decoded once per stem via WebAudio and pre-rendered offscreen.
- Docs / manifest — README and plugin.json updates (feedBack rename, version 1.1.0, screen id
plugin-lyrics_sync).
ℹ️ The tests described in the PR body aren't in the diff
The "Testing" section claims a round-trip persistence test through core's own sloppak.load_song (including after wiping the extraction cache), a full TestClient exercise of GET /lyrics and POST /save-lyrics (with the empty-list clear case), path-traversal rejection tests, and node --check. None of it is committed — the diff touches five files and no test file, and the repo has no tests/ directory. CI already self-detects tests/test_*.py / test/test_*.py (org-wide reusable ci.yml), so the claimed tests would run for free if added. The zip-persistence fix in particular repairs a class of silent data-loss bug that shipped before; a committed regression test for the re-zip + cache-wipe path would be high-value.
ℹ️ Nitpicks
_lsEditorStartLoopkeeps a rAF loop redrawing every frame for as long as the editor is open, even when idle; gating the redraw on actual state changes (audio position, selection, drag) would avoid constant full-canvas redraws on low-end hardware.
Big Pickle (free) | 𝕏
- _lsAlignmentResult wasn't bound to the song it was computed for: align song A, select song B, then "Edit Manually"/"Save to Song" would persist A's alignment into B. Clear it (and hide the preview) whenever the selected song changes. - Re-opening the editor, or navigating away from the screen, silently discarded unsaved tap-to-time/drag edits with no warning. Added dirty tracking (generation + history-index pair, so undoing back to exactly the loaded/saved snapshot correctly reports clean again) and a confirm() guard in lsOpenEditor's reopen path, lsCloseEditor, and the showScreen navigation hook. Callers that switch songs now check lsCloseEditor()'s return value and abort the switch if the user kept the editor open, rather than moving on to a new song while the editor still points at the old one. - Saving a session with only untimed syllables (text seeded via "Add", nothing tapped/dragged yet) silently posted an empty list, which the server correctly treats as "clear the track" — wiping any lyrics already saved on the song, with the seeded text already gone from the textarea and no way to recover it. Now confirms first.
- ls_save_lyrics's docstring said validation matched the sloppak loader's own read-side filter. It doesn't — the loader only checks type (str / int-or-float), with no finiteness check, no d<=0 drop, no rounding. This handler is stricter, not equivalent; reworded to say so. - The original PR body described round-trip, traversal, and endpoint tests that were run ad-hoc but never committed — this repo had no tests/ directory at all. Adds tests/test_helpers.py (mirrors lyrics_karaoke's existing isolated-test convention: pure/file-IO helpers only, no dependency on core's sloppak module since that isn't available outside the feedBack host). Covers path-traversal rejection, manifest IO, and — the load-bearing case — _persist_lyrics for both directory-form and zip-form sloppaks, asserting the write lands inside the actual .zip archive rather than only the extraction cache. That's a direct regression test for the persistence bug this PR fixed.
|
Thanks for the thorough review — all four points addressed, pushed in Stale alignment result across song switches. Unsaved edits discarded silently. Added dirty tracking — a Untimed-only save silently clearing the track. Docstring accuracy + missing tests. Reworded Left the rAF-redraw-while-idle nitpick as-is per your own framing of it as deferable. Generated by Claude Code |
There was a problem hiding this comment.
Important
The discard-prompt fix has one remaining hole: declining the prompt from "Edit Manually" still seeds the editor and wipes the unsaved edits it was meant to protect (details inline). Everything else in this delta addresses the prior review cleanly.
Reviewed changes
- Dirty-state tracking for the editor — new
(generation, saved history index)scheme; re-opening or closing the editor now prompts on unsaved edits, song-switch and navigation abort when the user declines, and a successful save marks the editor clean. - Stale-alignment guard —
lsSelectSong/lsClearSongnow clear the alignment result and hide the preview, so "Edit Manually"/"Save to Song" can't persist the previous song's alignment into the newly selected one. - All-untimed save guard — saving a list where every syllable is untimed now confirms before posting the track-clearing empty payload.
- Committed test suite — new
tests/test_helpers.pywith 17 passing tests: path containment, manifest IO, atomic write, re-zip (incl. the one-time backup and the zip-persistence regression),_persist_lyricsfor dir- and zip-form packs, the empty-list clear, and LRC formatting. - Doc/manifest fixes —
/save-lyricsdocstring now correctly attributes validation strictness vs. core's loader; plugin version bumped to 1.1.1.
ℹ️ Nitpicks
- The PR body's "Testing" section still claims a
TestClientexercise ofGET /lyrics/POST /save-lyrics, a round-trip through core'ssloppak.load_song, andnode --check screen.js— none of those are in the committed suite. The committed tests are solid and the module docstring honestly scopes out the core-dependent routes, so this is just a body/commit mismatch worth trimming.
Big Pickle (free) | 𝕏
- lsEditInEditor's .then() ran unconditionally, so declining the "discard unsaved edits?" prompt in lsOpenEditor (or having no song selected) didn't stop it — the alignment seed still overwrote _lsEditorSyllables and reset the undo history, destroying the exact edits the user just chose to keep. lsOpenEditor now returns whether it actually (re)loaded; lsEditInEditor checks that before seeding. - The (generation, history-index) dirty scheme broke once a session crossed the 50-entry undo cap: _lsEditorPushHistory shifts the oldest snapshot out on overflow, so "index 0" after enough edits no longer means the snapshot that was actually saved — undoing back to it could report clean (skipping the discard prompt) while showing genuinely different content. Replaced with a content-based comparison (JSON-serialized snapshot vs. the live array), which doesn't depend on history-array bookkeeping surviving intact across the whole session. Both caught by Pullfrog's review of the previous fix commit.
|
Both real, and both fixed in
History-cap eviction breaking the dirty check. You're right that keying "clean" to a history index doesn't survive the 50-entry cap shifting the saved snapshot out. Replaced the whole Also updated the PR body's Testing section — you were right that it still described tests ( Generated by Claude Code |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
lsEditInEditorseed gating —lsOpenEditornow returnstrueonly when it actually (re)loaded, andlsEditInEditorskips its alignment seed onfalse, closing the cancel-bypass where declining the discard prompt still wiped the unsaved edits it was meant to protect.- Content-based dirty tracking — the
(generation, history-index)scheme is replaced by comparing a serialized snapshot of_lsEditorSyllablesagainst the live array by value, so the 50-entry undo cap can no longer evict the saved snapshot and make undo-to-start report clean while holding different content. - PR body accuracy — the Testing section now correctly separates committed tests from manual checks, resolving the body/commit mismatch nitpick from the prior review.
Both open threads from the prior review are addressed and resolved. Verified locally: the 17 committed tests pass and node --check screen.js is clean.
Big Pickle (free) | 𝕏

What
Both existing lyrics plugins (
lyrics_sync,lyrics_karaoke) are generate-only: paste text → Whisper forced-align → save, with no way to fix the ~10% alignment gets wrong, or author lyrics for a song with no vocals stem at all.Adds a manual hand-editing surface to this plugin, whose manifest already describes it as "Author and time-align song lyrics":
lyrics.jsonvia a newGET /lyricsendpoint; "Edit Manually" on an alignment preview seeds the editor from a fresh align result before it's saved/-syllable splits, undo/redo, A–B loop, playback speed control, stem pickerSaves go through a new
POST /save-lyrics, validated server-side (finite numerict/d, drop non-positive durations, round, sort) — stricter than the sloppak loader's own read-side filter, so nothing the editor writes can be silently dropped on the next load. An empty list is a valid save (clears the track); saving a session where every syllable is still untimed confirms first, since that would otherwise clear the track's existing lyrics silently.Also fixes
The existing align-based
/savewrotelyrics.jsoninto the extraction cache for zip-form.sloppakfiles instead of the archive itself, so edits were silently lost whenever the cache was invalidated. Both save paths now funnel through one_persist_lyricshelper (ported fromlyrics_karaoke's zip-repack pattern) that rewrites directory-form packs in place and re-zips zip-form packs with a one-time.bak.Also hardens
_find_vocals_stemagainst a manifest-controlled path escaping the pack directory, matching the containment check already applied to request-supplied filenames (mirrors the existing_safe_dlc_pathfix from #5/#6, one layer further in).Testing
tests/test_helpers.py(17 tests, committed) — path-traversal rejection for both request-supplied and manifest-supplied paths, manifest IO, atomic write,_rezip_sloppak(including the one-time.bakand that a second call doesn't clobber it), and — the load-bearing case —_persist_lyricsfor both directory-form and zip-form sloppaks, asserting the write lands inside the actual.ziparchive rather than only the extraction cache. That's a direct regression test for the persistence bug this PR fixes. Also covers the empty-list clear case and LRC formatting._resolve_sloppak/_find_vocals_stem/thesetup()-registered routes aren't covered by the committed suite — they import core'ssloppakmodule, which isn't available in this plugin repo's isolated CI (only supplied by the feedBack host at plugin-load time). Matches the existing scope boundarylyrics_karaoke's own test suite already draws for the same reason.POST /save(align path) manually against a local core checkout, unchanged behavior.node --check screen.js, cross-checked everyonclick/onchangehandler inscreen.htmlresolves to a defined JS function.