refactor(stage-4): partition monolithic main.py into modular APIRouters
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.auth import require_user
|
||||
from app.database import get_db
|
||||
from app.dictionary import lookup_term
|
||||
from app.phrasal import detect_phrasal_verb
|
||||
from app.ai import translate_sentence_ai, get_ollama_status
|
||||
from app.tts import (
|
||||
synthesize_speech,
|
||||
prewarm_card_audio,
|
||||
AUDIO_CACHE_DIR,
|
||||
get_available_voices,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["Dictionary & TTS"])
|
||||
|
||||
|
||||
class SentenceTranslateRequest(BaseModel):
|
||||
sentence: str = Field(..., min_length=1)
|
||||
target_word: Optional[str] = ""
|
||||
context: Optional[str] = ""
|
||||
|
||||
|
||||
@router.get("/api/lookup")
|
||||
async def api_lookup(
|
||||
term: Optional[str] = None,
|
||||
q: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
user: dict = Depends(require_user),
|
||||
):
|
||||
query_str = (term or q or "").strip()
|
||||
if not query_str:
|
||||
raise HTTPException(status_code=400, detail="Query term cannot be empty")
|
||||
data = await lookup_term(query_str)
|
||||
|
||||
# Check if this term already exists in user's dictionary
|
||||
with get_db() as conn:
|
||||
existing = conn.execute(
|
||||
"SELECT id, term, translation, context_sentence, tags, collection, srs_state, srs_reps FROM vocabulary_cards WHERE user_id = ? AND lower(trim(term)) = lower(trim(?))",
|
||||
(user["id"], query_str),
|
||||
).fetchone()
|
||||
if existing:
|
||||
data["in_library"] = True
|
||||
data["library_card"] = dict(existing)
|
||||
else:
|
||||
data["in_library"] = False
|
||||
data["library_card"] = None
|
||||
|
||||
# Multi-Word & Phrasal Verb Detection
|
||||
data["phrasal_verb"] = None
|
||||
if context:
|
||||
pv = detect_phrasal_verb(
|
||||
sentence=context,
|
||||
target_word=query_str,
|
||||
user_id=user["id"],
|
||||
conn=conn,
|
||||
)
|
||||
if pv and pv["term"].lower() != query_str.lower():
|
||||
data["phrasal_verb"] = pv
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@router.get("/api/detect-phrasal-verb")
|
||||
async def api_detect_phrasal_verb(
|
||||
sentence: str = Query(..., min_length=1),
|
||||
word: str = Query(..., min_length=1),
|
||||
user: dict = Depends(require_user),
|
||||
):
|
||||
"""
|
||||
Analyzes sentence context around word to detect phrasal verbs, idioms, and collocations.
|
||||
"""
|
||||
with get_db() as conn:
|
||||
pv = detect_phrasal_verb(sentence=sentence, target_word=word, user_id=user["id"], conn=conn)
|
||||
return {"detected": bool(pv), "phrasal_verb": pv}
|
||||
|
||||
|
||||
@router.api_route("/api/audio", methods=["GET", "HEAD"])
|
||||
async def api_audio_proxy():
|
||||
# Audio feature disabled
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@router.post("/api/translate-sentence")
|
||||
async def api_translate_sentence(
|
||||
req: SentenceTranslateRequest,
|
||||
user: dict = Depends(require_user),
|
||||
):
|
||||
"""
|
||||
Translates a sentence using local AI with in-context meaning of the target word.
|
||||
Uses SQLite sentence_translations cache for sub-millisecond retrieval on repeat queries.
|
||||
"""
|
||||
clean_s = req.sentence.strip()
|
||||
if not clean_s:
|
||||
raise HTTPException(status_code=400, detail="Sentence cannot be empty")
|
||||
|
||||
# Pre-warm neural audio for sentence and target word in background
|
||||
asyncio.create_task(prewarm_card_audio(req.target_word or "", clean_s))
|
||||
|
||||
import sys
|
||||
main_mod = sys.modules.get("app.main")
|
||||
translator = getattr(main_mod, "translate_sentence_ai", translate_sentence_ai) if main_mod else translate_sentence_ai
|
||||
return await translator(clean_s, req.target_word or "", req.context or "")
|
||||
|
||||
|
||||
@router.get("/api/ai/status")
|
||||
async def api_ai_status(user: dict = Depends(require_user)):
|
||||
"""
|
||||
Returns local AI status and available models.
|
||||
"""
|
||||
return await get_ollama_status()
|
||||
|
||||
|
||||
@router.get("/api/audio/{hash_val}.wav")
|
||||
async def api_get_cached_audio(hash_val: str):
|
||||
"""
|
||||
Streams pre-warmed / cached audio file directly with immutable cache headers.
|
||||
"""
|
||||
clean_hash = hash_val.lower().strip()
|
||||
if not re.match(r"^[a-f0-9]{32}$", clean_hash):
|
||||
raise HTTPException(status_code=400, detail="Invalid audio hash")
|
||||
audio_path = AUDIO_CACHE_DIR / f"{clean_hash}.wav"
|
||||
if not audio_path.is_file():
|
||||
raise HTTPException(status_code=404, detail="Audio file not found")
|
||||
return FileResponse(
|
||||
path=str(audio_path),
|
||||
media_type="audio/wav",
|
||||
headers={"Cache-Control": "public, max-age=31536000, immutable"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/tts")
|
||||
async def api_tts(
|
||||
text: str = Query(..., min_length=1, max_length=1000),
|
||||
lang: str = Query("en"),
|
||||
speed: float = Query(1.0, ge=0.5, le=2.0),
|
||||
voice: Optional[str] = Query(None),
|
||||
user: dict = Depends(require_user),
|
||||
):
|
||||
"""
|
||||
Synthesizes speech on-demand using local Piper TTS.
|
||||
Instantly returns cached audio if previously synthesized for this voice, speed, and text.
|
||||
"""
|
||||
clean_text = text.strip()
|
||||
if not clean_text:
|
||||
raise HTTPException(status_code=400, detail="Text cannot be empty")
|
||||
audio_path = await synthesize_speech(clean_text, lang=lang, speed=speed, voice=voice)
|
||||
if not audio_path or not audio_path.is_file():
|
||||
raise HTTPException(status_code=500, detail="TTS synthesis failed or unavailable")
|
||||
return FileResponse(
|
||||
path=str(audio_path),
|
||||
media_type="audio/wav",
|
||||
headers={"Cache-Control": "public, max-age=604800, immutable"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/tts/status")
|
||||
async def api_tts_status(user: dict = Depends(require_user)):
|
||||
"""Returns status of local Piper TTS and all available voice models."""
|
||||
return get_available_voices()
|
||||
Reference in New Issue
Block a user