Skip to content

feat: request and persist OCR 4 blocks + confidence scores - #27

Merged
r-uben merged 1 commit into
mainfrom
feat/23-ocr4-blocks
Aug 13, 2026
Merged

feat: request and persist OCR 4 blocks + confidence scores#27
r-uben merged 1 commit into
mainfrom
feat/23-ocr4-blocks

Conversation

@r-uben

@r-uben r-uben commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Closes #23. Builds on #26 (mistralai 2.x), now merged.

Summary

Adds --include-blocks/--no-include-blocks (INCLUDE_BLOCKS) and
--confidence-scores-granularity {page,block,word}
(CONFIDENCE_SCORES_GRANULARITY), forwards both to the OCR API, and persists the
returned payload to a blocks.json sidecar in the doc dir.

The two open questions, decided

Defaults: both off. Existing scripts get zero new files, zero new cost, and —
critically — an unchanged fingerprint.

Fingerprint: the new keys enter extra only when enabled. This is the one
place I departed from the issue text, and it is deliberate. run_fingerprint
hashes the whole extra dict, so emitting include_blocks: False would change
every fingerprint the moment this shipped, invalidating every completed document
and forcing a full re-OCR of existing corpora at $4/1k pages — for a flag nobody
turned on. Omitting at default keeps an untouched run fingerprint-identical to a
pre-feature run, while toggling a flag still forces re-OCR (adding or removing a
key changes the hash). test_defaults_do_not_change_the_fingerprint pins the
exact pre-feature digest so this can never regress silently.

Item 5 is dropped. The API echoes back whichever model string it was sent
(evidence in the issue thread), so there is no resolved id to record. Documented
in the README instead: pin --model mistral-ocr-4-1 if you need the version in
metadata.json.

Design notes

  • Sidecar sits beside metadata.json — not inside it (DocMetadata is a closed
    field set) and not under figures/ (the only directory conformance enumerates).
  • Page keys are the globally re-indexed 1-based numbers used by the ## Page N
    headers, so the sidecar joins to the body even for chunked PDFs.
  • Base64 is stripped from blocks: figures already go to figures/, and the API's
    image ids are request-local, so embedded payloads would bloat the file with
    bytes that resolve to nothing.
  • The write happens inside save_results, so a sidecar failure is caught by
    _persist's isolation rather than leaving status=completed beside a
    truncated file.
  • Turning blocks off unlinks a stale sidecar, so a doc dir never carries blocks
    from one fingerprint next to metadata from another.

Test plan — observed results

uv run pytest161 passed (141 existing, unmodified + 20 new in
tests/test_blocks.py). ruff check / ruff format --check clean. mypy 18
errors, unchanged from main — no new ones.

New tests cover kwargs construction, all three granularities, the five
fingerprint behaviours, sidecar shape, page-number alignment, base64 stripping,
stale-sidecar removal, the no-blocks-returned case, and body byte-identity.

Live API, 2-page PDF, OCR 4.1:

  • --include-blocks --confidence-scores-granularity block produced blocks.json
    with 3 blocks on page 1, keys
    type / top_left_x / top_left_y / bottom_right_x / bottom_right_y / content / confidence_scores, and page-level scores.
  • The .md is byte-identical (cmp clean) to the no-blocks run, and contains
    no bbox/confidence/type tokens.
  • Fingerprint with blocks fp:1c85ec03… vs without fp:e1ece341… — differs, so
    toggling forces re-OCR.
  • Default run after this change still produces fp:e1ece341…, the same digest as
    before the feature existed.
  • Re-running with blocks off reprocessed the doc and removed the sidecar,
    leaving the body identical.

One shape correction worth recording: the issue assumed a bbox field. The live
API returns four scalar corner fields instead. Test fixtures match the real shape.

Out of scope

socr consuming the sidecar (r-uben/socr, after a bake-off). Also unaddressed, and
worth its own ticket: _is_retryable has two dead limbs on both SDKs, noted in
#26.


Summary by cubic

Adds optional OCR 4 per-page blocks and confidence scores and persists them to a blocks.json sidecar. Previously we did not request or store these; now, when enabled, we forward flags to the API, write the sidecar, and keep the markdown body and default fingerprints unchanged.

  • CLI/config: adds --include-blocks/--no-include-blocks (INCLUDE_BLOCKS) and --confidence-scores-granularity {page,block,word} (CONFIDENCE_SCORES_GRANULARITY); forwarded to OCR 4 only.
  • Sidecar: writes blocks.json beside metadata.json; pages are 1-based to match “## Page N”; strips any base64 payloads; never inlines into the markdown; removes a stale sidecar when flags are off.
  • Fingerprint: adds new keys only when enabled, preserving the pre-feature digest by default; enabling either flag changes the fingerprint and forces re-OCR.
  • Tests/docs: adds tests for kwargs, fingerprint behavior, sidecar shape, and body byte-identity; README documents flags and sidecar.

Rollout

  • No action required: defaults keep output and fingerprints identical, with no new files or cost.
  • To opt in: pass --include-blocks and/or --confidence-scores-granularity (or set env vars). Enabling forces re-OCR of affected documents; turning flags off removes blocks.json.

Written for commit bc32d86. Summary will update on new commits.

Review in cubic

Adds --include-blocks/--no-include-blocks (INCLUDE_BLOCKS) and
--confidence-scores-granularity {page,block,word}
(CONFIDENCE_SCORES_GRANULARITY), forwards both to the OCR API, and writes
the returned payload to a blocks.json sidecar in the doc dir.

Both default OFF. The sidecar sits beside metadata.json, never inside it
(DocMetadata is a closed field set) and never under figures/ (the only
directory the conformance harness enumerates).

Fingerprint: the new keys are added to the extra dict ONLY when enabled.
run_fingerprint hashes the whole dict, so emitting include_blocks=False
would change every fingerprint the moment this shipped and force a full
re-OCR of existing corpora at $4/1k pages for a flag nobody turned on.
Omitting at default keeps an untouched run fingerprint-identical to a
pre-feature run while still forcing re-OCR when a flag is toggled.

Sidecar page keys are the globally re-indexed 1-based numbers used by the
## Page N headers, so it joins to the body across chunked PDFs. Base64 is
stripped: figures are already written to figures/, and the API's image ids
are request-local. Turning blocks off unlinks any stale sidecar. The write
happens inside save_results so a sidecar failure is caught by _persist's
isolation rather than leaving status=completed beside a truncated file.

Item 5 of the issue (record the resolved response.model) is dropped: the
API echoes back whichever model string it was sent, so there is nothing to
resolve. Documented in the README instead.

Closes #23
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@r-uben, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9df4d941-6470-4e6d-8ce9-58a22005e202

📥 Commits

Reviewing files that changed from the base of the PR and between bb4b8b0 and bc32d86.

📒 Files selected for processing (5)
  • README.md
  • mistral_ocr/cli.py
  • mistral_ocr/config.py
  • mistral_ocr/processor.py
  • tests/test_blocks.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add optional OCR4 blocks + confidence capture with blocks.json sidecar

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Add CLI/env flags to request OCR4 blocks and confidence score granularity.
• Persist returned blocks/confidence to blocks.json sidecar without changing markdown output.
• Preserve existing run fingerprints by hashing new keys only when enabled; add regression tests.
Diagram

graph TD
  A["CLI (click)"] --> B["Config"] --> C["OCRProcessor"] --> D["Mistral OCR API"]
  C --> E[("Doc output dir")]
  F["tests/test_blocks.py"] --> C
  E --> G["metadata.json"]
  E --> H["blocks.json (optional)"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Embed blocks/confidence into metadata.json
  • ➕ Single sidecar to read per document
  • ➕ No new top-level file in output directory
  • ➖ Breaks the closed DocMetadata field set / shared output contract
  • ➖ Higher risk of downstream consumer breakage
2. Write per-page block files (e.g., blocks/page_0001.json)
  • ➕ Avoids loading a large blocks.json for big PDFs
  • ➕ Easier incremental reads for page-based consumers
  • ➖ Many small files; slower IO and more filesystem overhead
  • ➖ More complex cleanup semantics when rerunning with flags toggled
3. Always include fingerprint keys with explicit false/off values
  • ➕ More uniform fingerprint schema over time
  • ➕ Less conditional logic when building the extra dict
  • ➖ Would invalidate all existing fingerprints immediately on upgrade
  • ➖ Forces expensive re-OCR of previously completed corpora without user intent

Recommendation: Keep the current approach: a contract-safe blocks.json sidecar plus conditional fingerprint keys only when enabled. This preserves backwards compatibility (byte-identical markdown and unchanged default fingerprints) while still guaranteeing cache invalidation when blocks/confidence behavior changes.

Files changed (5) +437 / -2

Enhancement (2) +134 / -1
cli.pyAdd CLI flags for blocks capture and confidence granularity +25/-1

Add CLI flags for blocks capture and confidence granularity

• Introduces click options for include-blocks/no-include-blocks and confidence-scores-granularity, validating granularity against a shared constant. Wires parsed values into the runtime Config, lowercasing granularity for consistency.

mistral_ocr/cli.py

processor.pyForward OCR4 flags, capture blocks payload, and persist blocks.json +109/-0

Forward OCR4 flags, capture blocks payload, and persist blocks.json

• Adds result storage for per-page block/confidence payload keyed by 1-based page number and forwards requested flags to the OCR API. Persists payload to a blocks.json sidecar (or removes stale sidecar when disabled), strips any base64 fields to avoid bloating, and updates run fingerprint logic to include new keys only when enabled to preserve pre-feature defaults.

mistral_ocr/processor.py

Tests (1) +231 / -0
test_blocks.pyAdd comprehensive tests for blocks/confidence, sidecar, and fingerprint invariants +231/-0

Add comprehensive tests for blocks/confidence, sidecar, and fingerprint invariants

• Adds tests covering OCR kwargs construction, all confidence granularities, fingerprint stability at defaults (including pinned baseline digest), and fingerprint changes when enabled. Verifies sidecar write/remove behavior, page-number alignment with markdown headers, base64 stripping, and that markdown output remains byte-identical regardless of blocks settings.

tests/test_blocks.py

Documentation (1) +54 / -1
README.mdDocument blocks.json sidecar and new OCR4 flags +54/-1

Document blocks.json sidecar and new OCR4 flags

• Adds CLI option docs for --include-blocks and --confidence-scores-granularity and updates the output tree to include blocks.json. Introduces a dedicated section describing the sidecar schema, page-number alignment guarantees, fingerprint/re-OCR implications, and base64 stripping rationale.

README.md

Other (1) +18 / -0
config.pyExtend Config with blocks + confidence settings and env parsing +18/-0

Extend Config with blocks + confidence settings and env parsing

• Adds Config fields include_blocks and confidence_scores_granularity with OCR4-focused documentation. Implements env var support (INCLUDE_BLOCKS, CONFIDENCE_SCORES_GRANULARITY) including validation against allowed granularities.

mistral_ocr/config.py

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Block confidence scores dropped 🐞 Bug ≡ Correctness
Description
Selecting block granularity without --include-blocks is accepted, but _page_block_payload
discards page.blocks unless the separate blocks flag is true. Because Mistral stores block-level
scores on each block, this combination silently writes only page-level confidence data and loses the
requested per-block scores.
Code

mistral_ocr/processor.py[R603-606]

+        if self.config.include_blocks:
+            blocks = getattr(page, "blocks", None)
+            if blocks:
+                payload["blocks"] = self._jsonable(blocks)
Evidence
The CLI and request builder permit confidence granularity independently, while the parser gates the
only objects containing block-level scores on include_blocks. Mistral's OCR documentation states
that block granularity places confidence scores on each returned block and demonstrates the
setting together with include_blocks=True.

mistral_ocr/cli.py[91-98]
mistral_ocr/processor.py[324-327]
mistral_ocr/processor.py[603-610]
🌐 For block granularity, each block carries its own confidence_scores, and the documented request enables include_blocks together with block confidence granularity.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Block confidence scores are nested in `page.blocks`, but those blocks are persisted only when `include_blocks` is independently enabled. A user selecting block granularity alone therefore loses the requested block-level scores.

## Issue Context
Either automatically enable/request and persist blocks for block granularity, or reject that option combination with a clear configuration error. Add coverage for block granularity without an explicit blocks flag using the real response shape.

## Fix Focus Areas
- mistral_ocr/processor.py[324-327]
- mistral_ocr/processor.py[603-610]
- mistral_ocr/cli.py[91-98]
- tests/test_blocks.py[80-87]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. SDK floor rejects blocks 🐞 Bug ≡ Correctness
Description
The project still allows mistralai>=2.9, although older allowed 2.x clients do not define the
newly passed include_blocks argument. Enabling blocks in such an existing environment fails at
runtime instead of being rejected during dependency resolution.
Code

mistral_ocr/processor.py[R324-325]

+        if self.config.include_blocks:
+            ocr_kwargs["include_blocks"] = True
Evidence
The dependency declaration permits clients as old as 2.9, while an older 2.x OCR client signature
includes confidence granularity but not include_blocks; current SDK documentation includes the new
argument, showing that support was added later. The PR invokes the keyword without a version or
capability check.

pyproject.toml[26-29]
mistral_ocr/processor.py[324-327]
🌐 This older 2.x OCR process signature exposes confidence_scores_granularity but has no include_blocks parameter.
🌐 Current SDK documentation lists include_blocks as an OCR process argument, confirming it requires a newer client surface.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new request keyword is not supported by every `mistralai` version permitted by the current dependency range, causing `--include-blocks` to fail at runtime in valid installations.

## Issue Context
Determine the first 2.x SDK release that exposes `include_blocks` on `ocr.process` and raise the lower dependency bound to that version. Alternatively, add an explicit compatibility check that fails early with a clear message.

## Fix Focus Areas
- pyproject.toml[26-29]
- mistral_ocr/processor.py[324-327]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Missing sidecar remains cached 🐞 Bug ☼ Reliability
Description
A previously written blocks.json is not represented in completion metadata or checked by resume
logic. If that sidecar is later deleted or corrupted, an ordinary rerun with the same fingerprint
skips processing and leaves the completed document without its requested auxiliary payload.
Code

mistral_ocr/processor.py[722]

+        sidecar.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
Evidence
The PR creates a separate blocks artifact, but DocMetadata records only the markdown output path
and resume delegates completion to the existing index check. No sidecar presence, checksum, or
validity marker is stored, so the resume branches cannot distinguish an intact result from one whose
previously produced sidecar is gone.

mistral_ocr/processor.py[693-722]
mistral_ocr/processor.py[838-849]
mistral_ocr/processor.py[1008-1021]
mistral_ocr/processor.py[1075-1083]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Resume validation checks the canonical markdown output but has no state describing whether a blocks sidecar was produced. A sidecar that existed after a successful run can disappear or become invalid while the document continues to be treated as complete.

## Issue Context
Persist enough sidecar state to distinguish an intentionally absent payload from a produced sidecar, then verify its existence and JSON validity before accepting a cache hit. Force reprocessing when the recorded auxiliary output is missing or invalid.

## Fix Focus Areas
- mistral_ocr/processor.py[693-722]
- mistral_ocr/processor.py[832-849]
- mistral_ocr/processor.py[1008-1021]
- mistral_ocr/processor.py[1075-1083]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Sidecar write not covered by _persist isolation as claimed 🐞 Bug ☼ Reliability
Description
_write_blocks_sidecar is called directly inside save_results after the markdown is already
written; if it raises, the exception propagates up through save_results into _persist's try
block so the document is recorded status=failed even though the OCR itself succeeded and a valid
markdown file was already written to disk. This means a transient sidecar I/O error (e.g. disk full)
discards a perfectly good, already-persisted markdown result and marks the whole document as failed,
contradicting the intended “markdown succeeds independently” design.
Code

mistral_ocr/processor.py[R687-688]

        markdown_path.write_text(body, encoding="utf-8")
+        self._write_blocks_sidecar(result, doc_dir)
Evidence
save_results() writes markdown_path.write_text(body) then unconditionally calls
self._write_blocks_sidecar(result, doc_dir) with no try/except around it. Because save_results is
invoked inside _persist's try block (processor.py line 869), any exception raised by
_write_blocks_sidecar (e.g. json.dumps failure on non-serializable data, or an OSError writing the
file) causes the whole document, including its already-written valid markdown, to be recorded as
status=failed via _build_failed_persist_metadata.

mistral_ocr/processor.py[684-691]
mistral_ocr/processor.py[866-883]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failure while writing the `blocks.json` sidecar in `_write_blocks_sidecar` currently propagates and causes the entire document to be recorded as `status=failed`, even though the markdown body OCR succeeded and was already written to disk.

## Issue Context
`save_results` writes markdown, then unconditionally calls `_write_blocks_sidecar`. Both run inside `_persist`'s try/except, so any exception in the sidecar write (disk full, permission error, non-JSON-serializable payload) discards the successful markdown result.

## Fix Focus Areas
- mistral_ocr/processor.py[684-691]
- mistral_ocr/processor.py[693-722]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
✅ Web pages:
  +24 more
Review mode: 🧠 Deep: This adds substantial behavior across CLI/configuration, API request construction, fingerprinting, response parsing, sidecar persistence, chunk/page alignment, and cleanup, creating multiple independent defect opportunities across a broad OCR data path.

Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread mistral_ocr/processor.py
Comment on lines +603 to +606
if self.config.include_blocks:
blocks = getattr(page, "blocks", None)
if blocks:
payload["blocks"] = self._jsonable(blocks)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Block confidence scores dropped 🐞 Bug ≡ Correctness

Selecting block granularity without --include-blocks is accepted, but _page_block_payload
discards page.blocks unless the separate blocks flag is true. Because Mistral stores block-level
scores on each block, this combination silently writes only page-level confidence data and loses the
requested per-block scores.
Agent Prompt
## Issue description
Block confidence scores are nested in `page.blocks`, but those blocks are persisted only when `include_blocks` is independently enabled. A user selecting block granularity alone therefore loses the requested block-level scores.

## Issue Context
Either automatically enable/request and persist blocks for block granularity, or reject that option combination with a clear configuration error. Add coverage for block granularity without an explicit blocks flag using the real response shape.

## Fix Focus Areas
- mistral_ocr/processor.py[324-327]
- mistral_ocr/processor.py[603-610]
- mistral_ocr/cli.py[91-98]
- tests/test_blocks.py[80-87]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread mistral_ocr/processor.py
Comment on lines +324 to +325
if self.config.include_blocks:
ocr_kwargs["include_blocks"] = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Sdk floor rejects blocks 🐞 Bug ≡ Correctness

The project still allows mistralai>=2.9, although older allowed 2.x clients do not define the
newly passed include_blocks argument. Enabling blocks in such an existing environment fails at
runtime instead of being rejected during dependency resolution.
Agent Prompt
## Issue description
The new request keyword is not supported by every `mistralai` version permitted by the current dependency range, causing `--include-blocks` to fail at runtime in valid installations.

## Issue Context
Determine the first 2.x SDK release that exposes `include_blocks` on `ocr.process` and raise the lower dependency bound to that version. Alternatively, add an explicit compatibility check that fails early with a clear message.

## Fix Focus Areas
- pyproject.toml[26-29]
- mistral_ocr/processor.py[324-327]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread mistral_ocr/processor.py
for page_no in sorted(result.page_blocks)
],
}
sidecar.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Missing sidecar remains cached 🐞 Bug ☼ Reliability

A previously written blocks.json is not represented in completion metadata or checked by resume
logic. If that sidecar is later deleted or corrupted, an ordinary rerun with the same fingerprint
skips processing and leaves the completed document without its requested auxiliary payload.
Agent Prompt
## Issue description
Resume validation checks the canonical markdown output but has no state describing whether a blocks sidecar was produced. A sidecar that existed after a successful run can disappear or become invalid while the document continues to be treated as complete.

## Issue Context
Persist enough sidecar state to distinguish an intentionally absent payload from a produced sidecar, then verify its existence and JSON validity before accepting a cache hit. Force reprocessing when the recorded auxiliary output is missing or invalid.

## Fix Focus Areas
- mistral_ocr/processor.py[693-722]
- mistral_ocr/processor.py[832-849]
- mistral_ocr/processor.py[1008-1021]
- mistral_ocr/processor.py[1075-1083]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread mistral_ocr/processor.py
Comment on lines 687 to +688
markdown_path.write_text(body, encoding="utf-8")
self._write_blocks_sidecar(result, doc_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

4. Sidecar write not covered by _persist isolation as claimed 🐞 Bug ☼ Reliability

_write_blocks_sidecar is called directly inside save_results after the markdown is already
written; if it raises, the exception propagates up through save_results into _persist's try
block so the document is recorded status=failed even though the OCR itself succeeded and a valid
markdown file was already written to disk. This means a transient sidecar I/O error (e.g. disk full)
discards a perfectly good, already-persisted markdown result and marks the whole document as failed,
contradicting the intended “markdown succeeds independently” design.
Agent Prompt
## Issue description
A failure while writing the `blocks.json` sidecar in `_write_blocks_sidecar` currently propagates and causes the entire document to be recorded as `status=failed`, even though the markdown body OCR succeeded and was already written to disk.

## Issue Context
`save_results` writes markdown, then unconditionally calls `_write_blocks_sidecar`. Both run inside `_persist`'s try/except, so any exception in the sidecar write (disk full, permission error, non-JSON-serializable payload) discards the successful markdown result.

## Fix Focus Areas
- mistral_ocr/processor.py[684-691]
- mistral_ocr/processor.py[693-722]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@r-uben
r-uben merged commit a981cb8 into main Aug 13, 2026
6 checks passed
@r-uben
r-uben deleted the feat/23-ocr4-blocks branch August 13, 2026 23:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: request and persist OCR 4 include_blocks + confidence

1 participant