xxyyy123 commited on
Commit
94459f0
·
verified ·
1 Parent(s): a431a14

Deploy OvisOCR2 OCR demo

Browse files
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. README.md +100 -4
  3. app.py +574 -0
  4. dist/assets/__vite-browser-external-DYxpcVy9-BIHI7g3E.js +1 -0
  5. dist/assets/index-5Ju-rOmV.js +0 -0
  6. dist/assets/index-CCa7-Cez.css +1 -0
  7. dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  8. dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  9. dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  10. dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  11. dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  12. dist/assets/manrope-cyrillic-wght-normal-Dvxsihut.woff2 +0 -0
  13. dist/assets/manrope-greek-wght-normal-DL7QRZyv.woff2 +0 -0
  14. dist/assets/manrope-latin-ext-wght-normal-Ch3YOpNY.woff2 +0 -0
  15. dist/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 +0 -0
  16. dist/assets/manrope-vietnamese-wght-normal-usUDDRr7.woff2 +0 -0
  17. dist/brand/ovis-logo.png +0 -0
  18. dist/examples/handwriting-040745.jpg +3 -0
  19. dist/examples/handwriting-042609.jpg +3 -0
  20. dist/examples/page-9477c155-19b3-4f2d-ac18-ee3f8997d2f8.png +3 -0
  21. dist/examples/table-1305dae7.webp +0 -0
  22. dist/examples/table-3ba1139c.webp +0 -0
  23. dist/favicon.ico +0 -0
  24. dist/index.html +31 -0
  25. dist/vendor/mathjax/output/chtml/fonts/tex.js +0 -0
  26. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff +0 -0
  27. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff +0 -0
  28. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff +0 -0
  29. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff +0 -0
  30. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff +0 -0
  31. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff +0 -0
  32. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff +0 -0
  33. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff +0 -0
  34. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff +0 -0
  35. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff +0 -0
  36. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff +0 -0
  37. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff +0 -0
  38. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff +0 -0
  39. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff +0 -0
  40. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff +0 -0
  41. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff +0 -0
  42. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff +0 -0
  43. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff +0 -0
  44. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff +0 -0
  45. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff +0 -0
  46. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff +0 -0
  47. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff +0 -0
  48. dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Zero.woff +0 -0
  49. dist/vendor/mathjax/tex-chtml-full.js +0 -0
  50. requirements.txt +5 -0
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ dist/examples/handwriting-040745.jpg filter=lfs diff=lfs merge=lfs -text
37
+ dist/examples/handwriting-042609.jpg filter=lfs diff=lfs merge=lfs -text
38
+ dist/examples/page-9477c155-19b3-4f2d-ac18-ee3f8997d2f8.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,109 @@
1
  ---
2
  title: OvisOCR2
3
- emoji: 🐢
4
- colorFrom: yellow
5
  colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: OvisOCR2
3
+ emoji: 🔎
4
+ colorFrom: blue
5
  colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
+ python_version: "3.12.12"
9
  app_file: app.py
10
  pinned: false
11
+ license: apache-2.0
12
+ hardware: zero-gpu
13
+ startup_duration_timeout: 30m
14
+ models:
15
+ - ATH-MaaS/OvisOCR2
16
+ preload_from_hub:
17
+ - ATH-MaaS/OvisOCR2
18
+ short_description: Stream structured Markdown from document images and PDFs.
19
  ---
20
 
21
+ # OvisOCR2 document OCR demo
22
+
23
+ A focused image and PDF document parsing demo for
24
+ [ATH-MaaS/OvisOCR2](https://huggingface.co/ATH-MaaS/OvisOCR2). It preserves
25
+ reading order and emits Markdown with LaTeX formulas, HTML tables, and rendered
26
+ visual-region crops. Model tokens stream into the interface as they are generated.
27
+ PDFs are rasterized locally and processed one page at a time in Base mode. Each
28
+ page uses an independent streamed GPU request and the browser merges the results,
29
+ so long documents do not have to fit inside one ZeroGPU lease. This demo
30
+ intentionally does not expose a separate Long mode.
31
+
32
+ The five packaged examples cover two financial tables, two Chinese handwriting
33
+ pages, and one formula-rich OmniDocBench research-paper page. They are selectable
34
+ directly in the interface and are also covered by the local smoke-test workflow.
35
+ The workspace opens empty so examples remain an explicit user choice rather than
36
+ an automatic input.
37
+
38
+ ## Local run
39
+
40
+ The verified local model directory is `/root/models/ATH-MaaS/OvisOCR2`.
41
+
42
+ ```bash
43
+ conda env create -f environment.yml
44
+ conda activate ovisocr2-demo
45
+ npm ci
46
+ npm run build
47
+ CUDA_VISIBLE_DEVICES=0 \
48
+ OVISOCR_MODEL_PATH=/root/models/ATH-MaaS/OvisOCR2 \
49
+ python app.py
50
+ ```
51
+
52
+ Open `http://127.0.0.1:7860`.
53
+
54
+ ### DSW proxy
55
+
56
+ The server already listens on `0.0.0.0`. Set a DSW id and the public proxy URL
57
+ is generated from the same `PORT` automatically:
58
+
59
+ ```bash
60
+ CUDA_VISIBLE_DEVICES=0 \
61
+ PORT=7869 \
62
+ OVISOCR_DSW_ID=1976932 \
63
+ OVISOCR_MODEL_PATH=/root/models/ATH-MaaS/OvisOCR2 \
64
+ python app.py
65
+ ```
66
+
67
+ The corresponding URL is
68
+ `https://1976932-proxy-7869.dsw-gateway-cn-hangzhou.data.aliyun.com/`.
69
+ For a different gateway or an explicit public URL, set `OVISOCR_ROOT_PATH`
70
+ instead; it takes precedence over `OVISOCR_DSW_ID`.
71
+
72
+ DSW's per-port proxy uses its own hostname and serves at `/`, so the ASGI root
73
+ path stays empty. If an explicit URL contains a real subpath, such as
74
+ `https://example.test/ocr/`, the app passes only `/ocr` to Gradio's `root_path`;
75
+ passing the whole URL into this custom FastAPI server would break static mounts.
76
+
77
+ For a UI-only smoke that does not load the model:
78
+
79
+ ```bash
80
+ OVISOCR_TEST_MODE=1 python app.py
81
+ ```
82
+
83
+ ## Hugging Face Space
84
+
85
+ The checked-in `dist/` directory is the production frontend, so the folder can
86
+ be uploaded directly to a Gradio Space without a Node build step. Select
87
+ ZeroGPU hardware in the Space settings. The README metadata pins Python 3.12.12
88
+ and Gradio 6.20.0
89
+ and declares `hardware: zero-gpu`; `app.py` decorates only the inference endpoint
90
+ with `@spaces.GPU` and loads the model on CUDA at module scope, following the
91
+ ZeroGPU loading contract.
92
+
93
+ On Hugging Face, the model source automatically falls back from the local path
94
+ to `ATH-MaaS/OvisOCR2`. PDFs are limited to 50 pages by default. Every page gets
95
+ its own ZeroGPU reservation while the frontend preserves already completed pages.
96
+
97
+ ## Runtime knobs
98
+
99
+ - `OVISOCR_MODEL_PATH`: local directory or Hub model id.
100
+ - `OVISOCR_MAX_NEW_TOKENS`: defaults to `16384`, matching the model README inference configuration.
101
+ - `OVISOCR_MAX_PDF_PAGES`: defaults to `50`.
102
+ - `OVISOCR_PDF_RENDER_SCALE`: PDF rasterization scale, defaults to `2.0`.
103
+ - `OVISOCR_STREAM_MIN_CHARS`: minimum new characters between stream updates, defaults to `24`.
104
+ - `OVISOCR_STREAM_MAX_INTERVAL`: maximum seconds between available stream updates, defaults to `0.12`.
105
+ - `OVISOCR_GPU_DURATION`: per-page ZeroGPU reservation in seconds, defaults to `300`.
106
+ - `OVISOCR_ATTN_IMPLEMENTATION`: defaults to `sdpa`.
107
+ - `OVISOCR_DSW_ID`: generates the Hangzhou DSW proxy root using the active `PORT`.
108
+ - `OVISOCR_ROOT_PATH`: explicit proxy URL or ASGI path prefix; overrides `OVISOCR_DSW_ID`.
109
+ - `OVISOCR_TEST_MODE=1`: deterministic mock inference for UI/integration tests.
app.py ADDED
@@ -0,0 +1,574 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import html
5
+ import io
6
+ import os
7
+ import re
8
+ import threading
9
+ import time
10
+ from collections.abc import Iterator
11
+ from pathlib import Path
12
+ from typing import Any, Callable
13
+ from urllib.parse import urlsplit
14
+
15
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
16
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
17
+
18
+ try:
19
+ import spaces # Must be imported before torch on Hugging Face ZeroGPU.
20
+ except ImportError: # Local development uses a no-op decorator.
21
+ class _LocalSpaces:
22
+ @staticmethod
23
+ def GPU(*decorator_args: Any, **decorator_kwargs: Any) -> Callable:
24
+ def decorate(function: Callable) -> Callable:
25
+ return function
26
+
27
+ if decorator_args and callable(decorator_args[0]) and len(decorator_args) == 1:
28
+ return decorator_args[0]
29
+ return decorate
30
+
31
+ spaces = _LocalSpaces()
32
+
33
+ import gradio as gr
34
+ import fitz
35
+ import torch
36
+ from fastapi.middleware.cors import CORSMiddleware
37
+ from fastapi.responses import FileResponse, JSONResponse
38
+ from gradio.data_classes import FileData
39
+ from PIL import Image, ImageOps
40
+ from starlette.staticfiles import StaticFiles
41
+
42
+
43
+ BASE_DIR = Path(__file__).resolve().parent
44
+ DIST_DIR = BASE_DIR / "dist"
45
+ MODEL_ID = "ATH-MaaS/OvisOCR2"
46
+ LOCAL_MODEL_DEFAULT = Path("/root/models/ATH-MaaS/OvisOCR2")
47
+ TEST_MODE = os.getenv("OVISOCR_TEST_MODE", "0").lower() in {"1", "true", "yes"}
48
+ MODEL_SOURCE = os.getenv(
49
+ "OVISOCR_MODEL_PATH",
50
+ str(LOCAL_MODEL_DEFAULT if LOCAL_MODEL_DEFAULT.is_dir() else MODEL_ID),
51
+ )
52
+ MAX_NEW_TOKENS = int(os.getenv("OVISOCR_MAX_NEW_TOKENS", "16384"))
53
+ MAX_PDF_PAGES = int(os.getenv("OVISOCR_MAX_PDF_PAGES", "50"))
54
+ PDF_RENDER_SCALE = float(os.getenv("OVISOCR_PDF_RENDER_SCALE", "2.0"))
55
+ STREAM_MIN_CHARS = int(os.getenv("OVISOCR_STREAM_MIN_CHARS", "24"))
56
+ STREAM_MAX_INTERVAL = float(os.getenv("OVISOCR_STREAM_MAX_INTERVAL", "0.12"))
57
+ MIN_PIXELS = 448 * 448
58
+ MAX_PIXELS = 2880 * 2880
59
+
60
+
61
+ def server_config() -> tuple[int, str | None, str | None]:
62
+ """Resolve the port, ASGI path prefix, and optional public proxy URL."""
63
+ port = int(os.getenv("PORT", os.getenv("GRADIO_SERVER_PORT", "7860")))
64
+ configured_root = (
65
+ os.getenv("OVISOCR_ROOT_PATH", "").strip()
66
+ or os.getenv("GRADIO_ROOT_PATH", "").strip()
67
+ )
68
+ dsw_id = os.getenv("OVISOCR_DSW_ID", "").strip()
69
+ public_url = None
70
+ root_path = None
71
+ if configured_root.startswith(("http://", "https://")):
72
+ public_url = configured_root
73
+ path = urlsplit(configured_root).path.rstrip("/")
74
+ root_path = path or None
75
+ elif configured_root:
76
+ root_path = configured_root.rstrip("/") or None
77
+ elif dsw_id:
78
+ public_url = (
79
+ f"https://{dsw_id}-proxy-{port}."
80
+ "dsw-gateway-cn-hangzhou.data.aliyun.com/"
81
+ )
82
+ return port, root_path, public_url
83
+
84
+
85
+ SERVER_PORT, ROOT_PATH, PUBLIC_URL = server_config()
86
+
87
+ OCR_PROMPT = (
88
+ "\nExtract all readable content from the image in natural human reading order "
89
+ "and output the result as a single Markdown document. For charts or images, "
90
+ 'represent them using an HTML image tag: <img src="images/bbox_{left}_{top}_{right}_{bottom}.jpg" />, '
91
+ "where left, top, right, bottom are bounding box coordinates scaled to [0, 1000). "
92
+ "Format formulas as LaTeX. Format tables as HTML: <table>...</table>. "
93
+ "Transcribe all other text as standard Markdown. Preserve the original text "
94
+ "without translation or paraphrasing."
95
+ )
96
+
97
+ BBOX_IMAGE_PATTERN = re.compile(
98
+ r'<img\s+src=["\']images/bbox_(\d+)_(\d+)_(\d+)_(\d+)\.jpg["\']\s*/?>',
99
+ flags=re.IGNORECASE,
100
+ )
101
+ UNMATERIALIZED_BBOX_IMAGE_PATTERN = re.compile(
102
+ r'<img\b[^>]*\bsrc=["\']images/bbox_[^"\']+["\'][^>]*>',
103
+ flags=re.IGNORECASE,
104
+ )
105
+
106
+ MOCK_MARKDOWN = r"""# 盈利预测、估值与评级
107
+
108
+ 我们预测公司 2024—2026 年营业收入与归母净利润将保持稳健增长,当前股价对应估值如下。
109
+
110
+ <table>
111
+ <thead><tr><th>项目</th><th>2023A</th><th>2024E</th><th>2025E</th><th>2026E</th></tr></thead>
112
+ <tbody>
113
+ <tr><td>营业收入(百万元)</td><td>9,423</td><td>10,516</td><td>11,873</td><td>13,441</td></tr>
114
+ <tr><td>归母净利润(百万元)</td><td>1,267</td><td>1,452</td><td>1,681</td><td>1,946</td></tr>
115
+ <tr><td>每股收益(元)</td><td>1.02</td><td>1.17</td><td>1.36</td><td>1.57</td></tr>
116
+ <tr><td>市盈率</td><td>18.4</td><td>16.1</td><td>13.8</td><td>12.0</td></tr>
117
+ </tbody>
118
+ </table>
119
+
120
+ ## 财务摘要
121
+
122
+ 净资产收益率采用 $ROE = \frac{NP}{E}$ 计算;预计 2025 年利润同比增速为:
123
+
124
+ \[
125
+ g = \frac{1{,}681 - 1{,}452}{1{,}452} \times 100\% = 15.8\%.
126
+ \]
127
+
128
+ <img src="images/bbox_120_130_880_420.jpg" />
129
+
130
+ 资料来源:公司公告,研究团队整理。"""
131
+
132
+
133
+ processor = None
134
+ model = None
135
+
136
+
137
+ def _load_model() -> None:
138
+ global processor, model
139
+ if TEST_MODE:
140
+ return
141
+
142
+ from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration
143
+
144
+ local_only = Path(MODEL_SOURCE).is_dir()
145
+ processor = AutoProcessor.from_pretrained(
146
+ MODEL_SOURCE,
147
+ min_pixels=MIN_PIXELS,
148
+ max_pixels=MAX_PIXELS,
149
+ local_files_only=local_only,
150
+ )
151
+ model = Qwen3_5ForConditionalGeneration.from_pretrained(
152
+ MODEL_SOURCE,
153
+ dtype=torch.bfloat16,
154
+ attn_implementation=os.getenv("OVISOCR_ATTN_IMPLEMENTATION", "sdpa"),
155
+ local_files_only=local_only,
156
+ ).to("cuda")
157
+ model.eval()
158
+
159
+
160
+ def clean_truncated_repeats(
161
+ text: str,
162
+ min_text_len: int = 8000,
163
+ max_period: int = 200,
164
+ min_period: int = 1,
165
+ min_repeat_chars: int = 100,
166
+ min_repeat_times: int = 5,
167
+ ) -> str:
168
+ """Remove a repeated suffix created when generation reaches its token ceiling."""
169
+ n = len(text)
170
+ if n < min_text_len:
171
+ return text
172
+
173
+ max_period = min(max_period, n - 1)
174
+ for unit_len in range(min_period, max_period + 1):
175
+ if text[n - 1] != text[n - 1 - unit_len]:
176
+ continue
177
+ match_len = 1
178
+ idx = n - 2
179
+ while idx >= unit_len and text[idx] == text[idx - unit_len]:
180
+ match_len += 1
181
+ idx -= 1
182
+ total_len = match_len + unit_len
183
+ repeat_times = total_len // unit_len
184
+ tail_len = total_len % unit_len
185
+ if repeat_times >= min_repeat_times and total_len >= min_repeat_chars:
186
+ return text[: n - total_len + unit_len] + text[n - tail_len :]
187
+ return text
188
+
189
+
190
+ def materialize_bbox_images(markdown: str, page_image: Image.Image) -> str:
191
+ """Replace bbox image placeholders in rendered output with safe data-URI crops.
192
+
193
+ Raw model Markdown is returned separately and remains unchanged.
194
+ """
195
+ width, height = page_image.size
196
+
197
+ def replace(match: re.Match[str]) -> str:
198
+ left, top, right, bottom = (int(value) for value in match.groups())
199
+ x1 = max(0, min(width, round(left * width / 1000)))
200
+ y1 = max(0, min(height, round(top * height / 1000)))
201
+ x2 = max(0, min(width, round(right * width / 1000)))
202
+ y2 = max(0, min(height, round(bottom * height / 1000)))
203
+ if x2 <= x1 or y2 <= y1:
204
+ return match.group(0)
205
+
206
+ crop = page_image.crop((x1, y1, x2, y2)).convert("RGB")
207
+ crop.thumbnail((1600, 1600), Image.Resampling.LANCZOS)
208
+ buffer = io.BytesIO()
209
+ crop.save(buffer, format="JPEG", quality=88, optimize=True)
210
+ payload = base64.b64encode(buffer.getvalue()).decode("ascii")
211
+ return f'<img src="data:image/jpeg;base64,{payload}" alt="Visual region" />'
212
+
213
+ return neutralize_unmaterialized_bbox_images(BBOX_IMAGE_PATTERN.sub(replace, markdown))
214
+
215
+
216
+ def neutralize_unmaterialized_bbox_images(markdown: str) -> str:
217
+ """Render placeholder examples as code instead of issuing broken requests."""
218
+
219
+ def replace(match: re.Match[str]) -> str:
220
+ escaped = html.escape(match.group(0), quote=False)
221
+ return f'<code class="unresolved-image-reference">{escaped}</code>'
222
+
223
+ return UNMATERIALIZED_BBOX_IMAGE_PATTERN.sub(replace, markdown)
224
+
225
+
226
+ def stream_safe_markdown(markdown: str) -> str:
227
+ """Avoid broken image requests until a page's bbox crops are materialized."""
228
+ return neutralize_unmaterialized_bbox_images(
229
+ BBOX_IMAGE_PATTERN.sub(
230
+ '<div class="visual-placeholder">Preparing visual region…</div>',
231
+ markdown,
232
+ )
233
+ )
234
+
235
+
236
+ def _file_path(file_data: FileData | dict[str, Any]) -> str:
237
+ if isinstance(file_data, dict):
238
+ path = file_data.get("path")
239
+ else:
240
+ path = getattr(file_data, "path", None)
241
+ if not path:
242
+ raise ValueError("No uploaded document was provided.")
243
+ return str(path)
244
+
245
+
246
+ def generation_token_ids(active_processor: Any) -> dict[str, int]:
247
+ """Use tokenizer stop IDs; this checkpoint's config and tokenizer differ."""
248
+ tokenizer = active_processor.tokenizer
249
+ return {
250
+ "eos_token_id": int(tokenizer.eos_token_id),
251
+ "pad_token_id": int(tokenizer.pad_token_id),
252
+ }
253
+
254
+
255
+ def document_info(path: str) -> tuple[str, int]:
256
+ suffix = Path(path).suffix.lower()
257
+ try:
258
+ with Path(path).open("rb") as file:
259
+ header = file.read(5)
260
+ except OSError as error:
261
+ raise ValueError("The uploaded document could not be read.") from error
262
+
263
+ # Browser uploads can arrive at Gradio as an extensionless temporary `blob`.
264
+ if suffix == ".pdf" or header == b"%PDF-":
265
+ with fitz.open(path) as document:
266
+ total_pages = document.page_count
267
+ if total_pages < 1:
268
+ raise ValueError("The uploaded PDF has no pages.")
269
+ if total_pages > MAX_PDF_PAGES:
270
+ raise ValueError(
271
+ f"This demo accepts up to {MAX_PDF_PAGES} PDF pages; received {total_pages}."
272
+ )
273
+ return "pdf", total_pages
274
+
275
+ try:
276
+ with Image.open(path) as source:
277
+ source.verify()
278
+ except Exception as error:
279
+ raise ValueError("Please upload a valid PNG, JPEG, WebP, or PDF file.") from error
280
+ return "image", 1
281
+
282
+
283
+ def load_document_page(path: str, document_type: str, page_index: int) -> Image.Image:
284
+ if document_type == "pdf":
285
+ with fitz.open(path) as document:
286
+ page = document.load_page(page_index)
287
+ pixmap = page.get_pixmap(
288
+ matrix=fitz.Matrix(PDF_RENDER_SCALE, PDF_RENDER_SCALE),
289
+ colorspace=fitz.csRGB,
290
+ alpha=False,
291
+ )
292
+ return Image.frombytes("RGB", (pixmap.width, pixmap.height), pixmap.samples)
293
+
294
+ with Image.open(path) as source:
295
+ return ImageOps.exif_transpose(source).convert("RGB")
296
+
297
+
298
+ def page_preview_data_uri(page_image: Image.Image) -> str:
299
+ preview = page_image.copy().convert("RGB")
300
+ preview.thumbnail((1400, 1800), Image.Resampling.LANCZOS)
301
+ buffer = io.BytesIO()
302
+ preview.save(buffer, format="JPEG", quality=82, optimize=True)
303
+ payload = base64.b64encode(buffer.getvalue()).decode("ascii")
304
+ return f"data:image/jpeg;base64,{payload}"
305
+
306
+
307
+ def _model_inputs(page_image: Image.Image) -> Any:
308
+ if processor is None or model is None:
309
+ raise RuntimeError("OvisOCR2 is not loaded.")
310
+
311
+ messages = [
312
+ {
313
+ "role": "user",
314
+ "content": [
315
+ {"type": "image", "image": page_image},
316
+ {"type": "text", "text": OCR_PROMPT},
317
+ ],
318
+ }
319
+ ]
320
+ return processor.apply_chat_template(
321
+ messages,
322
+ tokenize=True,
323
+ add_generation_prompt=True,
324
+ return_dict=True,
325
+ return_tensors="pt",
326
+ enable_thinking=False,
327
+ ).to(model.device)
328
+
329
+
330
+ def infer_stream(page_image: Image.Image) -> Iterator[str]:
331
+ if TEST_MODE:
332
+ for end in range(64, len(MOCK_MARKDOWN) + 64, 64):
333
+ yield MOCK_MARKDOWN[:end]
334
+ return
335
+ if processor is None or model is None:
336
+ raise RuntimeError("OvisOCR2 is not loaded.")
337
+
338
+ from transformers import TextIteratorStreamer
339
+
340
+ inputs = _model_inputs(page_image)
341
+ streamer = TextIteratorStreamer(
342
+ processor.tokenizer,
343
+ skip_prompt=True,
344
+ skip_special_tokens=True,
345
+ clean_up_tokenization_spaces=False,
346
+ timeout=30.0,
347
+ )
348
+ errors: list[BaseException] = []
349
+
350
+ def generate() -> None:
351
+ try:
352
+ with torch.inference_mode():
353
+ model.generate(
354
+ **inputs,
355
+ streamer=streamer,
356
+ max_new_tokens=MAX_NEW_TOKENS,
357
+ do_sample=False,
358
+ temperature=None,
359
+ top_p=None,
360
+ top_k=None,
361
+ **generation_token_ids(processor),
362
+ )
363
+ except BaseException as error:
364
+ errors.append(error)
365
+ streamer.on_finalized_text("", stream_end=True)
366
+
367
+ worker = threading.Thread(target=generate, name="ovisocr2-generate", daemon=True)
368
+ worker.start()
369
+ text = ""
370
+ last_yielded = ""
371
+ last_yield_time = time.monotonic()
372
+ for fragment in streamer:
373
+ text += fragment
374
+ now = time.monotonic()
375
+ if (
376
+ len(text) - len(last_yielded) >= STREAM_MIN_CHARS
377
+ or now - last_yield_time >= STREAM_MAX_INTERVAL
378
+ ):
379
+ yield text
380
+ last_yielded = text
381
+ last_yield_time = now
382
+
383
+ worker.join()
384
+ if errors:
385
+ raise RuntimeError("Model generation failed.") from errors[0]
386
+ final_text = clean_truncated_repeats(text.strip())
387
+ if final_text and final_text != last_yielded:
388
+ yield final_text
389
+
390
+
391
+ def combine_pages(pages: list[dict[str, Any]], field: str) -> str:
392
+ if len(pages) <= 1:
393
+ return pages[0].get(field, "") if pages else ""
394
+ return "\n\n---\n\n".join(
395
+ f"<!-- Page {page['page_number']} -->\n\n{page.get(field, '')}" for page in pages
396
+ )
397
+
398
+
399
+ def stream_payload(
400
+ *,
401
+ event: str,
402
+ pages: list[dict[str, Any]],
403
+ current_page: int,
404
+ total_pages: int,
405
+ document_type: str,
406
+ started: float,
407
+ page_preview: str | None = None,
408
+ ) -> dict[str, Any]:
409
+ return {
410
+ "event": event,
411
+ "markdown": combine_pages(pages, "markdown"),
412
+ "render_markdown": combine_pages(pages, "render_markdown"),
413
+ "pages": pages,
414
+ "current_page": current_page,
415
+ "total_pages": total_pages,
416
+ "document_type": document_type,
417
+ "page_preview": page_preview,
418
+ "char_count": sum(len(page.get("markdown", "")) for page in pages),
419
+ "elapsed_seconds": round(time.perf_counter() - started, 3),
420
+ "model": MODEL_ID,
421
+ "backend": "mock" if TEST_MODE else "transformers",
422
+ "mode": "base",
423
+ }
424
+
425
+
426
+ def _gpu_duration(
427
+ image_path: FileData | dict[str, Any], page_index: int = 0
428
+ ) -> int:
429
+ return int(os.getenv("OVISOCR_GPU_DURATION", "300"))
430
+
431
+
432
+ _load_model()
433
+ app = gr.Server()
434
+ app.add_middleware(
435
+ CORSMiddleware,
436
+ allow_origins=["http://127.0.0.1:4173", "http://localhost:4173"],
437
+ allow_credentials=True,
438
+ allow_methods=["*"],
439
+ allow_headers=["*"],
440
+ )
441
+
442
+
443
+ @app.api(name="run_ocr", concurrency_limit=1, time_limit=300)
444
+ @spaces.GPU(duration=_gpu_duration)
445
+ def run_ocr(image_path: FileData, page_index: int = 0) -> Iterator[dict[str, Any]]:
446
+ """Stream one document page per GPU request.
447
+
448
+ The browser calls this endpoint sequentially for PDFs. Keeping each page in
449
+ its own request prevents long documents from exhausting one ZeroGPU lease.
450
+ """
451
+ started = time.perf_counter()
452
+ path = _file_path(image_path)
453
+ document_type, total_pages = document_info(path)
454
+ page_index = int(page_index)
455
+ if page_index < 0 or page_index >= total_pages:
456
+ raise ValueError(
457
+ f"Requested PDF page {page_index + 1}, but this document has {total_pages} pages."
458
+ )
459
+
460
+ page_number = page_index + 1
461
+ page_image = load_document_page(path, document_type, page_index)
462
+ page_started = time.perf_counter()
463
+ current = {
464
+ "page_number": page_number,
465
+ "markdown": "",
466
+ "render_markdown": "",
467
+ "status": "streaming",
468
+ "elapsed_seconds": 0.0,
469
+ }
470
+ yield stream_payload(
471
+ event="page_start",
472
+ pages=[current],
473
+ current_page=page_number,
474
+ total_pages=total_pages,
475
+ document_type=document_type,
476
+ started=started,
477
+ page_preview=page_preview_data_uri(page_image),
478
+ )
479
+
480
+ markdown = ""
481
+ for partial in infer_stream(page_image):
482
+ markdown = partial
483
+ current = {
484
+ "page_number": page_number,
485
+ "markdown": markdown,
486
+ "render_markdown": stream_safe_markdown(markdown),
487
+ "status": "streaming",
488
+ "elapsed_seconds": round(time.perf_counter() - page_started, 3),
489
+ }
490
+ yield stream_payload(
491
+ event="stream",
492
+ pages=[current],
493
+ current_page=page_number,
494
+ total_pages=total_pages,
495
+ document_type=document_type,
496
+ started=started,
497
+ )
498
+
499
+ markdown = markdown.strip()
500
+ if not markdown:
501
+ raise RuntimeError(f"The model returned an empty result for page {page_number}.")
502
+ completed_page = {
503
+ "page_number": page_number,
504
+ "markdown": markdown,
505
+ "render_markdown": materialize_bbox_images(markdown, page_image),
506
+ "status": "complete",
507
+ "elapsed_seconds": round(time.perf_counter() - page_started, 3),
508
+ }
509
+ yield stream_payload(
510
+ event="page_complete",
511
+ pages=[completed_page],
512
+ current_page=page_number,
513
+ total_pages=total_pages,
514
+ document_type=document_type,
515
+ started=started,
516
+ )
517
+
518
+ yield stream_payload(
519
+ event="complete",
520
+ pages=[completed_page],
521
+ current_page=page_number,
522
+ total_pages=total_pages,
523
+ document_type=document_type,
524
+ started=started,
525
+ )
526
+
527
+
528
+ @app.get("/healthz")
529
+ def healthz() -> JSONResponse:
530
+ return JSONResponse(
531
+ {
532
+ "status": "ok",
533
+ "model": MODEL_ID,
534
+ "model_source": MODEL_SOURCE,
535
+ "backend": "mock" if TEST_MODE else "transformers",
536
+ "loaded": TEST_MODE or (processor is not None and model is not None),
537
+ "max_pdf_pages": MAX_PDF_PAGES,
538
+ "root_path": ROOT_PATH,
539
+ "public_url": PUBLIC_URL,
540
+ }
541
+ )
542
+
543
+
544
+ if DIST_DIR.is_dir():
545
+ for route, directory in (
546
+ ("/assets", DIST_DIR / "assets"),
547
+ ("/examples", DIST_DIR / "examples"),
548
+ ("/brand", DIST_DIR / "brand"),
549
+ ("/vendor", DIST_DIR / "vendor"),
550
+ ):
551
+ if directory.is_dir():
552
+ app.mount(route, StaticFiles(directory=directory), name=route.strip("/").replace("/", "-"))
553
+
554
+
555
+ @app.get("/")
556
+ def homepage() -> FileResponse:
557
+ index_path = DIST_DIR / "index.html"
558
+ if not index_path.is_file():
559
+ raise RuntimeError("Frontend build missing. Run `npm run build` before launching app.py.")
560
+ return FileResponse(index_path)
561
+
562
+
563
+ @app.get("/favicon.ico")
564
+ def favicon() -> FileResponse:
565
+ return FileResponse(DIST_DIR / "favicon.ico")
566
+
567
+
568
+ if __name__ == "__main__":
569
+ app.launch(
570
+ server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
571
+ server_port=SERVER_PORT,
572
+ root_path=ROOT_PATH,
573
+ show_error=True,
574
+ )
dist/assets/__vite-browser-external-DYxpcVy9-BIHI7g3E.js ADDED
@@ -0,0 +1 @@
 
 
1
+ const e={};export{e as default};
dist/assets/index-5Ju-rOmV.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/index-CCa7-Cez.css ADDED
@@ -0,0 +1 @@
 
 
1
+ @font-face{font-family:Manrope Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url(data:font/woff2;base64,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) format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Manrope Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url(./manrope-cyrillic-wght-normal-Dvxsihut.woff2) format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Manrope Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url(./manrope-greek-wght-normal-DL7QRZyv.woff2) format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Manrope Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url(./manrope-vietnamese-wght-normal-usUDDRr7.woff2) format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Manrope Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url(./manrope-latin-ext-wght-normal-Ch3YOpNY.woff2) format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Manrope Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url(./manrope-latin-wght-normal-DHIcAJRg.woff2) format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(data:font/woff2;base64,d09GMgABAAAAAAfsABQAAAAAEAwAAAeCAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbHhwoP0hWQVJbBmA/U1RBVIFiJyYAdC9qEQgKhGSEAAsgADCGCAE2AiQDOgQgBYlMB4EUDAcbLQ4onoexrSC/2ZyLAa8p8VHB8/x3Vue+V0hVJalMJg2nx/TCrQXxBeqLjQG7FyM1WEa/X1tEXN7cFz9EJEMmMUz3RihWSSKeQCbcIou0izz/C8v+fq3VfajEa9gDD11CImXS7qL/RJFVzC1qiB6KmKeD6TZdQ6IRGv78dL6uSVVCfgni5mzu7kcgQBgAEAQTQRCoL++STTYybkJxNfQxAAIAGu8OdEB9teW2jh4BpgDqFjAeSEByW3zFP0CBBgNMsMCGEDjgggdhiEAUAeIIED7ABTDUEnkIE9Q9ahFgKttcVhApo4ACB4qobHaccgDfEjFO6aaWUhjMLt2SyIvHKoDqoA4CSUwEIYQCEjhAO9R1G6keDeDZGjNo+AhxOjCEGTr1WeIF3kYBiLAOKvkJSMiKX0VdAyQt3SDJClCkxJCHkCzfqyVTriJZLcolS32JZHUekq2TYNkYtCtjYHMQXSxGjXDz2t/yLWXzDzxz+o3zFwDEaN23F+13pyMdQAEaSKAR9vcGq4A4MTSKCElGW+M7UcY7xqkggITb28ZJhlqc9q2twYKTt0NjixBgYvO9BIihEBLYuOFXQzfIQ7dXGUEEEgFDooBfAzqiQbpJrhiWSuKJCRFKYbHCyJKI2G5GiZbNAvgAu5pc3vwx4G+g3aDkhklABiSz0BICXrYghtYhx/cdJ+44rY2oZ0aMNRFz3VZjb6W33F3gzltqtOCV8tTHSpOeXuItfvr5lCdfzFpqtEitvqdcdGGFd28ZqqC0tPbeChGXgrIlnhSWu/eUso4uKWFLugyDzQJhflY4659+WjQ++6x72WUMv9G8mw6QJl7BVxX5fe/kpUsOvnZwee9uQ0cGXYd0o89XB2748sDSnt8d2VphdOTTgceDVvOds0v9P/s7HPq15aGun/6Vllb56f1dl0t1LejqrNkpdRZsG8TOnM5vkBG5oiVyVGnS8LHps5cfNWJs6qKPfaNSxiQNBUm3cKNWROr0GSur7Za31k1vieq7LH11VF+jXdRIasRKflc7jkobm1Z9te1IyZA0pDkhLR98+H37Zf1c/8at+dB7x+7GfVyTfJMPiYztsnl59Y5l4j+0n1RXlpHnF3Tq7HecmNF/CJodEMAikruxiyJaGLvHOdAfoA+oDvpjBm2b91cHGRZMU9n25xEU0A8fgEEAdKI3Q1iDtc034sug5YVMkE2jsE+BIkwSoQ3gxXMqz9tELp48bd0cFKOKS7xYjEuXBnZP5ia7DyiO/X/YI+PQSbt2uSdqAkWL9nQbV1XB94/+uPfdZz8dnXYFBYrcTl2SIR/ybxJNJPz/Gupb0JaZeens2ekC7EKr8t+Ls/P5VJPYJdHKyqfg2nqU6bhlidzcddQV/7MmecTzJ5VPcKXkNKSEogHjYFx6QZ7rQ+FSe8njaiNuOnXS8H2ScQ619c2mC3VTtauL0rRbXd/CkSOP37FY9Zkjz8+GibYUMOEWF+RdrFS8Ecv1SHOpPUPZGEIpjPvFyU5cXKjd6OXqorTqy9GwRd++HVufPGnVsW+aO3vggKZ18jR9sXaTC1PWTEsVUaK0FkNySbTQDqlm2PfDjZcu4aalnSLKjnOoYQ0nUlqqXcGpPu/4VgV/xU2pAqW4BW3qzhQ8/hFKhV2qE3+BKAtDqBXjfgnVdH4y0wg5tbVNRenNdTWOrenWLcupQdmsbq5b+18piTe/xRdp1xbILxNPJGInm2z6hoB21Lal0i+ePTtd7B45+3XhFJ329evskXm7qurUVREotqSluSo/L29d3qDhI4YOQqWhI4YNvBNfsMHeXKemXrxQfKeuPOGRVayA3JtkJKEgbPp+dXUDluddutRYLFoXGXWX6N3WFaGLbQtRSitVYNacTNSdy7AaG/HSaUEANcBoGXNdcZvZsOqQ1icBDv21/gzAoYPHH/WDW0qNR3QTYKEAEHig6o13NXbND06CQPlRtYjGNnSktRc09k1mAMDvAlDKfQjgy6fssInlfzmNAjKkDxoxHOBLdVRAIVt9j4qo+hA1w9T1aNBNTUOTTNUHLbqokE+UAfJXCIGw/IxCSL5GRUJeR40rL/UxTm4Q08H6MbCs70ObuNyIIXrINHQYInF06UUlevTjbQzTh5upiDMzMMogUtEnjPs/Y7jAHCJeB0GBHh04tC6FiB6ZFB1oArUSIoFoqhzCeAN6lHwm0T4C3VVPWvjpSMXReuWesMEcoqrmgtNBGd2noWeV0hNAz9rFeShNJxHGsPa3HXeKTk8b55hahySYHaYKKFFLpCfN8rsoaJn01CR04Gkc+5k7KVTCmClX8Q10HCrUEkVlSX+XO33oQR9609tJ516H497WSobWs5Up6TLaS10/dessIskgJSLiDlWvHVUywpkQ7hdPZqGyiEF0uVQerVcPamT1A3eKXdyI1vG9OoflrSXihZ1qqGE3nhmAgiIbRCQgPLEPtOM3UQwTLYaYYomNlpA44opnjV6jkD6id80OOrzf6BzmMD6eEa1zKyeYG1fzfEf16V6jw9XYOaar1/b2kP/IYX8oR2mcFvv2GtBV3JXgd437AQAA) format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(./jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2) format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(./jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2) format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(./jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2) format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(./jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2) format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url(./jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2) format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{font-family:Manrope Variable,Manrope,Arial,sans-serif;color:#080a0d;background:#fff;font-synthesis:none;--ink: #080a0d;--muted: #626872;--subtle: #8a9099;--soft: #f7f8fa;--canvas: #f3f5f8;--line: #e3e6ea;--line-strong: #cfd4da;--blue: #075ce8;--blue-deep: #0349cf;--blue-soft: #edf4ff;--green: #08a972;--red: #d42d3f}*{box-sizing:border-box}html{min-width:320px;background:#fff}body{margin:0;min-width:320px;min-height:100vh;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}button,input{font:inherit}button,a{-webkit-tap-highlight-color:transparent}button{color:inherit}.app-shell{min-height:100vh;background:#fff}.topbar{height:64px;padding:0 28px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);background:#fffffff5}.brand{display:inline-flex;align-items:center;gap:14px;color:var(--ink);text-decoration:none}.brand img{width:78px;height:auto;display:block}.brand-divider{width:1px;height:28px;background:var(--line)}.product-name{font-size:24px;line-height:1;font-weight:730;letter-spacing:-.045em}.product-name span{color:var(--blue)}.model-chip{margin-left:2px;padding:4px 9px 3px;border:1px solid #b9d3ff;border-radius:999px;color:var(--blue);background:var(--blue-soft);font-family:JetBrains Mono Variable,monospace;font-size:11px;font-weight:650;letter-spacing:-.02em}.topbar nav{display:flex;align-items:center;gap:32px}.topbar nav a{color:#333840;text-decoration:none;font-size:13px;font-weight:560}.topbar nav a:hover{color:var(--blue)}.main-content{width:min(100%,1600px);margin:0 auto;padding:22px 24px 32px}.intro-row{min-height:72px;display:flex;align-items:center;justify-content:space-between;gap:28px}.eyebrow{margin:0 0 5px;color:var(--blue);font-family:JetBrains Mono Variable,monospace;font-size:10px;font-weight:650;line-height:1.2;letter-spacing:.09em;text-transform:uppercase}h1,h2,p{text-wrap:pretty}h1{margin:0;font-size:clamp(21px,2.1vw,30px);line-height:1.16;letter-spacing:-.035em;font-weight:700}.intro-actions{display:flex;align-items:center;gap:22px;flex-shrink:0}.status-line{display:flex;align-items:center;gap:8px;color:var(--muted);font-family:JetBrains Mono Variable,monospace;font-size:10px;white-space:nowrap}.status-mode:before{content:"·";margin-right:8px;color:var(--line-strong)}.status-dot{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 3px #08a9721a}.status-line.running .status-dot{background:var(--blue);box-shadow:0 0 0 3px #075ce81a;animation:pulse 1.25s ease-in-out infinite}.status-line.error .status-dot{background:var(--red);box-shadow:0 0 0 3px #d42d3f1a}.primary-button{height:42px;min-width:132px;padding:0 22px;border:0;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;gap:10px;color:#fff;background:var(--blue);box-shadow:0 7px 18px #075ce82e;cursor:pointer;font-size:13px;font-weight:680;transition:background .14s ease,transform .14s ease,box-shadow .14s ease}.primary-button:hover:not(:disabled){background:var(--blue-deep);box-shadow:0 9px 22px #075ce83d;transform:translateY(-1px)}.primary-button:disabled{opacity:.58;cursor:not-allowed;box-shadow:none}.spinner{width:15px;height:15px;border:2px solid rgba(255,255,255,.38);border-top-color:#fff;border-radius:50%;animation:spin .8s linear infinite}.workspace{height:clamp(510px,61vh,650px);min-height:510px;display:grid;grid-template-columns:minmax(360px,.92fr) minmax(520px,1.22fr);border:1px solid var(--line);border-radius:8px;overflow:hidden;background:#fff}.document-panel,.result-panel{min-width:0;min-height:0;display:flex;flex-direction:column}.document-panel{border-right:1px solid var(--line)}.panel-toolbar,.result-toolbar{height:48px;min-height:48px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);background:#fffffffa}.panel-title{min-width:0;display:flex;align-items:center;gap:8px;color:#4a5059;font-size:11px}.panel-title>span{max-width:210px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.panel-title strong{margin-left:3px;padding:2px 6px;border-radius:4px;color:var(--green);background:#eaf9f3;font-family:JetBrains Mono Variable,monospace;font-size:8px;letter-spacing:.06em}.text-button,.copy-button,.zoom-toolbar button,.toast button{border:0;background:transparent;cursor:pointer}.text-button{display:inline-flex;align-items:center;gap:6px;color:var(--blue);font-size:11px;font-weight:620}.text-button:hover{color:var(--blue-deep)}.document-canvas{position:relative;min-height:0;flex:1;padding:40px 28px 24px;display:flex;align-items:center;justify-content:center;overflow:auto;background:var(--canvas)}.document-canvas:fullscreen{padding:72px;background:var(--canvas)}.document-canvas.dragging{background:var(--blue-soft)}.document-image{display:block;max-width:100%;height:auto;max-height:calc(100% - 8px);object-fit:contain;background:#fff;box-shadow:0 8px 22px #0f172a26,0 0 0 1px #0f172a0f;transition:width .15s ease}.document-pdf{width:min(88%,720px);height:calc(100% - 8px);min-height:360px;border:0;background:#fff;box-shadow:0 8px 22px #0f172a26,0 0 0 1px #0f172a0f}.pdf-fallback{height:100%;min-height:360px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:var(--muted);background:#fff;text-align:center}.pdf-fallback strong{color:var(--ink)}.pdf-fallback span{max-width:260px;font-size:11px}.zoom-toolbar{position:absolute;z-index:2;top:14px;left:14px;height:32px;padding:0 6px;display:flex;align-items:center;gap:2px;border:1px solid var(--line);border-radius:6px;background:#fffffff0;box-shadow:0 2px 7px #0f172a12;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.zoom-toolbar button{width:28px;height:28px;display:grid;place-items:center;border-radius:4px;color:#5e646d}.zoom-toolbar button:hover{color:var(--blue);background:var(--blue-soft)}.zoom-toolbar>span:not(.toolbar-divider){min-width:38px;text-align:center;color:#444a53;font-family:JetBrains Mono Variable,monospace;font-size:10px}.zoom-toolbar .toolbar-divider{width:1px;height:16px;margin:0 4px;background:var(--line)}.drop-overlay{position:absolute;top:12px;right:12px;bottom:12px;left:12px;z-index:4;display:grid;place-items:center;border:2px dashed var(--blue);border-radius:8px;color:var(--blue);background:#edf4fff0;font-weight:700}.upload-empty{width:min(360px,80%);min-height:210px;border:1px dashed #aab6c7;border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:var(--muted);background:#ffffff9e;cursor:pointer}.upload-empty strong{color:var(--ink)}.upload-empty span{font-size:11px}.result-toolbar{padding-left:18px}.tabs{height:100%;display:flex;align-items:stretch;gap:24px}.tabs button{position:relative;padding:0 4px;border:0;color:#666c75;background:transparent;cursor:pointer;font-size:11px;font-weight:600}.tabs button:after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:transparent}.tabs button.active{color:var(--blue)}.tabs button.active:after{background:var(--blue)}.copy-button{height:30px;padding:0 11px;border:1px solid #a9c9ff;border-radius:5px;display:inline-flex;align-items:center;gap:7px;color:var(--blue);font-size:10px;font-weight:650}.copy-button:hover:not(:disabled){background:var(--blue-soft)}.copy-button:disabled{opacity:.42;cursor:not-allowed}.page-strip{min-height:40px;padding:5px 12px;display:flex;align-items:center;gap:7px;border-bottom:1px solid var(--line);background:#fbfcfe}.page-pills{min-width:0;flex:1;display:flex;align-items:center;gap:6px;overflow-x:auto;scrollbar-width:thin}.page-pills button,.page-arrow{height:28px;border:1px solid var(--line);border-radius:5px;display:inline-flex;align-items:center;justify-content:center;gap:4px;color:#606771;background:#fff;cursor:pointer;font-family:JetBrains Mono Variable,monospace;font-size:9px;white-space:nowrap}.page-pills button{padding:0 9px}.page-pills button.active{border-color:#9fc2fb;color:var(--blue);background:var(--blue-soft)}.page-pills button:disabled{color:#adb3bc;background:#f6f7f9;cursor:default}.page-pills button svg{color:var(--green)}.page-arrow{width:28px;flex:0 0 auto}.page-arrow:hover:not(:disabled){color:var(--blue);border-color:#b8cff5}.page-arrow:disabled{opacity:.36;cursor:default}.result-content{min-height:0;flex:1;overflow:auto;background:#fff}.result-state{min-height:100%;padding:48px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:var(--muted)}.result-state.empty svg{margin-bottom:12px;color:#b4bbc4}.result-state strong{color:#31363d;font-size:14px}.result-state p{max-width:400px;margin:7px 0 0;font-size:11px;line-height:1.55}.processing-mark{width:46px;height:46px;margin-bottom:15px;display:grid;place-items:center;border:1px solid #cfe0ff;border-radius:50%;background:var(--blue-soft)}.processing-mark span{width:16px;height:16px;border:2px solid #bdd4ff;border-top-color:var(--blue);border-radius:50%;animation:spin .8s linear infinite}.stream-banner{position:sticky;z-index:3;top:0;min-height:34px;padding:7px 16px;display:flex;align-items:center;gap:7px;border-bottom:1px solid #d9e7ff;color:#50627b;background:#f1f6fff5;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);font-size:10px}.stream-banner>span:first-child{width:7px;height:7px;border-radius:50%;background:var(--blue);animation:pulse 1s ease-in-out infinite}.stream-banner strong{color:var(--blue);font-size:10px}.stream-banner>span:last-child{margin-left:auto;font-family:JetBrains Mono Variable,monospace;font-size:8px}.rendered-markdown{padding:24px 30px 46px;color:#20242a;font-size:14px;line-height:1.68}.rendered-markdown>:first-child{margin-top:0}.rendered-markdown h1,.rendered-markdown h2,.rendered-markdown h3,.rendered-markdown h4{margin:1.5em 0 .56em;color:#111318;line-height:1.25;letter-spacing:-.02em}.rendered-markdown h1{font-size:22px}.rendered-markdown h2{font-size:18px}.rendered-markdown h3{font-size:15px}.rendered-markdown h4{font-size:14px}.rendered-markdown p,.rendered-markdown ul,.rendered-markdown ol{margin:.7em 0}.rendered-markdown a{color:var(--blue)}.rendered-markdown blockquote{margin:1em 0;padding:1px 0 1px 16px;border-left:3px solid #bad3ff;color:#535a64}.rendered-markdown table{width:100%;margin:1.2em 0;border-collapse:collapse;display:table;overflow-x:auto;font-size:12px}.rendered-markdown th,.rendered-markdown td{padding:8px 10px;border:1px solid #dfe3e8;text-align:left;vertical-align:top}.rendered-markdown th{background:#f5f7fa;font-weight:700}.rendered-markdown pre{max-width:100%;padding:14px;overflow-x:auto;border:1px solid #dce3ec;border-radius:6px;white-space:pre;background:#f7f9fc}.rendered-markdown code{font-family:JetBrains Mono Variable,monospace;font-size:.9em}.rendered-markdown .unresolved-image-reference{padding:2px 5px;border:1px solid #dce3ec;border-radius:4px;color:#4b5f7a;background:#f5f7fa;overflow-wrap:anywhere}.rendered-markdown img{display:block;max-width:min(100%,620px);height:auto;margin:18px auto;border-radius:3px}.rendered-markdown mjx-container{max-width:100%;overflow-x:auto;overflow-y:hidden}.rendered-markdown .visual-placeholder{min-height:82px;margin:16px 0;border:1px dashed #b7ccec;border-radius:6px;display:grid;place-items:center;color:#6680a5;background:#f5f8fd;font-family:JetBrains Mono Variable,monospace;font-size:9px}.source-code{min-height:100%;margin:0;padding:24px 28px 48px;overflow:visible;color:#183254;background:#fbfcfe;font-family:JetBrains Mono Variable,monospace;font-size:11px;line-height:1.66;white-space:pre-wrap;overflow-wrap:anywhere;-moz-tab-size:2;tab-size:2}.examples-section{padding-top:18px}.examples-heading{margin-bottom:10px;display:flex;align-items:flex-end;justify-content:space-between}.examples-heading h2{margin:0;font-size:14px;line-height:1.2;letter-spacing:-.02em}.examples-heading>p{margin:0;color:var(--subtle);font-family:JetBrains Mono Variable,monospace;font-size:9px}.example-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px}.example-card{min-width:0;padding:0;border:1px solid var(--line);border-radius:7px;overflow:hidden;color:#4a5059;background:#fff;cursor:pointer;text-align:left;transition:border-color .14s ease,transform .14s ease,box-shadow .14s ease}.example-card:hover{border-color:#aac8f9;transform:translateY(-1px);box-shadow:0 7px 17px #0f172a0f}.example-card.active{border-color:var(--blue);box-shadow:0 0 0 1px var(--blue)}.example-image-wrap{position:relative;height:112px;display:block;overflow:hidden;background:var(--soft)}.example-card img{width:100%;height:100%;display:block;object-fit:cover;object-position:top center;transition:transform .18s ease}.example-card:hover img{transform:scale(1.018)}.selected-badge{position:absolute;left:8px;bottom:8px;padding:4px 7px;display:inline-flex;align-items:center;gap:4px;border-radius:4px;color:#fff;background:var(--blue);font-size:9px;font-weight:700}.example-label{min-height:36px;padding:9px 11px;display:flex;align-items:center;font-size:10px;font-weight:610}.file-input{position:fixed;width:1px;height:1px;opacity:0;pointer-events:none}.toast{position:fixed;z-index:30;right:22px;bottom:22px;min-width:220px;min-height:42px;padding:0 12px;display:flex;align-items:center;gap:8px;border:1px solid #243142;border-radius:6px;color:#fff;background:#111926;box-shadow:0 14px 35px #0f172a33;opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity .14s ease,transform .14s ease;font-size:11px}.toast.show{opacity:1;transform:translateY(0);pointer-events:auto}.toast button{width:24px;height:24px;margin-left:auto;display:grid;place-items:center;color:#cbd5e1}button:focus-visible,a:focus-visible{outline:2px solid var(--blue);outline-offset:2px}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.42}}@media(max-width:1050px){.main-content{padding-inline:18px}.workspace{grid-template-columns:minmax(330px,.85fr) minmax(470px,1.15fr)}.status-mode{display:none}.example-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.example-image-wrap{height:112px}}@media(max-width:820px){.topbar{height:58px;padding:0 16px}.brand{gap:9px}.brand img{width:62px}.brand-divider{height:22px}.product-name{font-size:19px}.model-chip{display:none}.topbar nav{gap:16px}.topbar nav a{font-size:11px}.topbar nav a:last-child{display:none}.main-content{padding:15px 14px 24px}.intro-row{align-items:flex-start;flex-direction:column;gap:14px;padding-bottom:14px}.intro-actions{width:100%;justify-content:space-between;gap:12px}.status-line{min-width:0;white-space:normal}.primary-button{min-width:116px}.workspace{height:auto;min-height:0;grid-template-columns:1fr}.document-panel{border-right:0;border-bottom:1px solid var(--line)}.document-canvas,.result-content{min-height:430px;max-height:62vh}.example-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.examples-heading>p{display:none}}@media(max-width:480px){.topbar nav{display:none}.intro-actions{align-items:stretch;flex-direction:column}.primary-button{width:100%}.workspace{border-radius:6px}.panel-title>span{max-width:150px}.document-canvas{min-height:370px;padding-inline:16px}.result-content{min-height:400px}.rendered-markdown{padding:22px 18px 40px}.example-grid{gap:10px}.example-image-wrap{height:98px}.toast{left:14px;right:14px;bottom:14px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}
dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 ADDED
Binary file (12.1 kB). View file
 
dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 ADDED
Binary file (9 kB). View file
 
dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 ADDED
Binary file (15.2 kB). View file
 
dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 ADDED
Binary file (40.4 kB). View file
 
dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 ADDED
Binary file (7.5 kB). View file
 
dist/assets/manrope-cyrillic-wght-normal-Dvxsihut.woff2 ADDED
Binary file (14.5 kB). View file
 
dist/assets/manrope-greek-wght-normal-DL7QRZyv.woff2 ADDED
Binary file (9.44 kB). View file
 
dist/assets/manrope-latin-ext-wght-normal-Ch3YOpNY.woff2 ADDED
Binary file (15.1 kB). View file
 
dist/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 ADDED
Binary file (24.8 kB). View file
 
dist/assets/manrope-vietnamese-wght-normal-usUDDRr7.woff2 ADDED
Binary file (8.52 kB). View file
 
dist/brand/ovis-logo.png ADDED
dist/examples/handwriting-040745.jpg ADDED

Git LFS Details

  • SHA256: 6f02262a295680b5ea217409ebe4b61127323d69491132621d3fc4f7e824abd2
  • Pointer size: 131 Bytes
  • Size of remote file: 565 kB
dist/examples/handwriting-042609.jpg ADDED

Git LFS Details

  • SHA256: 4e904a1d6154f801872fbef52a8e5a2e32f3321fee62c89e3057e609a54f06d3
  • Pointer size: 131 Bytes
  • Size of remote file: 551 kB
dist/examples/page-9477c155-19b3-4f2d-ac18-ee3f8997d2f8.png ADDED

Git LFS Details

  • SHA256: fc92cd29cd1bb164b3b2cc434a5e6f9325a20b3c719c45d9051fa9f854f8aa04
  • Pointer size: 131 Bytes
  • Size of remote file: 293 kB
dist/examples/table-1305dae7.webp ADDED
dist/examples/table-3ba1139c.webp ADDED
dist/favicon.ico ADDED
dist/index.html ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#ffffff" />
7
+ <meta name="description" content="Streaming document parsing for images and PDFs with OvisOCR2." />
8
+ <link rel="icon" href="./favicon.ico" />
9
+ <title>OvisOCR2 — Document to Markdown</title>
10
+ <script>
11
+ window.MathJax = {
12
+ options: {
13
+ skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'code', 'annotation', 'annotation-xml']
14
+ },
15
+ tex: {
16
+ inlineMath: [['\\(', '\\)'], ['$', '$']],
17
+ displayMath: [['\\[', '\\]']],
18
+ processEscapes: true
19
+ },
20
+ chtml: { matchFontHeight: false },
21
+ startup: { typeset: false }
22
+ };
23
+ </script>
24
+ <script defer src="./vendor/mathjax/tex-chtml-full.js"></script>
25
+ <script type="module" crossorigin src="./assets/index-5Ju-rOmV.js"></script>
26
+ <link rel="stylesheet" crossorigin href="./assets/index-CCa7-Cez.css">
27
+ </head>
28
+ <body>
29
+ <div id="root"></div>
30
+ </body>
31
+ </html>
dist/vendor/mathjax/output/chtml/fonts/tex.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff ADDED
Binary file (40.8 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff ADDED
Binary file (9.91 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff ADDED
Binary file (9.6 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff ADDED
Binary file (22.3 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff ADDED
Binary file (21.5 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff ADDED
Binary file (34.5 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff ADDED
Binary file (20.8 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff ADDED
Binary file (34.2 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff ADDED
Binary file (19.8 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff ADDED
Binary file (19.4 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff ADDED
Binary file (19.3 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff ADDED
Binary file (15.9 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff ADDED
Binary file (14.6 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff ADDED
Binary file (12.7 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff ADDED
Binary file (11.9 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff ADDED
Binary file (5.79 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff ADDED
Binary file (5.46 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff ADDED
Binary file (3.24 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff ADDED
Binary file (5.15 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff ADDED
Binary file (17.6 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff ADDED
Binary file (1.12 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff ADDED
Binary file (1.14 kB). View file
 
dist/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Zero.woff ADDED
Binary file (1.37 kB). View file
 
dist/vendor/mathjax/tex-chtml-full.js ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ transformers>=5.13.0,<6
2
+ accelerate>=1.12.0
3
+ torchvision>=0.25.0,<0.27
4
+ Pillow>=10.0.0
5
+ PyMuPDF>=1.24.0,<2