API: allow free access to audio files; create the article_service

This commit is contained in:
wilson 2026-03-26 20:47:15 +00:00
parent 6a08da1ff6
commit 407d423a4c
11 changed files with 132 additions and 18 deletions

View file

View file

@ -0,0 +1,15 @@
from dataclasses import dataclass
@dataclass
class TranslatedArticle:
id: str
source_lang: str
source_title: str
source_text: str
target_lang: str
target_title: str
target_text: str

View file

@ -0,0 +1,30 @@
import re
from ..models.summarise_job import SummariseJob
from ..models.translated_article import TranslatedArticle
def first_heading(md: str) -> str | None:
m = re.search(r'^#{1,2}\s+(.+)', md, re.MULTILINE)
return m.group(1).strip() if m else None
class ArticleService:
def __init__(self, summarise_job_repository):
self.summarise_job_repository = summarise_job_repository
async def get_all_articles(self) -> list[TranslatedArticle]:
summarise_jobs = await self.summarise_job_repository.list_all()
return summarise_jobs.map(self.summarise_job_to_translated_article)
def summarise_job_to_translated_article(
self,
summarise_job: SummariseJob,
) -> TranslatedArticle:
return TranslatedArticle(
id=summarise_job.id,
source_lang=summarise_job.target_language, # The source language for the article is the target language of the job
source_title=first_heading(summarise_job.translated_text) or "",
source_text=summarise_job.translated_text,
target_lang=summarise_job.source_language, # The target language for the article is the source language of the job
target_title=first_heading(summarise_job.generated_text) or "",
target_text=summarise_job.generated_text,
)

View file

@ -8,6 +8,7 @@ from .routers.api import jobs
from .routers import auth as auth_router
from .routers import media as media_router
from .routers.api.main import api_router
from .routers.bff.main import bff_router
from .storage import ensure_bucket_exists
from . import worker
@ -27,6 +28,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="Language Learning API", lifespan=lifespan)
app.include_router(api_router)
app.include_router(bff_router)
app.include_router(auth_router.router)
app.include_router(media_router.router)

View file

@ -11,6 +11,6 @@ class Base(DeclarativeBase):
pass
async def get_db():
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session

View file

@ -5,7 +5,51 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from ..entities.summarise_job_entity import SummariseJobEntity
from ....domain.models.summarise_job import SummariseJob
class PostgresSummariseJobRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def list_all(self) -> list[SummariseJob]:
result = self.db.execute(
select(SummariseJobEntity).order_by(SummariseJobEntity.created_at.desc())
)
return list(result.scalars().all()).map(self.entity_to_model)
async def get_by_audio_url(
self,
audio_url: str
) -> SummariseJob | None:
result = await self.db.execute(
select(SummariseJobEntity).where(
SummariseJobEntity.audio_url == audio_url
)
)
return self.entity_to_model(result.scalar_one_or_none())
def entity_to_model(self, entity: SummariseJobEntity | None) -> SummariseJob:
if entity is None:
return None
return SummariseJob(
id=str(entity.id),
user_id=str(entity.user_id),
status=entity.status,
source_language=entity.source_language,
target_language=entity.target_language,
complexity_level=entity.complexity_level,
input_summary=entity.input_summary,
generated_text=entity.generated_text,
translated_text=entity.translated_text,
error_message=entity.error_message,
audio_url=entity.audio_url,
created_at=entity.created_at,
started_at=entity.started_at,
completed_at=entity.completed_at,
)
async def update(db: AsyncSession, job: SummariseJobEntity) -> None:
await db.commit()
@ -41,18 +85,6 @@ async def list_all(db: AsyncSession) -> list[SummariseJobEntity]:
return list(result.scalars().all())
async def get_by_audio_url_and_user(
db: AsyncSession, audio_url: str, user_id: uuid.UUID
) -> SummariseJobEntity | None:
result = await db.execute(
select(SummariseJobEntity).where(
SummariseJobEntity.audio_url == audio_url,
SummariseJobEntity.user_id == user_id,
)
)
return result.scalar_one_or_none()
async def mark_processing(db: AsyncSession, job: SummariseJobEntity) -> None:
job.status = "processing"
job.started_at = datetime.now(timezone.utc)

View file

@ -23,7 +23,6 @@ class GenerationRequest(BaseModel):
target_language: str
complexity_level: str
input_texts: list[str]
topic: str | None = None
source_language: str = "en"

View file

View file

@ -0,0 +1,30 @@
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from ...domain.services.article_service import ArticleService
from ...outbound.postgres.database import get_db, AsyncSessionLocal
from ...outbound.postgres.repositories.summarise_job_repository import PostgresSummariseJobRepository
router = APIRouter(prefix="/articles", tags=["articles"])
class ArticleResponse(BaseModel):
target_language: str
complexity_level: str
input_texts: list[str]
class ArticlesResponse(BaseModel):
articles: list[ArticleResponse]
@router.get("", response_model=ArticlesResponse, status_code=200)
async def get_articles(
db = Depends(get_db),
) -> ArticlesResponse:
service = ArticleService(PostgresSummariseJobRepository(db))
try:
articles = await service.get_all_articles()
return ArticlesResponse(articles=articles)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View file

@ -0,0 +1,7 @@
from .articles import router as article_router
from fastapi import APIRouter
bff_router = APIRouter(prefix="/bff", tags=["bff"])
bff_router.include_router(article_router)

View file

@ -7,7 +7,7 @@ from botocore.exceptions import ClientError
from ..auth import verify_token
from ..outbound.postgres.database import get_db
from ..outbound.postgres.repositories import summarise_job_repository
from ..outbound.postgres.repositories.summarise_job_repository import PostgresSummariseJobRepository
from ..storage import download_audio
router = APIRouter(prefix="/media", tags=["media"])
@ -17,11 +17,10 @@ router = APIRouter(prefix="/media", tags=["media"])
async def get_media_file(
filename: str,
db: AsyncSession = Depends(get_db),
token_data: dict = Depends(verify_token),
) -> Response:
user_id = uuid.UUID(token_data["sub"])
repository = PostgresSummariseJobRepository(db)
job = await repository.get_by_audio_url(filename)
job = await summarise_job_repository.get_by_audio_url_and_user(db, filename, user_id)
if job is None:
raise HTTPException(status_code=404, detail="File not found")