141 lines
4.8 KiB
Python
141 lines
4.8 KiB
Python
import uuid
|
|
from datetime import datetime
|
|
from functools import partial
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from ...auth import require_admin
|
|
from ...outbound.postgres.database import get_db, AsyncSessionLocal
|
|
from ...outbound.postgres.repositories import summarise_job_repository
|
|
from ...outbound.postgres.repositories.translated_article_repository import TranslatedArticleRepository
|
|
from ...outbound.postgres.entities.translated_article_entity import TranslatedArticleEntity
|
|
from ...outbound.gemini.gemini_client import GeminiClient
|
|
from ...storage import upload_audio
|
|
from ...config import settings
|
|
from ... import worker
|
|
|
|
router = APIRouter(prefix="/jobs", dependencies=[Depends(require_admin)])
|
|
|
|
|
|
class JobResponse(BaseModel):
|
|
id: uuid.UUID
|
|
status: str
|
|
translated_article_id: uuid.UUID | None = None
|
|
created_at: datetime
|
|
started_at: datetime | None = None
|
|
completed_at: datetime | None = None
|
|
error_message: str | None = None
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class JobSummary(BaseModel):
|
|
id: uuid.UUID
|
|
status: str
|
|
created_at: datetime
|
|
completed_at: datetime | None = None
|
|
error_message: str | None = None
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class JobListResponse(BaseModel):
|
|
jobs: list[JobSummary]
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
@router.get("/", response_model=JobListResponse)
|
|
async def get_jobs(
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> JobListResponse:
|
|
try:
|
|
jobs = await summarise_job_repository.list_all(db)
|
|
return {"jobs": jobs}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/{job_id}", response_model=JobResponse)
|
|
async def get_job(
|
|
job_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> JobResponse:
|
|
try:
|
|
uid = uuid.UUID(job_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid job ID format")
|
|
|
|
job = await summarise_job_repository.get_by_id(db, uid)
|
|
if job is None:
|
|
raise HTTPException(status_code=404, detail="Job not found")
|
|
|
|
return JobResponse(
|
|
id=job.id,
|
|
status=job.status,
|
|
translated_article_id=job.translated_article_id,
|
|
created_at=job.created_at,
|
|
started_at=job.started_at,
|
|
completed_at=job.completed_at,
|
|
error_message=job.error_message,
|
|
)
|
|
|
|
|
|
async def _run_regenerate_audio(job_id: uuid.UUID) -> None:
|
|
gemini_client = GeminiClient(settings.gemini_api_key)
|
|
async with AsyncSessionLocal() as db:
|
|
job = await summarise_job_repository.get_by_id(db, job_id)
|
|
article_repo = TranslatedArticleRepository(db)
|
|
article_entity = await db.get(TranslatedArticleEntity, job.translated_article_id)
|
|
await summarise_job_repository.mark_processing(db, job)
|
|
|
|
try:
|
|
voice = gemini_client.get_voice_by_language(article_entity.target_language)
|
|
wav_bytes = await gemini_client.generate_audio(article_entity.target_body, voice)
|
|
audio_key = f"audio/{job_id}.wav"
|
|
upload_audio(audio_key, wav_bytes)
|
|
|
|
await article_repo.update_audio(
|
|
article_entity.id,
|
|
audio_url=audio_key,
|
|
target_body_transcript=article_entity.target_body_transcript,
|
|
)
|
|
await summarise_job_repository.mark_succeeded(db, job)
|
|
|
|
except Exception as exc:
|
|
await summarise_job_repository.mark_failed(db, job, str(exc))
|
|
|
|
|
|
@router.post("/{job_id}/regenerate-audio", status_code=202)
|
|
async def regenerate_audio(
|
|
job_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
token_data: dict = Depends(require_admin),
|
|
) -> dict:
|
|
try:
|
|
uid = uuid.UUID(job_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid job ID format")
|
|
|
|
job = await summarise_job_repository.get_by_id(db, uid)
|
|
if job is None:
|
|
raise HTTPException(status_code=404, detail="Job not found")
|
|
|
|
if str(job.user_id) != token_data["sub"]:
|
|
raise HTTPException(status_code=403, detail="Not authorized to modify this job")
|
|
|
|
if job.translated_article_id is None:
|
|
raise HTTPException(status_code=400, detail="Job has no associated article")
|
|
|
|
article_entity = await db.get(TranslatedArticleEntity, job.translated_article_id)
|
|
|
|
if not article_entity or not article_entity.target_body:
|
|
raise HTTPException(status_code=400, detail="Job has no generated text to synthesize")
|
|
|
|
if article_entity.audio_url:
|
|
raise HTTPException(status_code=409, detail="Job already has audio")
|
|
|
|
if job.status == "processing":
|
|
raise HTTPException(status_code=409, detail="Job is already processing")
|
|
|
|
await worker.enqueue(partial(_run_regenerate_audio, uid))
|
|
return {"job_id": job_id}
|