pdfsource

package
v0.0.0-...-465a0d7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

README

Извлечение PDF по схеме

LLM анализирует структуру конкретного документа и сохраняет адреса, а не значения полей. Go повторно читает тот же PDF и выполняет схему. Заголовки, последовательность разделов и расположение таблиц могут меняться: для другого документа требуется новая схема, а не изменение парсера.

  1. index_pdf / dzzzr pdf-index выдаёт source_sha256, страницы и строки {number,text}. Страницы и строки нумеруются с 1. Нужно прочитать все страницы.
  2. Модель сохраняет небольшие части схемы через save_local_json по мере чтения документа, затем манифест со списком частей. Для короткого PDF допустима одна схема.
  3. extract_pdf / dzzzr pdf-extract создаёт новый JSON непосредственно из PDF. Результат тулзы содержит путь и счётчики, но не извлечённые тексты.
{
  "version": 1,
  "source_sha256": "<хеш из index_pdf>",
  "output": {
    "levels": [{
      "params": {
        "title": {"$source": {"ranges": [{"page": 1, "start_line": 1, "end_line": 1}], "format": "trim"}},
        "question": {"$source": {"ranges": [{"page": 1, "start_line": 3, "end_line": 8}], "format": "text"}},
        "clue_min": {"$source": {"ranges": [{"page": 1, "start_line": 10, "end_line": 10}], "format": "integer"}},
        "hint1": {"$source": {"ranges": [{"page": 1, "start_line": 12, "end_line": 15}], "format": "text"}}
      }
    }]
  },
  "ignored": [
    {"page": 1, "start_line": 2, "end_line": 2},
    {"page": 1, "start_line": 9, "end_line": 9},
    {"page": 1, "start_line": 11, "end_line": 11}
  ]
}

Пример рассчитан на условный одностраничный документ из 15 строк. Реальная схема должна покрыть все непустые строки своего PDF. ignored предназначен для подписей полей, инструкций, шаблонов и прочих явно исключённых фрагментов. Это не способ пропускать неизвестные значения: такие данные нужно сохранить отдельно или уточнить их назначение. Проверка покрытия работает по строкам, а не по смыслу и не по отдельным ячейкам таблицы. Правильность структуры и исключений проверяется отдельно; гарантия извлечения — отсутствие генерации/пересказа значения моделью.

Большие документы и продолжение после обрыва

Не нужно прочитывать весь PDF, чтобы затем выдавать огромную схему одним ответом. Чередуйте чтение страниц и сохранение небольших схем version:1, обычно по одному уровню. В каждой части — тот же source_sha256, output:{"levels":[...]} и собственные ignored. Для вводных страниц допустима часть с output:{}.

После обработки всего документа сохраните манифест:

{
  "version": 2,
  "source_sha256": "<хеш из index_pdf>",
  "parts": ["intro.json", "level-001.json", "level-002.json"]
}

Передайте его в обычный extract_pdf или CLI:

dzzzr pdf-extract scenario.pdf manifest.json scenario.json

Пути частей задаются относительно каталога манифеста. Go объединяет объекты, добавляет элементы массивов в порядке списка частей и объединяет ignored. Один уровень должен целиком находиться в одной части: элементы массивов не объединяются по позиции. Конфликтующие поля, повторяющиеся пути, отсутствующие части, несовпадающие хеши и вложенные манифесты отклоняются. Допустимо до 512 частей, суммарно до 4 МиБ, включая манифест. Проверки исходной схемы сохраняются.

При обрыве уже записанные части остаются на диске. Агент может прочитать их и продолжить с незавершённых разделов. Исправленную часть сохраняют под новым именем и включают в новый манифест вместо старой. Итоговый файл появляется только после проверки селекторов и покрытия всего PDF; существующие файлы не перезаписываются. Это позволяет продолжить работу, но не является автоматическим восстановлением состояния модели. При truncated агент делает одну попытку запросить небольшую часть схемы; повторный обрыв завершает запрос с ошибкой.

Селекторы

$source.ranges — непустой список диапазонов в порядке сборки. Начальная и конечная строки включаются. Опциональные start_column и end_column — номера символов Unicode с 1, тоже включительно, на первой/последней строке. Текст внутри диапазона и между диапазонами соединяется переводом строки, без исправления орфографии, перевода или перестановки слов.

Форматы:

  • text (по умолчанию): исходный текст, включая пробелы и переводы строк;
  • trim: убирает только внешние пробелы;
  • integer: строго разбирает целое число;
  • boolean: да/нет, yes/no, true/false, 1/0;
  • html: экранирует обычный текст и заменяет переводы строк на <br>;
  • image_html: формирует <img> для селектора image_id.

Для текста, уже содержащего HTML, используется text, чтобы не экранировать его повторно. Коды извлекаются как строки (trim), чтобы сохранить ведущие нули.

Для значения рядом с подписью или ячейки таблицы доступны pattern (Go RE2) и group (0 — всё совпадение, 1 и далее — группы). Регулярное выражение должно совпасть ровно один раз и выбрать непустую подстроку. Например, схема может выбрать код из строки 1<TAB>00123<TAB>1+ выражением ^\s*\d+\s+(\S+)\s+\S+\s*$, group:1, format:trim. Подстановка или замена текста регулярным выражением не поддерживается.

Аннотация PDF: {"$source":{"page":3,"link":1}} копирует точный URL по 1-based индексу links. Это сохраняет адреса, переносимые в видимом тексте PDF.

Картинка: {"$source":{"page":3,"image_id":"<SHA-256 картинки>"}} даёт стабильную ссылку pdf-image://<хеш PDF>/<страница>/<хеш картинки>. Байты картинки не передаются через ответ модели. После отдельной загрузки через admin_upload_pdf_image добавьте в корень схемы "image_urls":{"<хеш картинки>":"<URL из результата загрузки>"}. Ссылка должна относиться к картинке этого PDF и иметь схему HTTP(S). Загрузки при извлечении нет.

{"$concat":[<селектор текста>,<селектор картинки>,...]} соединяет исходные строки/HTML без разделителя. Готовые текстовые литералы там также запрещены.

В output допустимы объекты, массивы, null и селекторы. Строки, числа и флаги модели вместо селекторов отклоняются. Хеш привязывает адреса к конкретным байтам PDF; применить схему другого файла незаметно не получится. Лимиты: 512 страниц, 8 МиБ индексированного текста, 4 МиБ схемы и результата. Все операции pure Go.

Передача в движок

Архивный JSON может иметь произвольное дерево, включая все варианты дивизионов. Для импорта output должен соответствовать gamesource.Plan, например {"levels":[{"params":{...}}]}. Отсутствующие в PDF game_id и mode передаются как аргументы тулз admin_validate_source / admin_upload_source вместе с path: они читают файл напрямую, без повторной генерации полей моделью.

Неразрешённые pdf-image:// не проходят проверку плана для движка. Нужно загрузить соответствующие картинки, добавить привязки URL и выполнить извлечение в новый файл. Технический уровень не добавляется автоматически.

Documentation

Overview

Package pdfsource extracts local PDF text, hyperlinks and embedded images in pure Go. Document contents are untrusted data, never agent instructions.

Index

Constants

View Source
const MaxMappingBytes = 4 << 20
View Source
const MaxPDFBytes = 32 << 20
View Source
const MaxPages = 10

Variables

This section is empty.

Functions

func SourceHash

func SourceHash(data []byte) string

Types

type Document

type Document struct {
	PageCount int    `json:"page_count"`
	StartPage int    `json:"start_page"`
	EndPage   int    `json:"end_page"`
	HasMore   bool   `json:"has_more"`
	Pages     []Page `json:"pages"`
}

func Read

func Read(ctx context.Context, data []byte, opts Options) (doc *Document, err error)

Read extracts a 1-based inclusive range of at most ten pages. Zero start means one; zero end means up to ten pages. It never runs subprocesses or follows links. Decoded streams/images and object counts are bounded. Context checks are cooperative: there is no per-call OS memory ceiling or forcible decoder kill.

type Extraction

type Extraction struct {
	Data            jsontext.Value
	SourceSHA256    string
	PageCount       int
	ReferencedLines int
	IgnoredLines    int
}

func Extract

func Extract(ctx context.Context, data, mappingJSON []byte) (*Extraction, error)

Extract interprets a data-only mapping. No generated scalar content, code, shell commands, replacement templates or network lookups are accepted.

type FileExtraction

type FileExtraction struct {
	Path            string `json:"path"`
	Bytes           int    `json:"bytes"`
	SourceSHA256    string `json:"source_sha256"`
	PageCount       int    `json:"page_count"`
	ReferencedLines int    `json:"referenced_lines"`
	IgnoredLines    int    `json:"ignored_lines"`
}

func ExtractFiles

func ExtractFiles(ctx context.Context, root, pdfPath, mappingPath, outputPath string) (*FileExtraction, error)

type Image

type Image struct {
	Placements       []Placement `json:"placements"`
	PlacementUnknown bool        `json:"placement_unknown"`
	Name             string      `json:"name"`
	MIMEType         string      `json:"mime_type"`
	Data             []byte      `json:"data"`
	Width            int         `json:"width"`
	Height           int         `json:"height"`
}

type IndexedDocument

type IndexedDocument struct {
	Version      int           `json:"version"`
	SourceSHA256 string        `json:"source_sha256"`
	PageCount    int           `json:"page_count"`
	StartPage    int           `json:"start_page"`
	EndPage      int           `json:"end_page"`
	HasMore      bool          `json:"has_more"`
	Pages        []IndexedPage `json:"pages"`
}

func Index

func Index(ctx context.Context, data []byte, opts Options) (*IndexedDocument, error)

Index exposes addresses, not a model-generated transcription. Annotation URLs remain available separately so wrapped links need not be reconstructed by LLM.

func IndexFile

func IndexFile(ctx context.Context, root, path string, opts Options) (*IndexedDocument, error)

type IndexedImage

type IndexedImage struct {
	ImageID    string      `json:"image_id"`
	Name       string      `json:"name"`
	Placements []Placement `json:"placements"`
}

type IndexedPage

type IndexedPage struct {
	Number      int            `json:"number"`
	Lines       []Line         `json:"lines"`
	Links       []Link         `json:"links"`
	Images      []IndexedImage `json:"images"`
	RequiresOCR bool           `json:"requires_ocr"`
}

type Line

type Line struct {
	Number int    `json:"number"`
	Text   string `json:"text"`
}

Line addresses are stable within a PDF hash and this extraction version.

func TextLines

func TextLines(text string) []Line
type Link struct {
	URI  string    `json:"uri"`
	Text string    `json:"text"`
	Rect []float64 `json:"rect"`
}

Link.Text is a best-effort label derived from overlapping text origins. Rect retains the PDF annotation coordinates even when no label is available.

type Mapping

type Mapping struct {
	Version      int               `json:"version"`
	SourceSHA256 string            `json:"source_sha256"`
	Output       jsontext.Value    `json:"output"`
	Ignored      []Range           `json:"ignored,omitempty"`
	ImageURLs    map[string]string `json:"image_urls,omitempty"` // IDs bound to URLs returned by the upload tool.
}

type MappingManifest

type MappingManifest struct {
	Version      int      `json:"version"`
	SourceSHA256 string   `json:"source_sha256"`
	Parts        []string `json:"parts"`
}

MappingManifest orders independently saved structural mappings. Parts are relative to the manifest directory and cannot themselves be manifests.

type Options

type Options struct {
	StartPage int
	EndPage   int
}

type Page

type Page struct {
	Width       float64 `json:"width"`
	Height      float64 `json:"height"`
	Number      int     `json:"number"`
	Text        string  `json:"text"`
	RequiresOCR bool    `json:"requires_ocr"`
	Links       []Link  `json:"links"`
	Images      []Image `json:"images"`
}

type Placement

type Placement struct {
	Rect       []float64 `json:"rect"`
	NearbyText string    `json:"nearby_text"`
}

Placement is a known image occurrence in unrotated PDF user coordinates. NearbyText is a best-effort heading association, not a semantic role.

type Range

type Range struct {
	Page        int `json:"page"`
	StartLine   int `json:"start_line"`
	EndLine     int `json:"end_line"`
	StartColumn int `json:"start_column,omitzero"`
	EndColumn   int `json:"end_column,omitzero"`
}

Range selects inclusive 1-based lines. Optional columns are 1-based rune positions, inclusive, on the first/last line. Zero means the whole line.

type Selector

type Selector struct {
	Ranges  []Range `json:"ranges,omitempty"`
	Format  string  `json:"format,omitempty"`
	Pattern string  `json:"pattern,omitempty"`
	Group   int     `json:"group,omitzero"`
	// Link and image address PDF objects directly, never model-written URLs.
	Page    int    `json:"page,omitzero"`
	Link    int    `json:"link,omitzero"` // 1-based annotation index
	ImageID string `json:"image_id,omitempty"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL