Jiawei Dong commited on
Commit
d77d8e5
·
1 Parent(s): b1b2cec

api fixed

Browse files
Files changed (4) hide show
  1. README.md +1 -1
  2. __pycache__/api_client.cpython-310.pyc +0 -0
  3. api_client.py +183 -26
  4. app.py +41 -64
README.md CHANGED
@@ -21,7 +21,7 @@ Gradio demo with two tabs sharing one Bearer API key from the environment.
21
  | Tab | Gateway | Notes |
22
  |-----|---------|--------|
23
  | **Smart Doc** | `https://connect.zhihuiya.com/rd-llm/v1/documents/doc_parsing` | Multipart upload · `output_format=both` · **10 MB** max |
24
- | **Translation** | `https://connect.zhihuiya.com/hiro_translation` | `stream` (SSE segments) or `fast` (single JSON) |
25
 
26
  ## Auth
27
 
 
21
  | Tab | Gateway | Notes |
22
  |-----|---------|--------|
23
  | **Smart Doc** | `https://connect.zhihuiya.com/rd-llm/v1/documents/doc_parsing` | Multipart upload · `output_format=both` · **10 MB** max |
24
+ | **Translation** | `https://connect.zhihuiya.com/hiro_translation` | `stream`: SSE via `POST /translate`; `fast`: async submit + poll (`POST /translate/async` → `GET /translate/async/{taskId}`) |
25
 
26
  ## Auth
27
 
__pycache__/api_client.cpython-310.pyc CHANGED
Binary files a/__pycache__/api_client.cpython-310.pyc and b/__pycache__/api_client.cpython-310.pyc differ
 
api_client.py CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
5
  import json
6
  import os
7
  import re
 
8
  from collections.abc import Iterator
9
  from pathlib import Path
10
  from typing import Any
@@ -20,6 +21,10 @@ API_KEY_ENV_FALLBACK = "RD_LLM_API_KEY"
20
 
21
  MAX_SMARTDOC_BYTES = 10 * 1024 * 1024
22
 
 
 
 
 
23
 
24
  def resolve_api_key(api_key: str | None = None) -> str:
25
  """Prefer explicit key, then HIRO_API_KEY, then RD_LLM_API_KEY."""
@@ -38,18 +43,13 @@ def lang_to_codes(lang: str) -> tuple[str, str]:
38
  return src, tgt
39
 
40
 
41
- def translate_payload(
42
- content: str,
43
- lang: str,
44
- *,
45
- mode: str,
46
- ) -> dict[str, Any]:
47
  source, target = lang_to_codes(lang)
48
  return {
49
  "content": content,
50
  "sourceLanguageCode": source,
51
  "targetLanguageCode": target,
52
- "mode": mode,
53
  }
54
 
55
 
@@ -81,6 +81,16 @@ def _original_text(data: dict[str, Any], fallback: str = "") -> str:
81
  return value if isinstance(value, str) else fallback
82
 
83
 
 
 
 
 
 
 
 
 
 
 
84
  def health_ok(
85
  base_url: str = DEFAULT_BASE,
86
  *,
@@ -104,39 +114,189 @@ def health_ok(
104
  return False, str(exc)
105
 
106
 
107
- def translate_fast(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  text: str,
109
  lang: str,
110
  *,
111
  base_url: str = DEFAULT_BASE,
112
  api_key: str | None = None,
113
- timeout: int = 1800,
114
  ) -> dict[str, Any]:
 
115
  r = requests.post(
116
- _api_url(base_url, "/translate"),
117
- json=translate_payload(text, lang, mode="fast"),
118
  headers=_request_headers(api_key),
119
  timeout=timeout,
120
  )
121
  if r.status_code >= 400:
122
- try:
123
- err = r.json().get("error", r.text[:500])
124
- except json.JSONDecodeError:
125
- err = r.text[:500]
126
- raise RuntimeError(err)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  data = r.json()
128
- if data.get("state") not in (None, "success") and "error" in data:
129
- raise RuntimeError(str(data.get("error", data)))
130
  return {
131
- "state": data.get("state", "success"),
132
- "text_original": _original_text(data, text),
133
  "text_translated": _translated_text(data),
134
  "translated_character_count": data.get("translatedCharacterCount"),
 
135
  "billing_amount": r.headers.get("X-Openapi-Amount"),
136
  "raw": data,
137
  }
138
 
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  def _iter_sse_json(resp: requests.Response) -> Iterator[dict[str, Any]]:
141
  for raw in resp.iter_lines(decode_unicode=True):
142
  if not raw:
@@ -169,19 +329,16 @@ def stream_translate(
169
  api_key: str | None = None,
170
  timeout: int = 1800,
171
  ) -> Iterator[dict[str, Any]]:
 
172
  with requests.post(
173
  _api_url(base_url, "/translate"),
174
- json=translate_payload(text, lang, mode="stream"),
175
  headers=_request_headers(api_key),
176
  stream=True,
177
  timeout=timeout,
178
  ) as resp:
179
  if resp.status_code >= 400:
180
- try:
181
- err = resp.json().get("error", resp.text[:500])
182
- except (json.JSONDecodeError, ValueError):
183
- err = resp.text[:500]
184
- raise RuntimeError(err)
185
  yield from _iter_sse_json(resp)
186
 
187
 
 
5
  import json
6
  import os
7
  import re
8
+ import time
9
  from collections.abc import Iterator
10
  from pathlib import Path
11
  from typing import Any
 
21
 
22
  MAX_SMARTDOC_BYTES = 10 * 1024 * 1024
23
 
24
+ # Async fast: submit + poll (aligned with hiro-translation-api).
25
+ DEFAULT_POLL_INTERVAL_S = 1.5
26
+ DEFAULT_POLL_TIMEOUT_S = 1800
27
+
28
 
29
  def resolve_api_key(api_key: str | None = None) -> str:
30
  """Prefer explicit key, then HIRO_API_KEY, then RD_LLM_API_KEY."""
 
43
  return src, tgt
44
 
45
 
46
+ def translate_payload(content: str, lang: str) -> dict[str, Any]:
47
+ """Request body for POST /translate and POST /translate/async (no mode field)."""
 
 
 
 
48
  source, target = lang_to_codes(lang)
49
  return {
50
  "content": content,
51
  "sourceLanguageCode": source,
52
  "targetLanguageCode": target,
 
53
  }
54
 
55
 
 
81
  return value if isinstance(value, str) else fallback
82
 
83
 
84
+ def _http_error_message(resp: requests.Response) -> str:
85
+ try:
86
+ body = resp.json()
87
+ if isinstance(body, dict):
88
+ return str(body.get("error") or body.get("message") or body)
89
+ except (json.JSONDecodeError, ValueError):
90
+ pass
91
+ return resp.text[:500]
92
+
93
+
94
  def health_ok(
95
  base_url: str = DEFAULT_BASE,
96
  *,
 
114
  return False, str(exc)
115
 
116
 
117
+ def _gateway_error_message(data: Any, fallback: str = "") -> str | None:
118
+ """Extract connect-gateway style errors (error_msg / error_code)."""
119
+ if not isinstance(data, dict):
120
+ return None
121
+ if data.get("error_msg") or data.get("error_code") is not None:
122
+ msg = data.get("error_msg") or data.get("error") or "gateway error"
123
+ code = data.get("error_code")
124
+ if code is not None:
125
+ return f"[{code}] {msg}"
126
+ return str(msg)
127
+ if data.get("error"):
128
+ return str(data["error"])
129
+ if data.get("status") is False:
130
+ return fallback or str(data)
131
+ return None
132
+
133
+
134
+ def submit_async_translate(
135
  text: str,
136
  lang: str,
137
  *,
138
  base_url: str = DEFAULT_BASE,
139
  api_key: str | None = None,
140
+ timeout: int = 60,
141
  ) -> dict[str, Any]:
142
+ """POST /translate/async → {taskId, state}."""
143
  r = requests.post(
144
+ _api_url(base_url, "/translate/async"),
145
+ json=translate_payload(text, lang),
146
  headers=_request_headers(api_key),
147
  timeout=timeout,
148
  )
149
  if r.status_code >= 400:
150
+ raise RuntimeError(_http_error_message(r))
151
+ try:
152
+ data = r.json()
153
+ except (json.JSONDecodeError, ValueError) as exc:
154
+ raise RuntimeError(f"invalid JSON from async submit: {r.text[:500]}") from exc
155
+
156
+ gateway_err = _gateway_error_message(data)
157
+ if gateway_err:
158
+ raise RuntimeError(gateway_err)
159
+
160
+ task_id = data.get("taskId")
161
+ if not task_id:
162
+ raise RuntimeError(f"missing taskId in submit response: {data}")
163
+ return {
164
+ "task_id": str(task_id),
165
+ "state": data.get("state", "pending"),
166
+ "billing_amount": r.headers.get("X-Openapi-Amount"),
167
+ "raw": data,
168
+ }
169
+
170
+
171
+ def get_async_translate_result(
172
+ task_id: str,
173
+ *,
174
+ base_url: str = DEFAULT_BASE,
175
+ api_key: str | None = None,
176
+ timeout: int = 60,
177
+ ) -> dict[str, Any]:
178
+ """GET /translate/async/{taskId}."""
179
+ r = requests.get(
180
+ _api_url(base_url, f"/translate/async/{task_id}"),
181
+ headers=_request_headers(api_key, json_body=False),
182
+ timeout=timeout,
183
+ )
184
+ if r.status_code == 404:
185
+ raise RuntimeError(f"task not found: {_http_error_message(r)}")
186
+ if r.status_code >= 400:
187
+ raise RuntimeError(_http_error_message(r))
188
  data = r.json()
 
 
189
  return {
190
+ "task_id": data.get("taskId", task_id),
191
+ "state": data.get("state"),
192
  "text_translated": _translated_text(data),
193
  "translated_character_count": data.get("translatedCharacterCount"),
194
+ "error": data.get("error"),
195
  "billing_amount": r.headers.get("X-Openapi-Amount"),
196
  "raw": data,
197
  }
198
 
199
 
200
+ def iter_translate_fast(
201
+ text: str,
202
+ lang: str,
203
+ *,
204
+ base_url: str = DEFAULT_BASE,
205
+ api_key: str | None = None,
206
+ poll_interval_s: float = DEFAULT_POLL_INTERVAL_S,
207
+ poll_timeout_s: float = DEFAULT_POLL_TIMEOUT_S,
208
+ ) -> Iterator[dict[str, Any]]:
209
+ """Submit async fast translation and poll until succeeded/failed.
210
+
211
+ Yields status events, then a final event with ``done=True``.
212
+ """
213
+ submitted = submit_async_translate(text, lang, base_url=base_url, api_key=api_key)
214
+ task_id = submitted["task_id"]
215
+ yield {
216
+ "done": False,
217
+ "task_id": task_id,
218
+ "state": submitted.get("state", "pending"),
219
+ "phase": "submitted",
220
+ }
221
+
222
+ deadline = time.perf_counter() + poll_timeout_s
223
+ while True:
224
+ if time.perf_counter() > deadline:
225
+ raise RuntimeError(
226
+ f"async translation timed out after {poll_timeout_s:.0f}s "
227
+ f"(taskId={task_id})"
228
+ )
229
+ time.sleep(poll_interval_s)
230
+ result = get_async_translate_result(
231
+ task_id, base_url=base_url, api_key=api_key
232
+ )
233
+ state = result.get("state")
234
+ if state in ("succeeded", "failed"):
235
+ if state == "failed":
236
+ err = result.get("error") or "async translation failed"
237
+ raise RuntimeError(str(err))
238
+ translated = result.get("text_translated") or ""
239
+ if not str(translated).strip():
240
+ raise RuntimeError("empty translation from async result")
241
+ yield {
242
+ "done": True,
243
+ "task_id": task_id,
244
+ "state": "succeeded",
245
+ "phase": "done",
246
+ "text_original": text,
247
+ "text_translated": translated,
248
+ "translated_character_count": result.get(
249
+ "translated_character_count"
250
+ ),
251
+ "billing_amount": result.get("billing_amount"),
252
+ "raw": result.get("raw"),
253
+ }
254
+ return
255
+ yield {
256
+ "done": False,
257
+ "task_id": task_id,
258
+ "state": state or "pending",
259
+ "phase": "polling",
260
+ }
261
+
262
+
263
+ def translate_fast(
264
+ text: str,
265
+ lang: str,
266
+ *,
267
+ base_url: str = DEFAULT_BASE,
268
+ api_key: str | None = None,
269
+ poll_interval_s: float = DEFAULT_POLL_INTERVAL_S,
270
+ poll_timeout_s: float = DEFAULT_POLL_TIMEOUT_S,
271
+ timeout: int | None = None,
272
+ ) -> dict[str, Any]:
273
+ """Async fast translate: submit + poll until complete (blocking)."""
274
+ if timeout is not None:
275
+ poll_timeout_s = float(timeout)
276
+ final: dict[str, Any] | None = None
277
+ for event in iter_translate_fast(
278
+ text,
279
+ lang,
280
+ base_url=base_url,
281
+ api_key=api_key,
282
+ poll_interval_s=poll_interval_s,
283
+ poll_timeout_s=poll_timeout_s,
284
+ ):
285
+ if event.get("done"):
286
+ final = event
287
+ if final is None:
288
+ raise RuntimeError("async translation ended without a result")
289
+ return {
290
+ "state": "success",
291
+ "task_id": final.get("task_id"),
292
+ "text_original": final.get("text_original", text),
293
+ "text_translated": final.get("text_translated", ""),
294
+ "translated_character_count": final.get("translated_character_count"),
295
+ "billing_amount": final.get("billing_amount"),
296
+ "raw": final.get("raw"),
297
+ }
298
+
299
+
300
  def _iter_sse_json(resp: requests.Response) -> Iterator[dict[str, Any]]:
301
  for raw in resp.iter_lines(decode_unicode=True):
302
  if not raw:
 
329
  api_key: str | None = None,
330
  timeout: int = 1800,
331
  ) -> Iterator[dict[str, Any]]:
332
+ """POST /translate (stream-only; do not send mode)."""
333
  with requests.post(
334
  _api_url(base_url, "/translate"),
335
+ json=translate_payload(text, lang),
336
  headers=_request_headers(api_key),
337
  stream=True,
338
  timeout=timeout,
339
  ) as resp:
340
  if resp.status_code >= 400:
341
+ raise RuntimeError(_http_error_message(resp))
 
 
 
 
342
  yield from _iter_sse_json(resp)
343
 
344
 
app.py CHANGED
@@ -26,11 +26,10 @@ import gradio as gr
26
  from api_client import (
27
  DEFAULT_BASE,
28
  SMARTDOC_URL,
29
- health_ok,
30
  parse_document,
31
  resolve_api_key,
 
32
  stream_translate,
33
- translate_fast,
34
  )
35
 
36
  CUSTOM_CSS = """
@@ -280,40 +279,6 @@ def _resolve_base(api_base: str) -> str:
280
  return (api_base or DEFAULT_BASE).strip().rstrip("/")
281
 
282
 
283
- def _has_health_endpoint(base: str) -> bool:
284
- """Only the internal gateway exposes /health; the public connect gateway does not.
285
-
286
- The public OpenAPI gateway (connect.zhihuiya.com) only routes the purchased
287
- endpoints (e.g. /translate); probing /health there returns a permission/quota
288
- error, not a real health signal.
289
- """
290
- return "/compute/hiro_translation_api" in base
291
-
292
-
293
- def check_service() -> str:
294
- base = _resolve_base(DEFAULT_BASE)
295
- key = resolve_api_key()
296
- key_hint = "HIRO_API_KEY / RD_LLM_API_KEY" if not key else "API key loaded from env"
297
-
298
- if not _has_health_endpoint(base):
299
- return (
300
- f'<span class="status-pill ok">● Public gateway<br>'
301
- f'<span style="opacity:0.85">{html.escape(base)} · {html.escape(key_hint)}</span>'
302
- f"</span>"
303
- )
304
-
305
- ok, msg = health_ok(base, api_key=key or None)
306
- if ok:
307
- return (
308
- f'<span class="status-pill ok">● Online<br>'
309
- f'<span style="opacity:0.85">{html.escape(base)} · {html.escape(key_hint)}</span></span>'
310
- )
311
- return (
312
- f'<span class="status-pill err">● Unavailable<br>'
313
- f'<span style="opacity:0.85">{html.escape(msg)}</span></span>'
314
- )
315
-
316
-
317
  def _parse_progress(progress: str | None) -> tuple[int, int]:
318
  if not progress or "/" not in progress:
319
  return 0, 0
@@ -635,17 +600,39 @@ def run_translation(text: str, lang: str, mode: str):
635
 
636
  try:
637
  if mode == "fast":
638
- data = translate_fast(text, lang, base_url=base, api_key=auth)
639
- orig = data.get("text_original", text)
640
- trans = data.get("text_translated", "")
641
- elapsed = time.perf_counter() - t0
642
- pairs = [(orig, trans)]
643
- status = f"Done · fast mode · {elapsed:.1f}s"
644
- out_chars = data.get("translated_character_count")
645
- if out_chars is not None:
646
- status += f" · {out_chars} chars"
647
- yield emit(percent=100, status=status, force=True)
648
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
 
650
  for chunk in stream_translate(text, lang, base_url=base, api_key=auth):
651
  orig = chunk.get("text_original") or ""
@@ -751,18 +738,11 @@ def build_theme() -> gr.Theme:
751
 
752
  def build_ui() -> gr.Blocks:
753
  with gr.Blocks(
754
- title="Document Processing",
755
  css=CUSTOM_CSS,
756
  theme=build_theme(),
757
  ) as demo:
758
- with gr.Sidebar():
759
- gr.HTML('<p class="main-title">Document Processing</p>')
760
- gr.Markdown(
761
- "Smart Doc + Translation · shared Bearer key from "
762
- "`HIRO_API_KEY` (fallback `RD_LLM_API_KEY`)"
763
- )
764
- gateway_status = gr.HTML(value=check_service())
765
- refresh_btn = gr.Button("Refresh status", size="sm", variant="secondary")
766
 
767
  with gr.Tabs():
768
  with gr.Tab("Smart Doc"):
@@ -833,7 +813,10 @@ def build_ui() -> gr.Blocks:
833
  choices=["stream", "fast"],
834
  value="stream",
835
  label="Mode",
836
- info="stream: segment streaming; fast: single full response",
 
 
 
837
  scale=1,
838
  )
839
  lang = gr.Textbox(value="zh2en", visible=False)
@@ -884,8 +867,6 @@ def build_ui() -> gr.Blocks:
884
  elem_id="hiro-stream-state",
885
  )
886
 
887
- refresh_btn.click(check_service, outputs=[gateway_status])
888
-
889
  def _combine_lang(src: str, tgt: str) -> str:
890
  return f"{src}2{tgt}"
891
 
@@ -961,11 +942,7 @@ def build_ui() -> gr.Blocks:
961
  outputs=[doc_file, smartdoc_status, smartdoc_md, smartdoc_json],
962
  )
963
 
964
- demo.load(
965
- check_service,
966
- outputs=[gateway_status],
967
- js=INIT_STREAM_WATCH_JS,
968
- )
969
 
970
  return demo
971
 
 
26
  from api_client import (
27
  DEFAULT_BASE,
28
  SMARTDOC_URL,
 
29
  parse_document,
30
  resolve_api_key,
31
+ iter_translate_fast,
32
  stream_translate,
 
33
  )
34
 
35
  CUSTOM_CSS = """
 
279
  return (api_base or DEFAULT_BASE).strip().rstrip("/")
280
 
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  def _parse_progress(progress: str | None) -> tuple[int, int]:
283
  if not progress or "/" not in progress:
284
  return 0, 0
 
600
 
601
  try:
602
  if mode == "fast":
603
+ # Async fast: POST /translate/async then poll GET /translate/async/{taskId}
604
+ task_id = ""
605
+ for event in iter_translate_fast(text, lang, base_url=base, api_key=auth):
606
+ if not event.get("done"):
607
+ task_id = str(event.get("task_id") or task_id)
608
+ short = f"{task_id[:8]}…" if task_id else "…"
609
+ state = event.get("state") or "pending"
610
+ phase = event.get("phase") or "polling"
611
+ elapsed = time.perf_counter() - t0
612
+ if phase == "submitted":
613
+ status = f"Submitted async task {short} · {elapsed:.1f}s"
614
+ pct = 5.0
615
+ else:
616
+ status = f"Polling {short} · {state} · {elapsed:.1f}s"
617
+ pct = min(90.0, 10.0 + elapsed)
618
+ out = emit(percent=pct, status=status, force=True)
619
+ if out is not None:
620
+ yield out
621
+ continue
622
+
623
+ orig = event.get("text_original", text)
624
+ trans = event.get("text_translated", "")
625
+ elapsed = time.perf_counter() - t0
626
+ pairs = [(orig, trans)]
627
+ status = f"Done · fast (async) · {elapsed:.1f}s"
628
+ out_chars = event.get("translated_character_count")
629
+ if out_chars is not None:
630
+ status += f" · {out_chars} chars"
631
+ if task_id or event.get("task_id"):
632
+ tid = str(event.get("task_id") or task_id)
633
+ status += f" · task {tid[:8]}…"
634
+ yield emit(percent=100, status=status, force=True)
635
+ return
636
 
637
  for chunk in stream_translate(text, lang, base_url=base, api_key=auth):
638
  orig = chunk.get("text_original") or ""
 
738
 
739
  def build_ui() -> gr.Blocks:
740
  with gr.Blocks(
741
+ title="Document Processing Demo",
742
  css=CUSTOM_CSS,
743
  theme=build_theme(),
744
  ) as demo:
745
+ gr.HTML('<p class="main-title">Document Processing Demo</p>')
 
 
 
 
 
 
 
746
 
747
  with gr.Tabs():
748
  with gr.Tab("Smart Doc"):
 
813
  choices=["stream", "fast"],
814
  value="stream",
815
  label="Mode",
816
+ info=(
817
+ "stream: SSE segments via POST /translate; "
818
+ "fast: async submit + poll via /translate/async"
819
+ ),
820
  scale=1,
821
  )
822
  lang = gr.Textbox(value="zh2en", visible=False)
 
867
  elem_id="hiro-stream-state",
868
  )
869
 
 
 
870
  def _combine_lang(src: str, tgt: str) -> str:
871
  return f"{src}2{tgt}"
872
 
 
942
  outputs=[doc_file, smartdoc_status, smartdoc_md, smartdoc_json],
943
  )
944
 
945
+ demo.load(fn=None, js=INIT_STREAM_WATCH_JS)
 
 
 
 
946
 
947
  return demo
948