Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import json | |
| import os | |
| import statistics | |
| from pathlib import Path | |
| from typing import Any | |
| import gradio as gr | |
| from ocr_workbench.client import WorkerError, check_health, normalize_endpoint, run_page | |
| from ocr_workbench.documents import ( | |
| DocumentError, | |
| cleanup_stale_runs, | |
| gallery_items, | |
| normalize_gradio_path, | |
| prepare_document, | |
| ) | |
| from ocr_workbench.export import build_exports | |
| from ocr_workbench.registry import ModelSpec, load_registry | |
| REGISTRY = load_registry() | |
| MAX_INPUT_MB = int(os.getenv("MAX_INPUT_MB", "80")) | |
| DEFAULT_MAX_PAGES = int(os.getenv("DEFAULT_MAX_PAGES", "8")) | |
| ABSOLUTE_MAX_PAGES = int(os.getenv("ABSOLUTE_MAX_PAGES", "40")) | |
| STALE_RUN_HOURS = float(os.getenv("STALE_RUN_HOURS", "12")) | |
| try: | |
| import spaces | |
| except Exception: | |
| spaces = None | |
| def _gateway_probe() -> str: | |
| return "ready" | |
| if spaces is not None: | |
| gateway_zero_gpu_probe = spaces.GPU(duration=1)(_gateway_probe) | |
| else: | |
| gateway_zero_gpu_probe = _gateway_probe | |
| def _spec(model_id: str) -> ModelSpec: | |
| try: | |
| return REGISTRY[model_id] | |
| except KeyError as exc: | |
| raise gr.Error(f"Unknown model: {model_id}") from exc | |
| def _model_info(spec: ModelSpec) -> str: | |
| endpoint = spec.endpoint() or "未設定" | |
| return ( | |
| f"### {spec.label}\n\n" | |
| f"{spec.description}\n\n" | |
| f"**Worker URL:** `{endpoint}` \n" | |
| f"**環境変数:** `{spec.endpoint_env}` \n" | |
| f"**画像出力:** {spec.result_note}" | |
| ) | |
| def on_model_change(model_id: str) -> tuple[str, str, str, int, int, int, int, bool, str]: | |
| spec = _spec(model_id) | |
| return ( | |
| spec.default_prompt, | |
| spec.endpoint(), | |
| _model_info(spec), | |
| spec.default_max_tokens, | |
| min(spec.default_max_pages, ABSOLUTE_MAX_PAGES), | |
| spec.default_dpi, | |
| spec.default_request_timeout, | |
| spec.default_layout_as_thought, | |
| spec.default_image_mode, | |
| ) | |
| def health_check(model_id: str, endpoint_override: str) -> str: | |
| spec = _spec(model_id) | |
| endpoint = normalize_endpoint(endpoint_override) or spec.endpoint() | |
| try: | |
| payload = check_health(endpoint) | |
| except WorkerError as exc: | |
| return f"❌ **Health check failed:** {exc}" | |
| return "✅ **Worker reachable**\n\n```json\n" + json.dumps(payload, ensure_ascii=False, indent=2) + "\n```" | |
| def run_ocr( | |
| file_value: object, | |
| model_id: str, | |
| prompt: str, | |
| endpoint_override: str, | |
| page_selection: str, | |
| max_pages: int, | |
| dpi: int, | |
| max_new_tokens: int, | |
| layout_as_thought: bool, | |
| unlimited_image_mode: str, | |
| request_timeout: int, | |
| progress: gr.Progress = gr.Progress(track_tqdm=False), | |
| ) -> tuple[ | |
| list[tuple[str, str]], | |
| list[tuple[str, str]], | |
| str, | |
| str, | |
| str, | |
| str, | |
| str, | |
| ]: | |
| cleanup_stale_runs(STALE_RUN_HOURS) | |
| source_path = normalize_gradio_path(file_value) | |
| spec = _spec(model_id) | |
| endpoint = normalize_endpoint(endpoint_override) or spec.endpoint() | |
| if not endpoint: | |
| raise gr.Error( | |
| f"{spec.label} のWorker URLが未設定です。Space Variable " | |
| f"`{spec.endpoint_env}` または画面のWorker URL欄を設定してください。" | |
| ) | |
| max_pages = min(max(1, int(max_pages)), ABSOLUTE_MAX_PAGES) | |
| progress(0.03, desc="入力を展開しています") | |
| try: | |
| run_dir, pages = prepare_document( | |
| source_path, | |
| dpi=int(dpi), | |
| selection=page_selection, | |
| max_pages=max_pages, | |
| max_input_mb=MAX_INPUT_MB, | |
| ) | |
| except DocumentError as exc: | |
| raise gr.Error(str(exc)) from exc | |
| source_gallery = gallery_items(pages) | |
| options: dict[str, Any] = { | |
| "max_new_tokens": int(max_new_tokens), | |
| "layout_as_thought": bool(layout_as_thought), | |
| "image_mode": unlimited_image_mode, | |
| } | |
| responses: list[dict[str, Any]] = [] | |
| elapsed_values: list[float] = [] | |
| for index, page in enumerate(pages, start=1): | |
| progress( | |
| 0.08 + 0.82 * ((index - 1) / len(pages)), | |
| desc=f"Page {page.page_number} / {pages[-1].page_number}: {spec.label}", | |
| ) | |
| try: | |
| response = run_page( | |
| endpoint=endpoint, | |
| model_id=model_id, | |
| page_path=page.image_path, | |
| prompt=prompt, | |
| options=options, | |
| timeout_seconds=float(request_timeout), | |
| ) | |
| except WorkerError as exc: | |
| raise gr.Error(f"Page {page.page_number} failed: {exc}") from exc | |
| responses.append(response) | |
| elapsed = response.get("metrics", {}).get("elapsed_seconds") | |
| if isinstance(elapsed, (int, float)): | |
| elapsed_values.append(float(elapsed)) | |
| progress(0.94, desc="結果をまとめています") | |
| annotated, markdown, text, raw_json, archive = build_exports( | |
| run_dir=run_dir, | |
| model_id=model_id, | |
| model_label=spec.label, | |
| pages=pages, | |
| responses=responses, | |
| ) | |
| total_elapsed = sum(elapsed_values) | |
| median_elapsed = statistics.median(elapsed_values) if elapsed_values else 0.0 | |
| warning_count = sum(len(response.get("warnings", []) or []) for response in responses) | |
| status = ( | |
| f"完了: {spec.label} / {len(pages)} page(s). " | |
| f"Worker inference total {total_elapsed:.1f}s, median {median_elapsed:.1f}s/page. " | |
| f"Warnings: {warning_count}." | |
| ) | |
| progress(1.0, desc="完了") | |
| return source_gallery, annotated, markdown, text, raw_json, archive, status | |
| model_choices = [(spec.label, spec.id) for spec in REGISTRY.values()] | |
| default_model_id = next(iter(REGISTRY)) | |
| default_spec = REGISTRY[default_model_id] | |
| CSS = """ | |
| #status-box textarea {font-family: ui-monospace, SFMono-Regular, Menlo, monospace;} | |
| .result-gallery {min-height: 460px;} | |
| """ | |
| with gr.Blocks(title="OCR Model Workbench") as demo: | |
| gr.Markdown( | |
| "# OCR Model Workbench\n" | |
| "PDF・画像を共通UIから最新OCRモデルへ送り、ページ画像、可視化画像、Markdown、テキスト、JSONを比較します。" | |
| ) | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=5): | |
| input_file = gr.File( | |
| label="PDF / 画像", | |
| file_count="single", | |
| type="filepath", | |
| file_types=["image", ".pdf"], | |
| ) | |
| model = gr.Dropdown( | |
| choices=model_choices, | |
| value=default_model_id, | |
| label="モデル", | |
| ) | |
| prompt = gr.Textbox( | |
| value=default_spec.default_prompt, | |
| label="プロンプト(VLM系のみ)", | |
| lines=4, | |
| ) | |
| with gr.Accordion("ページ・推論設定", open=False): | |
| page_selection = gr.Textbox( | |
| label="PDFページ指定", | |
| placeholder="空欄=先頭から。例: 1-3,5", | |
| value="", | |
| ) | |
| max_pages = gr.Slider( | |
| minimum=1, | |
| maximum=ABSOLUTE_MAX_PAGES, | |
| step=1, | |
| value=min(default_spec.default_max_pages, DEFAULT_MAX_PAGES, ABSOLUTE_MAX_PAGES), | |
| label="最大ページ数", | |
| ) | |
| dpi = gr.Slider(96, 300, value=default_spec.default_dpi, step=12, label="PDF rasterize DPI") | |
| max_new_tokens = gr.Slider( | |
| 256, | |
| 32768, | |
| value=default_spec.default_max_tokens, | |
| step=256, | |
| label="生成上限(max_new_tokens / max_length)", | |
| ) | |
| layout_as_thought = gr.Checkbox( | |
| label="Qianfan Layout-as-Thought", | |
| value=default_spec.default_layout_as_thought, | |
| ) | |
| unlimited_image_mode = gr.Radio( | |
| choices=["gundam", "base"], | |
| value=default_spec.default_image_mode, | |
| label="Unlimited-OCR image mode", | |
| ) | |
| request_timeout = gr.Slider( | |
| 60, | |
| 1800, | |
| value=default_spec.default_request_timeout, | |
| step=30, | |
| label="1ページのタイムアウト(秒)", | |
| ) | |
| with gr.Accordion("Worker接続", open=False): | |
| endpoint_override = gr.Textbox( | |
| value=default_spec.endpoint(), | |
| label="Worker URL(空欄ならSpace Variableを使用)", | |
| placeholder="https://username-space-name.hf.space", | |
| ) | |
| health_button = gr.Button("Health check") | |
| run_button = gr.Button("OCRを実行", variant="primary") | |
| status = gr.Textbox(label="Status", interactive=False, elem_id="status-box") | |
| with gr.Column(scale=7): | |
| model_info = gr.Markdown(_model_info(default_spec)) | |
| health_output = gr.Markdown() | |
| with gr.Tabs(): | |
| with gr.Tab("入力ページ"): | |
| source_gallery = gr.Gallery( | |
| label="Source pages", | |
| columns=2, | |
| height=520, | |
| elem_classes=["result-gallery"], | |
| ) | |
| with gr.Tab("結果画像"): | |
| result_gallery = gr.Gallery( | |
| label="Annotated / normalized result images", | |
| columns=2, | |
| height=520, | |
| elem_classes=["result-gallery"], | |
| ) | |
| with gr.Tab("Markdown"): | |
| markdown_output = gr.Markdown() | |
| with gr.Tab("Text"): | |
| text_output = gr.Textbox(lines=28, buttons=["copy"]) | |
| with gr.Tab("JSON"): | |
| json_output = gr.Code(language="json", lines=28) | |
| with gr.Tab("Download"): | |
| download_output = gr.File(label="全結果ZIP") | |
| model.change( | |
| fn=on_model_change, | |
| inputs=model, | |
| outputs=[ | |
| prompt, | |
| endpoint_override, | |
| model_info, | |
| max_new_tokens, | |
| max_pages, | |
| dpi, | |
| request_timeout, | |
| layout_as_thought, | |
| unlimited_image_mode, | |
| ], | |
| ) | |
| health_button.click( | |
| fn=health_check, | |
| inputs=[model, endpoint_override], | |
| outputs=health_output, | |
| ) | |
| run_button.click( | |
| fn=run_ocr, | |
| inputs=[ | |
| input_file, | |
| model, | |
| prompt, | |
| endpoint_override, | |
| page_selection, | |
| max_pages, | |
| dpi, | |
| max_new_tokens, | |
| layout_as_thought, | |
| unlimited_image_mode, | |
| request_timeout, | |
| ], | |
| outputs=[ | |
| source_gallery, | |
| result_gallery, | |
| markdown_output, | |
| text_output, | |
| json_output, | |
| download_output, | |
| status, | |
| ], | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(default_concurrency_limit=2, max_size=12).launch( | |
| server_name="0.0.0.0", | |
| server_port=int(os.getenv("PORT", "7860")), | |
| css=CSS, | |
| ) | |