Files
office_translator/routes/waitlist_routes.py
sepehr 526c87348f
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m20s
feat(translation): quality pipeline overhaul + new features (audit 2026-08-29)
Translation quality & format preservation:
- Word: merge adjacent same-format runs into one unit (sentence-level
  coherence like inline-tag handling); translate comments/balloons;
  dedupe textbox collection (was translated twice); RTL no longer
  overrides center/justify alignment; CJK/Arabic font hints (eastAsia/cs)
- PPTX: chart translations now actually reach the output file
  (ChartPart.blob is read-only — rewrite chart XML in the saved ZIP);
  CJK typeface hints (a:ea)
- Excel: sheet renames no longer break references — rewrite cell
  formulas (3D/quoted), defined names, data validations, cond. formats
- PDF: bold/italic honored (hebo/heit/hebi); table cells never merge;
  unchanged blocks left untouched (typography preserved, fixes duplicate
  hyperlinks); attempted/changed stats + route gate now cover PDF;
  CJK font paths; scanned PDFs via Mistral OCR (detection + admin settings)

Features:
- formality param (formal/informal) + automatic regional-variant prompts
- output_mode=bilingual docx (source above translation)
- per-user translation memory on Redis (falls back to LRU), context-hashed
- QA report + 0-100 confidence score in job status; L0 on by default
- OpenAI-compatible providers: whole chunk in ONE numbered-JSON request
  (~15x fewer calls) with per-item fallback; base prompt always present
  (custom prompt no longer replaces translation instructions)

Infra & marketing alignment:
- plan-based engine gating + vision gating (closes paid-engine leak);
  /providers/available filtered per plan; 107 languages exposed
- zh-CN/zh-TW validation fixed; libmagic disabled on Windows (native crash)
- admin: Mistral OCR settings + engine status dashboard; httpx<0.28 pin
  (TestClient breakage); Prometheus test fixture fixed
- marketing docs aligned with code (PDF+OCR, retention, engines, pricing)
- security: .env.ionos/.env.production/provider_settings.json removed

Tests: 1173 passed / 0 failed (6 network tests deselected: free Google
endpoint temporarily blocked from this machine)
2026-08-29 18:38:09 +02:00

99 lines
2.8 KiB
Python

"""
Waitlist / email-capture routes for the marketing landing page.
Public endpoint: POST /api/v1/waitlist (no auth) — deduplicated by email,
persisted in data/waitlist.json (same JSON-file pattern as provider settings).
"""
import json
import logging
import threading
from datetime import datetime, timezone
from typing import List, Optional
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from pydantic import BaseModel, EmailStr
from config import config
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/waitlist", tags=["Waitlist"])
WAITLIST_FILE = config.BASE_DIR / "data" / "waitlist.json"
_write_lock = threading.Lock()
class WaitlistEntry(BaseModel):
email: EmailStr
interest: Optional[str] = None
def _load_waitlist() -> List[dict]:
if not WAITLIST_FILE.exists():
return []
try:
with open(WAITLIST_FILE, encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, list) else []
except Exception as e:
logger.warning(f"Failed to load waitlist: {e}")
return []
def _save_waitlist(entries: List[dict]) -> None:
WAITLIST_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(WAITLIST_FILE, "w", encoding="utf-8") as f:
json.dump(entries, f, indent=2)
@router.post("", status_code=201, summary="Join the waitlist")
async def join_waitlist(entry: WaitlistEntry):
email = entry.email.lower().strip()
now = datetime.now(timezone.utc).isoformat()
with _write_lock:
entries = _load_waitlist()
for existing in entries:
if existing.get("email", "").lower() == email:
existing["updated_at"] = now
if entry.interest:
existing["interest"] = entry.interest
_save_waitlist(entries)
logger.info(f"Waitlist rejoin: {email}")
return JSONResponse(
status_code=200,
content={
"data": {"email": email, "status": "already_joined"},
"message": "You are already on the list.",
},
)
entries.append(
{
"email": email,
"interest": entry.interest,
"joined_at": now,
"updated_at": now,
}
)
_save_waitlist(entries)
logger.info(f"Waitlist join: {email} (total={len(entries)})")
return JSONResponse(
status_code=201,
content={
"data": {"email": email, "status": "joined"},
"message": "Welcome aboard!",
},
)
@router.get("/count", summary="Waitlist count")
async def waitlist_count():
return JSONResponse(
status_code=200,
content={"data": {"count": len(_load_waitlist())}},
)