feat(erindringer): samtalestyring med forberedte spørgsmål, talt udgave med lydtags, spørgsmålets lyd gemmes med svaret
CD / deploy (push) Successful in 17s
CI / check (push) Successful in 23s

Intervieweren når de forberedte spørgsmål, følger op højst N gange pr. spørgsmål
(indstilles pr. emne), trækker tilbage til emnet og afrunder. 'Spring over' i UI.
Spørgsmålet oplæses med ElevenLabs-tags ([warmly], [chuckles] …) som ikke vises
i teksten; oplæsningen gemmes som <id>-sp.mp3 ved siden af svaret og følger med i
zip-eksporten, så spørgsmål + svar kan klippes ind i en podcast.

Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
This commit is contained in:
root
2026-10-06 22:15:46 +02:00
parent 7f8ddcf7af
commit 3dee428a37
6 changed files with 182 additions and 48 deletions
+25 -6
View File
@@ -680,6 +680,8 @@ async def erindringer_tts(sha: str):
def _klip_svar(pid: str, kid: str):
if kid.endswith("-sp"): # spørgsmålets oplæsning hører til svaret: <id>-sp.mp3
return _spoergsmaal_svar(pid, kid[:-3])
f = erindringer.klip_fil(pid, kid)
if not f:
raise HTTPException(404)
@@ -698,6 +700,15 @@ async def erindringer_gaest_klip(request: Request, key: str, kid: str):
return _klip_svar(p["pid"], kid)
def _spoergsmaal_svar(pid: str, kid: str):
f = erindringer.spoergsmaal_fil(pid, kid)
if not f:
raise HTTPException(404)
return FileResponse(f, media_type="audio/mpeg", headers={"Cache-Control": "private, max-age=86400"})
def _fejl(status: int, tekst: str):
return JSONResponse({"fejl": tekst}, status_code=status)
@@ -716,10 +727,12 @@ async def erindringer_naeste(request: Request):
samtale = str(data.get("samtale") or "")
if not re.match(r"^[0-9]{8}-[a-f0-9]{6}$", samtale):
samtale = erindringer.ny_samtale()
q, afslut = await erindringer.naeste_spoergsmaal(p, pr, samtale)
lyd = await asyncio.to_thread(erindringer.tts_sti, q)
return {"samtale": samtale, "spoergsmaal": q, "afslut": afslut,
"lyd_url": f"/erindringer/tts/{lyd.stem}.mp3" if lyd else None,
sprunget = [str(x)[:400] for x in (data.get("sprunget") or []) if isinstance(x, str)][:50]
q = await erindringer.naeste_spoergsmaal(p, pr, samtale, sprunget)
lyd = await asyncio.to_thread(erindringer.tts_sti, q["talt"])
return {"samtale": samtale, "spoergsmaal": q["spoergsmaal"], "talt": q["talt"], "afslut": q["afslut"],
"kilde": q["kilde"], "lyd_url": f"/erindringer/tts/{lyd.stem}.mp3" if lyd else None,
"lyd_sha": lyd.stem if lyd else "", "tilbage": len([s for s in (pr.get("start") or []) if s not in sprunget]),
"nr": len(erindringer.klip_for(p["pid"], slug, samtale)) + 1}
@@ -743,7 +756,8 @@ async def erindringer_svar(request: Request):
tekst = str(form.get("tekst") or "")
try:
meta = await asyncio.to_thread(erindringer.gem_svar, p["pid"], slug, samtale, str(form.get("spoergsmaal") or ""),
data, filnavn, tekst or None)
data, filnavn, tekst or None, str(form.get("spoergsmaal_talt") or ""),
str(form.get("lyd_sha") or ""), str(form.get("kilde") or ""))
except ValueError as e:
return _fejl(400, str(e))
return {"id": meta["id"], "tekst": meta["tekst"], "varighed": meta["varighed"], "samtale": samtale}
@@ -782,8 +796,12 @@ async def admin_erindringer_projekt(request: Request):
erindringer.slet_projekt(slug)
return RedirectResponse("/admin/erindringer?saved=Projektet+er+slettet", status_code=303)
start = [s for s in str(form.get("start") or "").splitlines()]
try:
opfoelg = int(str(form.get("opfoelg") or "2"))
except ValueError:
opfoelg = 2
if not erindringer.gem_projekt(slug, str(form.get("titel") or ""), str(form.get("beskrivelse") or ""),
str(form.get("kontekst") or ""), start):
str(form.get("kontekst") or ""), start, opfoelg):
return RedirectResponse("/admin/erindringer?saved=Projektet+skal+have+et+navn+(a-z,+0-9,+bindestreg)+og+en+titel", status_code=303)
return RedirectResponse(f"/admin/erindringer?saved=Projektet+{urllib.parse.quote(slug)}+er+gemt", status_code=303)
@@ -839,6 +857,7 @@ async def admin_erindringer_klip(request: Request, pid: str, kid: str):
return _klip_svar(pid, kid)
@app.post("/admin/erindringer/p/{pid}/klip/{kid}")
async def admin_erindringer_klip_post(request: Request, pid: str, kid: str):
require_admin(request)