Full-stack Amazon AI Transcreation Platform with: - FastAPI backend (async, PostgreSQL, Redis, Celery) with 11 DB tables - JWT auth (SSO-ready abstract provider pattern) - 6-agent pipeline orchestrator with deterministic modules - Next.js 14 frontend with Amazon branding (Ember fonts, orange/dark theme) - Job wizard, monitoring HUD, output review, admin screens - 154 TM/reference files imported, 12 locales configured - Docker Compose for all services Agents 2-5 (TM retrieval, ranker, transcreator, compliance) are stubs pending Phase 3 LLM integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
5.9 KiB
Python
179 lines
5.9 KiB
Python
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, status
|
|
from fastapi.responses import FileResponse
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.dependencies import get_current_user, get_db
|
|
from app.models.files import ReferenceFileType
|
|
from app.schemas.files import (
|
|
FileUploadResponse,
|
|
ReferenceFileResponse,
|
|
TMFileResponse,
|
|
)
|
|
from app.services.file_service import FileService
|
|
|
|
router = APIRouter(prefix="/files", tags=["files"])
|
|
file_service = FileService()
|
|
|
|
|
|
# ---- TM Files ----
|
|
|
|
|
|
@router.post("/tm", response_model=FileUploadResponse, status_code=status.HTTP_201_CREATED)
|
|
async def upload_tm_file(
|
|
client_id: UUID = Query(...),
|
|
locale_code: str = Query(...),
|
|
channel: str = Query(...),
|
|
file: UploadFile = File(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> FileUploadResponse:
|
|
"""Upload a Translation Memory (JSONL) file."""
|
|
if not file.filename:
|
|
raise HTTPException(status_code=400, detail="File must have a filename")
|
|
if not file_service.validate_file_extension(file.filename, [".jsonl", ".json"]):
|
|
raise HTTPException(status_code=400, detail="Only .jsonl/.json files accepted")
|
|
|
|
tm = await file_service.upload_tm_file(
|
|
db, client_id, locale_code, channel, file.file, file.filename,
|
|
uploaded_by=current_user["user_id"],
|
|
)
|
|
return FileUploadResponse(
|
|
id=tm.id,
|
|
filename=tm.filename,
|
|
file_path=tm.file_path,
|
|
message=f"Uploaded TM file with {tm.segment_count} segments",
|
|
)
|
|
|
|
|
|
@router.get("/tm", response_model=list[TMFileResponse])
|
|
async def list_tm_files(
|
|
client_id: UUID = Query(...),
|
|
locale_code: str | None = Query(None),
|
|
channel: str | None = Query(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> list[TMFileResponse]:
|
|
"""List TM files for a client."""
|
|
files = await file_service.list_tm_files(db, client_id, locale_code, channel)
|
|
return [TMFileResponse.model_validate(f) for f in files]
|
|
|
|
|
|
@router.get("/tm/{file_id}/download")
|
|
async def download_tm_file(
|
|
file_id: UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> FileResponse:
|
|
"""Download a TM file."""
|
|
from sqlalchemy import select
|
|
from app.models.files import TMFileRegistry
|
|
|
|
result = await db.execute(
|
|
select(TMFileRegistry).where(TMFileRegistry.id == file_id)
|
|
)
|
|
tm = result.scalar_one_or_none()
|
|
if tm is None:
|
|
raise HTTPException(status_code=404, detail="TM file not found")
|
|
|
|
path = file_service.get_file_path(tm.file_path)
|
|
if path is None:
|
|
raise HTTPException(status_code=404, detail="File not found on disk")
|
|
|
|
return FileResponse(path=str(path), filename=tm.filename)
|
|
|
|
|
|
@router.delete("/tm/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_tm_file(
|
|
file_id: UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> None:
|
|
"""Delete a TM file."""
|
|
deleted = await file_service.delete_tm_file(db, file_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=404, detail="TM file not found")
|
|
|
|
|
|
# ---- Reference Files ----
|
|
|
|
|
|
@router.post(
|
|
"/reference",
|
|
response_model=FileUploadResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
)
|
|
async def upload_reference_file(
|
|
client_id: UUID = Query(...),
|
|
file_type: ReferenceFileType = Query(...),
|
|
locale_scope: str = Query(...),
|
|
file: UploadFile = File(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> FileUploadResponse:
|
|
"""Upload a reference file (glossary, blacklist, TOV, etc.)."""
|
|
if not file.filename:
|
|
raise HTTPException(status_code=400, detail="File must have a filename")
|
|
|
|
ref = await file_service.upload_reference_file(
|
|
db, client_id, file_type, locale_scope, file.file, file.filename,
|
|
uploaded_by=current_user["user_id"],
|
|
)
|
|
return FileUploadResponse(
|
|
id=ref.id,
|
|
filename=ref.filename,
|
|
file_path=ref.file_path,
|
|
message=f"Uploaded {file_type.value} reference file",
|
|
)
|
|
|
|
|
|
@router.get("/reference", response_model=list[ReferenceFileResponse])
|
|
async def list_reference_files(
|
|
client_id: UUID = Query(...),
|
|
file_type: ReferenceFileType | None = Query(None),
|
|
locale_scope: str | None = Query(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> list[ReferenceFileResponse]:
|
|
"""List reference files for a client."""
|
|
files = await file_service.list_reference_files(
|
|
db, client_id, file_type, locale_scope
|
|
)
|
|
return [ReferenceFileResponse.model_validate(f) for f in files]
|
|
|
|
|
|
@router.get("/reference/{file_id}/download")
|
|
async def download_reference_file(
|
|
file_id: UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> FileResponse:
|
|
"""Download a reference file."""
|
|
from sqlalchemy import select
|
|
from app.models.files import ReferenceFile
|
|
|
|
result = await db.execute(
|
|
select(ReferenceFile).where(ReferenceFile.id == file_id)
|
|
)
|
|
ref = result.scalar_one_or_none()
|
|
if ref is None:
|
|
raise HTTPException(status_code=404, detail="Reference file not found")
|
|
|
|
path = file_service.get_file_path(ref.file_path)
|
|
if path is None:
|
|
raise HTTPException(status_code=404, detail="File not found on disk")
|
|
|
|
return FileResponse(path=str(path), filename=ref.filename)
|
|
|
|
|
|
@router.delete("/reference/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_reference_file(
|
|
file_id: UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> None:
|
|
"""Delete a reference file."""
|
|
deleted = await file_service.delete_reference_file(db, file_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=404, detail="Reference file not found")
|