Files
All-in-one-Financial-Analysis/atlas-terminal/server/routers/video_transcript.py
T

114 lines
4.0 KiB
Python

"""Video transcript endpoints."""
from __future__ import annotations
from fastapi import APIRouter, File, Form, Header, HTTPException, Query, UploadFile
from server.models.schemas import VideoJob, VideoSearchHit, VideoSubmitRequest, VideoTranscript, VideoTranslation
from server.services.video_transcript_service import (
delete_job,
get_job_detail,
list_jobs,
search_jobs,
submit_job,
submit_upload,
translate_job_content,
)
router = APIRouter()
def _resolve_api_key(header_value: str | None) -> str | None:
return (header_value or "").strip() or None
@router.post("/submit", response_model=VideoJob, summary="Submit a video or audio transcript job")
async def submit_video_job(
body: VideoSubmitRequest,
x_gemini_api_key: str | None = Header(default=None),
) -> VideoJob:
job_id = await submit_job(
body.url,
body.source_type,
language=body.language,
api_key=_resolve_api_key(x_gemini_api_key),
)
detail = await get_job_detail(job_id)
if detail is None:
raise HTTPException(status_code=500, detail="Failed to create transcript job.")
return VideoJob(**detail["job"])
@router.post("/upload", response_model=VideoJob, summary="Upload local media and start transcript extraction")
async def upload_video_file(
file: UploadFile = File(...),
language: str | None = Form(default=None),
x_gemini_api_key: str | None = Header(default=None),
) -> VideoJob:
job_id = await submit_upload(
file,
language=language,
api_key=_resolve_api_key(x_gemini_api_key),
)
detail = await get_job_detail(job_id)
if detail is None:
raise HTTPException(status_code=500, detail="Failed to create transcript job.")
return VideoJob(**detail["job"])
@router.get("/jobs", response_model=list[VideoJob], summary="List recent transcript jobs")
async def video_jobs(limit: int = Query(50, ge=1, le=200)) -> list[VideoJob]:
rows = await list_jobs(limit=limit)
return [VideoJob(**row) for row in rows]
@router.get("/jobs/{job_id}", summary="Get transcript job details")
async def video_job_detail(job_id: str) -> dict[str, VideoJob | VideoTranscript | None]:
payload = await get_job_detail(job_id)
if payload is None:
raise HTTPException(status_code=404, detail="Transcript job not found.")
transcript = payload["transcript"]
return {
"job": VideoJob(**payload["job"]),
"transcript": None if transcript is None else VideoTranscript(**transcript),
}
@router.post("/jobs/{job_id}/translate", response_model=VideoTranslation, summary="Translate a stored transcript on demand")
async def translate_video_job(
job_id: str,
x_gemini_api_key: str | None = Header(default=None),
target_language: str = Query("ko", pattern="^[a-z]{2}$"),
) -> VideoTranslation:
try:
payload = await translate_job_content(
job_id,
target_language=target_language,
api_key=_resolve_api_key(x_gemini_api_key),
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if payload is None:
raise HTTPException(status_code=404, detail="Transcript job not found.")
return VideoTranslation(**payload)
@router.get("/search", response_model=list[VideoSearchHit], summary="Full-text search saved transcripts")
async def search_transcripts(
q: str = Query(..., min_length=1, description="Query text"),
limit: int = Query(20, ge=1, le=100),
) -> list[VideoSearchHit]:
rows = await search_jobs(q, limit=limit)
return [VideoSearchHit(**row) for row in rows]
@router.delete("/jobs/{job_id}", summary="Delete a transcript job")
async def delete_video_job(job_id: str) -> dict[str, object]:
deleted = await delete_job(job_id)
if not deleted:
raise HTTPException(status_code=404, detail="Transcript job not found.")
return {"job_id": job_id, "deleted": True}