Introduces the new Studio section (Source -> Characters -> Voices -> Perform & Export) that reuses the existing Read Aloud/Library/Script Rehearsal code via DOM reparenting instead of duplicating it, and rolls up a long tail of bugs found while producing a real audiobook through it: umlaut-eating name sanitizers, a voice picker that mispositioned itself and capped results at 60, PDF pagination silently breaking on trimmed \f markers, a race letting stale audio keep playing after a new line was clicked, an alias-overlap bug that could silently redirect a voice/image save onto the wrong character, voice design failing outright during brief TTS backend restarts instead of retrying, sparse cast entries defaulting to English/wrong gender, and a reassigned voice never reaching an already-open Stage session or invalidating its cached audio. Also adds a persistent per-line audio cache, audiobook export browsing/download, and an inline voice-design prompt editor. Full details in CHANGELOG.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
77 lines
2.3 KiB
Python
77 lines
2.3 KiB
Python
"""Rehearsal library REST API — backed by SQLite via core.database.
|
|
|
|
Mirrors the IndexedDB API in rehearser.js:
|
|
GET /api/rehearsals → { rehearsals: [...] }
|
|
GET /api/rehearsals/{id} → record | 404
|
|
PUT /api/rehearsals/{id} → full replace (id must exist)
|
|
POST /api/rehearsals → insert (no id), returns { id }
|
|
DELETE /api/rehearsals/{id} → { ok: true }
|
|
POST /api/rehearsals/migrate → bulk import from JS IndexedDB dump
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
|
|
from core.database import reh_get_all, reh_get, reh_put, reh_delete, reh_compact_titles
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/api/rehearsals")
|
|
async def rehearsals_list():
|
|
reh_compact_titles()
|
|
return {"rehearsals": reh_get_all()}
|
|
|
|
|
|
@router.get("/api/rehearsals/{reh_id:int}")
|
|
async def rehearsals_get(reh_id: int):
|
|
rec = reh_get(reh_id)
|
|
if rec is None:
|
|
raise HTTPException(404, "Rehearsal not found")
|
|
return rec
|
|
|
|
|
|
@router.post("/api/rehearsals")
|
|
async def rehearsals_add(request: Request):
|
|
try:
|
|
body = await request.json()
|
|
except Exception:
|
|
raise HTTPException(400, "Invalid JSON")
|
|
body.pop("id", None)
|
|
saved = reh_put(body)
|
|
return {"id": saved["id"]}
|
|
|
|
|
|
@router.put("/api/rehearsals/{reh_id:int}")
|
|
async def rehearsals_put(reh_id: int, request: Request):
|
|
if reh_get(reh_id) is None:
|
|
raise HTTPException(404, "Rehearsal not found")
|
|
try:
|
|
body = await request.json()
|
|
except Exception:
|
|
raise HTTPException(400, "Invalid JSON")
|
|
body["id"] = reh_id
|
|
reh_put(body)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.delete("/api/rehearsals/{reh_id:int}")
|
|
async def rehearsals_delete(reh_id: int):
|
|
reh_delete(reh_id)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/api/rehearsals/migrate")
|
|
async def rehearsals_migrate(request: Request):
|
|
"""Accept a batch of rehearsal records from the browser's IndexedDB dump."""
|
|
try:
|
|
body = await request.json()
|
|
except Exception:
|
|
raise HTTPException(400, "Invalid JSON")
|
|
records = body if isinstance(body, list) else body.get("rehearsals", [])
|
|
ids = []
|
|
for rec in records:
|
|
saved = reh_put(rec)
|
|
ids.append(saved["id"])
|
|
return {"imported": len(ids), "ids": ids}
|