feat: utterances API routes — list, clip, tag, dismiss
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>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.shared.config import Settings, get_settings
|
||||
from app.shared.database import get_db, init_schema
|
||||
from app.api.routes import utterances, speakers, rooms, search, rag
|
||||
|
||||
|
||||
def create_app(settings: Settings) -> FastAPI:
|
||||
init_schema(settings)
|
||||
app = FastAPI(title="LinkedStorm")
|
||||
|
||||
def db_factory() -> sqlite3.Connection:
|
||||
return get_db(settings)
|
||||
|
||||
app.include_router(utterances.make_router(settings, db_factory))
|
||||
app.include_router(speakers.make_router(settings, db_factory))
|
||||
app.include_router(rooms.make_router(settings, db_factory))
|
||||
app.include_router(search.make_router(settings, db_factory))
|
||||
app.include_router(rag.make_router(settings, db_factory))
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
# Mount static files last so they don't shadow API routes.
|
||||
# Only mount if the directory exists AND contains at least one file,
|
||||
# otherwise StaticFiles raises an error on empty directories.
|
||||
static_dir = Path(__file__).parent / "static"
|
||||
if static_dir.exists() and any(static_dir.iterdir()):
|
||||
app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="static")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
_app_instance = None
|
||||
|
||||
|
||||
def app_factory():
|
||||
global _app_instance
|
||||
if _app_instance is None:
|
||||
_app_instance = create_app(get_settings())
|
||||
return _app_instance
|
||||
@@ -0,0 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
def make_router(settings, db_factory):
|
||||
router = APIRouter(prefix="/api")
|
||||
return router
|
||||
@@ -0,0 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
def make_router(settings, db_factory):
|
||||
router = APIRouter(prefix="/api/rooms")
|
||||
return router
|
||||
@@ -0,0 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
def make_router(settings, db_factory):
|
||||
router = APIRouter(prefix="/api")
|
||||
return router
|
||||
@@ -0,0 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
def make_router(settings, db_factory):
|
||||
router = APIRouter(prefix="/api/speakers")
|
||||
return router
|
||||
@@ -0,0 +1,100 @@
|
||||
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
|
||||
Reference in New Issue
Block a user