2026, Jan 06 15:02

Почему экспорт Docling пустой и как правильно вызывать load_from_doctags

Почему в Docling получается пустой экспорт после DocTags SmolDocling: load_from_doctags — фабричный метод. Даем рабочий пример кода и быстрый фикс Markdown.

Экспорт структурированного текста после успешного мультимодального прогона может неожиданно оказаться непростым. Частая ловушка при преобразовании результата SmolDocling в документ Docling — получить пустой экспорт, хотя содержимое DocTags выглядит корректно и ошибок не возникает. Ниже — минимальный, воспроизводимый сценарий и способ его исправить.

Как воспроизвести проблему

Конвейер генерирует валидные DocTags из JPEG с таблицей. Декодирование токенов дает корректную строку DocTags. Пустой результат появляется на последнем шаге преобразования перед экспортом.

import torch
from transformers import AutoConfig, AutoProcessor
from transformers.image_utils import load_image
import onnxruntime
import numpy as np
import os
from docling_core.types.doc import DoclingDocument
from docling_core.types.doc.document import DocTagsDocument
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["ORT_CUDA_USE_MAX_WORKSPACE"] = "1"
# 1. Загрузка моделей
model_ref = "ds4sd/SmolDocling-256M-preview"
cfg = AutoConfig.from_pretrained(model_ref)
proc = AutoProcessor.from_pretrained(model_ref)
# 2. Сеансы ONNX (CPU)
sess_vis = onnxruntime.InferenceSession("./models/smoldocling/vision_encoder.onnx")
sess_tok = onnxruntime.InferenceSession("./models/smoldocling/embed_tokens.onnx")
sess_dec = onnxruntime.InferenceSession("./models/smoldocling/decoder_model_merged.onnx")
# 3. Параметры конфигурации
kv_heads = cfg.text_config.num_key_value_heads
dim_head = cfg.text_config.head_dim
layers_hidden = cfg.text_config.num_hidden_layers
id_eos = cfg.text_config.eos_token_id
id_imgtok = cfg.image_token_id
id_eou = proc.tokenizer.convert_tokens_to_ids("<end_of_utterance>")
# 4. Входные данные
chat_msgs = [
    {
        "role": "user",
        "content": [
            {"type": "image"},
            {"type": "text", "text": "Convert this page to docling."}
        ]
    },
]
pic = load_image("./data/image-with-table.jpeg")
tmpl = proc.apply_chat_template(chat_msgs, add_generation_prompt=True)
batch_inputs = proc(text=tmpl, images=[pic], return_tensors="np")
bsz = batch_inputs["input_ids"].shape[0]
mem_kv = {
    f"past_key_values.{layer}.{kv}": np.zeros([bsz, kv_heads, 0, dim_head], dtype=np.float32)
    for layer in range(layers_hidden)
    for kv in ("key", "value")
}
img_feats = None
ids_in = batch_inputs["input_ids"]
mask_attn = batch_inputs["attention_mask"]
pos_idx = np.cumsum(batch_inputs["attention_mask"], axis=-1)
# 5. Цикл генерации
max_new = 8192
out_tokens = np.array([[]], dtype=np.int64)
for _ in range(max_new):
    embeds = sess_tok.run(None, {"input_ids": ids_in})[0]
    if img_feats is None:
        img_feats = sess_vis.run(
            ["image_features"],
            {
                "pixel_values": batch_inputs["pixel_values"],
                "pixel_attention_mask": batch_inputs["pixel_attention_mask"].astype(np.bool_)
            }
        )[0]
        embeds[batch_inputs["input_ids"] == id_imgtok] = img_feats.reshape(-1, img_feats.shape[-1])
    logits, *present = sess_dec.run(None, dict(
        inputs_embeds=embeds,
        attention_mask=mask_attn,
        position_ids=pos_idx,
        **mem_kv,
    ))
    ids_in = logits[:, -1].argmax(-1, keepdims=True)
    mask_attn = np.ones_like(ids_in)
    pos_idx = pos_idx[:, -1:] + 1
    for j, key in enumerate(mem_kv):
        mem_kv[key] = present[j]
    out_tokens = np.concatenate([out_tokens, ids_in], axis=-1)
    if (ids_in == id_eos).all() or (ids_in == id_eou).all():
        break
# 6. Декодирование в DocTags
doc_markup = proc.batch_decode(out_tokens, skip_special_tokens=False)[0].lstrip()
print(doc_markup)  # Видно, выглядит корректно
# 7. Сформировать DocTagsDocument и попробовать экспортировать
dt_doc = DocTagsDocument.from_doctags_and_image_pairs([doc_markup], [pic])
print(doc_markup)
paper = DoclingDocument(name="Document")
paper.load_from_doctags(
    doctag_document=dt_doc,
    document_name="Document"
)
print(paper.export_to_markdown())  # Пустая строка

Что на самом деле происходит

Вызов преобразования, который выглядит как мутирующий метод, на самом деле ничего не изменяет. DoclingDocument.load_from_doctags — это статический конструктор. Он возвращает новый, заполненный экземпляр документа и не заполняет тот экземпляр, на котором вы его вызвали. Поскольку исходный объект остается нетронутым, экспорт из него дает пустую строку без предупреждений и ошибок.

Как исправить

Относитесь к load_from_doctags как к фабричному методу и сохраняйте возвращаемый документ. Не вызывайте его на заранее созданном экземпляре в расчете на заполнение «на месте».

paper = DoclingDocument.load_from_doctags(
    doctag_document=dt_doc,
    document_name="Document"
)
print(paper.export_to_markdown())

Почему это важно

Такой «тихий» no-op легко пропустить в конвейерах извлечения данных, особенно когда на предыдущих шагах видны хорошие промежуточные артефакты вроде DocTags. Осознание, что некоторые API предоставляют статические конструкторы с названиями, похожими на методы экземпляра, помогает не гоняться за фантомными ошибками в инференсе модели, сеансах ONNX, декодировании токенов или предобработке изображений, когда истинная причина — немутирующий вызов.

Вывод

Если строка DocTags корректна, а экспорт пустой, убедитесь, что вы присваиваете результат DoclingDocument.load_from_doctags новой переменной документа. Как только вы сохраните возвращаемое значение, экспорт в Markdown будет отражать сгенерированное содержимое.