83 lines
4.2 KiB
Python
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
|