Files
local-translator/examples/translate_game.py
T

83 lines
4.2 KiB
Python

"""Python standard-library client. No third-party packages required.
python examples/translate_game.py --input strings.json --output translated.json
Rerun with the same output path to resume an existing job.
"""
import argparse
import json
from pathlib import Path
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def api(base, path, payload=None, method=None):
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
request = Request(base + path, data=data, headers={"Content-Type": "application/json"}, method=method)
try:
with urlopen(request, timeout=45) as response:
return json.load(response)
except HTTPError as exc:
raise RuntimeError(f"API HTTP {exc.code}: {exc.read().decode('utf-8', errors='replace')}") from exc
def save(path, value):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")
temporary.replace(path)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--base", default="http://127.0.0.1:8787")
parser.add_argument("--input", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--project", help="Existing project ID; otherwise create one")
parser.add_argument("--project-file", type=Path, help="Project settings JSON used when creating a project")
parser.add_argument("--context", default="Fantasy RPG. Characters speak naturally. Preserve names consistently.")
parser.add_argument("--target", default="Russian")
parser.add_argument("--retry", action="store_true", help="Retry failed items from the saved job")
parser.add_argument("--new-job", action="store_true", help="Ignore saved job and translate using current project settings")
args = parser.parse_args()
base = args.base.rstrip("/")
items = json.loads(args.input.read_text(encoding="utf-8"))
if not isinstance(items, list) or not all(isinstance(x, dict) and isinstance(x.get("id"), str) and isinstance(x.get("text"), str) for x in items):
parser.error("Input must be an array of {id: string, text: string}")
if not 1 <= len(items) <= 500:
parser.error("Send 1..500 items per saved job; split larger files")
state_path = args.output.with_suffix(args.output.suffix + ".job.json")
if state_path.exists() and not args.new_job:
state = json.loads(state_path.read_text(encoding="utf-8"))
if state["base"] != base or state["items"] != items or (args.project and args.project != state["project_id"]):
parser.error("Saved job belongs to a different input, project or server; use --new-job")
job = api(base, "/jobs/" + state["job_id"])
if args.retry and job["status"] in {"failed", "completed_with_errors"}:
job = api(base, "/jobs/" + job["id"] + "/retry", {})
else:
project_id = args.project
if not project_id:
project_config = json.loads(args.project_file.read_text(encoding="utf-8")) if args.project_file else {
"name": args.input.stem, "target_language": args.target, "context": args.context,
"glossary": {"Moonstone": "Лунный камень"}}
project = api(base, "/projects", project_config)
project_id = project["id"]
job = api(base, f"/projects/{project_id}/jobs", {"items": items, "batch_size": 10})
save(state_path, {"base": base, "project_id": project_id, "job_id": job["id"], "items": items})
print(f"Project: {job['project_id']} Job: {job['id']}")
while job["status"] in {"queued", "running"}:
print(f"Status: {job['status']} {job['progress']}", flush=True)
time.sleep(1)
job = api(base, "/jobs/" + job["id"])
save(args.output, job)
print(f"Saved {args.output}: {job['status']}")
if job["status"] != "completed":
raise SystemExit("Some items failed. See output; rerun with --retry after fixing the cause.")
if __name__ == "__main__":
try:
main()
except (RuntimeError, URLError, OSError, ValueError) as exc:
raise SystemExit(str(exc)) from exc