add processing.pictures enum (none|description|image), replacing the implicit pair of generate_picture_images+picture_description.enabled flags

This commit is contained in:
Yiorgis Gozadinos 2026-04-30 14:26:14 +03:00
parent b01c649684
commit 7831057339
No known key found for this signature in database
14 changed files with 316 additions and 81 deletions

View file

@ -3,6 +3,7 @@
### Added
- **Picture-handling mode enum.** `processing.pictures: "none" | "description" | "image"` (default `"none"`) replaces the previous two-flag dance (`generate_picture_images` + `picture_description.enabled`) where flipping picture descriptions on silently forced docling to also generate picture images, leaving raw bytes inside the compressed `docling_document` blob with no user-facing knob to control it. The new enum makes intent explicit: `description` runs the VLM at ingest *and* preserves bytes (so a vision-capable QA model can be turned on later without reingesting), `image` preserves bytes without running the VLM, `none` is bytes-free. The legacy fields are removed from the schema (`ConversionOptions.generate_picture_images` and `PictureDescriptionConfig.enabled`); the YAML loader silently translates pre-existing configs to the new enum and emits a one-line deprecation warning so existing `haiku.rag.yaml` files keep working until the user updates them. No data migration is required — the 0.45.0 backfill already populates `picture_data` for every doc that had bytes inline. Rebuild and update flows now skip the picture-bytes snapshot/merge under `pictures="none"`, so users who downgrade to `none` and rebuild reclaim storage.
- **Storage column for embedded picture bytes.** `DocumentItemRecord` gains a `picture_data: bytes | None` column (Arrow `large_binary`) to hold per-`PictureItem` image bytes addressable by `(document_id, self_ref)`. New repository accessors `get_picture_bytes` and `get_pictures_for_chunk` expose them; the existing items read paths (`get_all_items`, `get_all_items_grouped`, `get_items_in_range`, `_record_to_item`) now project an explicit lightweight column set so context expansion and the analysis-sandbox `items.jsonl` build never pull picture bytes into memory. Existing databases pick up the column via the `0.45.0` migration alongside the picture-byte backfill (see below). Foundation for upcoming vision-in-context retrieval; not yet wired into ingestion or search.
- **Embedded picture bytes captured at ingestion.** `extract_items` now decodes each `PictureItem.image.uri` data URI into raw bytes and writes them to `document_items.picture_data` so per-figure lookups don't require decompressing the full docling blob. The same path also surfaces VLM-generated picture descriptions (`meta.description.text`) into `DocumentItem.text` so picture-only chunks survive `expand_with_items`' text filter. The `0.45.0` migration adds the `picture_data` column to existing databases and backfills it by extracting bytes out of `docling_document`, stripping picture URIs from the structure blob in the process; `compress_docling_split` does the same for new ingests so the structure stays lean. Rebuild and update flows snapshot picture bytes via the new `DocumentItemRepository.get_all_picture_data` accessor before re-extraction so a re-chunk doesn't drop them.
- **Picture image bytes in search results and vision-capable QA.** `SearchResult` gains an `image_data: dict[str, str] | None` field carrying base64-encoded picture bytes keyed by `self_ref` for picture-labeled chunks. `client.search()` and the MCP `search_documents` tool gain an `include_images: bool = True` flag; set False to omit the bytes for plain-text consumers. `expand_context` now preserves picture self_refs with empty text so they aren't filtered out before reaching the image-data lookup. The agent-facing search tool (`tools/search.py`) returns `pydantic_ai.messages.ToolReturn(return_value=text, content=[BinaryContent(...), ...])` when picture data is present so a vision-capable QA model sees the figures alongside the text; otherwise it returns a plain string and non-vision flows are unchanged.

View file

@ -28,6 +28,9 @@ processing:
name: gpt-oss
enable_thinking: false
# Picture handling (none / description / image)
pictures: none # See "Picture Handling" below
# Conversion options (works with both local and remote converters)
conversion_options:
# OCR settings
@ -44,11 +47,9 @@ processing:
# Image settings
images_scale: 2.0 # Image scale factor
generate_page_images: true # Include rendered page images (for visualize_chunk)
generate_picture_images: false # Include embedded figure/diagram images
# VLM picture description (optional)
# VLM picture description settings (only effective when pictures: description)
picture_description:
enabled: false # Enable VLM image descriptions
model:
provider: ollama
name: ministral-3
@ -100,34 +101,44 @@ conversion_options:
conversion_options:
images_scale: 2.0 # Image resolution scale factor
generate_page_images: true # Include rendered page images
generate_picture_images: false # Include embedded figure/diagram images
```
- **images_scale**: Scale factor for extracted images. Higher values = better quality but larger size. Typical range: 1.0-3.0.
- **generate_page_images**: When `true` (default), rendered images of each PDF page are included in the document. Required for `visualize_chunk()` to show visual grounding. When `false`, page images are excluded to reduce document size.
- **generate_picture_images**: When `true`, embedded images (figures, diagrams) are included as base64-encoded data in the document. When `false` (default), images are excluded to reduce chunk size and avoid context bloat.
**Note:** With `docling-serve`, `generate_picture_images` has limited support - picture image data may not be returned in the JSON response. Page images work correctly with both local and remote converters.
Embedded picture extraction is controlled by `processing.pictures` (see [Picture Handling](#picture-handling) below), not by an image-settings flag.
#### Picture Description (VLM)
#### Picture Handling
Use a Vision Language Model (VLM) to automatically describe images in documents. Descriptions become searchable text, improving RAG retrieval for visual content.
`processing.pictures` is a single enum that decides how embedded picture images (figures, diagrams) are handled at ingest. Three modes:
| Mode | VLM at ingest | `picture_data` populated | Chunk text contains description |
|---|---|---|---|
| `none` (default) | no | no | no |
| `description` | yes | yes | yes |
| `image` | no | yes | no |
- **`none`**: docling skips picture-image generation. `label="picture"` rows still appear in the items table for structural metadata, but they carry no bytes and no description. Cheapest mode; non-vision QA is unaffected.
- **`description`**: docling generates picture images, the configured VLM produces a description woven into chunk text, and the bytes are also retained in `document_items.picture_data`. Picture-text is searchable via FTS, vision-capable QA models also receive the bytes via the agent's search tool. Bytes are kept (not just thrown away after the VLM runs) so a vision-only QA strategy can be turned on later without reingesting.
- **`image`**: docling generates picture images and stores them in `document_items.picture_data` without running the VLM. Vision-capable QA models reason directly about the figures; non-vision QA only sees the picture's caption/surrounding text.
```yaml
conversion_options:
picture_description:
enabled: true # Enable VLM picture description
model:
provider: ollama # ollama, openai, or custom
name: ministral-3 # VLM model name
temperature: 0.0 # Default: 0.0 (factual descriptions)
timeout: 90 # Request timeout in seconds
max_tokens: 200 # Maximum tokens in response
processing:
pictures: description # none / description / image
conversion_options:
picture_description: # only effective when pictures: description
model:
provider: ollama # ollama, openai, or custom
name: ministral-3 # VLM model name
temperature: 0.0
timeout: 90 # Request timeout in seconds
max_tokens: 200 # Maximum tokens in response
```
**Configuration options:**
**Switching modes on an existing database.** No reingest is required if you only need to change between `description` and `image` — the bytes are already there. Run `haiku-rag rebuild --rechunk` after the config change so the chunk-text composition reflects the new mode. Switching *down* to `none` clears `picture_data` on rebuild, reclaiming storage.
**`picture_description.model` configuration** (used only under `pictures: description`):
- **enabled**: When `true`, each embedded image is sent to a VLM for description. Requires `generate_picture_images` to be `true` (automatically enabled).
- **model**: Standard model configuration
- `provider`: `ollama` (default), `openai`, or use `base_url` for custom endpoints
- `name`: Model name (e.g., `ministral-3`, `granite3.2-vision`, `gpt-4-vision`)
@ -155,12 +166,13 @@ prompts:
**Using with Ollama:**
```yaml
conversion_options:
picture_description:
enabled: true
model:
provider: ollama
name: ministral-3
processing:
pictures: description
conversion_options:
picture_description:
model:
provider: ollama
name: ministral-3
```
Requires Ollama running with a vision-capable model:
@ -173,13 +185,14 @@ ollama serve
**Using with vLLM or custom endpoints:**
```yaml
conversion_options:
picture_description:
enabled: true
model:
provider: openai # Use OpenAI-compatible API format
name: granite-vision
base_url: http://my-vllm-server:8000
processing:
pictures: description
conversion_options:
picture_description:
model:
provider: openai # Use OpenAI-compatible API format
name: granite-vision
base_url: http://my-vllm-server:8000
```
**How it works:**
@ -199,12 +212,14 @@ When using `converter: docling-serve`, the VLM calls are made by the docling-ser
**Docker networking:** If docling-serve runs in Docker and your VLM runs on the host, use `host.docker.internal` instead of `localhost`:
```yaml
picture_description:
enabled: true
model:
provider: ollama
name: ministral-3
base_url: http://host.docker.internal:11434 # NOT localhost!
processing:
pictures: description
conversion_options:
picture_description:
model:
provider: ollama
name: ministral-3
base_url: http://host.docker.internal:11434 # NOT localhost!
```
See [VLM Picture Description with docling-serve](../remote-processing.md#vlm-picture-description-with-docling-serve) for a complete example.

View file

@ -119,7 +119,7 @@ prompts:
Be concise and factual.
```
The prompt is used when `processing.conversion_options.picture_description.enabled` is `true`. See [Picture Description (VLM)](processing.md#picture-description-vlm) for full configuration.
The prompt is used when `processing.pictures` is set to `description`. See [Picture Handling](processing.md#picture-handling) for full configuration.
## Programmatic Configuration

View file

@ -99,12 +99,16 @@ async def _update_document_with_chunks(
# Replace document items when a new DoclingDocument is provided.
# Snapshot existing picture bytes first so they survive the
# delete-and-re-extract cycle when the live docling has already had
# its picture URIs stripped (rebuild / round-trip scenarios).
# its picture URIs stripped (rebuild / round-trip scenarios). Under
# `pictures="none"` we skip the snapshot so updates reclaim storage.
if docling_document is not None:
keep_picture_data = client._config.processing.pictures != "none"
existing_picture_data = (
await client.document_item_repository.get_all_picture_data(
updated_doc.id
)
if keep_picture_data
else None
)
await client.document_item_repository.delete_by_document_id(updated_doc.id)
items = extract_items(

View file

@ -105,7 +105,10 @@ async def download_models(
if config.reranking.model and config.reranking.model.provider == "ollama":
required_models.add(config.reranking.model.name)
pic_desc = config.processing.conversion_options.picture_description
if pic_desc.enabled and pic_desc.model.provider == "ollama":
if (
config.processing.pictures == "description"
and pic_desc.model.provider == "ollama"
):
required_models.add(pic_desc.model.name)
if (
config.processing.auto_title

View file

@ -197,14 +197,19 @@ async def _flush_rebuild_batch(
# Repopulate document items from stored docling data. The stored docling
# blob has had its picture URIs stripped (compress_docling_split), so
# re-extracting from it would lose picture_data; snapshot the existing
# bytes per document and merge them back during extraction.
# re-extracting from it would lose picture_data; under modes that retain
# bytes (`description`/`image`) we snapshot the existing bytes per
# document and merge them back. Under `none`, we deliberately skip the
# snapshot so the rebuild reclaims storage.
keep_picture_data = client._config.processing.pictures != "none"
for doc in documents:
assert doc.id is not None
docling_doc = doc.get_docling_document()
if docling_doc is not None:
existing_picture_data = (
await client.document_item_repository.get_all_picture_data(doc.id)
if keep_picture_data
else None
)
await client.document_item_repository.delete_by_document_id(doc.id)
items = extract_items(

View file

@ -1,8 +1,11 @@
import logging
import os
from pathlib import Path
import yaml
logger = logging.getLogger(__name__)
def find_config_file(cli_path: Path | None = None) -> Path | None:
"""Find the YAML config file using the search path.
@ -44,7 +47,65 @@ def load_yaml_config(path: Path) -> dict:
"""Load and parse a YAML config file."""
with open(path) as f:
data = yaml.safe_load(f)
return data or {}
data = data or {}
_translate_legacy_picture_fields(data)
return data
def _translate_legacy_picture_fields(data: dict) -> None:
"""Map pre-A4 picture knobs onto ``processing.pictures``.
Pre-A4 the same intent was expressed by two booleans on
``conversion_options``: ``generate_picture_images`` and
``picture_description.enabled``. Translation, in priority order:
- ``picture_description.enabled = true`` (regardless of the image flag)
``pictures = "description"``. Mirrors the original behavior where
enabling the VLM implicitly forced docling to produce picture bytes.
- ``generate_picture_images = true`` (and no description) ``"image"``.
- both false / missing no translation; default ``"none"`` applies.
If ``pictures`` is already set on the loaded YAML it wins users who
have migrated keep their explicit choice. Mutates ``data`` in-place
and emits one warning per legacy field encountered.
"""
processing = data.get("processing")
if not isinstance(processing, dict):
return
if "pictures" in processing:
# User has migrated; legacy fields may still be present but should not
# override the explicit choice. Drop them silently to avoid confusion.
opts = processing.get("conversion_options")
if isinstance(opts, dict):
opts.pop("generate_picture_images", None)
pic = opts.get("picture_description")
if isinstance(pic, dict):
pic.pop("enabled", None)
return
opts = processing.get("conversion_options")
if not isinstance(opts, dict):
return
pic = opts.get("picture_description") if isinstance(opts, dict) else None
legacy_describe = pic.pop("enabled", None) if isinstance(pic, dict) else None
legacy_image = opts.pop("generate_picture_images", None)
if legacy_describe:
processing["pictures"] = "description"
logger.warning(
"Config: 'processing.conversion_options.picture_description.enabled=true' is "
"deprecated; mapped to 'processing.pictures: description'. Please update your "
"haiku.rag.yaml."
)
elif legacy_image:
processing["pictures"] = "image"
logger.warning(
"Config: 'processing.conversion_options.generate_picture_images=true' is "
"deprecated; mapped to 'processing.pictures: image'. Please update your "
"haiku.rag.yaml."
)
def generate_default_config() -> dict:

View file

@ -110,9 +110,12 @@ class AnalysisConfig(BaseModel):
class PictureDescriptionConfig(BaseModel):
"""Configuration for VLM-based picture description."""
"""Settings for the VLM that runs at ingest under ``pictures="description"``.
Whether the VLM runs at all is decided by ``ProcessingConfig.pictures``;
these fields only describe *how* it runs once enabled.
"""
enabled: bool = False
model: ModelConfig = Field(
default_factory=lambda: ModelConfig(
provider="ollama",
@ -143,14 +146,16 @@ class ConversionOptions(BaseModel):
# Image options
images_scale: float = 2.0
generate_page_images: bool = True
generate_picture_images: bool = False
# VLM picture description
# VLM picture description (only effective when ProcessingConfig.pictures == "description")
picture_description: PictureDescriptionConfig = Field(
default_factory=PictureDescriptionConfig
)
PicturesMode = Literal["none", "description", "image"]
class ProcessingConfig(BaseModel):
chunk_size: int = 256
converter: str = "docling-local"
@ -160,6 +165,18 @@ class ProcessingConfig(BaseModel):
chunking_merge_peers: bool = True
chunking_use_markdown_tables: bool = False
conversion_options: ConversionOptions = Field(default_factory=ConversionOptions)
pictures: PicturesMode = "none"
"""How embedded pictures are handled at ingest.
- ``"none"``: docling skips picture-image generation; structural
``label="picture"`` rows still exist but carry no bytes or description.
- ``"description"``: docling generates picture images, the VLM produces
text descriptions woven into chunk text, and the bytes are also
retained in ``document_items.picture_data`` so a vision-capable QA
model can be turned on later without reingesting.
- ``"image"``: docling generates picture images and stores them in
``document_items.picture_data``; no VLM runs at ingest.
"""
auto_title: bool = False
title_model: ModelConfig = Field(
default_factory=lambda: ModelConfig(

View file

@ -123,13 +123,16 @@ class DoclingLocalConverter(DocumentConverter):
opts = self.config.processing.conversion_options
pic_desc = opts.picture_description
pictures_mode = self.config.processing.pictures
wants_picture_images = pictures_mode != "none"
runs_vlm = pictures_mode == "description"
pipeline_options = PdfPipelineOptions(
do_ocr=opts.do_ocr,
do_table_structure=opts.do_table_structure,
images_scale=opts.images_scale,
generate_page_images=opts.generate_page_images,
generate_picture_images=opts.generate_picture_images or pic_desc.enabled,
generate_picture_images=wants_picture_images,
table_structure_options=TableStructureOptions(
do_cell_matching=opts.table_cell_matching,
mode=(
@ -139,10 +142,10 @@ class DoclingLocalConverter(DocumentConverter):
),
),
ocr_options=self._get_ocr_options(opts),
do_picture_description=pic_desc.enabled,
do_picture_description=runs_vlm,
)
if pic_desc.enabled:
if runs_vlm:
from pydantic import AnyUrl
prompt = self.config.prompts.picture_description

View file

@ -85,13 +85,8 @@ class DoclingServeConverter(DocumentConverter):
raise ValueError(f"Unsupported VLM provider: {model.provider}")
def _picture_images_enabled(self) -> bool:
"""Whether the conversion should produce embedded picture images.
True if the user explicitly enabled ``generate_picture_images`` or if
``picture_description.enabled`` is on (the VLM needs the picture bytes).
"""
opts = self.config.processing.conversion_options
return opts.generate_picture_images or opts.picture_description.enabled
"""Whether the conversion should produce embedded picture images."""
return self.config.processing.pictures != "none"
def _build_conversion_data(self) -> dict[str, str | list[str]]:
"""Build form data for conversion request.
@ -108,7 +103,9 @@ class DoclingServeConverter(DocumentConverter):
"""
opts = self.config.processing.conversion_options
pic_desc = opts.picture_description
picture_images_enabled = self._picture_images_enabled()
pictures_mode = self.config.processing.pictures
picture_images_enabled = pictures_mode != "none"
runs_vlm = pictures_mode == "description"
if picture_images_enabled:
image_export_mode = "referenced"
@ -128,7 +125,7 @@ class DoclingServeConverter(DocumentConverter):
"images_scale": str(opts.images_scale),
"image_export_mode": image_export_mode,
"include_images": str(picture_images_enabled).lower(),
"do_picture_description": str(pic_desc.enabled).lower(),
"do_picture_description": str(runs_vlm).lower(),
}
if picture_images_enabled:
@ -137,7 +134,7 @@ class DoclingServeConverter(DocumentConverter):
if opts.ocr_lang:
data["ocr_lang"] = opts.ocr_lang
if pic_desc.enabled:
if runs_vlm:
prompt = self.config.prompts.picture_description
picture_description_api = {
"url": self._get_vlm_api_url(pic_desc.model),

View file

@ -431,6 +431,8 @@ class TestPictureDataStorage:
async with HaikuRAG(temp_db_path, create=True) as rag:
schema = await rag.store.document_items_table.schema()
assert "picture_data" in {f.name for f in schema}
def _docling_doc_with_picture():
"""Build a tiny DoclingDocument with one PictureItem carrying real PNG bytes
via ImageRef.from_pil. Used by the picture-extraction tests."""
@ -634,6 +636,8 @@ class TestPictureDataPreservedThroughRoundTrip:
docling_doc = _docling_doc_with_picture()
async with HaikuRAG(temp_db_path, create=True) as rag:
# Preservation only kicks in under modes that retain picture bytes.
rag._config.processing.pictures = "image"
document = Document(content="Hello world", uri="test://doc")
document.set_docling(docling_doc)
created = await _store_document_with_chunks(rag, document, [], docling_doc)

View file

@ -225,3 +225,92 @@ def test_init_config_creates_valid_yaml(tmp_path):
# Validate it
config = AppConfig.model_validate(loaded_data)
assert config.environment == "production"
# A4: legacy `generate_picture_images` + `picture_description.enabled` translation
def _write(tmp_path, body: str):
p = tmp_path / "haiku.rag.yaml"
p.write_text(body)
return p
def test_load_yaml_legacy_picture_description_maps_to_description(tmp_path, caplog):
"""`picture_description.enabled=true` (with or without the image flag)
maps to `processing.pictures: description`."""
config_file = _write(
tmp_path,
"""
processing:
conversion_options:
generate_picture_images: false
picture_description:
enabled: true
timeout: 120
""",
)
with caplog.at_level("WARNING", logger="haiku.rag.config.loader"):
data = load_yaml_config(config_file)
cfg = AppConfig.model_validate(data)
assert cfg.processing.pictures == "description"
assert cfg.processing.conversion_options.picture_description.timeout == 120
assert any("picture_description.enabled=true" in m.message for m in caplog.records)
def test_load_yaml_legacy_generate_picture_images_maps_to_image(tmp_path, caplog):
"""`generate_picture_images=true` alone maps to `pictures: image`."""
config_file = _write(
tmp_path,
"""
processing:
conversion_options:
generate_picture_images: true
""",
)
with caplog.at_level("WARNING", logger="haiku.rag.config.loader"):
data = load_yaml_config(config_file)
cfg = AppConfig.model_validate(data)
assert cfg.processing.pictures == "image"
assert any("generate_picture_images=true" in m.message for m in caplog.records)
def test_load_yaml_no_legacy_fields_keeps_default_none(tmp_path, caplog):
"""Empty processing block leaves the default `none` mode untouched and
does not warn."""
config_file = _write(
tmp_path,
"""
processing:
chunk_size: 256
""",
)
with caplog.at_level("WARNING", logger="haiku.rag.config.loader"):
data = load_yaml_config(config_file)
cfg = AppConfig.model_validate(data)
assert cfg.processing.pictures == "none"
assert not caplog.records
def test_load_yaml_explicit_pictures_wins_over_legacy(tmp_path):
"""When the user has migrated to `pictures: ...` we keep their choice
and silently drop legacy fields if both are present (e.g. from a
half-migrated config)."""
config_file = _write(
tmp_path,
"""
processing:
pictures: image
conversion_options:
generate_picture_images: false
picture_description:
enabled: true
""",
)
data = load_yaml_config(config_file)
cfg = AppConfig.model_validate(data)
assert cfg.processing.pictures == "image"
# Legacy fields scrubbed so Pydantic validation doesn't trip on extras.
opts = data["processing"]["conversion_options"]
assert "generate_picture_images" not in opts
assert "enabled" not in opts.get("picture_description", {})

View file

@ -378,7 +378,7 @@ class TestDoclingLocalConverter:
async def test_convert_pdf_without_picture_images(self, config):
"""Test PDF conversion excludes embedded images by default."""
pdf_path = Path("tests/data/doclaynet.pdf")
config.processing.conversion_options.generate_picture_images = False
config.processing.pictures = "none"
converter = DoclingLocalConverter(config)
doc = await converter.convert_file(pdf_path)
@ -387,14 +387,14 @@ class TestDoclingLocalConverter:
# Check that pictures don't have image data
for picture in doc.pictures:
assert picture.image is None, (
"Pictures should not have image data when generate_picture_images=False"
'Pictures should not have image data when pictures="none"'
)
@pytest.mark.asyncio
async def test_convert_pdf_with_picture_images(self, config):
"""Test PDF conversion includes embedded images when enabled."""
pdf_path = Path("tests/data/doclaynet.pdf")
config.processing.conversion_options.generate_picture_images = True
config.processing.pictures = "image"
converter = DoclingLocalConverter(config)
doc = await converter.convert_file(pdf_path)
@ -404,7 +404,7 @@ class TestDoclingLocalConverter:
pictures_with_images = [p for p in doc.pictures if p.image is not None]
if doc.pictures:
assert len(pictures_with_images) > 0, (
"Pictures should have image data when generate_picture_images=True"
'Pictures should have image data when pictures="image"'
)
@pytest.mark.asyncio
@ -549,7 +549,7 @@ class TestDoclingLocalConverter:
def test_picture_description_config_defaults(self, config):
"""Test that picture description config has correct defaults."""
assert config.processing.conversion_options.picture_description.enabled is False
assert config.processing.pictures == "none"
assert (
config.processing.conversion_options.picture_description.model.provider
== "ollama"
@ -567,12 +567,12 @@ class TestDoclingLocalConverter:
def test_picture_description_config_applied(self, config):
"""Test that picture description config is applied to converter."""
config.processing.conversion_options.picture_description.enabled = True
config.processing.pictures = "description"
config.processing.conversion_options.picture_description.timeout = 120
converter = DoclingLocalConverter(config)
assert converter.config.processing.pictures == "description"
pic_desc = converter.config.processing.conversion_options.picture_description
assert pic_desc.enabled is True
assert pic_desc.timeout == 120
@pytest.mark.asyncio
@ -584,7 +584,7 @@ class TestDoclingLocalConverter:
# Disable OCR (not needed for native PDF, avoids model downloads)
config.processing.conversion_options.do_ocr = False
# Enable picture description with Ollama
config.processing.conversion_options.picture_description.enabled = True
config.processing.pictures = "description"
config.processing.conversion_options.picture_description.model.provider = (
"ollama"
)
@ -700,7 +700,7 @@ class TestDoclingServeConverter:
config.processing.conversion_options.table_cell_matching = False
config.processing.conversion_options.do_table_structure = False
config.processing.conversion_options.images_scale = 3.0
config.processing.conversion_options.generate_picture_images = False
config.processing.pictures = "none"
converter = DoclingServeConverter(config)
doc_json = create_mock_docling_document("test")
@ -754,11 +754,11 @@ class TestDoclingServeConverter:
@pytest.mark.asyncio
async def test_picture_images_request_uses_referenced_zip(self, config):
"""When generate_picture_images is on, the request flips to
"""When pictures="image" the request flips to
image_export_mode=referenced + target_type=zip and consumes a zip
response mirrors the upstream docling-serve#576 workaround.
"""
config.processing.conversion_options.generate_picture_images = True
config.processing.pictures = "image"
converter = DoclingServeConverter(config)
doc_json = create_mock_docling_document("test")
@ -1038,13 +1038,13 @@ class TestDoclingServeConverterPictureDescription:
async def test_picture_description_options_passed_to_api(self, config):
"""Test that picture description options are passed to docling-serve API.
Picture descriptions force ``generate_picture_images=True`` upstream,
which routes the request through the ``target_type=zip`` path so the
VLM can see the actual figures. The test mocks the zip workflow.
``pictures="description"`` requires picture images for the VLM, which
routes the request through the ``target_type=zip`` path. The test
mocks the zip workflow.
"""
import json
config.processing.conversion_options.picture_description.enabled = True
config.processing.pictures = "description"
config.processing.conversion_options.picture_description.model.provider = (
"ollama"
)
@ -1155,7 +1155,7 @@ class TestDoclingServeConverterIntegration:
Note: Not using VCR because this test involves polling with changing task IDs.
"""
pdf_path = Path("tests/data/doclaynet.pdf")
config.processing.conversion_options.picture_description.enabled = True
config.processing.pictures = "description"
config.processing.conversion_options.picture_description.model.provider = (
"ollama"
)
@ -1228,7 +1228,7 @@ class TestDoclingServeConverterIntegration:
async def test_convert_pdf_without_picture_images(self, config):
"""Test PDF conversion excludes picture images when disabled."""
pdf_path = Path("tests/data/doclaynet.pdf")
config.processing.conversion_options.generate_picture_images = False
config.processing.pictures = "none"
converter = DoclingServeConverter(config)
doc = await converter.convert_file(pdf_path)
@ -1237,7 +1237,7 @@ class TestDoclingServeConverterIntegration:
# Check that pictures don't have image data
for picture in doc.pictures:
assert picture.image is None, (
"Pictures should not have image data when generate_picture_images=False"
'Pictures should not have image data when pictures="none"'
)
@pytest.mark.vcr()
@ -1266,7 +1266,7 @@ class TestDoclingServeConverterIntegration:
URIs so the result is shape-equivalent to the local converter.
"""
pdf_path = Path("tests/data/doclaynet.pdf")
config.processing.conversion_options.generate_picture_images = True
config.processing.pictures = "image"
converter = DoclingServeConverter(config)
doc = await converter.convert_file(pdf_path)
@ -1275,7 +1275,7 @@ class TestDoclingServeConverterIntegration:
pictures_with_images = [p for p in doc.pictures if p.image is not None]
assert doc.pictures, "doclaynet.pdf is expected to contain at least one picture"
assert len(pictures_with_images) > 0, (
"Pictures should have image data when generate_picture_images=True"
'Pictures should have image data when pictures="image"'
)
sample = pictures_with_images[0]
assert sample.image is not None

View file

@ -166,6 +166,42 @@ async def test_expand_context_preserves_picture_refs_with_empty_text(temp_db_pat
assert "picture" in out.labels
@pytest.mark.asyncio
async def test_update_clears_picture_data_when_mode_none(temp_db_path):
"""Switching to ``pictures="none"`` and re-running update_document
drops picture_data the snapshot/merge gate is what gives users a
"rebuild reclaims storage" path when they downgrade modes."""
from haiku.rag.client.documents import (
_store_document_with_chunks,
_update_document_with_chunks,
)
from haiku.rag.store.models.document import Document
from tests.store.test_document_items import _docling_doc_with_picture
docling_doc = _docling_doc_with_picture()
async with HaikuRAG(temp_db_path, create=True) as rag:
# Ingest under "image" so picture bytes land in document_items.
rag._config.processing.pictures = "image"
document = Document(content="x", uri="test://doc")
document.set_docling(docling_doc)
created = await _store_document_with_chunks(rag, document, [], docling_doc)
assert created.id is not None
before = await rag.document_item_repository.get_all_picture_data(created.id)
assert before.get("#/pictures/0") is not None
# Downgrade to "none" and re-run update with the (already stripped)
# docling pulled from storage. The snapshot/merge must be skipped so
# picture_data is cleared on the new items rows.
rag._config.processing.pictures = "none"
from_blob = created.get_docling_document()
assert from_blob is not None
await _update_document_with_chunks(rag, created, [], from_blob)
after = await rag.document_item_repository.get_all_picture_data(created.id)
assert after.get("#/pictures/0") is None
@pytest.mark.asyncio
async def test_expand_context_repopulates_image_data(temp_db_path):
"""expand_context rebuilds SearchResult objects via expand_with_items, so