Skip to content

Commit fadc871

Browse files
committed
docs(demo): embed Bilibili walkthrough
1 parent 953a8bf commit fadc871

4 files changed

Lines changed: 110 additions & 2 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
![Folio-OCR 界面截图](demo.png)
1010

11-
在线体验:打开 [互动 Demo](https://vorojar.github.io/Folio-OCR/demo.html),用内置样例查看“扫描页 → OCR → Markdown/Word/EPUB”的完整流程。
11+
视频演示:先看 [B 站 1 分钟真实运行视频](https://www.bilibili.com/video/BV17LFpzrEpW),快速理解“上传扫描件 → 本地 OCR → 可编辑/可导出”的完整流程。
12+
13+
在线体验:打开 [互动 Demo](https://vorojar.github.io/Folio-OCR/demo.html),用内置样例查看“扫描页 → OCR → Markdown/Word/EPUB”的交互流程。
1214

1315
## 30 秒看懂
1416

@@ -119,6 +121,8 @@ LAYOUT_DEVICE=cuda # 强制使用 CUDA,不可用时启动失败
119121

120122
Folio-OCR 默认会给 Ollama 请求传入 `OLLAMA_NUM_CTX=16384`,避免 GLM-OCR 处理图片时因上下文太小触发底层 RoPE / KV cache 错误。内存非常紧张时可以调小,复杂图片或高分辨率扫描件建议保持默认或调大。
121123

124+
如果遇到模型输出过长或重复内容,可通过 `OLLAMA_NUM_PREDICT` 限制单次 OCR 输出长度。默认值为 `4096`,短文档 demo 或低显存机器可以适当调小。
125+
122126
---
123127

124128
## 从源码运行
@@ -156,6 +160,7 @@ start.bat
156160
| `OLLAMA_BASE` | `http://localhost:11434` | Ollama 服务地址 |
157161
| `OLLAMA_MODEL` | `glm-ocr` | OCR 模型名 |
158162
| `OLLAMA_NUM_CTX` | `16384` | 传给 Ollama `/api/chat` 的上下文窗口 |
163+
| `OLLAMA_NUM_PREDICT` | `4096` | 传给 Ollama `/api/chat` 的最大输出 token 数 |
159164
| `OCR_REQUEST_TIMEOUT_MS` | `300000` | 前端 OCR 请求超时,长 PDF 或慢 GPU 可调大 |
160165
| `LAYOUT_DEVICE` | `cpu` | 版面分析设备:`cpu``cuda``auto` |
161166
| `DB_PATH` | `./folio_ocr.db` | SQLite 数据库路径 |

docs/index.html

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,35 @@
138138
}
139139
.hero-screenshot img { width: 100%; display: block; }
140140

141+
/* ── Video ── */
142+
.video { background: #fff; }
143+
.video-frame {
144+
max-width: 980px;
145+
margin: 0 auto;
146+
border-radius: 12px;
147+
overflow: hidden;
148+
background: #111;
149+
box-shadow: 0 20px 70px rgba(0,0,0,.16), 0 0 0 1px rgba(0,0,0,.06);
150+
}
151+
.video-frame iframe {
152+
display: block;
153+
width: 100%;
154+
aspect-ratio: 16 / 9;
155+
border: 0;
156+
}
157+
.video-link {
158+
margin-top: 18px;
159+
text-align: center;
160+
font-size: 14px;
161+
color: rgba(45,45,45,.56);
162+
}
163+
.video-link a {
164+
color: var(--accent-dark);
165+
font-weight: 700;
166+
text-decoration: none;
167+
}
168+
.video-link a:hover { color: var(--accent); }
169+
141170
/* ── Section base ── */
142171
section { padding: 80px clamp(20px, 4vw, 48px); }
143172
.section-title {
@@ -326,6 +355,7 @@
326355
<a class="nav-brand" href="#">Folio-OCR</a>
327356
<div class="nav-links">
328357
<a href="#features">功能</a>
358+
<a href="#video">视频</a>
329359
<a href="demo.html">Demo</a>
330360
<a href="#proof">示例</a>
331361
<a href="#deploy">安装</a>
@@ -353,6 +383,10 @@ <h1>Folio-<span>OCR</span></h1>
353383
<svg viewBox="0 0 24 24"><path d="M4 4h16v4H4zM4 10h16v10H4zM6 12h5v2H6zM6 16h10v2H6z"/></svg>
354384
打开互动 Demo
355385
</a>
386+
<a class="btn-secondary" href="#video">
387+
<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
388+
看 1 分钟视频
389+
</a>
356390
</div>
357391
<div class="quick-command"><code>ollama pull glm-ocr
358392
uvx --from git+https://github.com/vorojar/Folio-OCR folio-ocr</code></div>
@@ -363,6 +397,23 @@ <h1>Folio-<span>OCR</span></h1>
363397
</div>
364398
</section>
365399

400+
<!-- Video -->
401+
<section class="video" id="video">
402+
<h2 class="section-title">1 分钟看真实批量 OCR</h2>
403+
<p class="section-sub">B 站演示视频:GLM-OCR + Ollama + PP-DocLayoutV3,本地处理扫描文档</p>
404+
<div class="video-frame">
405+
<iframe
406+
src="https://player.bilibili.com/player.html?bvid=BV17LFpzrEpW&page=1&high_quality=1&danmaku=0"
407+
title="Folio-OCR Bilibili demo video"
408+
allowfullscreen>
409+
</iframe>
410+
</div>
411+
<p class="video-link">
412+
播放器加载失败时,直接打开
413+
<a href="https://www.bilibili.com/video/BV17LFpzrEpW" target="_blank" rel="noopener">B 站演示视频</a>
414+
</p>
415+
</section>
416+
366417
<!-- Proof -->
367418
<section class="proof" id="proof">
368419
<h2 class="section-title">从扫描页到可编辑文档</h2>

folio_ocr/server.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,20 @@
7373
OLLAMA_BASE = os.environ.get("OLLAMA_BASE", "http://localhost:11434")
7474
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "glm-ocr")
7575
OLLAMA_NUM_CTX = int(os.environ.get("OLLAMA_NUM_CTX", "16384"))
76+
OLLAMA_NUM_PREDICT = int(os.environ.get("OLLAMA_NUM_PREDICT", "4096"))
7677
if OLLAMA_NUM_CTX <= 0:
7778
raise RuntimeError("OLLAMA_NUM_CTX must be a positive integer")
79+
if OLLAMA_NUM_PREDICT <= 0:
80+
raise RuntimeError("OLLAMA_NUM_PREDICT must be a positive integer")
7881
OCR_REQUEST_TIMEOUT_MS = int(os.environ.get("OCR_REQUEST_TIMEOUT_MS", "300000"))
7982
if OCR_REQUEST_TIMEOUT_MS <= 0:
8083
raise RuntimeError("OCR_REQUEST_TIMEOUT_MS must be a positive integer")
81-
OCR_PROMPT = "识别图片中的全部内容,输出Markdown格式。表格请保留为Markdown或HTML表格,不要转为纯文本。跳过页眉页脚和页码。"
84+
OCR_PROMPT = (
85+
"请只转写图片中清晰可见的文字,并输出 Markdown。"
86+
"保留原有换行、列表和表格结构;表格请使用 Markdown 或 HTML 表格。"
87+
"不要解释、总结、补全、翻译或编造图片中不存在的内容。"
88+
"跳过页眉、页脚和页码;如果没有可识别文字,只输出空字符串。"
89+
)
8290

8391
# LaTeX → Unicode mapping (loaded once at import time)
8492
_LATEX_MAP_FILE = APP_DIR / "latex_unicode.json"
@@ -659,6 +667,8 @@ def _ollama_chat_payload(content: str, image_b64: str | None = None) -> dict:
659667
"stream": False,
660668
"options": {
661669
"num_ctx": OLLAMA_NUM_CTX,
670+
"num_predict": OLLAMA_NUM_PREDICT,
671+
"temperature": 0,
662672
},
663673
}
664674

@@ -667,13 +677,34 @@ def _postprocess(text: str) -> str:
667677
"""Strip markdown fences and convert LaTeX to Unicode."""
668678
text = re.sub(r'^```\w*\n?', '', text.strip())
669679
text = re.sub(r'\n?```$', '', text.strip())
680+
text = re.sub(r'(?m)^\s*```\w*\s*$', '', text)
681+
text = _remove_empty_html_tables(text)
682+
text = _unwrap_html_paragraphs(text)
670683
# Remove standalone $$...$$ lines whose content duplicates nearby $...$ inline math
671684
text = _preserve_html_tables(text, _remove_duplicate_display_math)
672685
text = _latex_to_unicode(text)
673686
text = _preserve_html_tables(text, _dedup_lines)
674687
return text.strip()
675688

676689

690+
def _remove_empty_html_tables(text: str) -> str:
691+
"""Remove hallucinated empty table shells from OCR output."""
692+
empty_table = re.compile(
693+
r'<table\b[^>]*>\s*'
694+
r'(?:<tbody>\s*)?'
695+
r'<tr>\s*(?:<t[dh]\b[^>]*>\s*</t[dh]>\s*)+</tr>'
696+
r'\s*(?:</tbody>\s*)?'
697+
r'</table>',
698+
flags=re.IGNORECASE,
699+
)
700+
return empty_table.sub('', text)
701+
702+
703+
def _unwrap_html_paragraphs(text: str) -> str:
704+
"""Convert plain HTML paragraph wrappers to Markdown-friendly text."""
705+
return re.sub(r'</?p\b[^>]*>', '', text, flags=re.IGNORECASE)
706+
707+
677708
def _preserve_html_tables(text: str, transform) -> str:
678709
"""Apply a text transform outside HTML table blocks only."""
679710
parts = re.split(r'(<table[\s\S]*?</table>)', text, flags=re.IGNORECASE)

tests/test_exports_and_postprocess.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def load_server_helpers():
2424
"_ollama_chat_payload",
2525
"_preserve_html_tables",
2626
"_remove_duplicate_display_math",
27+
"_remove_empty_html_tables",
28+
"_unwrap_html_paragraphs",
2729
"_dedup_lines",
2830
"_latex_to_unicode",
2931
"_convert_math_interior",
@@ -55,6 +57,7 @@ def load_server_helpers():
5557
"_CIRCLED": {str(i): chr(0x2460 + i - 1) for i in range(1, 21)},
5658
"OLLAMA_MODEL": "glm-ocr",
5759
"OLLAMA_NUM_CTX": 16384,
60+
"OLLAMA_NUM_PREDICT": 2048,
5861
"datetime": datetime,
5962
"timezone": timezone,
6063
"html_escape": html_escape,
@@ -100,6 +103,22 @@ def test_postprocess_deduplicates_text_outside_html_tables_only(self):
100103
self.assertEqual(result.count("<tr>"), 2)
101104
self.assertEqual(result.count("<td>同名</td>"), 2)
102105

106+
def test_postprocess_removes_inline_fences_and_empty_tables(self):
107+
text = """Title
108+
```markdown
109+
Body
110+
<table border="1"><tr><td></td></tr></table>
111+
<p>Done</p>"""
112+
113+
result = self.helpers["_postprocess"](text)
114+
115+
self.assertNotIn("```", result)
116+
self.assertNotIn("<table", result)
117+
self.assertNotIn("<p>", result)
118+
self.assertIn("Title", result)
119+
self.assertIn("Body", result)
120+
self.assertIn("Done", result)
121+
103122

104123
class OllamaPayloadTests(unittest.TestCase):
105124
def setUp(self):
@@ -111,6 +130,8 @@ def test_chat_payload_sets_num_ctx_for_image_ocr(self):
111130
self.assertEqual(payload["model"], "glm-ocr")
112131
self.assertFalse(payload["stream"])
113132
self.assertEqual(payload["options"]["num_ctx"], 16384)
133+
self.assertEqual(payload["options"]["num_predict"], 2048)
134+
self.assertEqual(payload["options"]["temperature"], 0)
114135
self.assertEqual(payload["messages"][0]["content"], "OCR")
115136
self.assertEqual(payload["messages"][0]["images"], ["abc123"])
116137

0 commit comments

Comments
 (0)