2025-12-17 19:48:24 +08:00
|
|
|
#
|
|
|
|
|
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
|
|
|
#
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
#
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
#
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
|
# limitations under the License.
|
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_layout_recognizer(layout_recognizer_raw: Any) -> tuple[Any, str | None]:
|
|
|
|
|
parser_model_name: str | None = None
|
|
|
|
|
layout_recognizer = layout_recognizer_raw
|
|
|
|
|
|
|
|
|
|
if isinstance(layout_recognizer_raw, str):
|
|
|
|
|
lowered = layout_recognizer_raw.lower()
|
|
|
|
|
if lowered.endswith("@mineru"):
|
2026-06-10 13:04:13 +08:00
|
|
|
parser_model_name = layout_recognizer_raw
|
2025-12-17 19:48:24 +08:00
|
|
|
layout_recognizer = "MinerU"
|
2026-01-09 17:48:45 +08:00
|
|
|
elif lowered.endswith("@paddleocr"):
|
2026-06-10 13:04:13 +08:00
|
|
|
parser_model_name = layout_recognizer_raw
|
2026-01-09 17:48:45 +08:00
|
|
|
layout_recognizer = "PaddleOCR"
|
2026-05-06 15:00:55 +08:00
|
|
|
elif lowered.endswith("@opendataloader"):
|
2026-06-10 13:04:13 +08:00
|
|
|
parser_model_name = layout_recognizer_raw
|
2026-05-06 15:00:55 +08:00
|
|
|
layout_recognizer = "OpenDataLoader"
|
2026-07-01 13:29:28 +08:00
|
|
|
elif lowered.endswith("@somark"):
|
|
|
|
|
# Keep the full 3-segment form ``<llm_name>@<instance_name>@<provider>``
|
|
|
|
|
# produced by the new Tenant LLM Provider UI (#14595); downstream
|
|
|
|
|
# ``get_model_config_from_provider_instance`` -> ``split_model_name``
|
|
|
|
|
# expects all three segments to locate the provider/instance row.
|
|
|
|
|
parser_model_name = layout_recognizer_raw
|
|
|
|
|
layout_recognizer = "SoMark"
|
feat: add Mistral OCR (/v1/ocr) as a document parser; fix "Can't find model" mis-tag (#5782, #7075) (#17057)
### What problem does this PR solve?
Adds first-class support for **Mistral OCR** (`POST /v1/ocr`) as a
document parser, and fixes the long-standing bug where selecting
`mistral-ocr-latest` fails with `Can't find model for
<tenant>/image2text/mistral-ocr-latest`.
`mistral-ocr-latest` is Mistral's dedicated document-OCR endpoint, not a
vision-chat (`image2text`) model, but the catalog tagged it `image2text`
— so it resolved to the `CvModel` registry, which has no `Mistral`
entry, and there was no `OcrModel` entry either. This PR registers it
correctly and wires it end to end.
Closes #17056
Closes #5782
Closes #7075
**What it does**
1. **`MistralParser` + `MistralOcrModel`**
(`deepdoc/parser/mistral_parser.py`, `rag/llm/ocr_model.py`) — a proper
`OcrModel` factory `Mistral OCR`, mirroring the SoMark cloud-OCR
template. Tables stay inline as HTML; the page range maps to Mistral's
native `pages` selector (absolute page indices, billed per selected
page, so multi-task documents do not re-OCR the whole file); documents
over the inline limit go through the `/v1/files` signed-URL flow with
cleanup.
2. **Removes the `image2text` mis-tag** for `mistral-ocr-latest` from
the `Mistral` factory in `conf/llm_factories.json` (it now lives only in
the `Mistral OCR` factory, typed `ocr`). This is what closes the `Can't
find model` path.
3. **`MistralCV`** (`rag/llm/cv_model.py`) — a thin `GptV4` subclass
over Mistral's OpenAI-compatible endpoint, registering a `Mistral` entry
in the `CvModel` registry so Mistral vision models (`pixtral-*`) become
usable as `image2text` at all.
4. **Figure description** — Mistral-OCR-extracted figures are captioned
using the tenant's configured `image2text` model (any provider),
matching MinerU/deepdoc behaviour.
5. **Wires the parser into every chunking method** (`naive`, `paper`,
`book`, `laws`, `manual`, `one`, `presentation`) and the `rag/flow` DAG
path. This also fixes a related latent gap where those chunkers
forwarded only `mineru_llm_name`, so any model-based OCR provider
selected on a non-`naive` method silently fell through.
**Notes on the API contract** (verified against the live Mistral API):
`pages` is a selector (returns absolute `index`, bills only the
requested pages); `include_blocks: true` returns per-block bounding
boxes usable for chunk highlighting and figure cropping; large files use
`POST /v1/files` → signed URL → OCR → `DELETE`.
**Testing**: new unit tests cover the response→sections contract (both
the 2-tuple `naive` path and the typed 3-tuple DAG path), the
position-tag rescale, the HTTP client incl. upload failure/cleanup
paths, `parse_pdf` page-range threading, registry registration, env
config, the suffix normalization, the factory catalog entry, `MistralCV`
registration, and figure-description injection. Verified end to end
against the live Mistral API on real PDFs (table extraction,
page-selector cost avoidance, figure captioning).
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
2026-07-24 15:07:48 +02:00
|
|
|
elif lowered.endswith("@mistral ocr"):
|
|
|
|
|
# Separate OCR-only factory (never the multi-type "Mistral" factory),
|
|
|
|
|
# so this suffix cannot collide with pixtral vision models.
|
|
|
|
|
parser_model_name = layout_recognizer_raw
|
|
|
|
|
layout_recognizer = "Mistral OCR"
|
2025-12-17 19:48:24 +08:00
|
|
|
|
|
|
|
|
return layout_recognizer, parser_model_name
|