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 будет отражать сгенерированное содержимое.