Initial commit: Nat20 Notes — TTRPG session transcription & summarization

This commit is contained in:
KansaiGaijin
2026-07-06 23:45:54 +12:00
commit 524c8ab96d
50 changed files with 2969 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
import json
from pathlib import Path
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from .. import database as db, config
from ..pipeline.turns import distinct_speakers, load_turns
router = APIRouter(prefix="/api/sessions/{session_id}/speakers", tags=["speakers"])
def _get_session_or_404(session_id: str):
row = db.get_conn().execute("SELECT * FROM sessions WHERE id = ?", (session_id,)).fetchone()
if not row:
raise HTTPException(404, "Session not found")
return dict(row)
@router.get("")
def get_speakers(session_id: str):
session = _get_session_or_404(session_id)
if not session["transcript_path"]:
raise HTTPException(400, "Session hasn't been transcribed yet")
speakers = distinct_speakers(Path(session["transcript_path"]))
existing = {r["raw_label"]: r["display_name"] for r in
db.get_conn().execute("SELECT raw_label, display_name FROM speakers WHERE session_id = ?", (session_id,))}
for sp in speakers:
sp["display_name"] = existing.get(sp["raw_label"], "")
return speakers
class SpeakerName(BaseModel):
raw_label: str
display_name: str
@router.put("")
def set_speaker_name(session_id: str, body: SpeakerName):
_get_session_or_404(session_id)
with db.tx() as conn:
conn.execute(
"INSERT INTO speakers (id, session_id, raw_label, display_name) VALUES (?, ?, ?, ?) "
"ON CONFLICT(session_id, raw_label) DO UPDATE SET display_name = excluded.display_name",
(db.new_id(), session_id, body.raw_label, body.display_name),
)
return {"ok": True}
class MergeSpeakersBody(BaseModel):
target_label: str
source_labels: list[str]
@router.post("/merge")
def merge_speakers(session_id: str, body: MergeSpeakersBody):
session = _get_session_or_404(session_id)
if not session["transcript_path"]:
raise HTTPException(400, "Session hasn't been transcribed yet")
if body.target_label in body.source_labels:
raise HTTPException(400, "Target label cannot also be a source label")
tp = Path(session["transcript_path"])
data = json.loads(tp.read_text())
changed = 0
for seg in data.get("segments", []):
if seg.get("speaker") in body.source_labels:
seg["speaker"] = body.target_label
changed += 1
tp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
with db.tx() as conn:
for label in body.source_labels:
conn.execute("DELETE FROM speakers WHERE session_id = ? AND raw_label = ?", (session_id, label))
return {"ok": True, "segments_reassigned": changed}
@router.get("/turns")
def get_turns(session_id: str):
"""Full transcript with display names applied - powers the review/playback screen."""
session = _get_session_or_404(session_id)
if not session["transcript_path"]:
raise HTTPException(400, "Session hasn't been transcribed yet")
speaker_map = {r["raw_label"]: r["display_name"] for r in
db.get_conn().execute("SELECT raw_label, display_name FROM speakers WHERE session_id = ?", (session_id,))
if r["display_name"]}
return load_turns(Path(session["transcript_path"]), speaker_map)