feat: complete translator stack
This commit is contained in:
commit
6fe7c5121b
9946 files changed
+1747495
No files matched your search
@@ -0,0 +1 @@
|
||||
from . import health, translation
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "ok", "service": "traduttore"}
|
||||
@@ -0,0 +1,223 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, File, Form, HTTPException, UploadFile
|
||||
from langdetect import DetectorFactory, detect
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from backend.app.models.session import SessionState
|
||||
from backend.app.processors.registry import select_processor
|
||||
from backend.app.services.chunking import chunk_document, chunk_store
|
||||
from backend.app.utils.logger import logger
|
||||
from backend.app.utils.merge import write_final_document
|
||||
from backend.app.utils.ollama_client import OllamaClient
|
||||
from backend.app.utils.session_store import session_store
|
||||
|
||||
DetectorFactory.seed = 0
|
||||
router = APIRouter()
|
||||
OLLAMA_BASE_URL = os.getenv("OLLAMA_URL", "https://ollama.solarpi.net")
|
||||
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "translategemma:latest")
|
||||
ollama_client = OllamaClient(OLLAMA_BASE_URL, OLLAMA_MODEL)
|
||||
|
||||
|
||||
def _detect_language(paragraphs: List[str]) -> str:
|
||||
snippet = " ".join(paragraphs[:5])
|
||||
if not snippet.strip():
|
||||
return "auto"
|
||||
try:
|
||||
return detect(snippet)
|
||||
except Exception:
|
||||
return "auto"
|
||||
|
||||
|
||||
async def _save_upload(upload: UploadFile) -> Path:
|
||||
suffix = Path(upload.filename or "").suffix or ".bin"
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
||||
payload = await upload.read()
|
||||
tmp.write(payload)
|
||||
tmp.flush()
|
||||
return Path(tmp.name)
|
||||
|
||||
|
||||
def _build_session(session_id: str, file_title: str, extraction_title: str, source_lang: str, target_lang: str, chunk_count: int) -> SessionState:
|
||||
return SessionState(
|
||||
session_id=session_id,
|
||||
file_title=file_title,
|
||||
text_title=extraction_title or file_title,
|
||||
source_lang=source_lang,
|
||||
target_lang=target_lang,
|
||||
total_chunks=chunk_count,
|
||||
current_chunk_index=0,
|
||||
status="pending",
|
||||
chunks_remaining=chunk_count,
|
||||
)
|
||||
|
||||
|
||||
async def _run_translation(session_id: str) -> None:
|
||||
session = session_store.get(session_id)
|
||||
if not session:
|
||||
return
|
||||
|
||||
session.status = "running"
|
||||
session.started_at = datetime.utcnow()
|
||||
session_store.update(session)
|
||||
logger.info("Sessione %s: avvio traduzione", session_id)
|
||||
|
||||
chunks = chunk_store.get_chunks(session_id)
|
||||
durations: List[float] = []
|
||||
|
||||
for chunk in chunks:
|
||||
session = session_store.get(session_id)
|
||||
if not session:
|
||||
return
|
||||
if session.stop_requested:
|
||||
session.status = "stopped"
|
||||
session_store.update(session)
|
||||
logger.info("Sessione %s: interrotta dall'utente", session_id)
|
||||
return
|
||||
|
||||
start = datetime.utcnow()
|
||||
try:
|
||||
translation = await ollama_client.translate(chunk.text, session.source_lang, session.target_lang)
|
||||
except Exception as exc:
|
||||
session.status = "error"
|
||||
session.error_message = str(exc)
|
||||
session_store.update(session)
|
||||
logger.error("Sessione %s chunk %s fallito: %s", session_id, chunk.index, exc)
|
||||
return
|
||||
|
||||
now = datetime.utcnow()
|
||||
duration = (now - start).total_seconds()
|
||||
durations.append(duration)
|
||||
if len(durations) > 3:
|
||||
durations.pop(0)
|
||||
|
||||
session.translated_chunks.append(translation)
|
||||
session.translated_text = "\n\n".join(session.translated_chunks)
|
||||
session.current_chunk_index = chunk.index
|
||||
session.chunks_remaining = max(0, session.total_chunks - chunk.index)
|
||||
session.last_chunk_at = now
|
||||
avg = sum(durations) / len(durations) if durations else 0
|
||||
session.eta_seconds = int(avg * session.chunks_remaining) if session.chunks_remaining else 0
|
||||
session_store.update(session)
|
||||
logger.info("Sessione %s: chunk %s tradotto", session_id, chunk.index)
|
||||
|
||||
session.status = "completed"
|
||||
session.ended_at = datetime.utcnow()
|
||||
session.chunks_remaining = 0
|
||||
session.eta_seconds = 0
|
||||
session.final_document_path = str(write_final_document(session.session_id, session.translated_text))
|
||||
logger.info("Sessione %s completata. Documento finale: %s", session_id, session.final_document_path)
|
||||
session_store.update(session)
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_document(target_lang: str = Form(...), file: UploadFile = File(...)):
|
||||
processor = select_processor(Path(file.filename or ""))
|
||||
if not processor:
|
||||
raise HTTPException(status_code=400, detail="Formato file non supportato")
|
||||
|
||||
temp_path = await _save_upload(file)
|
||||
try:
|
||||
extraction = processor(temp_path)
|
||||
finally:
|
||||
temp_path.unlink(missing_ok=True)
|
||||
|
||||
language = extraction.source_language or _detect_language(extraction.paragraphs)
|
||||
session_id = uuid4().hex
|
||||
chunks = chunk_document(session_id, extraction)
|
||||
logger.info("Sessione %s: %s chunk generati", session_id, len(chunks))
|
||||
|
||||
session = _build_session(
|
||||
session_id,
|
||||
file.filename or "sconosciuto",
|
||||
extraction.title,
|
||||
language,
|
||||
target_lang,
|
||||
len(chunks),
|
||||
)
|
||||
session_store.create(session)
|
||||
|
||||
return {
|
||||
"session_id": session.session_id,
|
||||
"total_chunks": session.total_chunks,
|
||||
"source_lang": session.source_lang,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
async def start_translation(session_id: str, background: BackgroundTasks):
|
||||
session = session_store.get(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Sessione non trovata")
|
||||
|
||||
if session.status == "running":
|
||||
return {"message": "Traduzione già in corso", "session_id": session_id}
|
||||
|
||||
session.status = "running"
|
||||
session.stop_requested = False
|
||||
session_store.update(session)
|
||||
logger.info("Sessione %s: start richiesta", session_id)
|
||||
background.add_task(_run_translation, session_id)
|
||||
|
||||
return {"message": "Traduzione avviata", "session_id": session_id}
|
||||
|
||||
|
||||
@router.post("/stop")
|
||||
async def stop_translation(session_id: str):
|
||||
session = session_store.get(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Sessione non trovata")
|
||||
|
||||
session.stop_requested = True
|
||||
session.status = "stopping"
|
||||
session_store.update(session)
|
||||
logger.info("Sessione %s: stop richiesto", session_id)
|
||||
return {"message": "Stop richiesto", "session_id": session_id}
|
||||
|
||||
|
||||
@router.get("/status/{session_id}")
|
||||
async def translation_status(session_id: str):
|
||||
session = session_store.get(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Sessione non trovata")
|
||||
return session
|
||||
|
||||
|
||||
async def _sse_generator(session_id: str):
|
||||
while True:
|
||||
session = session_store.get(session_id)
|
||||
if not session:
|
||||
break
|
||||
payload = {
|
||||
"status": session.status,
|
||||
"current_chunk": session.current_chunk_index,
|
||||
"total_chunks": session.total_chunks,
|
||||
"chunks_remaining": session.chunks_remaining,
|
||||
"eta_seconds": session.eta_seconds,
|
||||
"translated_text": session.translated_text,
|
||||
"error_message": session.error_message,
|
||||
"file_title": session.file_title,
|
||||
"text_title": session.text_title,
|
||||
"started_at": session.started_at.isoformat() if session.started_at else None,
|
||||
"ended_at": session.ended_at.isoformat() if session.ended_at else None,
|
||||
}
|
||||
yield f"data: {json.dumps(payload)}\n\n"
|
||||
if session.status in {"completed", "error", "stopped"}:
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@router.get("/stream/{session_id}")
|
||||
async def translation_stream(session_id: str):
|
||||
if not session_store.get(session_id):
|
||||
raise HTTPException(status_code=404, detail="Sessione non trovata")
|
||||
headers = {"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
|
||||
return StreamingResponse(_sse_generator(session_id), media_type="text/event-stream", headers=headers)
|
||||
Reference in new issue
Block a user