"""Progressive document workspace: choose a PDF, run a decision, inspect the pages.""" from __future__ import annotations import html import logging from functools import lru_cache import blink import documents import gradio as gr import ui e = html.escape LOG = logging.getLogger(__name__) LABELS = {"text": "Selectable text", "ocr": "English OCR", "blank": "Nearly blank page", "partial": "Partial text / review", "unreadable": "No extracted text / review"} REVISION = r"""(...a) => { a[0] = String(window.__blinkDocRev = (window.__blinkDocRev || 0) + 1); return a; }""" READ_REVISION = r"""(...a) => { a[0] = String(window.__blinkDocRev || 0); return a; }""" GUARD_JS = r""" (() => { if (window.__blinkDocGuard) return; window.__blinkDocGuard = true; document.addEventListener('change', (event) => { const input = event.target; if (!(input instanceof HTMLInputElement) || input.type !== 'file' || !input.closest('#doc-upload')) return; const file = input.files?.[0]; if (!file || !file.name.toLowerCase().endsWith('.pdf') || file.size > 12 * 1024 * 1024) return; window.__blinkDocRev = (window.__blinkDocRev || 0) + 1; const node = document.getElementById('doc-run'); const button = node instanceof HTMLButtonElement ? node : node?.querySelector('button'); if (button) button.disabled = true; }, true); setInterval(() => { const panel = document.querySelector('#doc-result [data-doc-rev]'); const stale = panel && panel.dataset.docRev !== String(window.__blinkDocRev || 0); const outputs = document.getElementById('doc-outputs'); if (outputs) outputs.classList.toggle('doc-stale', Boolean(stale)); }, 150); })(); """ @lru_cache(maxsize=3) def sample_document(key: str) -> documents.Document: return documents.parse_document(documents.sample_path(key)) FILE_ICON = ('') SOURCES = {"saved": "Saved model run", "live": "Live model run", "mock": "Mock: fabricated scores"} DOT = " \u00b7 " FLAG = ' class="flag"' COMPARE_HEAD = '

Same PDF and rules, other model

' def intro() -> str: return ( '

PDF in. Documents out.

' '

Label a document or separate a packet. See the original pages, scores, and boundaries.

' '
' ) def step(number: int, title: str) -> str: return f'

{number}{e(title)}

' def _count(n: int, word: str) -> str: return f"{n} {word}" if n == 1 else f"{n} {word}s" def page_choices(document: documents.Document) -> list[tuple[str, int]]: return [(f"Page {p.number} / {len(document.pages)}", p.number) for p in document.pages] def preview_html(document: documents.Document | None, number: int = 1) -> str: if document is None: return '

Choose a sample or upload a PDF to see its pages.

' page = document.pages[int(number) - 1] return ( '
' f'
' f'
Page {page.number} / {len(document.pages)}' f'{e(LABELS[page.method])}
' ) def source_html(document: documents.Document, sample: str | None) -> str: scans = sum(p.method == "ocr" for p in document.pages) review = sum(p.method in ("partial", "unreadable") for p in document.pages) caption = "Original fictional sample. Not an accuracy benchmark." if sample else "Your uploaded PDF." facts = [_count(len(document.pages), "page")] if scans: facts.append(f"{scans} OCR") elif not review: facts.append("selectable text") if review: facts.append(f"{_count(review, 'page')} need text review") return ( f'
{FILE_ICON}' f'

{e(document.name)}{DOT.join(facts)}

' f'

{caption}

' ) def _range(numbers: list[int]) -> str: return f"Page {numbers[0]}" if len(numbers) == 1 else f"Pages {numbers[0]}-{numbers[-1]}" def _bars(segment: dict, labels: dict) -> str: return "".join( f'
  • ' f'{e(labels.get(key, key))}' f'' f'{p:.1%}
  • ' for i, (key, p) in enumerate(sorted(segment["probabilities"].items(), key=lambda item: -item[1])) ) def _reasons(segment: dict) -> str: items = "".join(f'
  • {e(r["message"])} ({_range(r["pages"])})
  • ' for r in segment["review_reasons"]) return f'' if items else "" def result_html(result: dict | None, revision: str = "0") -> str: if result is None: return ('

    Ready to run. Blink scores your categories and page boundaries ' 'without writing text.

    ') segments = result["segments"] model = str(result["model"]).split("/")[-1] kind = result["source"] count = len(segments) review = sum(bool(s["review_reasons"]) for s in segments) pages = result["document"]["page_count"] labels = {c["id"]: c["label"] for c in result["rules"]["categories"]} split = result["mode"] != "classify" title = f"{count} documents" if split else segments[0]["label"] needs = "document needs" if review == 1 else "documents need" note = _count(pages, "original page") + (f"{DOT}{review} {needs} review" if review else f"{DOT}no review flags") if split: timeline = "".join( f'
  • ' f'{e(s["label"])}{_range(s["pages"])}
  • ' for s in segments ) rows = "".join( '
  • ' f'{e(s["label"])}{_range(s["pages"])}' + ('Review' if s["review_reasons"] else "") + 'Option probabilities, top score ' f'{s["probabilities"][s["category"]]:.1%}' f'
    {_reasons(s)}
  • ' for s in segments ) body = (f'
      {timeline}
    ' f'

    DocumentTop score

    ' f'
      {rows}
    ') else: segment = segments[0] body = (f'

    Option probabilities' f'{_range(segment["pages"])}

    ' f'{_reasons(segment)}
    ') model_ms = result["metrics"].get("model_ms") call_ms = result["metrics"].get("latency_ms") timing = (f'{model_ms:,.0f} ms model only' if model_ms is not None else f'{call_ms:,.0f} ms decision call' if call_ms is not None else "") return ( f'
    ' f'

    ' f'{SOURCES[kind]} / {e(model)}

    ' f'

    {e(title)}

    {note}

    ' + body + '
    ' + (f'{result["document"]["preparation_ms"]:,.0f} ms PDF preparation' if kind != "saved" else "") + timing + '0 generated tokens
    ' '
    What the scores and timings mean' f'

    {e(result["probability_note"])}

    ' '

    Review flags use fixed demo thresholds, not a fitted confidence guarantee. ' 'Nearly blank pages stay with the preceding document; leading blank pages stay with the first. ' 'Unreadable pages are attached provisionally and always need review.

    ' '

    Model-only time excludes PDF preparation and the wait for a device. ' 'Preparation includes text extraction, scan OCR when needed, and preview rendering. ' 'Saved model timings describe the recorded Space call, not this page load. ' 'The saved JSON also retains separately measured preparation from the recording client; ' 'it is not a hosted parse measurement and is omitted from this timing strip.

    ' + (f'

    Decision call: {call_ms:,.0f} ms. This includes the device wait inside the call.

    ' if call_ms is not None else "") + '
    ' ) def _cache_exports(document: documents.Document, result: dict, component: gr.File) -> list[str]: paths = [] for name, data in documents.export_bytes(document, result).items(): path = gr.processing_utils.save_bytes_to_cache(data, name, component.GRADIO_CACHE) component.temp_files.add(path) paths.append(path) return paths def document_api(file: str, mode: str, category_rules: str, model: str) -> dict: """File-component preprocessing permits only uploaded files, not arbitrary local paths.""" try: document = documents.parse_document(file) return documents.process(document, category_rules, mode, model) except (documents.DocumentError, blink.BlinkError) as exc: raise gr.Error(str(exc)) from None def build() -> dict: sample = sample_document("packet") model = documents.default_model() saved = documents.saved_result(sample, documents.DEFAULT_RULES, "split", model) held = gr.State(sample) result = gr.State(saved) source_seen = gr.State({"source": 0, "selection": ("packet", None)}) rev = gr.Textbox("0", visible=False) gr.HTML(intro()) with gr.Row(equal_height=False, elem_classes="doc-workspace"): with gr.Column(scale=5, min_width=280, elem_classes="doc-col doc-col-source"): gr.HTML(step(1, "Choose a PDF"), elem_classes="doc-stephead") preset = gr.Radio(choices=[(s["title"], s["id"]) for s in documents.sample_info()], value="packet", show_label=False, elem_id="doc-samples") with gr.Accordion("Use your own PDF", open=False, elem_id="doc-own"): upload = gr.File(label="PDF \u00b7 up to 12 MiB and 12 pages", file_types=[".pdf"], type="filepath", elem_id="doc-upload") gr.HTML('

    Processing runs in this Space. Temporary uploads and downloads ' 'are cleared within an hour. Do not upload confidential documents.

    ') source = gr.HTML(source_html(sample, "packet"), elem_id="doc-source") page = gr.Dropdown(choices=page_choices(sample), value=1, label="Original page", elem_id="doc-page") preview = gr.HTML(preview_html(sample), elem_id="doc-preview") with gr.Accordion("Extracted page text", open=False, elem_id="doc-text-fold"): text = gr.Textbox(sample.pages[0].text, label="Text that Blink reads", lines=12, interactive=False, elem_id="doc-text") with gr.Column(scale=6, min_width=280, elem_classes="doc-col doc-col-task"): gr.HTML(step(2, "Choose a task"), elem_classes="doc-stephead") mode = gr.Radio([("Classify", "classify"), ("Split packet", "split")], value="split", show_label=False, elem_id="doc-mode") gr.HTML('

    Classify gives one label. Split packet finds separate documents, ' 'including adjacent documents with the same label.

    ') with gr.Accordion("Categories and rules", open=False, elem_id="doc-rules-fold"): rule = gr.Code(documents.rules_text(), language="json", label="Your categories and boundary rules", lines=18, wrap_lines=True, elem_id="doc-rules") with gr.Accordion("Run options", open=False, elem_id="doc-options"): models = gr.State(model) compare = gr.Checkbox(False, label="Compare with Blink 4B on the same page text", visible=len(blink.models()) == 2, elem_id="doc-compare") gr.HTML('

    The PDF is prepared once. Each model gets identical text and rules. ' 'This is a demonstration, not a matched accuracy study.

    ') run = gr.Button("Run Blink", variant="primary", elem_id="doc-run") gr.HTML('

    This button runs the live model. The initial result, ' 'when present, is a saved run on the fictional sample.

    ') gr.HTML(step(3, "Inspect the result"), elem_classes="doc-stephead") with gr.Column(elem_id="doc-outputs", elem_classes="doc-panel"): answer = gr.HTML(result_html(saved), elem_id="doc-result") inspect = gr.Dropdown( choices=[(f'{s["label"]} / {_range(s["pages"])}', s["pages"][0]) for s in saved["segments"]] if saved else [], value=None, label="Jump to a document", visible=saved is not None, info="Shows the document's first page in the preview.", elem_id="doc-inspect", ) comparison = gr.HTML(visible=False, elem_id="doc-comparison") files = gr.File(label="Download result JSON and split PDFs", file_count="multiple", interactive=False, visible=saved is not None, elem_id="doc-downloads") if saved is not None: initial = files.postprocess(_cache_exports(sample, saved, files)) files.value = gr.processing_utils.move_files_to_cache( initial.model_dump(), files, postprocess=True, keep_in_cache=True, ) with gr.Accordion("Full result JSON", open=False, elem_id="doc-json-fold"): raw = gr.JSON(saved, label="Page decisions, review flags, and provenance", elem_id="doc-json") gr.HTML('

    Inputs changed. Run Blink again to see a matching result.

    ') def clear(revision): return (None, result_html(None), gr.update(value=None, visible=False), gr.update(value=None, visible=False), None, gr.update(value="", visible=False), revision) clear_out = [result, answer, files, inspect, raw, comparison, rev] for control in (mode, rule, compare): control.input(clear, [rev], clear_out, js=REVISION, queue=False, show_progress="hidden", api_visibility="private") def load_source(revision, key, uploaded, current_mode, current_rules, current_model): try: document = documents.parse_document(uploaded) if uploaded else sample_document(key) except (documents.DocumentError, blink.BlinkError) as exc: return (None, gr.update(choices=[], value=None), preview_html(None), "", "", None, f'
    {e(str(exc))}
    ', gr.update(value=None, visible=False), gr.update(choices=[], value=None, visible=False), None, gr.update(value="", visible=False), revision) warning = "" try: recorded = documents.saved_result(document, current_rules, current_mode, current_model) except documents.DocumentError as exc: recorded = None warning = f'
    {e(str(exc))} The PDF remains loaded.
    ' return (document, gr.update(choices=page_choices(document), value=1), preview_html(document), document.pages[0].text, source_html(document, None if uploaded else key), recorded, warning or result_html(recorded, revision), gr.update(value=_cache_exports(document, recorded, files) if recorded else None, visible=bool(recorded)), gr.update(choices=[(f'{s["label"]} / {_range(s["pages"])}', s["pages"][0]) for s in recorded["segments"]] if recorded else [], value=None, visible=bool(recorded)), recorded, gr.update(value="", visible=False), revision) load_out = [held, page, preview, text, source, result, answer, files, inspect, raw, comparison, rev] def preparing(revision, key, uploaded, seen): if not ui.newest(seen, "source", revision): return [gr.skip() for _ in preparing_out] seen["selection"] = (key, str(uploaded) if uploaded else None) return [None, '

    Preparing the selected PDF

    ', "", *clear(revision), gr.update(interactive=False)] preparing_out = [held, preview, source, *clear_out, run] def load_ready(revision, key, uploaded, current_mode, current_rules, current_model, seen): selection = (key, str(uploaded) if uploaded else None) if ui.as_rev(revision) < ui.applied(seen, "source") or seen.get("selection") != selection: return [gr.skip() for _ in [*load_out, run]] source_revision = ui.applied(seen, "source") loaded = load_source(revision, key, uploaded, current_mode, current_rules, current_model) if source_revision != ui.applied(seen, "source") or seen.get("selection") != selection: return [gr.skip() for _ in [*load_out, run]] return [*loaded, gr.update(interactive=loaded[0] is not None)] for event in (upload.upload, upload.clear): event(preparing, [rev, preset, upload, source_seen], preparing_out, js=REVISION, queue=False, show_progress="hidden", api_visibility="private").then( load_ready, [rev, preset, upload, mode, rule, models, source_seen], [*load_out, run], js=READ_REVISION, show_progress="minimal", api_visibility="private", ) def pick_sample(revision, key, seen): if ui.as_rev(revision) < ui.applied(seen, "source"): return [gr.skip() for _ in [upload, mode, *preparing_out]] return [None, next(s["mode"] for s in documents.sample_info() if s["id"] == key), *preparing(revision, key, None, seen)] preset.input(pick_sample, [rev, preset, source_seen], [upload, mode, *preparing_out], js=REVISION, queue=False, api_visibility="private").then( load_ready, [rev, preset, upload, mode, rule, models, source_seen], [*load_out, run], js=READ_REVISION, show_progress="minimal", api_visibility="private", ) def show_page(document, number): if document is None or number is None: return preview_html(None), "" return preview_html(document, int(number)), document.pages[int(number) - 1].text page.change(show_page, [held, page], [preview, text], queue=False, show_progress="hidden", api_visibility="private") inspect.input(lambda number: gr.update(value=number), [inspect], [page], queue=False, show_progress="hidden", api_visibility="private") def run_document(revision, document, category_rules, task, selected, both, uploaded, selected_sample): if document is None: return (None, '
    Choose a readable PDF before running Blink.
    ', gr.update(value=None, visible=False), gr.update(value=None, visible=False), None, gr.update(value="", visible=False)) try: source_path = uploaded or documents.sample_path(selected_sample) if not documents.matches_source(document, source_path): raise documents.DocumentError("The new PDF is still being prepared. Wait for its page preview before running Blink.") outcome = documents.process(document, category_rules, task, selected) other = None if both and len(blink.models()) == 2: other_model = next(m for m in blink.models() if m != selected) other = documents.process(document, category_rules, task, other_model) paths = _cache_exports(document, outcome, files) except (documents.DocumentError, blink.BlinkError) as exc: return (None, f'
    {e(str(exc))}
    ', gr.update(value=None, visible=False), gr.update(value=None, visible=False), None, gr.update(value="", visible=False)) except (RuntimeError, gr.Error): LOG.exception("Live document processing failed") return (None, ('
    The live document run could not finish. ' 'Check your remaining Space quota or try again later. No saved result was substituted.
    '), gr.update(value=None, visible=False), gr.update(value=None, visible=False), None, gr.update(value="", visible=False)) return (outcome, result_html(outcome, revision), gr.update(value=paths, visible=True), gr.update(choices=[(f'{s["label"]} / {_range(s["pages"])}', s["pages"][0]) for s in outcome["segments"]], value=None, visible=True), {"primary": outcome, "comparison": other} if other else outcome, gr.update(value=(COMPARE_HEAD + result_html(other, revision)) if other else "", visible=other is not None)) run.click(run_document, [rev, held, rule, mode, models, compare, upload, preset], [result, answer, files, inspect, raw, comparison], js=READ_REVISION, show_progress="minimal", concurrency_id="blink-document-read", concurrency_limit=1, api_visibility="private") return {"model": models, "api": document_api} def mount_api() -> None: with gr.Row(visible=False): file = gr.File(file_types=[".pdf"], type="filepath") mode = gr.Dropdown(choices=list(documents.MODES), value="classify") rule = gr.Textbox(documents.rules_text()) model = gr.Dropdown(choices=blink.models(), value=documents.default_model()) output = gr.JSON() gr.Button().click(document_api, [file, mode, rule, model], output, api_name="v1_document", concurrency_id="blink-document-read", concurrency_limit=1)