from __future__ import annotations from typing import Any import gradio as gr from ocr_studio.config import ( EXAMPLES_DIR, LOCALE_EN, LOCALE_FA, MAX_PDF_PAGES, MAX_UPLOAD_MB, OCR_API_KEY, ) from ocr_studio.i18n import language_choices, locale_choices, mode_choices, normalize_locale, t from ocr_studio.pipeline import history_headers CSS = """ @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Vazirmatn:wght@400;600;700&display=swap"); :root { --ocr-bg: #f4f1ea; --ocr-ink: #1c1915; --ocr-muted: #5c564c; --ocr-line: #d8d0c3; --ocr-card: #fffcf7; --ocr-accent: #1f4f4a; } .gradio-container { font-family: "IBM Plex Sans", "Vazirmatn", sans-serif !important; max-width: 1180px !important; margin: 0 auto !important; } #ocr-shell { background: var(--ocr-card); border: 1px solid var(--ocr-line); border-radius: 22px; padding: 28px 28px 18px; } .rtl-ui { direction: rtl; font-family: "Vazirmatn", "IBM Plex Sans", sans-serif; } .ltr-ui { direction: ltr; } #hero h1 { font-size: 2rem; letter-spacing: -0.03em; margin-bottom: 0.35rem; } #hero p { color: var(--ocr-muted); font-size: 1.02rem; line-height: 1.7; } .note-grid { display: grid; grid-template-columns: 1fr; gap: 10px; margin: 16px 0 8px; } .note { border: 1px solid var(--ocr-line); background: #faf7f1; border-radius: 14px; padding: 12px 14px; color: var(--ocr-muted); font-size: 0.92rem; line-height: 1.6; } #run-btn, #rebuild-btn { min-height: 48px; } #result-text textarea { font-family: "Vazirmatn", "IBM Plex Sans", sans-serif; font-size: 1.02rem; line-height: 1.85; } footer, .built-with { display: none !important; } @media (max-width: 720px) { #ocr-shell { padding: 18px 14px 10px; } #hero h1 { font-size: 1.55rem; } } """ THEME = gr.themes.Soft( font=[gr.themes.GoogleFont("IBM Plex Sans"), "Vazirmatn", "sans-serif"], primary_hue="teal", neutral_hue="stone", ) def _notes(locale: str) -> str: return ( '
' f'
{t(locale, "note.limits", max_mb=MAX_UPLOAD_MB, max_pages=MAX_PDF_PAGES)}
' f'
{t(locale, "note.privacy")}
' f'
{t(locale, "note.quality")}
' "
" ) def _example_rows() -> list[list[object]]: rows: list[list[object]] = [] mapping = [ ("english-print.png", "en", "document"), ("persian-print.png", "fa", "document"), ("mixed-print.png", "fa+en", "precise"), ] for name, language, mode in mapping: path = EXAMPLES_DIR / name if path.exists(): rows.append([str(path), language, mode]) return rows def _warning_markdown(warnings: list[str]) -> str: if not warnings: return "" return "\n".join(f"- {item}" for item in warnings) def locale_updates( locale: str, language_value: str | None = None, mode_value: str | None = None, ) -> list[Any]: locale = normalize_locale(locale) require_key = bool(OCR_API_KEY) return [ gr.update(value=t(locale, "intro")), gr.update(value=_notes(locale)), gr.update(label=t(locale, "image")), gr.update(label=t(locale, "files")), gr.update( label=t(locale, "language"), choices=language_choices(locale), value=language_value or "auto", ), gr.update( label=t(locale, "mode"), choices=mode_choices(locale), value=mode_value or "document", ), gr.update(label=t(locale, "deskew")), gr.update(label=t(locale, "accurate")), gr.update(label=t(locale, "api_key"), visible=require_key), gr.update(value=t(locale, "run")), gr.update(value=t(locale, "rebuild")), gr.update(label=t(locale, "text")), gr.update(label=t(locale, "preview")), gr.update(label=t(locale, "pdf")), gr.update(label=t(locale, "txt")), gr.update(label=t(locale, "md")), gr.update(label=t(locale, "json")), gr.update(label=t(locale, "docx")), gr.update(label=t(locale, "zip")), gr.update(label=t(locale, "history"), headers=history_headers(locale)), gr.update(label=t(locale, "ui_language"), choices=locale_choices(locale), value=locale), gr.update(value=t(locale, "footer")), gr.update(elem_classes=["rtl-ui"] if locale == LOCALE_FA else ["ltr-ui"]), locale, ] def build_demo(run_fn, rebuild_fn, health_fn) -> gr.Blocks: locale0 = LOCALE_EN require_key = bool(OCR_API_KEY) with gr.Blocks(title="Document OCR") as demo: job_state = gr.State({}) history_state = gr.State([]) locale_state = gr.State(locale0) saved_locale = gr.BrowserState(locale0) with gr.Column(elem_id="ocr-shell", elem_classes=["ltr-ui"]) as shell: intro = gr.Markdown(t(locale0, "intro"), elem_id="hero") notes = gr.HTML(_notes(locale0)) with gr.Row(): locale_dd = gr.Dropdown( label=t(locale0, "ui_language"), choices=locale_choices(locale0), value=locale0, scale=1, ) with gr.Row(): with gr.Column(scale=5): image = gr.Image( label=t(locale0, "image"), type="pil", sources=["upload", "webcam", "clipboard"], height=320, ) files = gr.File( label=t(locale0, "files"), file_count="multiple", file_types=["image", ".pdf", ".heic", ".heif"], type="filepath", ) language = gr.Dropdown( label=t(locale0, "language"), choices=language_choices(locale0), value="auto", ) mode = gr.Dropdown( label=t(locale0, "mode"), choices=mode_choices(locale0), value="document", ) deskew = gr.Checkbox(label=t(locale0, "deskew"), value=True) high_accuracy = gr.Checkbox(label=t(locale0, "accurate"), value=False) api_key = gr.Textbox( label=t(locale0, "api_key"), type="password", visible=require_key, ) run = gr.Button(t(locale0, "run"), variant="primary", elem_id="run-btn") rebuild = gr.Button(t(locale0, "rebuild"), elem_id="rebuild-btn") with gr.Column(scale=7): status = gr.Markdown(t(locale0, "status.wait")) warnings = gr.Markdown("") preview = gr.Gallery( label=t(locale0, "preview"), columns=2, height=280, preview=True, ) text = gr.Textbox( label=t(locale0, "text"), lines=12, elem_id="result-text", rtl=False, buttons=["copy"], ) with gr.Row(): pdf = gr.File(label=t(locale0, "pdf")) txt = gr.File(label=t(locale0, "txt")) with gr.Row(): markdown = gr.File(label=t(locale0, "md")) json_file = gr.File(label=t(locale0, "json")) with gr.Row(): docx = gr.File(label=t(locale0, "docx")) archive = gr.File(label=t(locale0, "zip")) history = gr.Dataframe( label=t(locale0, "history"), headers=history_headers(locale0), wrap=True, interactive=False, ) examples = _example_rows() if examples: gr.Examples( label="Examples", examples=examples, inputs=[image, language, mode], cache_examples=False, ) footer = gr.Markdown(t(locale0, "footer")) locale_outputs = [ intro, notes, image, files, language, mode, deskew, high_accuracy, api_key, run, rebuild, text, preview, pdf, txt, markdown, json_file, docx, archive, history, locale_dd, footer, shell, locale_state, ] def switch_locale(locale, saved, language_value, mode_value): chosen = normalize_locale(locale or saved or locale0) return locale_updates(chosen, language_value, mode_value) + [chosen] locale_dd.change( fn=switch_locale, inputs=[locale_dd, saved_locale, language, mode], outputs=locale_outputs + [saved_locale], api_visibility="private", ) demo.load( fn=lambda saved: locale_updates(normalize_locale(saved)), inputs=[saved_locale], outputs=locale_outputs, api_visibility="private", ) run_outputs = [ status, warnings, text, preview, pdf, txt, markdown, json_file, docx, archive, job_state, history_state, history, ] run.click( fn=run_fn, inputs=[ image, files, language, mode, deskew, high_accuracy, api_key, locale_state, history_state, ], outputs=run_outputs, api_name="ocr", concurrency_limit=1, ) rebuild.click( fn=rebuild_fn, inputs=[text, job_state, locale_state, history_state], outputs=run_outputs, api_name="rebuild", concurrency_limit=1, ) health_in = gr.Button(visible=False) health_out = gr.Textbox(visible=False) health_in.click(fn=health_fn, outputs=health_out, api_name="health") return demo