feat(phase-1): persistent generation library

- Save every completed generation to SQLite (generation_store.py) with
  WAV and waveform peaks written to data/generations/<id>/
- Deferred DB write until success — cancelled/errored generations never
  touch the DB and never appear in the library
- Fixed cancel+regenerate IndexError: _reset_scheduler_caches() now
  directly zeros scheduler._step_index and running state in addition to
  clearing VibePod cache dicts; same explicit resets added in the fresh
  path of prepare_noise_scheduler as belt-and-suspenders
- Added /library page with GenerationCard, WaveformPreview, waveform
  fetch, play/pause, download, delete, pagination, empty + error states
- Added generation API routes (list, single, audio stream, waveform,
  delete) proxying to Python server
- Added Library nav link to Header with active state
- Persist script/speaker/CFG to localStorage so generate page state
  survives navigation
- Updated build plan: Phase 0+1 ticked off, better-sqlite3 moved to
  Phase 2, architectural note on Python owning all persistence
This commit is contained in:
2026-05-02 23:05:11 +01:00
parent 47e0c7e512
commit 13085166fb
13 changed files with 913 additions and 29 deletions
+32 -1
View File
@@ -171,8 +171,39 @@ const initialState: AppState = {
serverConfig: null,
};
const STORAGE_KEY = "vibepod_form";
function loadSavedForm(): Partial<Pick<AppState, "script" | "speaker" | "cfgScale">> {
if (typeof window === "undefined") return {};
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return {};
return JSON.parse(raw) as Partial<Pick<AppState, "script" | "speaker" | "cfgScale">>;
} catch {
return {};
}
}
export default function HomePage() {
const [state, dispatch] = useReducer(reducer, initialState);
const [state, dispatch] = useReducer(reducer, initialState, (base) => {
const saved = loadSavedForm();
return {
...base,
...(saved.script !== undefined && { script: saved.script }),
...(saved.speaker !== undefined && { speaker: saved.speaker }),
...(typeof saved.cfgScale === "number" && { cfgScale: saved.cfgScale }),
};
});
// Persist user-editable form fields across navigation.
useEffect(() => {
try {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({ script: state.script, speaker: state.speaker, cfgScale: state.cfgScale })
);
} catch {}
}, [state.script, state.speaker, state.cfgScale]);
const wordCount = state.script.trim() === "" ? 0 : state.script.trim().split(/\s+/).length;