4def6a526c
Adds app/api/main.py (create_app factory with health endpoint), utterances router (GET /api/utterances with date/room/speaker/status filters, GET clip, POST tag, POST dismiss), stub routers for speakers/rooms/search/rag, and tests/routes/ with 6 passing TDD tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
import sqlite3
|
|
from typing import Optional
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import FileResponse
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class TagRequest(BaseModel):
|
|
speaker_id: Optional[int] = None
|
|
name: Optional[str] = None
|
|
create_new: bool = False
|
|
|
|
|
|
def make_router(settings, db_factory):
|
|
router = APIRouter(prefix="/api/utterances")
|
|
|
|
@router.get("")
|
|
def list_utterances(
|
|
date: Optional[str] = None,
|
|
room_id: Optional[int] = None,
|
|
speaker_id: Optional[int] = None,
|
|
match_status: Optional[str] = None,
|
|
):
|
|
db: sqlite3.Connection = db_factory()
|
|
query = """
|
|
SELECT u.*, r.name as room_name, s.name as speaker_name
|
|
FROM utterances u
|
|
JOIN rooms r ON u.room_id = r.id
|
|
LEFT JOIN speakers s ON u.speaker_id = s.id
|
|
WHERE 1=1
|
|
"""
|
|
params: list = []
|
|
if date:
|
|
query += " AND date(u.start_time) = ?"
|
|
params.append(date)
|
|
if room_id:
|
|
query += " AND u.room_id = ?"
|
|
params.append(room_id)
|
|
if speaker_id:
|
|
query += " AND u.speaker_id = ?"
|
|
params.append(speaker_id)
|
|
if match_status:
|
|
query += " AND u.match_status = ?"
|
|
params.append(match_status)
|
|
query += " ORDER BY u.start_time DESC LIMIT 200"
|
|
rows = db.execute(query, params).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
@router.get("/{utterance_id}/clip")
|
|
def get_clip(utterance_id: int):
|
|
db: sqlite3.Connection = db_factory()
|
|
row = db.execute(
|
|
"SELECT audio_clip_path FROM utterances WHERE id=?", (utterance_id,)
|
|
).fetchone()
|
|
if not row or not row["audio_clip_path"]:
|
|
raise HTTPException(404, "Clip not found or expired")
|
|
return FileResponse(row["audio_clip_path"], media_type="audio/wav")
|
|
|
|
@router.post("/{utterance_id}/tag")
|
|
def tag_utterance(utterance_id: int, body: TagRequest):
|
|
db: sqlite3.Connection = db_factory()
|
|
row = db.execute("SELECT * FROM utterances WHERE id=?", (utterance_id,)).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "Utterance not found")
|
|
|
|
speaker_id = body.speaker_id
|
|
if body.create_new and body.name:
|
|
cur = db.execute("INSERT INTO speakers (name) VALUES (?)", (body.name,))
|
|
db.commit()
|
|
speaker_id = cur.lastrowid
|
|
|
|
if speaker_id is None:
|
|
raise HTTPException(400, "Provide speaker_id or name+create_new")
|
|
|
|
db.execute(
|
|
"UPDATE utterances SET speaker_id=?, match_status='known', match_confidence=1.0 WHERE id=?",
|
|
(speaker_id, utterance_id),
|
|
)
|
|
db.commit()
|
|
|
|
# Store utterance embedding as a reference sample for this speaker
|
|
if row["embedding"]:
|
|
db.execute(
|
|
"INSERT INTO voice_embeddings (speaker_id, embedding) VALUES (?,?)",
|
|
(speaker_id, row["embedding"]),
|
|
)
|
|
db.commit()
|
|
|
|
return {"ok": True, "speaker_id": speaker_id}
|
|
|
|
@router.post("/{utterance_id}/dismiss")
|
|
def dismiss_utterance(utterance_id: int):
|
|
db: sqlite3.Connection = db_factory()
|
|
db.execute(
|
|
"UPDATE utterances SET match_status='unknown' WHERE id=?", (utterance_id,)
|
|
)
|
|
db.commit()
|
|
return {"ok": True}
|
|
|
|
return router
|