92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
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)
|