Jiawei Dong commited on
Commit
067d7b2
·
1 Parent(s): 4a9a269
README.md CHANGED
@@ -14,24 +14,41 @@ short_description: Patent translation demo via hiro-translation-api
14
 
15
  # HIRO Translation
16
 
17
- Patent translation demo powered by **hiro-translation-api** (stage).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- - **stream** — SSE streaming, segment-by-segment display
20
- - **fast** — single JSON response with full translation
21
- - Default backend: `stage-s-patsnaprd-hiro-translation-api-internet.patsnap.info`
22
 
23
  ## Files
24
 
25
  | File | Purpose |
26
  |------|---------|
27
  | `app.py` | Gradio UI |
28
- | `api_client.py` | HTTP client for `/compute/hiro_translation_api` |
29
  | `requirements.txt` | Python dependencies (`requests`; Gradio is pre-installed on Spaces) |
30
 
31
  ## Local run
32
 
33
  ```bash
34
  pip install -r requirements.txt
 
35
  python app.py
36
  ```
37
 
 
14
 
15
  # HIRO Translation
16
 
17
+ Patent translation demo for the public HIRO Translation API.
18
+
19
+ - **stream** — SSE streaming (`textOriginal` / `textTranslated` per segment)
20
+ - **fast** — single JSON with full `textTranslated`
21
+ - Default gateway: `https://connect.zhihuiya.com/hiro_translation`
22
+ - Auth: Bearer token via **Advanced → API Key** or env `HIRO_API_KEY`
23
+
24
+ Request body (aligned with terminal curl):
25
+
26
+ ```json
27
+ {
28
+ "content": "本发明公开了一种档案管理文件储存用分类标识装置。",
29
+ "sourceLanguageCode": "zh",
30
+ "targetLanguageCode": "en",
31
+ "mode": "stream"
32
+ }
33
+ ```
34
+
35
+ Internal stage URL (optional in Advanced):
36
 
37
+ `http://stage-s-patsnaprd-hiro-translation-api-internet.patsnap.info/compute/hiro_translation_api`
 
 
38
 
39
  ## Files
40
 
41
  | File | Purpose |
42
  |------|---------|
43
  | `app.py` | Gradio UI |
44
+ | `api_client.py` | HTTP client for `/translate` (camelCase request/response) |
45
  | `requirements.txt` | Python dependencies (`requests`; Gradio is pre-installed on Spaces) |
46
 
47
  ## Local run
48
 
49
  ```bash
50
  pip install -r requirements.txt
51
+ export HIRO_API_KEY="sk-..." # optional if set in UI
52
  python app.py
53
  ```
54
 
__pycache__/api_client.cpython-310.pyc ADDED
Binary file (4.78 kB). View file
 
__pycache__/api_client.cpython-38.pyc CHANGED
Binary files a/__pycache__/api_client.cpython-38.pyc and b/__pycache__/api_client.cpython-38.pyc differ
 
__pycache__/app.cpython-38.pyc CHANGED
Binary files a/__pycache__/app.cpython-38.pyc and b/__pycache__/app.cpython-38.pyc differ
 
api_client.py CHANGED
@@ -1,26 +1,75 @@
1
- """HTTP client for hiro-translation-api (stage gateway)."""
2
 
3
  from __future__ import annotations
4
 
5
  import json
 
6
  from collections.abc import Iterator
7
  from typing import Any
8
 
9
  import requests
10
 
11
- DEFAULT_BASE = (
12
- "http://stage-s-patsnaprd-hiro-translation-api-internet.patsnap.info"
13
- )
14
- API_PREFIX = "/compute/hiro_translation_api"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
 
17
  def _api_url(base_url: str, path: str) -> str:
18
- return f"{base_url.rstrip('/')}{API_PREFIX}{path}"
19
 
20
 
21
- def health_ok(base_url: str = DEFAULT_BASE) -> tuple[bool, str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  try:
23
- r = requests.get(_api_url(base_url, "/health"), timeout=12)
 
 
 
 
24
  r.raise_for_status()
25
  data = r.json()
26
  if data.get("status") != "OK":
@@ -38,11 +87,13 @@ def translate_fast(
38
  lang: str,
39
  *,
40
  base_url: str = DEFAULT_BASE,
 
41
  timeout: int = 1800,
42
  ) -> dict[str, Any]:
43
  r = requests.post(
44
  _api_url(base_url, "/translate"),
45
- json={"text": text, "lang": lang, "mode": "fast"},
 
46
  timeout=timeout,
47
  )
48
  if r.status_code >= 400:
@@ -51,7 +102,18 @@ def translate_fast(
51
  except json.JSONDecodeError:
52
  err = r.text[:500]
53
  raise RuntimeError(err)
54
- return r.json()
 
 
 
 
 
 
 
 
 
 
 
55
 
56
 
57
  def _iter_sse_json(resp: requests.Response) -> Iterator[dict[str, Any]]:
@@ -67,7 +129,15 @@ def _iter_sse_json(resp: requests.Response) -> Iterator[dict[str, Any]]:
67
  chunk = json.loads(payload)
68
  if isinstance(chunk, dict) and chunk.get("error"):
69
  raise RuntimeError(str(chunk["error"]))
70
- yield chunk
 
 
 
 
 
 
 
 
71
 
72
 
73
  def stream_translate(
@@ -75,11 +145,13 @@ def stream_translate(
75
  lang: str,
76
  *,
77
  base_url: str = DEFAULT_BASE,
 
78
  timeout: int = 1800,
79
  ) -> Iterator[dict[str, Any]]:
80
  with requests.post(
81
  _api_url(base_url, "/translate"),
82
- json={"text": text, "lang": lang, "mode": "stream"},
 
83
  stream=True,
84
  timeout=timeout,
85
  ) as resp:
 
1
+ """HTTP client for HIRO Translation API (public connect gateway or internal stage)."""
2
 
3
  from __future__ import annotations
4
 
5
  import json
6
+ import os
7
  from collections.abc import Iterator
8
  from typing import Any
9
 
10
  import requests
11
 
12
+ # Public gateway (matches curl examples against connect.zhihuiya.com).
13
+ DEFAULT_BASE = "https://connect.zhihuiya.com/hiro_translation"
14
+
15
+ API_KEY_ENV = "HIRO_API_KEY"
16
+
17
+
18
+ def lang_to_codes(lang: str) -> tuple[str, str]:
19
+ src, _, tgt = lang.partition("2")
20
+ if not src or not tgt:
21
+ raise ValueError(f"invalid lang {lang!r}; expected format like zh2en")
22
+ return src, tgt
23
+
24
+
25
+ def translate_payload(
26
+ content: str,
27
+ lang: str,
28
+ *,
29
+ mode: str,
30
+ ) -> dict[str, Any]:
31
+ source, target = lang_to_codes(lang)
32
+ return {
33
+ "content": content,
34
+ "sourceLanguageCode": source,
35
+ "targetLanguageCode": target,
36
+ "mode": mode,
37
+ }
38
 
39
 
40
  def _api_url(base_url: str, path: str) -> str:
41
+ return f"{base_url.rstrip('/')}{path}"
42
 
43
 
44
+ def _request_headers(api_key: str | None = None) -> dict[str, str]:
45
+ headers = {"Content-Type": "application/json"}
46
+ key = (api_key if api_key is not None else os.environ.get(API_KEY_ENV, "")).strip()
47
+ if key:
48
+ headers["Authorization"] = f"Bearer {key}"
49
+ return headers
50
+
51
+
52
+ def _translated_text(data: dict[str, Any]) -> str:
53
+ value = data.get("textTranslated", data.get("text_translated", ""))
54
+ return value if isinstance(value, str) else ""
55
+
56
+
57
+ def _original_text(data: dict[str, Any], fallback: str = "") -> str:
58
+ value = data.get("textOriginal", data.get("text_original", fallback))
59
+ return value if isinstance(value, str) else fallback
60
+
61
+
62
+ def health_ok(
63
+ base_url: str = DEFAULT_BASE,
64
+ *,
65
+ api_key: str | None = None,
66
+ ) -> tuple[bool, str]:
67
  try:
68
+ r = requests.get(
69
+ _api_url(base_url, "/health"),
70
+ headers=_request_headers(api_key),
71
+ timeout=12,
72
+ )
73
  r.raise_for_status()
74
  data = r.json()
75
  if data.get("status") != "OK":
 
87
  lang: str,
88
  *,
89
  base_url: str = DEFAULT_BASE,
90
+ api_key: str | None = None,
91
  timeout: int = 1800,
92
  ) -> dict[str, Any]:
93
  r = requests.post(
94
  _api_url(base_url, "/translate"),
95
+ json=translate_payload(text, lang, mode="fast"),
96
+ headers=_request_headers(api_key),
97
  timeout=timeout,
98
  )
99
  if r.status_code >= 400:
 
102
  except json.JSONDecodeError:
103
  err = r.text[:500]
104
  raise RuntimeError(err)
105
+ data = r.json()
106
+ if data.get("state") not in (None, "success") and "error" in data:
107
+ raise RuntimeError(str(data.get("error", data)))
108
+ # Normalize to keys the UI already understands (snake_case aliases).
109
+ return {
110
+ "state": data.get("state", "success"),
111
+ "text_original": _original_text(data, text),
112
+ "text_translated": _translated_text(data),
113
+ "translated_character_count": data.get("translatedCharacterCount"),
114
+ "billing_amount": r.headers.get("X-Openapi-Amount"),
115
+ "raw": data,
116
+ }
117
 
118
 
119
  def _iter_sse_json(resp: requests.Response) -> Iterator[dict[str, Any]]:
 
129
  chunk = json.loads(payload)
130
  if isinstance(chunk, dict) and chunk.get("error"):
131
  raise RuntimeError(str(chunk["error"]))
132
+ if isinstance(chunk, dict):
133
+ yield {
134
+ "state": chunk.get("state", "success"),
135
+ "text_original": _original_text(chunk),
136
+ "text_translated": _translated_text(chunk),
137
+ "translated_character_count": chunk.get("translatedCharacterCount"),
138
+ "progress": chunk.get("progress"),
139
+ "raw": chunk,
140
+ }
141
 
142
 
143
  def stream_translate(
 
145
  lang: str,
146
  *,
147
  base_url: str = DEFAULT_BASE,
148
+ api_key: str | None = None,
149
  timeout: int = 1800,
150
  ) -> Iterator[dict[str, Any]]:
151
  with requests.post(
152
  _api_url(base_url, "/translate"),
153
+ json=translate_payload(text, lang, mode="stream"),
154
+ headers=_request_headers(api_key),
155
  stream=True,
156
  timeout=timeout,
157
  ) as resp:
app.py CHANGED
@@ -1,8 +1,10 @@
1
  """
2
- Gradio demo — HIRO Translation API (hiro-translation-api).
3
 
4
- Calls stage gateway by default:
5
- http://stage-s-patsnaprd-hiro-translation-api-internet.patsnap.info
 
 
6
  """
7
 
8
  from __future__ import annotations
@@ -228,19 +230,19 @@ LANG_OPTIONS = [
228
  ]
229
 
230
  EXAMPLES = [
231
- ["一种低损耗换热器", "zh", "en"],
232
  [
233
- "The present invention relates to a heat exchanger comprising a plurality of fluid passages.",
234
- "en",
235
  "zh",
 
236
  ],
237
  [
238
- "1.一种蓝莓早促快发春梢的绿色生产技术,其特征在于,包括:(1)新主枝的培养、(2)调节树体结构。",
239
- "zh",
240
  "en",
 
241
  ],
242
  [
243
- "## 技术领域\n\n本发明涉及一种换热器。",
244
  "zh",
245
  "en",
246
  ],
@@ -254,9 +256,27 @@ def _resolve_base(api_base: str) -> str:
254
  return (api_base or DEFAULT_BASE).strip().rstrip("/")
255
 
256
 
257
- def check_service(api_base: str) -> str:
258
- ok, msg = health_ok(_resolve_base(api_base))
 
 
 
 
 
 
 
 
 
259
  base = _resolve_base(api_base)
 
 
 
 
 
 
 
 
 
260
  if ok:
261
  return (
262
  f'<span class="status-pill ok">● 在线<br>'
@@ -571,13 +591,14 @@ CLEAR_STREAM_JS = """
571
  """
572
 
573
 
574
- def run_translation(text: str, lang: str, mode: str, api_base: str):
575
  text = (text or "").strip()
576
  if not text:
577
  yield _empty_outputs()
578
  return
579
 
580
  base = _resolve_base(api_base)
 
581
  pairs: list[tuple[str, str]] = []
582
  t0 = time.perf_counter()
583
  last_progress = "0/0"
@@ -604,19 +625,19 @@ def run_translation(text: str, lang: str, mode: str, api_base: str):
604
 
605
  try:
606
  if mode == "fast":
607
- data = translate_fast(text, lang, base_url=base)
608
  orig = data.get("text_original", text)
609
  trans = data.get("text_translated", "")
610
- engine = data.get("engine", "")
611
  elapsed = time.perf_counter() - t0
612
  pairs = [(orig, trans)]
613
  status = f"完成 · fast 模式 · {elapsed:.1f}s"
614
- if engine:
615
- status += f" · {engine}"
 
616
  yield emit(percent=100, status=status, force=True)
617
  return
618
 
619
- for chunk in stream_translate(text, lang, base_url=base):
620
  orig = chunk.get("text_original") or ""
621
  trans = chunk.get("text_translated") or ""
622
  if orig or trans:
@@ -682,7 +703,7 @@ def build_ui() -> gr.Blocks:
682
  ) as demo:
683
  with gr.Sidebar():
684
  gr.HTML('<p class="main-title">HIRO Translation</p>')
685
- gr.Markdown("专利翻译 · `hiro-translation-api`")
686
 
687
  gateway_status = gr.HTML(value=check_service(DEFAULT_BASE))
688
  refresh_btn = gr.Button("检查网关", size="sm", variant="secondary")
@@ -711,6 +732,12 @@ def build_ui() -> gr.Blocks:
711
  label="API Base URL",
712
  value=DEFAULT_BASE,
713
  placeholder=DEFAULT_BASE,
 
 
 
 
 
 
714
  )
715
 
716
  gr.Markdown("**快速示例**")
@@ -754,8 +781,9 @@ def build_ui() -> gr.Blocks:
754
  elem_id="hiro-stream-state",
755
  )
756
 
757
- refresh_btn.click(check_service, inputs=[api_base], outputs=[gateway_status])
758
- api_base.change(check_service, inputs=[api_base], outputs=[gateway_status])
 
759
 
760
  def _combine_lang(src: str, tgt: str) -> str:
761
  return f"{src}2{tgt}"
@@ -775,7 +803,7 @@ def build_ui() -> gr.Blocks:
775
 
776
  submit_btn.click(
777
  fn=run_translation,
778
- inputs=[input_text, lang, mode, api_base],
779
  outputs=[stream_state, results_panel, copy_btn],
780
  js=RESET_STREAM_JS,
781
  )
@@ -817,7 +845,7 @@ def build_ui() -> gr.Blocks:
817
 
818
  demo.load(
819
  check_service,
820
- inputs=[api_base],
821
  outputs=[gateway_status],
822
  js=INIT_STREAM_WATCH_JS,
823
  )
 
1
  """
2
+ Gradio demo — HIRO Translation API.
3
 
4
+ Calls the public connect gateway by default:
5
+ https://connect.zhihuiya.com/hiro_translation/translate
6
+
7
+ Set HIRO_API_KEY or paste a Bearer token in Advanced options.
8
  """
9
 
10
  from __future__ import annotations
 
230
  ]
231
 
232
  EXAMPLES = [
233
+ ["本发明公开了一种档案管理文件储存用分类标识装置。", "zh", "en"],
234
  [
235
+ "本发明公开了一种档案管理文件储存用分类标识装置。\n我是智慧芽的研发",
 
236
  "zh",
237
+ "en",
238
  ],
239
  [
240
+ "The present invention relates to a heat exchanger comprising a plurality of fluid passages.",
 
241
  "en",
242
+ "zh",
243
  ],
244
  [
245
+ "1.一种蓝莓早促快发春梢的绿色生产技术,其特征在于,包括:(1)新主枝的培养、(2)调节树体结构。",
246
  "zh",
247
  "en",
248
  ],
 
256
  return (api_base or DEFAULT_BASE).strip().rstrip("/")
257
 
258
 
259
+ def _has_health_endpoint(base: str) -> bool:
260
+ """Only the internal gateway exposes /health; the public connect gateway does not.
261
+
262
+ The public OpenAPI gateway (connect.zhihuiya.com) only routes the purchased
263
+ endpoints (e.g. /translate); probing /health there returns a permission/quota
264
+ error, not a real health signal.
265
+ """
266
+ return "/compute/hiro_translation_api" in base
267
+
268
+
269
+ def check_service(api_base: str, api_key: str = "") -> str:
270
  base = _resolve_base(api_base)
271
+
272
+ if not _has_health_endpoint(base):
273
+ return (
274
+ f'<span class="status-pill ok">● 公开网关<br>'
275
+ f'<span style="opacity:0.85">调用时以 Bearer 鉴权 · {html.escape(base)}</span>'
276
+ f"</span>"
277
+ )
278
+
279
+ ok, msg = health_ok(base, api_key=api_key or None)
280
  if ok:
281
  return (
282
  f'<span class="status-pill ok">● 在线<br>'
 
591
  """
592
 
593
 
594
+ def run_translation(text: str, lang: str, mode: str, api_base: str, api_key: str = ""):
595
  text = (text or "").strip()
596
  if not text:
597
  yield _empty_outputs()
598
  return
599
 
600
  base = _resolve_base(api_base)
601
+ auth = api_key or None
602
  pairs: list[tuple[str, str]] = []
603
  t0 = time.perf_counter()
604
  last_progress = "0/0"
 
625
 
626
  try:
627
  if mode == "fast":
628
+ data = translate_fast(text, lang, base_url=base, api_key=auth)
629
  orig = data.get("text_original", text)
630
  trans = data.get("text_translated", "")
 
631
  elapsed = time.perf_counter() - t0
632
  pairs = [(orig, trans)]
633
  status = f"完成 · fast 模式 · {elapsed:.1f}s"
634
+ out_chars = data.get("translated_character_count")
635
+ if out_chars is not None:
636
+ status += f" · {out_chars} chars"
637
  yield emit(percent=100, status=status, force=True)
638
  return
639
 
640
+ for chunk in stream_translate(text, lang, base_url=base, api_key=auth):
641
  orig = chunk.get("text_original") or ""
642
  trans = chunk.get("text_translated") or ""
643
  if orig or trans:
 
703
  ) as demo:
704
  with gr.Sidebar():
705
  gr.HTML('<p class="main-title">HIRO Translation</p>')
706
+ gr.Markdown("专利翻译 · `connect.zhihuiya.com` / `hiro-translation-api`")
707
 
708
  gateway_status = gr.HTML(value=check_service(DEFAULT_BASE))
709
  refresh_btn = gr.Button("检查网关", size="sm", variant="secondary")
 
732
  label="API Base URL",
733
  value=DEFAULT_BASE,
734
  placeholder=DEFAULT_BASE,
735
+ info="公开网关默认 https://connect.zhihuiya.com/hiro_translation;内网可填 stage 地址",
736
+ )
737
+ api_key = gr.Textbox(
738
+ label="API Key (Bearer)",
739
+ type="password",
740
+ placeholder="sk-... 或设置环境变量 HIRO_API_KEY",
741
  )
742
 
743
  gr.Markdown("**快速示例**")
 
781
  elem_id="hiro-stream-state",
782
  )
783
 
784
+ refresh_btn.click(check_service, inputs=[api_base, api_key], outputs=[gateway_status])
785
+ api_base.change(check_service, inputs=[api_base, api_key], outputs=[gateway_status])
786
+ api_key.change(check_service, inputs=[api_base, api_key], outputs=[gateway_status])
787
 
788
  def _combine_lang(src: str, tgt: str) -> str:
789
  return f"{src}2{tgt}"
 
803
 
804
  submit_btn.click(
805
  fn=run_translation,
806
+ inputs=[input_text, lang, mode, api_base, api_key],
807
  outputs=[stream_state, results_panel, copy_btn],
808
  js=RESET_STREAM_JS,
809
  )
 
845
 
846
  demo.load(
847
  check_service,
848
+ inputs=[api_base, api_key],
849
  outputs=[gateway_status],
850
  js=INIT_STREAM_WATCH_JS,
851
  )