import math import os import re from pathlib import Path import gradio as gr import spaces import torch from transformers import AutoModel, AutoTokenizer from image_utils import load_image MODEL_PATH = os.getenv("MODEL_PATH", "baidu/Qianfan-OCR") MAX_TILES_PER_IMAGE = int(os.getenv("MAX_TILES_PER_IMAGE", "12")) DEFAULT_MAX_NEW_TOKENS = 2048 MAX_NEW_TOKENS_LIMIT = 8192 ZEROGPU_DURATION_MIN = 60 ZEROGPU_DURATION_MAX = 300 DEFAULT_PROMPT = "Please extract the text from the image." IMAGE_FILE_TYPES = [".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"] APP_DIR = Path(__file__).resolve().parent EXAMPLES_DIR = APP_DIR / "examples" LATEX_DELIMITERS = [ {"left": "$$", "right": "$$", "display": True}, {"left": "$", "right": "$", "display": False}, {"left": "\\(", "right": "\\)", "display": False}, {"left": "\\[", "right": "\\]", "display": True}, ] LATEX_FENCE_RE = re.compile( r"(^|\n)```(?:latex|tex)[ \t]*\n(?P.*?)(?:\n```)(?=\n|$)", re.IGNORECASE | re.DOTALL, ) THINK_BLOCK_RE = re.compile(r".*?", re.DOTALL | re.IGNORECASE) EXAMPLE_ITEMS = [ { "task": "Text Extraction", "prompt": "Please extract the text from the image.", "image": str(EXAMPLES_DIR / "text_block.png"), }, { "task": "Formula Parsing", "prompt": "Please convert the formula in the image to LaTeX.", "image": str(EXAMPLES_DIR / "formula_block.png"), }, { "task": "Table Parsing", "prompt": "Please convert the table in the image to HTML.", "image": str(EXAMPLES_DIR / "table_block.png"), }, { "task": "Document Parsing", "prompt": "Parse this document to Markdown.", "image": str(EXAMPLES_DIR / "document.png"), }, { "task": "Multilingual Scene Text Recognition", "prompt": "Please output the text content from the image.", "image": str(EXAMPLES_DIR / "information_board.jpg"), }, { "task": "Key Information Extraction", "prompt": """请从图片中提取以下信息:“发票号码、开票日期、发票种类、购方名称、购方纳税人识别号、合计金额、合计税额、价税合计(小写)、价税合计(大写)、货物名称、规格型号、单位、数量、单价、金额、税率、税额、销售方名称、销售方纳税人识别号、开票人、备注” 注意: 1. 仅提取清晰可见且可确定的文字内容;模糊、缺失或无法确认的部分请忽略,不要猜测或补全。 2. 值完整提取及格式保持:提取完整的字段值,包括数字、符号、单位等,保持原始格式,包括空格、标点符号等。 3. 使用标准JSON格式输出,字段值保持原始格式。""", "image": str(EXAMPLES_DIR / "invoice.jpg"), }, ] EXAMPLE_ITEMS = [example for example in EXAMPLE_ITEMS if Path(example["image"]).exists()] _MODEL = AutoModel.from_pretrained( MODEL_PATH, torch_dtype="auto", trust_remote_code=True, ).to("cuda") _TOKENIZER = AutoTokenizer.from_pretrained( MODEL_PATH, trust_remote_code=True, ) def load_images(file_paths, device, dtype: torch.dtype): if not file_paths: raise gr.Error("Please upload at least one image.") pixel_values_list = [] for path in file_paths: if not path: continue pixel_values = load_image(str(path), max_num=MAX_TILES_PER_IMAGE) pixel_values_list.append(pixel_values) if not pixel_values_list: raise gr.Error("No valid image files were received.") pixel_values = torch.cat(pixel_values_list, dim=0).to(device=device, dtype=dtype) return pixel_values def validate_prompt(prompt: str) -> str: prompt = (prompt or "").strip() if not prompt: raise gr.Error("Please enter a prompt.") return prompt def build_question(prompt: str, layout_as_thought: bool) -> str: question = validate_prompt(prompt) if layout_as_thought and not question.endswith(""): question = f"{question}" return question def preview_uploaded_images(file_paths): if not file_paths: return [] return [str(path) for path in file_paths if path] def clear_form(): return None, [], DEFAULT_PROMPT, False, DEFAULT_MAX_NEW_TOKENS, "" def load_example(example_index: int): if example_index is None or not (0 <= int(example_index) < len(EXAMPLE_ITEMS)): raise gr.Error(f"Unknown example index: {example_index}") example = EXAMPLE_ITEMS[int(example_index)] image_paths = [example["image"]] return ( image_paths, image_paths, example["prompt"], False, DEFAULT_MAX_NEW_TOKENS, "", ) def normalize_markdown_math(text: str) -> str: if not text: return text def replace_latex_fence(match: re.Match[str]) -> str: body = match.group("body").strip() if not body: return match.group(0) if body.startswith("$$") and body.endswith("$$"): return f"{match.group(1)}{body}" if body.startswith("\\[") and body.endswith("\\]"): return f"{match.group(1)}{body}" return f"{match.group(1)}$$\n{body}\n$$" return LATEX_FENCE_RE.sub(replace_latex_fence, text) def wrap_think_blocks(text: str) -> str: if not text: return text def replace_think_block(match: re.Match[str]) -> str: block = match.group(0).strip() return f"\n```text\n{block}\n```\n" return THINK_BLOCK_RE.sub(replace_think_block, text) def estimate_zerogpu_duration(file_paths, prompt, layout_as_thought, max_new_tokens): del file_paths, prompt, layout_as_thought estimated_duration = math.ceil(int(max_new_tokens) / 25 + 15) final_duration = max( ZEROGPU_DURATION_MIN, min(ZEROGPU_DURATION_MAX, estimated_duration), ) return final_duration @spaces.GPU(duration=estimate_zerogpu_duration) def run_inference(file_paths, prompt, layout_as_thought, max_new_tokens): if not file_paths: raise gr.Error("Please upload at least one image.") pixel_values = load_images(file_paths, _MODEL.device, _MODEL.dtype) question = build_question(prompt, layout_as_thought) generation_config = {"max_new_tokens": int(max_new_tokens)} with torch.no_grad(): response = _MODEL.chat( _TOKENIZER, pixel_values=pixel_values, question=question, generation_config=generation_config, ) return normalize_markdown_math(wrap_think_blocks(response)) def build_demo(): with gr.Blocks(title="Qianfan-OCR Demo") as demo: gr.Markdown( """ # Qianfan-OCR Demo 📄 Technical Report | 🖥️ Qianfan Platform | 💻 GitHub | 🧩 Skill Qianfan-OCR is a 4B-parameter end-to-end document intelligence model developed by the Baidu Qianfan Team. It unifies document parsing, layout analysis, and document understanding within a single vision-language architecture. """, sanitize_html=False, ) with gr.Row(equal_height=True): with gr.Column(scale=1): image_input = gr.File( label="Images", file_count="multiple", file_types=IMAGE_FILE_TYPES, type="filepath", ) image_preview = gr.Gallery( label="Uploaded Images", columns=2, height="auto", show_label=True, ) with gr.Column(scale=1): prompt_input = gr.Textbox( label="Prompt", lines=18, value=DEFAULT_PROMPT, placeholder="Describe the task for the uploaded image(s).", ) layout_as_thought_input = gr.Checkbox( label="Layout-as-Thought", value=False, ) max_new_tokens_input = gr.Slider( label="max_new_tokens", minimum=256, maximum=MAX_NEW_TOKENS_LIMIT, step=256, value=DEFAULT_MAX_NEW_TOKENS, ) with gr.Row(): submit_button = gr.Button("Run", variant="primary") clear_button = gr.Button("Clear") with gr.Column(scale=1): output_box = gr.Markdown( value="", line_breaks=True, sanitize_html=False, latex_delimiters=LATEX_DELIMITERS, height=520, buttons=["copy"], container=True, padding=True, ) if EXAMPLE_ITEMS: example_dataset = gr.Dataset( label="Examples", components=[ gr.Textbox(label="Task", render=False), gr.Image(label="Image", type="filepath", height=120, render=False), gr.Textbox(label="Prompt", render=False), ], samples=[ [example["task"], example["image"], example["prompt"]] for example in EXAMPLE_ITEMS ], type="index", layout="table", ) example_dataset.click( fn=load_example, inputs=example_dataset, outputs=[ image_input, image_preview, prompt_input, layout_as_thought_input, max_new_tokens_input, output_box, ], ) image_input.change( fn=preview_uploaded_images, inputs=image_input, outputs=image_preview, ) submit_button.click( fn=run_inference, inputs=[image_input, prompt_input, layout_as_thought_input, max_new_tokens_input], outputs=output_box, ) clear_button.click( fn=clear_form, outputs=[ image_input, image_preview, prompt_input, layout_as_thought_input, max_new_tokens_input, output_box, ], ) return demo if __name__ == "__main__": app = build_demo() app.queue().launch()