refactor: extract rehearser library persistence
This commit is contained in:
parent
c815b7941a
commit
2712843322
@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- **Rehearser library extraction.** SQLite persistence helpers and the one-time IndexedDB migration now live in
|
||||||
|
`rehearser-library.js`, loaded with the Rehearser/Reader/Library lazy groups; behavior remains global-compatible.
|
||||||
- **Expanded resource diagnostics.** The Performance panel now reports bounded browser cache counters, container
|
- **Expanded resource diagnostics.** The Performance panel now reports bounded browser cache counters, container
|
||||||
cgroup current/max memory, artifact-cache bytes, and sanitized largest recent operation durations.
|
cgroup current/max memory, artifact-cache bytes, and sanitized largest recent operation durations.
|
||||||
- **Current-release constrained validation.** Built `1.24.21` and served it under a hard 4 GiB cgroup limit;
|
- **Current-release constrained validation.** Built `1.24.21` and served it under a hard 4 GiB cgroup limit;
|
||||||
|
|||||||
1
TODO.md
1
TODO.md
@ -123,6 +123,7 @@ architecture change: the larger Audiobook/Rehearser/style split.
|
|||||||
- [x] Add a dependency-free frontend syntax and top-level declaration collision gate (`npm run check:frontend`);
|
- [x] Add a dependency-free frontend syntax and top-level declaration collision gate (`npm run check:frontend`);
|
||||||
a full style/lint policy remains a follow-up.
|
a full style/lint policy remains a follow-up.
|
||||||
- [ ] Extract Rehearser voice matching and voice design.
|
- [ ] Extract Rehearser voice matching and voice design.
|
||||||
|
- [x] Extract Rehearser library persistence and IndexedDB migration into `rehearser-library.js`.
|
||||||
- [ ] Extract Audiobook parsing and casting.
|
- [ ] Extract Audiobook parsing and casting.
|
||||||
- [ ] Split `style.css` by feature.
|
- [ ] Split `style.css` by feature.
|
||||||
- [ ] Do not migrate to TypeScript or rewrite the frontend until behavior is covered by tests.
|
- [ ] Do not migrate to TypeScript or rewrite the frontend until behavior is covered by tests.
|
||||||
|
|||||||
@ -33,7 +33,8 @@ Done incrementally as global-scope ordered files (no ES-module change needed)
|
|||||||
- ⬜ `rehearser-cast.js` — `renderCastList`, sort/filter, online voice picker, character research.
|
- ⬜ `rehearser-cast.js` — `renderCastList`, sort/filter, online voice picker, character research.
|
||||||
- ⬜ `rehearser-stage.js` — playback, synth cache, pre-decode window, transport.
|
- ⬜ `rehearser-stage.js` — playback, synth cache, pre-decode window, transport.
|
||||||
- ⬜ `rehearser-train.js` — mic record / STT compare.
|
- ⬜ `rehearser-train.js` — mic record / STT compare.
|
||||||
- ⬜ `rehearser-library.js` — IndexedDB session store, import/export, auto-save.
|
- ✅ `rehearser-library.js` — SQLite persistence helpers and IndexedDB migration; import/export and auto-save remain
|
||||||
|
in `rehearser.js` until their UI/event-handler dependencies are isolated.
|
||||||
|
|
||||||
**Pattern (proven):** extract a cohesive block whose only cross-file references resolve at *call time* (event handlers), not at module load. Top-level `const rehState` and top-level `addEventListener` registrations must stay ordered — extract those last and load them after their dependencies. Re-run `npx playwright test` after each extraction.
|
**Pattern (proven):** extract a cohesive block whose only cross-file references resolve at *call time* (event handlers), not at module load. Top-level `const rehState` and top-level `addEventListener` registrations must stay ordered — extract those last and load them after their dependencies. Re-run `npx playwright test` after each extraction.
|
||||||
|
|
||||||
|
|||||||
59
static/js/rehearser-library.js
Normal file
59
static/js/rehearser-library.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// ── Rehearser library persistence ────────────────────────────────────────────
|
||||||
|
// SQLite-backed rehearsal records and the one-time IndexedDB migration are
|
||||||
|
// isolated from parsing, casting, and stage playback. Functions stay global
|
||||||
|
// during the compatibility migration because audiobook.js/library.js call them.
|
||||||
|
|
||||||
|
async function rehDbGetAll() {
|
||||||
|
const r = await fetch('/api/rehearsals');
|
||||||
|
if (!r.ok) throw new Error('rehDbGetAll failed: ' + r.status);
|
||||||
|
const d = await r.json();
|
||||||
|
return d.rehearsals || [];
|
||||||
|
}
|
||||||
|
async function rehDbGetById(id) {
|
||||||
|
const r = await fetch('/api/rehearsals/' + id);
|
||||||
|
if (r.status === 404) return undefined;
|
||||||
|
if (!r.ok) throw new Error('rehDbGetById failed: ' + r.status);
|
||||||
|
return r.json();
|
||||||
|
}
|
||||||
|
async function rehDbAdd(record) {
|
||||||
|
const r = await fetch('/api/rehearsals', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(_rehSanitize(record)) });
|
||||||
|
if (!r.ok) throw new Error('rehDbAdd failed: ' + r.status);
|
||||||
|
return (await r.json()).id;
|
||||||
|
}
|
||||||
|
async function rehDbPut(record) {
|
||||||
|
if (!record.id) return rehDbAdd(record);
|
||||||
|
const r = await fetch('/api/rehearsals/' + record.id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(_rehSanitize(record)) });
|
||||||
|
if (!r.ok) throw new Error('rehDbPut failed: ' + r.status);
|
||||||
|
}
|
||||||
|
async function rehDbDelete(id) {
|
||||||
|
const r = await fetch('/api/rehearsals/' + id, { method: 'DELETE' });
|
||||||
|
if (!r.ok) throw new Error('rehDbDelete failed: ' + r.status);
|
||||||
|
}
|
||||||
|
function _rehSanitize(rec) {
|
||||||
|
const out = { ...rec };
|
||||||
|
if (out.clips) out.clips = (out.clips || []).map(c => ({ lineIndex: c.lineIndex, speaker: c.speaker, type: c.type }));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function _rehIdbGetAll() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const req = indexedDB.open('reh-library', 1);
|
||||||
|
req.onerror = () => resolve([]);
|
||||||
|
req.onsuccess = e => {
|
||||||
|
const db = e.target.result;
|
||||||
|
if (!db.objectStoreNames.contains('rehearsals')) { db.close(); resolve([]); return; }
|
||||||
|
const all = db.transaction('rehearsals', 'readonly').objectStore('rehearsals').getAll();
|
||||||
|
all.onsuccess = ev => { db.close(); resolve(ev.target.result || []); };
|
||||||
|
all.onerror = () => { db.close(); resolve([]); };
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
(async function _rehMigrateIfNeeded() {
|
||||||
|
try {
|
||||||
|
if ((await rehDbGetAll()).length) return;
|
||||||
|
const old = await _rehIdbGetAll();
|
||||||
|
if (!old.length) return;
|
||||||
|
const r = await fetch('/api/rehearsals/migrate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(old.map(_rehSanitize)) });
|
||||||
|
if (r.ok) console.log(`[rehearser] migrated ${(await r.json()).imported} records from IndexedDB → SQLite`);
|
||||||
|
} catch (e) { console.warn('[rehearser] migration skipped:', e); }
|
||||||
|
})();
|
||||||
|
window.rehDbGetById = rehDbGetById;
|
||||||
@ -120,92 +120,6 @@ const rehState = {
|
|||||||
};
|
};
|
||||||
window.rehState = rehState;
|
window.rehState = rehState;
|
||||||
|
|
||||||
// ── Library — SQLite via /api/rehearsals ──────────────────────────────────
|
|
||||||
|
|
||||||
async function rehDbGetAll() {
|
|
||||||
const r = await fetch('/api/rehearsals');
|
|
||||||
if (!r.ok) throw new Error('rehDbGetAll failed: ' + r.status);
|
|
||||||
const d = await r.json();
|
|
||||||
return d.rehearsals || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rehDbGetById(id) {
|
|
||||||
const r = await fetch('/api/rehearsals/' + id);
|
|
||||||
if (r.status === 404) return undefined;
|
|
||||||
if (!r.ok) throw new Error('rehDbGetById failed: ' + r.status);
|
|
||||||
return r.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rehDbAdd(record) {
|
|
||||||
const r = await fetch('/api/rehearsals', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(_rehSanitize(record)),
|
|
||||||
});
|
|
||||||
if (!r.ok) throw new Error('rehDbAdd failed: ' + r.status);
|
|
||||||
const d = await r.json();
|
|
||||||
return d.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rehDbPut(record) {
|
|
||||||
if (!record.id) { return rehDbAdd(record); }
|
|
||||||
const r = await fetch('/api/rehearsals/' + record.id, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(_rehSanitize(record)),
|
|
||||||
});
|
|
||||||
if (!r.ok) throw new Error('rehDbPut failed: ' + r.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rehDbDelete(id) {
|
|
||||||
const r = await fetch('/api/rehearsals/' + id, { method: 'DELETE' });
|
|
||||||
if (!r.ok) throw new Error('rehDbDelete failed: ' + r.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip binary clip blobs before sending — blobs can't be JSON-serialised and
|
|
||||||
// are session-only anyway (they live in rehState.clips, not the library record).
|
|
||||||
function _rehSanitize(rec) {
|
|
||||||
const out = { ...rec };
|
|
||||||
if (out.clips) out.clips = (out.clips || []).map(c => ({ lineIndex: c.lineIndex, speaker: c.speaker, type: c.type }));
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One-time IndexedDB → SQLite migration. Runs silently on page load.
|
|
||||||
(async function _rehMigrateIfNeeded() {
|
|
||||||
try {
|
|
||||||
const serverRecs = await rehDbGetAll();
|
|
||||||
if (serverRecs.length > 0) return;
|
|
||||||
const idbRecs = await _rehIdbGetAll().catch(() => []);
|
|
||||||
if (!idbRecs.length) return;
|
|
||||||
const r = await fetch('/api/rehearsals/migrate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(idbRecs.map(_rehSanitize)),
|
|
||||||
});
|
|
||||||
if (r.ok) {
|
|
||||||
const d = await r.json();
|
|
||||||
console.log(`[rehearser] migrated ${d.imported} records from IndexedDB → SQLite`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[rehearser] migration skipped:', e);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function _rehIdbGetAll() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const req = indexedDB.open('reh-library', 1);
|
|
||||||
req.onerror = () => resolve([]);
|
|
||||||
req.onsuccess = e => {
|
|
||||||
const db = e.target.result;
|
|
||||||
if (!db.objectStoreNames.contains('rehearsals')) { db.close(); resolve([]); return; }
|
|
||||||
const all = db.transaction('rehearsals', 'readonly').objectStore('rehearsals').getAll();
|
|
||||||
all.onsuccess = ev => { db.close(); resolve(ev.target.result || []); };
|
|
||||||
all.onerror = () => { db.close(); resolve([]); };
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.rehDbGetById = rehDbGetById;
|
|
||||||
window.rehLoadRecord = loadRecord; // expose so audiobook.js can open a record directly
|
window.rehLoadRecord = loadRecord; // expose so audiobook.js can open a record directly
|
||||||
|
|
||||||
// ── Serialization ─────────────────────────────────────────────────────────
|
// ── Serialization ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
"integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace"
|
"integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace"
|
||||||
],
|
],
|
||||||
"lazy": {
|
"lazy": {
|
||||||
"s-rehearser": ["rehearser-parse", "rehearser"],
|
"s-rehearser": ["rehearser-parse", "rehearser", "rehearser-library"],
|
||||||
"s-reader": ["rehearser-parse", "rehearser", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets"],
|
"s-reader": ["rehearser-parse", "rehearser", "rehearser-library", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets"],
|
||||||
"s-library": ["rehearser-parse", "rehearser", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
"s-library": ["rehearser-parse", "rehearser", "rehearser-library", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
||||||
"s-caststudio": ["rehearser-parse", "rehearser", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
"s-caststudio": ["rehearser-parse", "rehearser", "rehearser-library", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user