API: allow free access to audio files; create the article_service
This commit is contained in:
parent
6a08da1ff6
commit
407d423a4c
11 changed files with 132 additions and 18 deletions
0
api/app/domain/__init__.py
Normal file
0
api/app/domain/__init__.py
Normal file
15
api/app/domain/models/translated_article.py
Normal file
15
api/app/domain/models/translated_article.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
30
api/app/domain/services/article_service.py
Normal file
30
api/app/domain/services/article_service.py
Normal 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,
|
||||||
|
)
|
||||||
|
|
@ -8,6 +8,7 @@ from .routers.api import jobs
|
||||||
from .routers import auth as auth_router
|
from .routers import auth as auth_router
|
||||||
from .routers import media as media_router
|
from .routers import media as media_router
|
||||||
from .routers.api.main import api_router
|
from .routers.api.main import api_router
|
||||||
|
from .routers.bff.main import bff_router
|
||||||
from .storage import ensure_bucket_exists
|
from .storage import ensure_bucket_exists
|
||||||
from . import worker
|
from . import worker
|
||||||
|
|
||||||
|
|
@ -27,6 +28,7 @@ async def lifespan(app: FastAPI):
|
||||||
app = FastAPI(title="Language Learning API", lifespan=lifespan)
|
app = FastAPI(title="Language Learning API", lifespan=lifespan)
|
||||||
|
|
||||||
app.include_router(api_router)
|
app.include_router(api_router)
|
||||||
|
app.include_router(bff_router)
|
||||||
app.include_router(auth_router.router)
|
app.include_router(auth_router.router)
|
||||||
app.include_router(media_router.router)
|
app.include_router(media_router.router)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@ class Base(DeclarativeBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def get_db():
|
async def get_db() -> AsyncSession:
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
yield session
|
yield session
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,51 @@ from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from ..entities.summarise_job_entity import SummariseJobEntity
|
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:
|
async def update(db: AsyncSession, job: SummariseJobEntity) -> None:
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
@ -41,18 +85,6 @@ async def list_all(db: AsyncSession) -> list[SummariseJobEntity]:
|
||||||
return list(result.scalars().all())
|
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:
|
async def mark_processing(db: AsyncSession, job: SummariseJobEntity) -> None:
|
||||||
job.status = "processing"
|
job.status = "processing"
|
||||||
job.started_at = datetime.now(timezone.utc)
|
job.started_at = datetime.now(timezone.utc)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ class GenerationRequest(BaseModel):
|
||||||
target_language: str
|
target_language: str
|
||||||
complexity_level: str
|
complexity_level: str
|
||||||
input_texts: list[str]
|
input_texts: list[str]
|
||||||
topic: str | None = None
|
|
||||||
source_language: str = "en"
|
source_language: str = "en"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
0
api/app/routers/bff/__init__.py
Normal file
0
api/app/routers/bff/__init__.py
Normal file
30
api/app/routers/bff/articles.py
Normal file
30
api/app/routers/bff/articles.py
Normal 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))
|
||||||
7
api/app/routers/bff/main.py
Normal file
7
api/app/routers/bff/main.py
Normal 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)
|
||||||
|
|
@ -7,7 +7,7 @@ from botocore.exceptions import ClientError
|
||||||
|
|
||||||
from ..auth import verify_token
|
from ..auth import verify_token
|
||||||
from ..outbound.postgres.database import get_db
|
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
|
from ..storage import download_audio
|
||||||
|
|
||||||
router = APIRouter(prefix="/media", tags=["media"])
|
router = APIRouter(prefix="/media", tags=["media"])
|
||||||
|
|
@ -17,11 +17,10 @@ router = APIRouter(prefix="/media", tags=["media"])
|
||||||
async def get_media_file(
|
async def get_media_file(
|
||||||
filename: str,
|
filename: str,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
token_data: dict = Depends(verify_token),
|
|
||||||
) -> Response:
|
) -> 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:
|
if job is None:
|
||||||
raise HTTPException(status_code=404, detail="File not found")
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue