⟨/⟩ 60_Epics/CardExtraction/EPIC_CardExtraction.cs

287 строк · в начало

using Ban.Sdaid.Notation.Documents;

namespace Ban.Sdaid.Icd.Epics.CardExtraction
{
    /// <summary>Эпик «Извлечение атрибутов ИК (LLM)» — снимки бумажной информационной карты → структурированный JSON с уверенностью и привязкой к источнику.</summary>
    public class EPIC_CardExtraction : IEpicDocument
    {
        public string Name => "Извлечение атрибутов ИК (LLM)";

        public string Description =>
            @"Из снимков бумажной информационной карты документального памятника (фонд ОФО БАН) извлекаются структурированные атрибуты для последующей проверки оператором и выгрузки в АБИС. Носитель — фиксированный бланк: данные внесены рукописно, впечаткой или отметками в матрицах чекбоксов. Канонический промт и схема ответа версионируются здесь; сервис извлечения хранит копию и указывает версию промта в ответе.";

        public string Version => "0.3";
        public string Status => "draft";
        public string[] Comments => new[]
        {
            "2026-08-17. Первичный черновик промта v0 по образцу llm-bib-extractor (biblioservice-dispatcher). Разобрана книжная ИК (2 стороны: описание+консервация / сохранность). Состав полей и схема — черновой, будет скорректирован на реальных прогонах.",
            "2026-08-17. Согласовано с доменным словарём: вид карты — InformationCardKind (не свой card_type), роли — ImageRole.Front/Back, идентификация — 4 значения (шифр, инв. номер, место хранения, дата).",
            "2026-08-17. Состав полей сверен на папках 7487 и 76463: оборот «Сохранность» стабилен; библиоблок реально заполняется; добавлены form_title и поле «Конволют»; учтены ≥2 варианта бланка (маппинг по смыслу).",
            "2026-08-17. Прогон промта v0 на реальных картах: усилено правило 2 — сохранять дореформенную орфографию буква в букву (по итогу 76463 «Дневникъ войны»).",
            "2026-08-18. Координаты источника переведены с долей 0..1 на целочисленную шкалу 0..1000 (родная конвенция VLM — рамки прилегают заметно точнее, замерено на карте 76487: доли «плывут», особенно в большом ответе); bbox добавлен на КАЖДУЮ строку матрицы «Задано/Выполнено» — для подсветки конкретной отметки; prompt_version v0→v1. Формализация ответа — строгая JSON Schema через structured output (response_format json_schema, strict), поддержана сервером (llama.cpp/vLLM, проверено).",
            "2026-08-18. Схема сделана РОДОВОЙ (kind-agnostic, путь B): жёсткий вид-специфичный `description` заменён на родовой массив `fields:[{name,value,…}]` со свободными именами; identity и общие блоки (conservation_marks/preservation) остаются типизированными. Причина: строгая схема одного вида не вернула бы другие виды ИК, а FR-003 определяет `card_kind` В ХОДЕ извлечения (единый вызов, вид — выход), поэтому per-kind-схема потребовала бы два прохода. Вид-специфичная типизация — на слое проверки/маппинга в АБИС. prompt_version v1→v2.",
            "2026-08-19. Сверено с переработанным доменом: enum'ы InformationCardKind/ImageRole удалены. Значение `card_kind` (7 видов) — теперь FundDocumentKind; «роль/сторона» кадра — страница формы (FormPage) распознанной DocumentForm (ADR-006/007). Схема ответа (Front/Back как метки страниц) не меняется; точное кодирование `card_kind` — открытый вопрос OQ-CE-8.",
        };

        public static string S1_Goal = """
            ## Цель

            Превратить снимки бумажной информационной карты (ИК) в структурированный JSON: значения атрибутов,
            состояние отметок бланка, уверенность по каждому полю и привязка значения к фрагменту изображения.
            Результат предзаполняет карточку на проверке полей; истину устанавливает оператор (см. EPIC «Обработка пакета»).

            Промт **model-agnostic**: одна и та же схема ответа для облачной VLM (пилот), локальной модели и связки
            OCR + LLM. Извлечение НЕ принимает решений — только предзаполняет поля.
            """;

        public static string S2_Actors = """
            ## Акторы

            | Актор | Роль в эпике |
            |---|---|
            | Сервис извлечения | по снимкам ИК формирует JSON атрибутов + уверенность + источники (bbox) |
            | Оператор проверки | сверяет предзаполненные поля со снимками, правит, подтверждает (эпик «Обработка пакета») |
            """;

        public static string S3_Scope = """
            ## Границы

            **Входит:** канонический промт извлечения, схема ответа (состав полей, представление чекбоксов),
            роли изображений сторон карты, правила уверенности и привязки к источнику.

            **Не входит:** предобработка/нормализация снимков, проверка и корректировка оператором, формирование
            записи АБИС, версионирование записей — это соседние эпики/требования.

            Схема ответа **родовая (kind-agnostic)** — один контракт на все виды `FundDocumentKind` (издание —
            книга/листовой/карта/вложение; рукопись — книга/свиток/лист): вид-специфичные текстовые поля идут родовым
            массивом `fields[]` со свободными именами, а `card_kind` определяет модель (FR-003). Детально на реальных
            бланках проработан пока `EditionBook`; прочие виды укладываются в тот же контракт без изменения схемы —
            добавляется лишь понимание их полей в промте/маппинге. Блоки СОХРАННОСТЬ/КОНСЕРВАЦИЯ типизированы и
            переиспользуются между видами.
            """;

        public static string S4_ImageRoles = """
            ## Страницы формы

            Кадр относится к **странице распознанной формы** (`FormPage` формы `DocumentForm`); прежней роли кадра
            из перечисления (`ImageRole`) больше нет — её место заняла страница формы (ADR-007). Форма книжной ИК
            (`FundDocumentKind.EditionBook`) — **две страницы** (стороны листа). Главной страницы НЕТ — у каждой
            свой набор полей; извлечение работает по тем страницам, что переданы.

            | Страница | Заголовок | Что извлекается |
            |---|---|---|
            | `Front` | «Описание документального памятника» | шапка (шифр, инв. номер, место хранения, дата), библиоописание, провенанс, матрица «Отметка о наличии элемента данных» (консервация) |
            | `Back` | «Сохранность» | размеры, состояние/материалы переплёта и блока, повреждения, мониторинг, комментарий |

            У других видов карт форма содержит больше страниц (материальная основа, стабилизация, сведения об
            обработке); их разбор добавится вместе с их бланками. `Front`/`Back` — метки страниц, используемые в
            `sources[].image` схемы ответа.

            «Вар.2/вар.3» на снимках — повторные описания того же документа (учебные/контрольные); каждый снимок
            описывается независимо, объединение версий — вне эпика.
            """;

        public static string S5_ResponseSchema = """
            ## Схема ответа (черновик v2 — родовая / kind-agnostic)

            Схема **едина для всех видов карт** (`card_kind` — что за вид — определяет модель по макету, FR-003):
            - **`identity`** — фиксированный набор (keyed): `shifr, inv_number, storage_location, fill_date`.
            - **`fields`** — РОДОВОЙ массив вид-специфичных текстовых полей:
              `[ { name, value, confidence, handwritten?, reconstructed?, sources:[{image,bbox}] } ]`, где `name` — поле
              по СМЫСЛУ подписи бланка (author, title, …); состав НЕ фиксирован (виды и бланки эволюционируют).
            - **общие блоки** `conservation_marks` (Front) и `preservation` (Back) — ТИПИЗИРОВАНЫ (стабильны между видами).

            Так один контракт покрывает все виды: типизация — там, где стабильно (identity + консервация/сохранность),
            свобода имён — там, где состав плывёт (библио-поля). Вид-специфичную типизацию несёт слой проверки/маппинга
            в АБИС, а не схема извлечения.

            Каждое текстовое значение — объект `{ value, confidence, handwritten?, reconstructed?, sources: [ { image, bbox } ] }`,
            где `bbox = [x0, y0, x1, y1]` в **целочисленной шкале 0..1000** (x = доля ширины ×1000, y = доля высоты ×1000;
            левый-верх и правый-низ). Незаполненные текстовые поля в ответ не включаются.

            > Шкала 0..1000, а не доли 0..1: на прогонах VLM (llama.cpp Qwen) целочисленная шкала — её обучающая
            > конвенция — даёт заметно точнее прилегающие рамки; доли 0..1 «плывут», особенно в объёмном ответе.

            **Чекбоксы — полная матрица:** возвращается КАЖДАЯ клетка бланка со значением `true`/`false` (оператор мог
            рассмотреть и НЕ отметить — это значимо). Уверенность — на уровне группы; **`bbox` — на каждую строку
            матрицы «Задано/Выполнено»** (для подсветки конкретной отметки), для группы выбора — bbox на группу; всё в шкале 0..1000.

            ```jsonc
            {
              "card_kind": "EditionBook",                // ВЫХОД: вид документа определяет модель по макету (FR-003); один из FundDocumentKind (7 видов)
              "prompt_version": "v2",                    // проставляет сервис (v2: родовой fields[], координаты 0..1000, bbox на строки матрицы)
              "form_title",                              // печатный баннер бланка/программы («Издания из фондов БАН», «Фазовая консервация книги XVII–XX вв.»)
              "identity": { "shifr", "inv_number", "storage_location", "fill_date" },   // фикс. набор, keyed (см. глоссарий «Информационная карта»)

              "fields": [                                // РОДОВОЙ массив вид-специфичных ТЕКСТОВЫХ полей; name — по смыслу подписи; только заполненные
                { "name": "author", "value": "…", "confidence": 0.9, "handwritten": true, "sources": [ { "image": "Front", "bbox": [x0,y0,x1,y1] } ] }
                // состав СВОБОДНЫЙ (зависит от вида/бланка). Для EditionBook по смыслу: author, translator_compiler, title, place,
                // publisher, printing_house, year, language, binding, convolute, alligat_count, volume_number, pages_count,
                // separate_ill_sheets, ill_in_text, format, manuscript_sheets, bookplate, bookplate_desc, chamber_catalog,
                // marginalia, records, first_academic_stamp, private_library_evidence, acquisition_source, comments
              ],

              "conservation_marks": {                    // страница Front — «наличие элемента данных», матрица Задано/Выполнено
                "book":    [ { "item": "Реконструкция", "set": false, "done": false, "bbox": [x0,y0,x1,y1] }, /* все строки, bbox на каждую (0..1000) */ ],
                "binding": [ /* Реконструкция, Реставрация, Ремонт, Чистка, Умягчение — каждая {item,set,done,bbox} */ ],
                "block":   [ /* Реставрация, Нейтрализация, Ремонт — каждая {item,set,done,bbox} */ ],
                "storage_conditions": { "Вертикальное": true, "Горизонтальное": false, "Другое": false }   // группа выбора: bbox на группу
              },

              "preservation": {                          // страница Back — СОХРАННОСТЬ (переиспользуемый блок)
                "dimensions_mm": { "length", "width", "height" },
                "binding": {
                  "state":       { "Не повреждён": false, "Повреждён": true, "Ранее реставр.": false, "Ранее ремонт.": false, "Утрачен": false },
                  "composition": { "Комбинир.": false, "Цельный": true },
                  "cover_materials": { "Кожа": false, "Пергамент": false, "Ткань": false, "Бумага": false, "Другое": false, "Отсутствует": false },
                  "base_materials":  { "Дерево": false, "Картон": false, "Бумага": false, "Без основы": false },
                  "decor": {
                    "stamping":  { "Слепое": false, "Золотое": false, "Нет тиснения": false },
                    "ornaments": { "Накладки": false, "Застёжки": false, "Металл": false, "Другое": false, "Нет украшен.": false },
                    "edge":      { "Золото": false, "Тиснение": false, "Другое": false, "Без обреза": false }
                  },
                  "damage": {
                    "physical_mechanical": { "Оторван": false, "Утраты": true, "Разрывы": false, "Деформация": true, "Общ.загрязн.": false,
                                             "Пятна": false, "Затёки": true, "Изменение окраски": true, "Разрушение": false, "Ломкость": false },
                    "biological_micro":    { "Биологическая пигментация": true, "Колонии микромицетов": true, "Биологическое разрушение": true,
                                             "Жизнеспособная микрофлора": false, "Нет микробиологических повреждений": false },
                    "biological_ento":     { "Личиночные ходы": false, "Экзувии": false, "Личинки": false, "Имаго": false, "Нет энтомологических повреждений": false },
                    "degree": { "physical_mechanical": "", "micro": "", "ento": "" }   // Низкая | Средняя | Высокая
                  }
                },
                "block": {
                  "state":     { "Не повреждён": false, "Повреждён": true, "Ранее реставрирован": false },
                  "notebooks": { "Выпадают": false, "Отсутствуют": false, "Нарушено шитьё": false },
                  "sheets":    { "Выпадают": false, "Отсутствуют": false },
                  "ph_value",
                  "damage": { "physical_mechanical": { /* тот же список */ }, "biological_micro": { }, "biological_ento": { },
                              "degree": { "physical_mechanical": "Средняя", "micro": "", "ento": "" } }
                },
                "monitoring": {
                  "biotest":   { "date", "performer" },
                  "expertise": { "date", "expert", "performer", "result", "signature": true }
                },
                "comment"                                // рукописный КОММЕНТАРИЙ, дословно
              }
            }
            ```

            > **Формализация.** Схема выше — контракт ответа; сервис принуждает модель к ней через **structured output**
            > (`response_format: { type: json_schema, strict: true }`) — поддержано llama.cpp и vLLM (проверено на Qwen).
            > Это гарантирует СТРУКТУРУ (состав ключей, типы, наличие bbox), но НЕ точность значений и координат:
            > распознавание сверяет и правит оператор (уверенность + подсветка источника).
            """;

        public static string S6_Prompt = """
            ## Канонический промт (v2)

            Системный промт стабилен и кэшируется (prompt caching); при изменении промта версия (`prompt_version`)
            меняется и кэш инвалидируется.

            ```text
            Ты — оператор-архивист. По снимкам ИНФОРМАЦИОННОЙ КАРТЫ документального памятника
            (фонд ОФО БАН) извлеки заполнение карты для оцифровки и последующей выгрузки в АБИС.

            Карта — это фиксированный БЛАНК: печатные подписи полей и заголовки разделов заданы
            типографски, а данные внесены оператором — рукописно, впечаткой или ОТМЕТКОЙ в клетке
            (галочка, крест, штрих). Твоя задача — прочитать ЗАПОЛНЕНИЕ, а не подписи бланка.

            Карта книги — два изображения (страницы формы):
              • Front — «ОПИСАНИЕ ДОКУМЕНТАЛЬНОГО ПАМЯТНИКА»: шапка (шифр, инв. номер, место
                хранения, дата), библиоописание, провенанс и матрица «Отметка о наличии элемента
                данных» (консервация).
              • Back — «СОХРАННОСТЬ»: размеры, состояние и материалы переплёта/блока,
                повреждения, мониторинг, комментарий.
            Главного источника НЕТ — у каждой стороны свой набор полей; заполняй поля той стороны,
            к которой относится изображение. Сторон меньше — заполни что есть, остальное пропусти.

            Правила:
            1. Заполняй ТОЛЬКО то, что реально внесено в карту (текст или отметка). Ничего не выводи
               из внешних знаний и не додумывай по смыслу бланка: пустая клетка — значения нет.
            2. Значения — как написано в карте, на языке и в орфографии оригинала, БУКВА В БУКВУ, включая
               дореформенную орфографию и «ять» («Дневникъ войны», «русскій»); в современную орфографию не
               переводи. Даты — как в карте (формат не нормализуй). Печатные подписи полей в значения НЕ включай.
            2а. Бланк существует в нескольких печатных вариантах: подписи полей могут отличаться словами
               («Кол-во стр.» ↔ «Количество страниц», «Отд. листы ил.» ↔ «Отдельные листы иллюстраций»).
               Сопоставляй поле по СМЫСЛУ подписи, а не по точному тексту. Печатный заголовок-баннер
               карты («Издания из фондов БАН», «Фазовая консервация книги XVII–XX вв.») клади в form_title.
            2б. Раскладка ответа: идентификацию — в `identity` (shifr/inv_number/storage_location/fill_date);
               остальные вид-специфичные ТЕКСТОВЫЕ поля — в родовой массив `fields` объектами {name, value, …},
               где name — краткое имя поля по смыслу подписи (author, title, place, …). Сам ВИД карты определи
               по макету формы и составу полей и верни в `card_kind` (один из FundDocumentKind).
            3. РУКОПИСНЫЙ текст расшифровывай посимвольно. Неразборчивый фрагмент, восстановленный
               по смыслу, помечай reconstructed:true и снижай уверенность; полностью нечитаемое —
               не выдумывай, поле пропусти. Рукописное значение помечай handwritten:true.
            4. ОТМЕТКИ (галочки/кресты/штрихи) читай как булев факт «отмечено / не отмечено» и
               возвращай ПОЛНОЙ матрицей — КАЖДУЮ клетку группы, а не только помеченные (пустая
               клетка значима: оператор рассмотрел и не отметил).
               • Матрица «Задано / Выполнено» (наличие элемента данных): каждая строка — объект
                 {item, set, done, bbox}: item — подпись строки ДОСЛОВНО, set — есть отметка в «Задано»,
                 done — есть отметка в «Выполнено», bbox — область клеток строки (шкала 0..1000).
               • Группа выбора (Состояние, Состав, Материалы, Декор, Повреждения, Степень, Условия
                 хранения…) — объект {подпись клетки: true|false} по ВСЕМ клеткам группы; подписи
                 бери ДОСЛОВНО. Степень повреждения (Низкая/Средняя/Высокая) — одно значение строкой.
            5. Не путай разделы: отметки книги, переплёта и блока относи к своим блокам; степень
               повреждения указывай той группы (физ-мех / микробиол. / энтомол.), под которой она стоит.
            6. КОММЕНТАРИИ и КОММЕНТАРИЙ переписывай рукопись дословно, целиком.
            7. Для каждого заполненного текстового поля укажи уверенность 0..1: 1.0 — впечатано/написано
               явно и читается уверенно; 0.5–0.8 — плохо видно, спорная отметка, неразборчивый почерк;
               ниже 0.5 — догадка (лучше не заполнять). Для группы чекбоксов уверенность — на группу.
            8. Для каждого заполненного поля и каждой ОТМЕТКИ укажи ИСТОЧНИК: изображение и область
               bbox [x0, y0, x1, y1] в ЦЕЛЫХ координатах 0..1000 (x — по ширине, y — по высоте), охватывающую
               значение или клетку. Для матрицы «Задано/Выполнено» bbox — на КАЖДУЮ строку (её клетки), для
               группы выбора — на группу. Не уверен в границах — дай грубую область клетки, но источник не пропускай.
            9. Если запрошен OCR: отдельно верни весь текст каждого изображения (печатный + рукописный)
               дословно, в порядке чтения; отметки не транскрибируй.
            10. Незаполненные ТЕКСТОВЫЕ поля в ответ не включай (без null, пустых строк, прочерков).
                Чекбокс-матрицы возвращай ЦЕЛИКОМ, включая неотмеченные клетки (false).

            Отвечай строго JSON по заданной схеме, без пояснений.
            ```
            """;

        public static string S7_AcceptanceCriteria = """
            ## Критерии приёмки

            1. Идентификация (шифр, инв. номер, место хранения, дата) извлекается с роли Front; инв. номер сопоставим с номером папки/документа.
            2. Незаполненные текстовые поля в ответе отсутствуют; чекбокс-матрицы присутствуют целиком (true/false).
            3. Матрица «Задано/Выполнено» отражает обе колонки раздельно по каждой строке.
            4. Рукописные значения помечены handwritten; восстановленные по смыслу — reconstructed со сниженной уверенностью.
            5. У каждого заполненного поля и каждой группы чекбоксов есть источник с bbox на переданном изображении.
            6. Ответ — валидный JSON по схеме; в ответе указана версия промта.
            """;

        public static string S8_OpenQuestions = """
            ## Открытые вопросы

            | № | Вопрос |
            |---|---|
            | OQ-CE-1 | Состав полей и имена ключей схемы — черновые, требуют сверки на наборе реальных карт |
            | OQ-CE-2 | Виды документов перечислены в домене (FundDocumentKind); открыт конкретный состав полей и число страниц формы для каждого — прорабатывается по мере появления их бланков |
            | OQ-CE-3 | Нужен ли встроенный OCR-этап или связка с отдельным распознаванием (см. FR_002) |
            | OQ-CE-4 | Пороговая уверенность для подсветки на проверке полей — общая с OQ-PP-1 эпика «Обработка пакета» |
            | OQ-CE-5 | Полная матрица чекбоксов раздувает ответ — оценить, не перейти ли к «только отмеченные» после прогонов |
            | OQ-CE-6 | Бланк EditionBook встречается в ≥2 печатных вариантах (разные подписи полей, поле «Конволют» не во всех) — держать маппинг по смыслу; собрать перечень вариантов |
            | OQ-CE-7 | Инв. номер в шапке может расходиться с номером папки/документа (нечёткий почерк) — извлекать как напечатано, сверку вести ниже по потоку (эпик «Обработка пакета») |
            | OQ-CE-8 | Что именно кодирует `card_kind`: вид экземпляра фонда (`FundItemKind`) или распознанную форму бланка (`DocumentForm`) — согласовать с доменной моделью (ADR-006/007). Сейчас — `FundItemKind` (7 видов) |
            """;

        public static string S99_Related = $"""
            ## Связанные артефакты

            - {nameof(Ban.Sdaid.Icd.Epics.PacketProcessing.EPIC_PacketProcessing)} — эпик обработки пакета (проверка, создание ИК); извлечение предзаполняет его проверку полей
            - {nameof(Ban.Sdaid.Icd.Domain.Documents.FundItemKind)} — вид экземпляра фонда (7 видов: издание/рукопись × книга/лист/…); значение `card_kind` в схеме ответа
            - {nameof(Ban.Sdaid.Icd.Domain.Cards.Template.DocumentForm)} — распознанная форма бланка; она же определяет состав полей
            - {nameof(Ban.Sdaid.Icd.Domain.Cards.Template.FormPage)} — страница формы (заменила роль кадра `ImageRole`)
            - {nameof(Ban.Sdaid.Icd.Glossary.Terms.InformationMap)} — информационная карта: 4 значения идентификации, печатный + рукописный текст
            - {nameof(Ban.Sdaid.Icd.Glossary.Terms.DocumentKind)} — вид документа, определяющий вид карты
            - {nameof(Ban.Sdaid.Icd.Requirements.FR_002_TextRecognition)} — Распознавание текста
            - {nameof(Ban.Sdaid.Icd.Requirements.FR_003_AttributeExtraction)} — Сегментация и извлечение атрибутов
            - {nameof(Ban.Sdaid.Icd.Requirements.FR_004_ConfidenceScoring)} — Оценка достоверности распознавания
            - {nameof(Ban.Sdaid.Icd.Requirements.FR_005_Verification)} — Верификация и корректировка данных
            - {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_005_FieldValueSourceBinding)} — привязка значения к странице формы и области изображения
            - {nameof(Ban.Sdaid.Icd.Epics.RecognitionStand.EPIC_RecognitionStand)} — стенд распознавания карт: на нём отрабатывается подход к извлечению перед реализацией
            """;
    }
}