Spaces:
Running
Running
Jiawei Dong commited on
Commit ·
b1b2cec
1
Parent(s): bbc4064
merged
Browse files- .gitignore +1 -0
- README.md +22 -12
- __pycache__/api_client.cpython-310.pyc +0 -0
- __pycache__/api_client.cpython-38.pyc +0 -0
- __pycache__/app.cpython-38.pyc +0 -0
- api_client.py +102 -6
- app.py +216 -85
.gitignore
CHANGED
|
@@ -5,3 +5,4 @@ __pycache__/
|
|
| 5 |
venv/
|
| 6 |
.env
|
| 7 |
.DS_Store
|
|
|
|
|
|
| 5 |
venv/
|
| 6 |
.env
|
| 7 |
.DS_Store
|
| 8 |
+
.gradio_tmp/
|
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🦀
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: indigo
|
|
@@ -9,33 +9,43 @@ python_version: '3.13'
|
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
-
short_description:
|
| 13 |
---
|
| 14 |
|
| 15 |
-
#
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
## Files
|
| 25 |
|
| 26 |
| File | Purpose |
|
| 27 |
|------|---------|
|
| 28 |
-
| `app.py` | Gradio UI |
|
| 29 |
-
| `api_client.py` | HTTP
|
| 30 |
| `requirements.txt` | Python dependencies (`requests`; Gradio is pre-installed on Spaces) |
|
| 31 |
|
| 32 |
## Local run
|
| 33 |
|
| 34 |
```bash
|
| 35 |
pip install -r requirements.txt
|
| 36 |
-
export HIRO_API_KEY="sk-..." #
|
| 37 |
python app.py
|
| 38 |
```
|
| 39 |
|
| 40 |
Check out the [Spaces config reference](https://huggingface.co/docs/hub/spaces-config-reference).
|
| 41 |
-
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Document Processing
|
| 3 |
emoji: 🦀
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: indigo
|
|
|
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
+
short_description: Smart Doc + patent translation via connect gateway
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# Document Processing
|
| 16 |
|
| 17 |
+
Gradio demo with two tabs sharing one Bearer API key from the environment.
|
| 18 |
|
| 19 |
+
## Tabs
|
| 20 |
+
|
| 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 |
+
|
| 28 |
+
Set under Space **Settings → Variables / Secrets** (or local env):
|
| 29 |
+
|
| 30 |
+
1. `HIRO_API_KEY` (preferred)
|
| 31 |
+
2. else `RD_LLM_API_KEY`
|
| 32 |
+
|
| 33 |
+
No API key field in the UI.
|
| 34 |
|
| 35 |
## Files
|
| 36 |
|
| 37 |
| File | Purpose |
|
| 38 |
|------|---------|
|
| 39 |
+
| `app.py` | Gradio UI (tabs + streaming translation + document parse) |
|
| 40 |
+
| `api_client.py` | HTTP clients for translation + Smart Doc |
|
| 41 |
| `requirements.txt` | Python dependencies (`requests`; Gradio is pre-installed on Spaces) |
|
| 42 |
|
| 43 |
## Local run
|
| 44 |
|
| 45 |
```bash
|
| 46 |
pip install -r requirements.txt
|
| 47 |
+
export HIRO_API_KEY="sk-..." # or RD_LLM_API_KEY
|
| 48 |
python app.py
|
| 49 |
```
|
| 50 |
|
| 51 |
Check out the [Spaces config reference](https://huggingface.co/docs/hub/spaces-config-reference).
|
|
|
__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
|
|
|
__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,18 +1,34 @@
|
|
| 1 |
-
"""HTTP
|
| 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
|
| 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]:
|
|
@@ -41,9 +57,15 @@ def _api_url(base_url: str, path: str) -> str:
|
|
| 41 |
return f"{base_url.rstrip('/')}{path}"
|
| 42 |
|
| 43 |
|
| 44 |
-
def _request_headers(
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
if key:
|
| 48 |
headers["Authorization"] = f"Bearer {key}"
|
| 49 |
return headers
|
|
@@ -105,7 +127,6 @@ def translate_fast(
|
|
| 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),
|
|
@@ -162,3 +183,78 @@ def stream_translate(
|
|
| 162 |
err = resp.text[:500]
|
| 163 |
raise RuntimeError(err)
|
| 164 |
yield from _iter_sse_json(resp)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HTTP clients for HIRO Translation and Smart Doc (public connect gateway)."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 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
|
| 11 |
|
| 12 |
import requests
|
| 13 |
|
| 14 |
+
# Public gateways (matches connect.zhihuiya.com curl examples).
|
| 15 |
DEFAULT_BASE = "https://connect.zhihuiya.com/hiro_translation"
|
| 16 |
+
SMARTDOC_URL = "https://connect.zhihuiya.com/rd-llm/v1/documents/doc_parsing"
|
| 17 |
|
| 18 |
API_KEY_ENV = "HIRO_API_KEY"
|
| 19 |
+
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."""
|
| 26 |
+
if api_key is not None and str(api_key).strip():
|
| 27 |
+
return str(api_key).strip()
|
| 28 |
+
return (
|
| 29 |
+
os.environ.get(API_KEY_ENV, "").strip()
|
| 30 |
+
or os.environ.get(API_KEY_ENV_FALLBACK, "").strip()
|
| 31 |
+
)
|
| 32 |
|
| 33 |
|
| 34 |
def lang_to_codes(lang: str) -> tuple[str, str]:
|
|
|
|
| 57 |
return f"{base_url.rstrip('/')}{path}"
|
| 58 |
|
| 59 |
|
| 60 |
+
def _request_headers(
|
| 61 |
+
api_key: str | None = None,
|
| 62 |
+
*,
|
| 63 |
+
json_body: bool = True,
|
| 64 |
+
) -> dict[str, str]:
|
| 65 |
+
headers: dict[str, str] = {}
|
| 66 |
+
if json_body:
|
| 67 |
+
headers["Content-Type"] = "application/json"
|
| 68 |
+
key = resolve_api_key(api_key)
|
| 69 |
if key:
|
| 70 |
headers["Authorization"] = f"Bearer {key}"
|
| 71 |
return headers
|
|
|
|
| 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),
|
|
|
|
| 183 |
err = resp.text[:500]
|
| 184 |
raise RuntimeError(err)
|
| 185 |
yield from _iter_sse_json(resp)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def normalize_smartdoc_markdown(markdown: str) -> str:
|
| 189 |
+
"""Fix gateway-escaped newlines without breaking LaTeX commands like \\neq."""
|
| 190 |
+
text = markdown
|
| 191 |
+
text = re.sub(r"\\r\\n(?![A-Za-z])", "\n", text)
|
| 192 |
+
text = re.sub(r"\\n(?![A-Za-z])", "\n", text)
|
| 193 |
+
text = re.sub(r"\\r(?![A-Za-z])", "\n", text)
|
| 194 |
+
return text
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def parse_document(
|
| 198 |
+
file_path: str,
|
| 199 |
+
*,
|
| 200 |
+
api_key: str | None = None,
|
| 201 |
+
output_format: str = "both",
|
| 202 |
+
timeout: int = 300,
|
| 203 |
+
max_bytes: int = MAX_SMARTDOC_BYTES,
|
| 204 |
+
) -> dict[str, Any]:
|
| 205 |
+
"""POST multipart to Smart Doc doc_parsing endpoint."""
|
| 206 |
+
path = Path(file_path)
|
| 207 |
+
if not path.is_file():
|
| 208 |
+
raise RuntimeError(f"file not found: {file_path}")
|
| 209 |
+
size = path.stat().st_size
|
| 210 |
+
if size > max_bytes:
|
| 211 |
+
raise RuntimeError(
|
| 212 |
+
f"The file must not exceed {max_bytes // (1024 * 1024)} MB"
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
key = resolve_api_key(api_key)
|
| 216 |
+
if not key:
|
| 217 |
+
raise RuntimeError(
|
| 218 |
+
f"API key missing. Set {API_KEY_ENV} (or {API_KEY_ENV_FALLBACK}) "
|
| 219 |
+
"under Space Settings → Variables / Secrets"
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
with path.open("rb") as fh:
|
| 223 |
+
files = {"file": (path.name, fh)}
|
| 224 |
+
data = {"output_format": output_format}
|
| 225 |
+
r = requests.post(
|
| 226 |
+
SMARTDOC_URL,
|
| 227 |
+
headers=_request_headers(key, json_body=False),
|
| 228 |
+
files=files,
|
| 229 |
+
data=data,
|
| 230 |
+
timeout=timeout,
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
try:
|
| 234 |
+
body = r.json()
|
| 235 |
+
except json.JSONDecodeError as exc:
|
| 236 |
+
raise RuntimeError(f"HTTP {r.status_code}: {r.text[:500]}") from exc
|
| 237 |
+
|
| 238 |
+
if r.status_code >= 400 or body.get("status") != "success":
|
| 239 |
+
raise RuntimeError(
|
| 240 |
+
body.get("error_msg")
|
| 241 |
+
or body.get("message")
|
| 242 |
+
or body.get("error")
|
| 243 |
+
or f"HTTP {r.status_code}"
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
payload = body.get("data") if isinstance(body.get("data"), dict) else body
|
| 247 |
+
markdown = payload.get("markdown") if isinstance(payload, dict) else ""
|
| 248 |
+
if not isinstance(markdown, str):
|
| 249 |
+
markdown = ""
|
| 250 |
+
results = payload.get("results") if isinstance(payload, dict) else []
|
| 251 |
+
if not isinstance(results, list):
|
| 252 |
+
results = []
|
| 253 |
+
total_pages = payload.get("total_pages") if isinstance(payload, dict) else 0
|
| 254 |
+
|
| 255 |
+
return {
|
| 256 |
+
"markdown": normalize_smartdoc_markdown(markdown),
|
| 257 |
+
"results": results,
|
| 258 |
+
"total_pages": total_pages or 0,
|
| 259 |
+
"raw": body,
|
| 260 |
+
}
|
app.py
CHANGED
|
@@ -1,21 +1,37 @@
|
|
| 1 |
"""
|
| 2 |
-
Gradio demo —
|
| 3 |
|
| 4 |
-
|
| 5 |
-
https://connect.zhihuiya.com/
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
"""
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
import html
|
| 13 |
import json
|
|
|
|
| 14 |
import time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
import gradio as gr
|
| 17 |
|
| 18 |
-
from api_client import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
CUSTOM_CSS = """
|
| 21 |
.gradio-container {
|
|
@@ -187,6 +203,14 @@ CUSTOM_CSS = """
|
|
| 187 |
}
|
| 188 |
|
| 189 |
footer { display: none !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
"""
|
| 191 |
|
| 192 |
RESULTS_SHELL_HTML = """
|
|
@@ -266,21 +290,23 @@ def _has_health_endpoint(base: str) -> bool:
|
|
| 266 |
return "/compute/hiro_translation_api" in base
|
| 267 |
|
| 268 |
|
| 269 |
-
def check_service(
|
| 270 |
base = _resolve_base(DEFAULT_BASE)
|
|
|
|
|
|
|
| 271 |
|
| 272 |
if not _has_health_endpoint(base):
|
| 273 |
return (
|
| 274 |
f'<span class="status-pill ok">● Public gateway<br>'
|
| 275 |
-
f'<span style="opacity:0.85">
|
| 276 |
f"</span>"
|
| 277 |
)
|
| 278 |
|
| 279 |
-
ok, msg = health_ok(base, api_key=
|
| 280 |
if ok:
|
| 281 |
return (
|
| 282 |
f'<span class="status-pill ok">● Online<br>'
|
| 283 |
-
f'<span style="opacity:0.85">{html.escape(base)}</span></span>'
|
| 284 |
)
|
| 285 |
return (
|
| 286 |
f'<span class="status-pill err">● Unavailable<br>'
|
|
@@ -575,14 +601,14 @@ INIT_STREAM_WATCH_JS = """
|
|
| 575 |
"""
|
| 576 |
|
| 577 |
|
| 578 |
-
def run_translation(text: str, lang: str, mode: str
|
| 579 |
text = (text or "").strip()
|
| 580 |
if not text:
|
| 581 |
yield _empty_outputs()
|
| 582 |
return
|
| 583 |
|
| 584 |
base = _resolve_base(DEFAULT_BASE)
|
| 585 |
-
auth =
|
| 586 |
pairs: list[tuple[str, str]] = []
|
| 587 |
t0 = time.perf_counter()
|
| 588 |
last_progress = "0/0"
|
|
@@ -674,6 +700,44 @@ def run_translation(text: str, lang: str, mode: str, api_key: str = ""):
|
|
| 674 |
yield _pack_outputs(stream_payload=payload, panel_visible=True, copy_enabled=False)
|
| 675 |
|
| 676 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
def build_theme() -> gr.Theme:
|
| 678 |
return gr.themes.Soft(
|
| 679 |
primary_hue="blue",
|
|
@@ -687,91 +751,140 @@ def build_theme() -> gr.Theme:
|
|
| 687 |
|
| 688 |
def build_ui() -> gr.Blocks:
|
| 689 |
with gr.Blocks(
|
| 690 |
-
title="
|
| 691 |
css=CUSTOM_CSS,
|
| 692 |
theme=build_theme(),
|
| 693 |
) as demo:
|
| 694 |
with gr.Sidebar():
|
| 695 |
-
gr.HTML('<p class="main-title">
|
| 696 |
gr.Markdown(
|
| 697 |
-
"
|
|
|
|
| 698 |
)
|
| 699 |
-
|
| 700 |
gateway_status = gr.HTML(value=check_service())
|
| 701 |
-
refresh_btn = gr.Button("
|
| 702 |
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
)
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
|
|
|
| 727 |
)
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
if len(sample) > 36:
|
| 734 |
-
preview += "…"
|
| 735 |
-
btn = gr.Button(f"{i}. {preview}", size="sm", variant="secondary")
|
| 736 |
-
example_btns.append((btn, sample, src, tgt))
|
| 737 |
-
|
| 738 |
-
with gr.Column():
|
| 739 |
-
input_text = gr.Textbox(
|
| 740 |
-
label="Source text",
|
| 741 |
-
placeholder=(
|
| 742 |
-
"Enter patent-related text. Supports Chinese, English, "
|
| 743 |
-
"Japanese, Korean, and more."
|
| 744 |
-
),
|
| 745 |
-
lines=8,
|
| 746 |
-
elem_id="input-box",
|
| 747 |
-
)
|
| 748 |
-
with gr.Row(elem_classes=["toolbar-row"]):
|
| 749 |
-
submit_btn = gr.Button("Translate", variant="primary", scale=0)
|
| 750 |
-
clear_btn = gr.Button("Clear", scale=0)
|
| 751 |
-
copy_btn = gr.Button(
|
| 752 |
-
"Copy full translation",
|
| 753 |
-
variant="secondary",
|
| 754 |
-
interactive=False,
|
| 755 |
-
scale=0,
|
| 756 |
-
elem_id="copy-translation-btn",
|
| 757 |
)
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
),
|
| 769 |
-
|
| 770 |
-
elem_id="
|
| 771 |
)
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
|
| 776 |
def _combine_lang(src: str, tgt: str) -> str:
|
| 777 |
return f"{src}2{tgt}"
|
|
@@ -791,7 +904,7 @@ def build_ui() -> gr.Blocks:
|
|
| 791 |
|
| 792 |
submit_btn.click(
|
| 793 |
fn=run_translation,
|
| 794 |
-
inputs=[input_text, lang, mode
|
| 795 |
outputs=[stream_state, results_panel, copy_btn],
|
| 796 |
)
|
| 797 |
|
|
@@ -829,9 +942,27 @@ def build_ui() -> gr.Blocks:
|
|
| 829 |
outputs=[input_text, stream_state, results_panel, copy_btn],
|
| 830 |
)
|
| 831 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 832 |
demo.load(
|
| 833 |
check_service,
|
| 834 |
-
inputs=[api_key],
|
| 835 |
outputs=[gateway_status],
|
| 836 |
js=INIT_STREAM_WATCH_JS,
|
| 837 |
)
|
|
|
|
| 1 |
"""
|
| 2 |
+
Gradio demo — Document Processing (Smart Doc + Translation).
|
| 3 |
|
| 4 |
+
Tabs:
|
| 5 |
+
- Smart Doc → https://connect.zhihuiya.com/rd-llm/v1/documents/doc_parsing
|
| 6 |
+
- Translation → https://connect.zhihuiya.com/hiro_translation
|
| 7 |
|
| 8 |
+
Auth: env HIRO_API_KEY, fallback RD_LLM_API_KEY (shared for both tabs).
|
| 9 |
"""
|
| 10 |
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
import html
|
| 14 |
import json
|
| 15 |
+
import os
|
| 16 |
import time
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
# Shared hosts often leave /tmp/gradio owned by another user → PermissionError on upload.
|
| 20 |
+
_GRADIO_TMP = Path(__file__).resolve().parent / ".gradio_tmp"
|
| 21 |
+
_GRADIO_TMP.mkdir(parents=True, exist_ok=True)
|
| 22 |
+
os.environ.setdefault("GRADIO_TEMP_DIR", str(_GRADIO_TMP))
|
| 23 |
|
| 24 |
import gradio as gr
|
| 25 |
|
| 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 = """
|
| 37 |
.gradio-container {
|
|
|
|
| 203 |
}
|
| 204 |
|
| 205 |
footer { display: none !important; }
|
| 206 |
+
|
| 207 |
+
.smartdoc-status {
|
| 208 |
+
font-size: 0.9rem;
|
| 209 |
+
color: var(--body-text-color-subdued);
|
| 210 |
+
margin: 0.25rem 0 0.75rem 0;
|
| 211 |
+
}
|
| 212 |
+
.smartdoc-status.ok { color: var(--color-accent, #047857); }
|
| 213 |
+
.smartdoc-status.err { color: var(--error-text-color, #b42318); }
|
| 214 |
"""
|
| 215 |
|
| 216 |
RESULTS_SHELL_HTML = """
|
|
|
|
| 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>'
|
|
|
|
| 601 |
"""
|
| 602 |
|
| 603 |
|
| 604 |
+
def run_translation(text: str, lang: str, mode: str):
|
| 605 |
text = (text or "").strip()
|
| 606 |
if not text:
|
| 607 |
yield _empty_outputs()
|
| 608 |
return
|
| 609 |
|
| 610 |
base = _resolve_base(DEFAULT_BASE)
|
| 611 |
+
auth = resolve_api_key() or None
|
| 612 |
pairs: list[tuple[str, str]] = []
|
| 613 |
t0 = time.perf_counter()
|
| 614 |
last_progress = "0/0"
|
|
|
|
| 700 |
yield _pack_outputs(stream_payload=payload, panel_visible=True, copy_enabled=False)
|
| 701 |
|
| 702 |
|
| 703 |
+
def _file_path(file_obj) -> str | None:
|
| 704 |
+
if file_obj is None:
|
| 705 |
+
return None
|
| 706 |
+
if isinstance(file_obj, (list, tuple)):
|
| 707 |
+
return _file_path(file_obj[0]) if file_obj else None
|
| 708 |
+
if isinstance(file_obj, str):
|
| 709 |
+
return file_obj
|
| 710 |
+
return getattr(file_obj, "name", None) or getattr(file_obj, "path", None)
|
| 711 |
+
|
| 712 |
+
|
| 713 |
+
def run_smartdoc(file_obj):
|
| 714 |
+
path = _file_path(file_obj)
|
| 715 |
+
if not path:
|
| 716 |
+
return (
|
| 717 |
+
'<p class="smartdoc-status err">Please select a file first</p>',
|
| 718 |
+
"*Rendered Markdown will appear here after parsing.*",
|
| 719 |
+
"[]",
|
| 720 |
+
)
|
| 721 |
+
try:
|
| 722 |
+
data = parse_document(path)
|
| 723 |
+
pages = data.get("total_pages") or 0
|
| 724 |
+
results = data.get("results") or []
|
| 725 |
+
markdown = data.get("markdown") or ""
|
| 726 |
+
status = (
|
| 727 |
+
f'<p class="smartdoc-status ok">Complete · {pages} pages · '
|
| 728 |
+
f"{len(results)} results</p>"
|
| 729 |
+
)
|
| 730 |
+
if not markdown.strip():
|
| 731 |
+
markdown = "*No Markdown content was returned.*"
|
| 732 |
+
return status, markdown, json.dumps(results, ensure_ascii=False, indent=2)
|
| 733 |
+
except Exception as exc:
|
| 734 |
+
return (
|
| 735 |
+
f'<p class="smartdoc-status err">Parsing failed: {html.escape(str(exc))}</p>',
|
| 736 |
+
"*Parsing failed. Check the status message.*",
|
| 737 |
+
"[]",
|
| 738 |
+
)
|
| 739 |
+
|
| 740 |
+
|
| 741 |
def build_theme() -> gr.Theme:
|
| 742 |
return gr.themes.Soft(
|
| 743 |
primary_hue="blue",
|
|
|
|
| 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"):
|
| 769 |
+
gr.Markdown(
|
| 770 |
+
f"Upload a document for layout + OCR parsing via "
|
| 771 |
+
f"`{SMARTDOC_URL}` · PDF / image / Office · **10 MB** max"
|
| 772 |
)
|
| 773 |
+
doc_file = gr.File(
|
| 774 |
+
label="Document",
|
| 775 |
+
file_count="single",
|
| 776 |
+
file_types=[
|
| 777 |
+
".pdf",
|
| 778 |
+
".png",
|
| 779 |
+
".jpg",
|
| 780 |
+
".jpeg",
|
| 781 |
+
".doc",
|
| 782 |
+
".docx",
|
| 783 |
+
".ppt",
|
| 784 |
+
".pptx",
|
| 785 |
+
".xls",
|
| 786 |
+
".xlsx",
|
| 787 |
+
".odt",
|
| 788 |
+
".odp",
|
| 789 |
+
".ods",
|
| 790 |
+
".rtf",
|
| 791 |
+
],
|
| 792 |
)
|
| 793 |
+
with gr.Row(elem_classes=["toolbar-row"]):
|
| 794 |
+
parse_btn = gr.Button("Parse Document", variant="primary", scale=0)
|
| 795 |
+
clear_doc_btn = gr.Button("Clear", scale=0)
|
| 796 |
+
smartdoc_status = gr.HTML(
|
| 797 |
+
value='<p class="smartdoc-status">Select a file to begin</p>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 798 |
)
|
| 799 |
+
with gr.Tabs():
|
| 800 |
+
with gr.Tab("Markdown"):
|
| 801 |
+
smartdoc_md = gr.Markdown(
|
| 802 |
+
value="*Rendered Markdown will appear here after parsing.*",
|
| 803 |
+
latex_delimiters=[
|
| 804 |
+
{"left": "$$", "right": "$$", "display": True},
|
| 805 |
+
{"left": "$", "right": "$", "display": False},
|
| 806 |
+
{"left": "\\(", "right": "\\)", "display": False},
|
| 807 |
+
{"left": "\\[", "right": "\\]", "display": True},
|
| 808 |
+
],
|
| 809 |
+
)
|
| 810 |
+
with gr.Tab("Results"):
|
| 811 |
+
smartdoc_json = gr.Code(
|
| 812 |
+
value="[]",
|
| 813 |
+
language="json",
|
| 814 |
+
label="Structured results",
|
| 815 |
+
lines=22,
|
| 816 |
+
)
|
| 817 |
+
|
| 818 |
+
with gr.Tab("Translation"):
|
| 819 |
+
with gr.Row():
|
| 820 |
+
source_lang = gr.Dropdown(
|
| 821 |
+
choices=LANG_OPTIONS,
|
| 822 |
+
value="zh",
|
| 823 |
+
label="Source language",
|
| 824 |
+
scale=1,
|
| 825 |
+
)
|
| 826 |
+
target_lang = gr.Dropdown(
|
| 827 |
+
choices=LANG_OPTIONS,
|
| 828 |
+
value="en",
|
| 829 |
+
label="Target language",
|
| 830 |
+
scale=1,
|
| 831 |
+
)
|
| 832 |
+
mode = gr.Radio(
|
| 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)
|
| 840 |
+
|
| 841 |
+
gr.Markdown("**Examples**")
|
| 842 |
+
example_btns: list[tuple[gr.Button, str, str, str]] = []
|
| 843 |
+
with gr.Row():
|
| 844 |
+
for i, (sample, src, tgt) in enumerate(EXAMPLES, start=1):
|
| 845 |
+
preview = sample.replace("\n", " ")[:28]
|
| 846 |
+
if len(sample) > 28:
|
| 847 |
+
preview += "…"
|
| 848 |
+
btn = gr.Button(f"{i}. {preview}", size="sm", variant="secondary")
|
| 849 |
+
example_btns.append((btn, sample, src, tgt))
|
| 850 |
+
|
| 851 |
+
input_text = gr.Textbox(
|
| 852 |
+
label="Source text",
|
| 853 |
+
placeholder=(
|
| 854 |
+
"Enter patent-related text. Supports Chinese, English, "
|
| 855 |
+
"Japanese, Korean, and more."
|
| 856 |
),
|
| 857 |
+
lines=8,
|
| 858 |
+
elem_id="input-box",
|
| 859 |
)
|
| 860 |
+
with gr.Row(elem_classes=["toolbar-row"]):
|
| 861 |
+
submit_btn = gr.Button("Translate", variant="primary", scale=0)
|
| 862 |
+
clear_btn = gr.Button("Clear", scale=0)
|
| 863 |
+
copy_btn = gr.Button(
|
| 864 |
+
"Copy full translation",
|
| 865 |
+
variant="secondary",
|
| 866 |
+
interactive=False,
|
| 867 |
+
scale=0,
|
| 868 |
+
elem_id="copy-translation-btn",
|
| 869 |
+
)
|
| 870 |
+
|
| 871 |
+
with gr.Column(
|
| 872 |
+
visible=False, elem_classes=["results-panel-col"]
|
| 873 |
+
) as results_panel:
|
| 874 |
+
gr.HTML(RESULTS_SHELL_HTML, elem_id="translation-results")
|
| 875 |
+
stream_state = gr.Textbox(
|
| 876 |
+
value=_stream_state_payload(
|
| 877 |
+
visible_pairs=[],
|
| 878 |
+
full="",
|
| 879 |
+
lang="zh2en",
|
| 880 |
+
percent=0,
|
| 881 |
+
status="Ready",
|
| 882 |
+
),
|
| 883 |
+
visible=False,
|
| 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}"
|
|
|
|
| 904 |
|
| 905 |
submit_btn.click(
|
| 906 |
fn=run_translation,
|
| 907 |
+
inputs=[input_text, lang, mode],
|
| 908 |
outputs=[stream_state, results_panel, copy_btn],
|
| 909 |
)
|
| 910 |
|
|
|
|
| 942 |
outputs=[input_text, stream_state, results_panel, copy_btn],
|
| 943 |
)
|
| 944 |
|
| 945 |
+
parse_btn.click(
|
| 946 |
+
fn=run_smartdoc,
|
| 947 |
+
inputs=[doc_file],
|
| 948 |
+
outputs=[smartdoc_status, smartdoc_md, smartdoc_json],
|
| 949 |
+
)
|
| 950 |
+
|
| 951 |
+
def on_clear_doc():
|
| 952 |
+
return (
|
| 953 |
+
None,
|
| 954 |
+
'<p class="smartdoc-status">Select a file to begin</p>',
|
| 955 |
+
"*Rendered Markdown will appear here after parsing.*",
|
| 956 |
+
"[]",
|
| 957 |
+
)
|
| 958 |
+
|
| 959 |
+
clear_doc_btn.click(
|
| 960 |
+
on_clear_doc,
|
| 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 |
)
|