Spaces:
Running on Zero
Running on Zero
Fix LaTeX display and add quality-of-life improvements
Browse files- Fix display math mis-parsing: convert \[...\] to block $$\ncontent\n$$
form so arithmatex correctly identifies display math instead of treating
it as inline math surrounded by literal $ signs
- Fix MathJax typeset in Gradio shadow DOM: walk shadow roots to find
.math-preview instead of relying on document.querySelector which fails
inside Gradio's shadow DOM isolation; add 2s fallback for GPU cold-starts
- Add MathJax mathtools extension so \coloneqq / \eqqcolon render natively
without requiring text substitutions
- Text tab: convert \[...\] → $$...$$ and \(...\) → $...$ for readability
- Add Download Markdown button that appears after each extraction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -110,14 +110,14 @@ def clean_output(text, include_images=False):
|
|
| 110 |
else:
|
| 111 |
text = re.sub(rf'(?m)^[^\n]*{re.escape(match[0])}[^\n]*\n?', '', text)
|
| 112 |
|
| 113 |
-
text = text.replace('\\coloneqq', ':=').replace('\\eqqcolon', '=:')
|
| 114 |
-
|
| 115 |
return text.strip()
|
| 116 |
|
| 117 |
MATHJAX_HEAD = """
|
| 118 |
<script>
|
| 119 |
window.MathJax = {
|
|
|
|
| 120 |
tex: {
|
|
|
|
| 121 |
inlineMath: [['\\\\(', '\\\\)']],
|
| 122 |
displayMath: [['\\\\[', '\\\\]']],
|
| 123 |
processEscapes: true,
|
|
@@ -169,8 +169,16 @@ def to_math_html(text):
|
|
| 169 |
# Pre-convert \[...\] and \(...\) to $$...$$ and $...$
|
| 170 |
# Markdown strips backslashes before arithmatex can protect them,
|
| 171 |
# so convert to $-delimiters first (arithmatex recognises those).
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
html = md_lib.markdown(text, extensions=[
|
| 175 |
'pymdownx.arithmatex',
|
| 176 |
'tables',
|
|
@@ -354,6 +362,7 @@ with gr.Blocks(title="DeepSeek-OCR-2") as demo:
|
|
| 354 |
gallery = gr.Gallery(show_label=False, columns=3, height=400)
|
| 355 |
with gr.Tab("Raw Text", id="tab_raw"):
|
| 356 |
raw_out = gr.Textbox(lines=20, buttons=["copy"], show_label=False)
|
|
|
|
| 357 |
|
| 358 |
with gr.Accordion("Image Examples", open=True):
|
| 359 |
gr.Examples(
|
|
@@ -406,21 +415,48 @@ with gr.Blocks(title="DeepSeek-OCR-2") as demo:
|
|
| 406 |
elif image is not None:
|
| 407 |
cleaned, markdown, raw, img_out, crops = process_image(image, task, custom_prompt)
|
| 408 |
else:
|
| 409 |
-
return "Error: Upload a file or image", "", "", None, []
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
|
| 412 |
submit_event = btn.click(run, [input_img, file_in, task, prompt, page_selector],
|
| 413 |
-
[text_out, md_out, raw_out, img_out, gallery])
|
| 414 |
submit_event.then(select_boxes, [task], [tabs])
|
| 415 |
submit_event.then(fn=None, js="""() => {
|
| 416 |
const tryTypeset = () => {
|
| 417 |
if (!window.MathJax || !MathJax.typesetPromise) { setTimeout(tryTypeset, 100); return; }
|
| 418 |
-
|
| 419 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
MathJax.typesetClear([el]);
|
| 421 |
MathJax.typesetPromise([el]);
|
| 422 |
};
|
| 423 |
-
setTimeout(tryTypeset,
|
|
|
|
| 424 |
}""")
|
| 425 |
|
| 426 |
if __name__ == "__main__":
|
|
|
|
| 110 |
else:
|
| 111 |
text = re.sub(rf'(?m)^[^\n]*{re.escape(match[0])}[^\n]*\n?', '', text)
|
| 112 |
|
|
|
|
|
|
|
| 113 |
return text.strip()
|
| 114 |
|
| 115 |
MATHJAX_HEAD = """
|
| 116 |
<script>
|
| 117 |
window.MathJax = {
|
| 118 |
+
loader: {load: ['[tex]/mathtools']},
|
| 119 |
tex: {
|
| 120 |
+
packages: {'[+]': ['mathtools']},
|
| 121 |
inlineMath: [['\\\\(', '\\\\)']],
|
| 122 |
displayMath: [['\\\\[', '\\\\]']],
|
| 123 |
processEscapes: true,
|
|
|
|
| 169 |
# Pre-convert \[...\] and \(...\) to $$...$$ and $...$
|
| 170 |
# Markdown strips backslashes before arithmatex can protect them,
|
| 171 |
# so convert to $-delimiters first (arithmatex recognises those).
|
| 172 |
+
#
|
| 173 |
+
# IMPORTANT: the model outputs " \[ content \] " with surrounding spaces.
|
| 174 |
+
# Naively replacing to "$$ content $$" (space after $$) causes arithmatex
|
| 175 |
+
# to mis-parse as inline math surrounded by literal $ signs, which is why
|
| 176 |
+
# equations appeared as "$ ... $" instead of rendered display math.
|
| 177 |
+
# Fix: use the multi-line block form ($$\ncontent\n$$) and strip whitespace.
|
| 178 |
+
text = re.sub(r'\\\[(.+?)\\\]',
|
| 179 |
+
lambda m: f'\n\n$$\n{m.group(1).strip()}\n$$\n\n',
|
| 180 |
+
text, flags=re.DOTALL)
|
| 181 |
+
text = re.sub(r'\\\((.+?)\\\)', lambda m: f'${m.group(1).strip()}$', text)
|
| 182 |
html = md_lib.markdown(text, extensions=[
|
| 183 |
'pymdownx.arithmatex',
|
| 184 |
'tables',
|
|
|
|
| 362 |
gallery = gr.Gallery(show_label=False, columns=3, height=400)
|
| 363 |
with gr.Tab("Raw Text", id="tab_raw"):
|
| 364 |
raw_out = gr.Textbox(lines=20, buttons=["copy"], show_label=False)
|
| 365 |
+
download_btn = gr.DownloadButton("Download Markdown", visible=False, variant="secondary")
|
| 366 |
|
| 367 |
with gr.Accordion("Image Examples", open=True):
|
| 368 |
gr.Examples(
|
|
|
|
| 415 |
elif image is not None:
|
| 416 |
cleaned, markdown, raw, img_out, crops = process_image(image, task, custom_prompt)
|
| 417 |
else:
|
| 418 |
+
return "Error: Upload a file or image", "", "", None, [], gr.DownloadButton(visible=False)
|
| 419 |
+
|
| 420 |
+
# Text tab: convert \[...\] → $$...$$ and \(...\) → $...$ for readability
|
| 421 |
+
text_display = re.sub(r'\\\[(.+?)\\\]',
|
| 422 |
+
lambda m: f'\n$$\n{m.group(1).strip()}\n$$\n',
|
| 423 |
+
cleaned, flags=re.DOTALL)
|
| 424 |
+
text_display = re.sub(r'\\\((.+?)\\\)', lambda m: f'${m.group(1).strip()}$', text_display)
|
| 425 |
+
|
| 426 |
+
# Download file: write cleaned markdown to a temp .md file
|
| 427 |
+
dl_tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.md', mode='w', encoding='utf-8')
|
| 428 |
+
dl_tmp.write(cleaned)
|
| 429 |
+
dl_tmp.close()
|
| 430 |
+
|
| 431 |
+
return (text_display, to_math_html(markdown), raw, img_out, crops,
|
| 432 |
+
gr.DownloadButton(value=dl_tmp.name, visible=True))
|
| 433 |
|
| 434 |
submit_event = btn.click(run, [input_img, file_in, task, prompt, page_selector],
|
| 435 |
+
[text_out, md_out, raw_out, img_out, gallery, download_btn])
|
| 436 |
submit_event.then(select_boxes, [task], [tabs])
|
| 437 |
submit_event.then(fn=None, js="""() => {
|
| 438 |
const tryTypeset = () => {
|
| 439 |
if (!window.MathJax || !MathJax.typesetPromise) { setTimeout(tryTypeset, 100); return; }
|
| 440 |
+
// Gradio renders gr.HTML inside a shadow DOM, so document.querySelector
|
| 441 |
+
// won't find .math-preview. Walk all shadow roots to locate it.
|
| 442 |
+
const findInShadows = (root) => {
|
| 443 |
+
const el = root.querySelector('.math-preview');
|
| 444 |
+
if (el) return el;
|
| 445 |
+
for (const node of root.querySelectorAll('*')) {
|
| 446 |
+
if (node.shadowRoot) {
|
| 447 |
+
const found = findInShadows(node.shadowRoot);
|
| 448 |
+
if (found) return found;
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
return null;
|
| 452 |
+
};
|
| 453 |
+
const el = findInShadows(document);
|
| 454 |
+
if (!el) { MathJax.typesetPromise(); return; }
|
| 455 |
MathJax.typesetClear([el]);
|
| 456 |
MathJax.typesetPromise([el]);
|
| 457 |
};
|
| 458 |
+
setTimeout(tryTypeset, 200); // primary: catches most cases
|
| 459 |
+
setTimeout(tryTypeset, 2000); // fallback: GPU cold-start can delay DOM update
|
| 460 |
}""")
|
| 461 |
|
| 462 |
if __name__ == "__main__":
|