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)