Initial commit: Nat20 Notes — TTRPG session transcription & summarization
This commit is contained in:
91
backend/app/routers/speakers.py
Normal file
91
backend/app/routers/speakers.py
Normal 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)
|
||||
Reference in New Issue
Block a user