Document-Processing / api_client.py
Jiawei Dong
eng updated
9a40f1b
Raw
History Blame
5.06 kB
"""HTTP client for HIRO Translation API (public connect gateway or internal stage)."""
from __future__ import annotations
import json
import os
from collections.abc import Iterator
from typing import Any
import requests
# Public gateway (matches curl examples against connect.zhihuiya.com).
DEFAULT_BASE = "https://connect.zhihuiya.com/hiro_translation"
API_KEY_ENV = "HIRO_API_KEY"
def lang_to_codes(lang: str) -> tuple[str, str]:
src, _, tgt = lang.partition("2")
if not src or not tgt:
raise ValueError(f"invalid lang {lang!r}; expected format like zh2en")
return src, tgt
def translate_payload(
content: str,
lang: str,
*,
mode: str,
) -> dict[str, Any]:
source, target = lang_to_codes(lang)
return {
"content": content,
"sourceLanguageCode": source,
"targetLanguageCode": target,
"mode": mode,
}
def _api_url(base_url: str, path: str) -> str:
return f"{base_url.rstrip('/')}{path}"
def _request_headers(api_key: str | None = None) -> dict[str, str]:
headers = {"Content-Type": "application/json"}
key = (api_key if api_key is not None else os.environ.get(API_KEY_ENV, "")).strip()
if key:
headers["Authorization"] = f"Bearer {key}"
return headers
def _translated_text(data: dict[str, Any]) -> str:
value = data.get("textTranslated", data.get("text_translated", ""))
return value if isinstance(value, str) else ""
def _original_text(data: dict[str, Any], fallback: str = "") -> str:
value = data.get("textOriginal", data.get("text_original", fallback))
return value if isinstance(value, str) else fallback
def health_ok(
base_url: str = DEFAULT_BASE,
*,
api_key: str | None = None,
) -> tuple[bool, str]:
try:
r = requests.get(
_api_url(base_url, "/health"),
headers=_request_headers(api_key),
timeout=12,
)
r.raise_for_status()
data = r.json()
if data.get("status") != "OK":
return False, f"Unexpected response: {r.text[:200]}"
upstream = data.get("upstream", "UNKNOWN")
if upstream == "OK":
return True, "Healthy · upstream OK"
return False, f"Gateway up, upstream unavailable ({upstream})"
except requests.RequestException as exc:
return False, str(exc)
def translate_fast(
text: str,
lang: str,
*,
base_url: str = DEFAULT_BASE,
api_key: str | None = None,
timeout: int = 1800,
) -> dict[str, Any]:
r = requests.post(
_api_url(base_url, "/translate"),
json=translate_payload(text, lang, mode="fast"),
headers=_request_headers(api_key),
timeout=timeout,
)
if r.status_code >= 400:
try:
err = r.json().get("error", r.text[:500])
except json.JSONDecodeError:
err = r.text[:500]
raise RuntimeError(err)
data = r.json()
if data.get("state") not in (None, "success") and "error" in data:
raise RuntimeError(str(data.get("error", data)))
# Normalize to keys the UI already understands (snake_case aliases).
return {
"state": data.get("state", "success"),
"text_original": _original_text(data, text),
"text_translated": _translated_text(data),
"translated_character_count": data.get("translatedCharacterCount"),
"billing_amount": r.headers.get("X-Openapi-Amount"),
"raw": data,
}
def _iter_sse_json(resp: requests.Response) -> Iterator[dict[str, Any]]:
for raw in resp.iter_lines(decode_unicode=True):
if not raw:
continue
line = raw.strip()
if not line.startswith("data:"):
continue
payload = line[5:].lstrip()
if not payload:
continue
chunk = json.loads(payload)
if isinstance(chunk, dict) and chunk.get("error"):
raise RuntimeError(str(chunk["error"]))
if isinstance(chunk, dict):
yield {
"state": chunk.get("state", "success"),
"text_original": _original_text(chunk),
"text_translated": _translated_text(chunk),
"translated_character_count": chunk.get("translatedCharacterCount"),
"progress": chunk.get("progress"),
"raw": chunk,
}
def stream_translate(
text: str,
lang: str,
*,
base_url: str = DEFAULT_BASE,
api_key: str | None = None,
timeout: int = 1800,
) -> Iterator[dict[str, Any]]:
with requests.post(
_api_url(base_url, "/translate"),
json=translate_payload(text, lang, mode="stream"),
headers=_request_headers(api_key),
stream=True,
timeout=timeout,
) as resp:
if resp.status_code >= 400:
try:
err = resp.json().get("error", resp.text[:500])
except (json.JSONDecodeError, ValueError):
err = resp.text[:500]
raise RuntimeError(err)
yield from _iter_sse_json(resp)