Files
mrleo1nidandClaude Sonnet 5.5 f49ee90477 feat: browser access (allowed_origins, CORS with Private Network Access), job cancel, Tomie/Unity adapters, benchmark
Includes earlier uncommitted work: Tomie and Unity adapters, benchmark, structured output, call metrics.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 18:59:28 +03:00

142 lines
6.9 KiB
Python

import asyncio
from contextlib import suppress
import hashlib
import logging
from .config import ProviderConfig
from .models import Project
from .protection import protect
from .provider import Provider, ProviderError
from .storage import encode
log = logging.getLogger(__name__)
PROMPT_VERSION = 3
class Engine:
def __init__(self, storage, provider_factory=Provider):
self.storage = storage
self.provider_factory = provider_factory
self.wakeup = asyncio.Event()
self.task = None
def start(self):
self.storage.recover()
self.task = asyncio.create_task(self.run())
async def stop(self):
if self.task:
self.task.cancel()
with suppress(asyncio.CancelledError):
await self.task
async def run(self):
while True:
self.wakeup.clear()
identity = self.storage.next_job()
if identity:
try:
await self.process(identity)
except Exception:
log.exception("Translation job %s failed unexpectedly", identity)
self.storage.job_status(identity, "failed", "Internal processing error; see server log")
else:
await self.wakeup.wait()
async def process(self, identity):
job = self.storage.get_job(identity, internal=True)
snapshot, options = job["snapshot"], job["request"]
project = Project.model_validate({k: v for k, v in snapshot["project"].items() if k not in {"id", "revision"}})
config = ProviderConfig.model_validate(snapshot["provider"])
provider = self.provider_factory(config)
self.storage.job_status(identity, "running")
try:
model = job["model"] or await provider.resolve_model(project.model)
self.storage.job_status(identity, "running", model=model)
fingerprint = hashlib.sha256(encode({"snapshot": snapshot, "model": model, "prompt_version": PROMPT_VERSION}).encode()).hexdigest()
pending = []
for item in job["items"]:
if item["status"] == "completed":
continue
if not item["source"].strip():
self.storage.item_result(identity, item["position"], "completed", item["source"])
continue
if item["source"] in project.glossary:
# A complete dictionary match is authoritative, even when
# a model would mistake a short UI label for a code token.
translated = project.glossary[item["source"]]
original_tokens = list(protect(item["source"], project).tokens.values())
translated_tokens = list(protect(translated, project).tokens.values())
if not translated.strip() or original_tokens != translated_tokens:
self.storage.item_result(identity, item["position"], "failed", error="Exact glossary entry changes protected syntax or has empty text")
else:
self.storage.item_result(identity, item["position"], "completed", translated)
self.storage.cache_put(fingerprint, item["source"], translated)
continue
cached = self.storage.cache_get(fingerprint, item["source"]) if options["use_cache"] else None
if cached is not None:
self.storage.item_result(identity, item["position"], "completed", cached, cached=True)
else:
pending.append(item)
while pending and self.storage.job_state(identity) != "cancelled":
batch, size = [], 0
while pending and len(batch) < options["batch_size"]:
item = pending[0]
if batch and size + len(item["source"]) > options["max_batch_chars"]:
break
batch.append(pending.pop(0))
size += len(item["source"])
await self.translate_batch(provider, model, project, job, fingerprint, batch)
result = self.storage.get_job(identity)
self.storage.job_status(identity, "completed_with_errors" if result["progress"]["failed"] else "completed")
except ProviderError as exc:
self.storage.job_status(identity, "failed", str(exc))
finally:
await provider.close()
async def translate_batch(self, provider, model, project, job, fingerprint, items):
identity = job["id"]
remaining = items
for attempt in range(job["request"]["retries"] + 1):
eligible = []
for item in remaining:
if item["attempts"] >= job["request"]["retries"] + 1:
self.storage.item_result(identity, item["position"], "failed", error="Attempt budget exhausted; retry this job explicitly")
else:
eligible.append(item)
remaining = eligible
if not remaining:
break
protected = {item["id"]: protect(item["source"], project) for item in remaining}
self.storage.attempted(identity, [item["position"] for item in remaining])
for item in remaining:
item["attempts"] += 1
error = "Model omitted the item or returned a duplicate ID"
call_error = None
try:
output = await provider.translate(model, project, [{"id": item["id"], "text": protected[item["id"]].text} for item in remaining])
except ProviderError as exc:
output, error = {}, str(exc)
call_error = str(exc)
self.storage.record_call(identity, len(remaining), getattr(provider, "last_metrics", {}), call_error)
failed = []
for item in remaining:
item_error = error
try:
text = output[item["id"]]
if not text.strip():
raise ValueError("Model returned an empty translation")
translated = protected[item["id"]].restore(text)
except (KeyError, ValueError) as exc:
if isinstance(exc, ValueError):
item_error = str(exc)
failed.append(item)
# Keep retryable rows pending until the attempt budget ends.
self.storage.item_result(identity, item["position"], "failed" if item["attempts"] >= job["request"]["retries"] + 1 else "pending", error=item_error)
else:
self.storage.cache_put(fingerprint, item["source"], translated)
self.storage.item_result(identity, item["position"], "completed", translated)
remaining = failed
if remaining and attempt < job["request"]["retries"]:
await asyncio.sleep(min(2 ** attempt, 8))