Make SubFox production-ready with parallel translation and UI controls

This commit is contained in:
Eddie Nielsen 2026-03-25 11:24:54 +00:00
parent c40b8bed2b
commit 2b1d05f02c
6046 changed files with 798327 additions and 0 deletions

157
app/main.py Normal file
View file

@ -0,0 +1,157 @@
from __future__ import annotations
import threading
import uuid
from pathlib import Path
from fastapi import FastAPI, UploadFile, File, Form, Request
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from app.services.job_store import jobs
from app.services.subtitle_service import translate_srt_content, parse_srt
app = FastAPI()
templates = Jinja2Templates(directory="app/templates")
OUTPUT_DIR = Path("data/output")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
return templates.TemplateResponse(
request=request,
name="index.html",
context={},
)
@app.post("/start")
async def start_translation(
file: UploadFile = File(...),
mode: str = Form("fast"),
source_lang: str = Form("auto"),
target_lang: str = Form("da"),
model: str = Form("gpt-4o-mini"),
workers: int = Form(4),
):
try:
raw = await file.read()
content = raw.decode("utf-8-sig")
except UnicodeDecodeError:
return JSONResponse(
{"error": "Kunne ikke læse filen som UTF-8/UTF-8-SIG"},
status_code=400,
)
if workers < 1:
workers = 1
if workers > 16:
workers = 16
job_id = str(uuid.uuid4())
output_path = OUTPUT_DIR / f"{job_id}.srt"
try:
parsed_blocks = parse_srt(content)
block_count = len(parsed_blocks)
except Exception:
block_count = 0
jobs[job_id] = {
"status": "queued",
"progress": 0,
"filename": file.filename or "translated.srt",
"output_path": str(output_path),
"blocks": block_count,
"error": "",
"done": False,
"mode": mode,
"source_lang": source_lang,
"target_lang": target_lang,
"model": model,
"workers": workers,
}
def progress_callback(done_blocks: int, total_blocks: int):
if total_blocks <= 0:
jobs[job_id]["progress"] = 1
return
percent = int((done_blocks / total_blocks) * 100)
if done_blocks > 0:
percent = max(2, percent)
percent = min(99, percent)
jobs[job_id]["progress"] = percent
jobs[job_id]["status"] = "running"
jobs[job_id]["blocks"] = total_blocks
def worker():
try:
jobs[job_id]["status"] = "starting"
jobs[job_id]["progress"] = 1
translated_srt = translate_srt_content(
content=content,
mode=mode,
source_lang=source_lang,
target_lang=target_lang,
job_id=job_id,
progress_callback=progress_callback,
model=model,
workers=workers,
)
output_path.write_text(translated_srt, encoding="utf-8")
jobs[job_id]["status"] = "done"
jobs[job_id]["progress"] = 100
jobs[job_id]["done"] = True
except Exception as e:
jobs[job_id]["status"] = "error"
jobs[job_id]["progress"] = 0
jobs[job_id]["error"] = str(e)
jobs[job_id]["done"] = False
threading.Thread(target=worker, daemon=True).start()
return JSONResponse({"job_id": job_id})
@app.get("/status/{job_id}")
async def get_status(job_id: str):
job = jobs.get(job_id)
if not job:
return JSONResponse(
{"status": "unknown", "progress": 0, "error": "Job not found"},
status_code=404,
)
return JSONResponse(job)
@app.get("/download/{job_id}")
async def download_result(job_id: str):
job = jobs.get(job_id)
if not job:
return JSONResponse({"error": "Job not found"}, status_code=404)
if not job.get("done"):
return JSONResponse({"error": "File not ready yet"}, status_code=400)
path = Path(job["output_path"])
if not path.exists():
return JSONResponse({"error": "Output file missing"}, status_code=404)
original_name = job.get("filename", "translated.srt")
download_name = f"translated_{original_name}"
return FileResponse(
path=path,
media_type="application/x-subrip",
filename=download_name,
)