feat: evidence-grounded document slide alignment v2 - #12
Draft
fUS1ONd wants to merge 54 commits into
Draft
Conversation
Каталог молча терял целый batch и уходил в native text по трём причинам: - общий потолок ответа 4096 токенов обрезал JSON на многословных моделях (max_tokens стал параметром вызова, каталогу выделен предел моделей 65536); - срыв схемы сразу приводил к деградации без попытки починки (добавлен один повтор с текстом ошибки валидации); - в native text колонтитул колоды попадал в title, source_concepts и visible_text, из которых grounding строит claim слайда, поэтому страница «доказательно» совпадала с любой репликой про название курса (строки, повторяющиеся на большинстве страниц, отфильтрованы).
Обрезка ответа никогда не полезна: она молча ломала JSON каталога слайдов, а с ростом reasoning-эффорта затрагивала бы и остальные стадии, поскольку reasoning-токены входят в тот же бюджет. LLM_MAX_TOKENS задаёт потолок для всех вызовов, по умолчанию 65536 — предел моделей Gemini. Отдельный лимит каталога больше не нужен и удалён.
Матчер брал effort стадии SUBSPLIT, поэтому поднятие reasoning для контентных стадий поднимало его и структурированным вызовам. На medium модель начинает пропускать обязательные поля схемы (десять срывов каталога за прогон против единиц на low), и повтор с текстом ошибки не помогает — ошибка стабильна. LLM_EFFORT_SLIDE_MATCH задаёт effort каталога и семантической верификации независимо, по умолчанию low.
response_format json_object гарантирует лишь синтаксически валидный JSON, поэтому модель штатно опускала обязательные поля (десять срывов каталога за прогон), а temperature=0 и повтор с текстом ошибки против этого бессильны: ошибка воспроизводится стабильно. Каталог и семантическая верификация переведены на strict json_schema, схема выводится из существующих Pydantic-моделей: все поля объявлены обязательными, лишние запрещены.
Три прогона лекции 2026-02-12 (flash-lite, 3.6-flash, 3.6-flash с фиксами), оценённые одним судьёй с одинаковыми параметрами, плюс контекст для продолжения работы: что сделано, что осталось, как воспроизвести прогон.
Прод-инцидент 27.07 показал два пробела: workspace упавшей задачи занимает 494 МБ и не очищается по возрасту, а повторный запуск не умеет стартовать с готового SRT и заново оплачивает скачивание и транскрипцию.
OpenRouter отдаёт HTTP 200 с частичным контентом, нулевым usage и finish_reason="error", когда апстрим обрывает генерацию: так выглядят RECITATION-фильтр Gemini (детерминированно срабатывает на слайдах с дословными формулировками профстандартов) и 503 provider_overloaded. Клиент возвращал такой обрывок как успешный ответ, из-за чего падал разбор схемы каталога слайдов — это выглядело как «модель не соблюдает схему» и приводило к деградации каталога в native fallback. Ни LLM_MAX_TOKENS, ни strict json_schema на это повлиять не могли. Теперь обрыв распознаётся, модель уходит в короткий cooldown, попытка переходит на следующую (повтор той же бесполезен: RECITATION детерминирован). Нулевой usage оборванного ответа больше не попадает в статистику задачи. Заодно openai.InternalServerError (5xx апстрима, «high demand») перестал ронять задачу целиком: тот же короткий cooldown и нарастающая пауза, как у сетевых ошибок — без паузы все попытки сгорали за доли секунды.
Каталог слайдов рвётся на страницах, дословно цитирующих официальные документы: Gemini обрывает генерацию по фильтру цитирования Google (native_finish_reason=RECITATION), и каталог всего батча деградирует в нативный текст PDF, а слайды теряют верификацию. LLM_MODELS_SLIDE_MATCH задаёт матчеру свою ротацию, независимую от LLM_MODELS_SUBSPLIT: в хвост можно поставить модель с открытыми весами (gemma), которая этим фильтром не ограничена и при этом доступна через того же BYOK-провайдера google-ai-studio. Пустое значение (по умолчанию) — прежнее поведение, модели стадии SUBSPLIT. Здесь же документация: колонка D по итогам прогона со strict-схемами, корневая причина срывов каталога и описание нового поведения клиента при отказах апстрима.
Требование «только ключевой видимый текст» заставляло модель воспроизводить текст страницы дословно, и на слайдах с формулировками из официальных документов Gemini обрывала генерацию по фильтру цитирования (RECITATION). Замер на проблемном батче, 10 повторов на gemini-3.5-flash-lite: прежний промпт — 8 обрывов из 10, версия с пересказом — 0 из 10 (точный тест Фишера, p ≈ 0.0007). Опасение, что отказ от цитат ослабит grounding, замер не подтвердил: на 15 слайдах доля утверждений, дословно встречающихся в транскрипте, выросла с 13% до 29%, доказательств класса 1 стало 7 против 2, grounding в top-1 нашёлся у 15 слайдов из 15 против 14. Лектор пересказывает слайд своими словами, поэтому пересказ ближе к речи, чем канцелярит страницы. Промпт вынесен в v2, чтобы прежние прогоны оставались воспроизводимыми.
Правило `assignment_not_verified → section_gallery` отправляло в галерею любое неверифицированное назначение, не глядя на доказательства. В прогоне E так уехало 13 слайдов из 20, а всего галерейных размещений оказалось 80% — при том что судья насчитал acceptable topic accuracy 95%, то есть секции у этих слайдов почти всегда верные. Слайд в галерее оторван от своего материала, иногда на 8-19 минут. Теперь блок ищется для всех назначений, а решает класс доказательства: verified проходит с любым найденным блоком (как раньше), probable — только с дословным вхождением фразы слайда или совпавшим редким токеном. Слабое пересечение слов уходит в галерею с новым fallback_reason=weak_evidence_only. Inline-размещение probable помечается anchor_confidence=probable, чтобы в диагностике было видно ослабленный критерий. Галерея переехала под текст раздела: gallery_position=after_content больше не декларация — экспортёр раньше игнорировал это поле и всегда рисовал галерею перед контентом, из-за чего слайды опережали свой материал и вставали над чужими абзацами. Здесь же колонка E в docs/progress и отчёт судьи по прогону E.
Колонка F в сводной таблице, разбор распределения размещений (inline 4 → 13, галерея 16 → 6, все галереи after_content), отчёт судьи, метрика уместности инлайн-размещений 69.2%. Зафиксирована сквозная закономерность шести прогонов: каждый раз устранение очередного узкого места не поднимало потолок качества — его держало следующее. Ни один прогон не вышел за usable_with_alignment_issues, лучшим по совокупности метрик остаётся A. Источник ошибок сместился в ASR: слайд 7 признан необсуждённым из-за «Мониак» вместо «ENIAC».
ASR коверкает имена собственные и аббревиатуры, а на слайде они записаны верно. В прогоне F слайд 7 (ENIAC) ушёл в «Непривязанные» как unmentioned, потому что распознавание дало «Мониак»; тот же класс дефектов — «Сбер» вместо «ядро» и выдуманный «Course Hub» вместо HwProj. Механизм для этого уже был: _render_section умеет принимать слайды, но в режиме v2 туда намеренно передавался пустой список. Теперь рендер получает текстовый справочник написаний по слайдам своего раздела — заголовок и термины из каталога, который матчер и так строит. Картинки в рендер не уходят: дешевле по токенам и не рискуем RECITATION. Промпт ограничивает применение: справочник написаний, а не источник содержания — добавлять в конспект то, чего не было в речи, запрещено. align() возвращает AlignmentResult с назначениями и каталогом: раньше каталог строился внутри сервиса и наружу не выходил.
Подсказки брались только с привязанных слайдов, и это замыкало круг: слайд теряется как раз тогда, когда ASR исказил его термин, — значит подсказка не доходит именно туда, где нужна. В прогоне G слайд 7 снова остался unmentioned из-за «Мониак», и ENIAC в конспекте так и не появился, хотя HwProj и «кранового развлечения» правка уже починила. Теперь в промпт рендера идут написания со всех слайдов лекции. Проверка на побочный эффект (6-граммы конспекта, совпавшие со слайдами, но отсутствующие в транскрипте): E — 4, F — 3, G — 3. Подмешивание написаний не заставило рендер переносить содержание слайдов в текст.
Словарь по всей колоде (прогон H) починил главный кейс — «Мониак» стал ENIAC, — но вдвое поднял утечку: с 3 до 6 фраз, которых не было в речи. Проверка подтвердила перенос формулировок с экрана: «владение более чем одним языком программирования» — 0 из 6 слов подряд есть в транскрипте. Причина в содержимом справочника: туда шли заголовки и понятия целиком, то есть готовые формулировки. Теперь каталог возвращает отдельное поле proper_nouns — имена, названия, организации и аббревиатуры ровно в том написании, как напечатаны, — и справочник строится только из них. Исправлять написания по-прежнему есть чем, а заимствовать нечего: целых фраз в справочнике не осталось. Промпт каталога вынесен в v3 (v2 сохранён ради воспроизводимости прогонов E-H). Для деградировавшего каталога proper_nouns достаёт эвристика из нативного текста PDF: латиница, аббревиатуры и токены с цифрами. Проверка на живой модели (слайды 7-12, flash-lite): ENIAC, Fortran, NATO Software Engineering, Standish Group Chaos Report; слайды без имён — пустой список.
OpenRouter при отказе провайдера отдаёт 200 с телом из одной ошибки: choices приходит пустым или отсутствует. Обращение к choices[0] роняло задачу целиком с TypeError — так упал прогон лекции 2026-03-12 на стадии structurize. Теперь это трактуется как отказ модели: короткий cooldown и переход к следующей модели, как у оборванных ответов.
Сплиттер иногда выдаёт секцию с концом раньше начала. Такая шкала роняла ffmpeg на нарезке (-to value smaller than -ss) уже после оплаченной транскрипции и всех LLM-стадий, а выравнивание слайдов отбрасывало всю колоду разом по fail-closed «invalid section timeline». Теперь конец битой секции берётся из начала следующей, у последней — из конца транскрипта.
Документ перестал быть про одну лекцию: добавлены прогоны G/H/I со справочником написаний по 2026-02-12 и прогоны J/K по лекциям 2026-02-26 и 2026-03-12 со сводной таблицей метрик по трём лекциям. Отдельно описаны два прод-бага, найденные при этих прогонах, и их фиксы.
Четыре красных теста краевых случаев зафиксированы в истории: маркер внутри нумерованного списка, обнуление страницы фильтром колонтитулов, вердикт-массив без независимой проверки, повышение вердикта судьёй с потерей раздела.
Схемно валидная, но пустая запись модели (без title и visible_text) трактуется как явное утверждение "на странице ничего нет": роль меняется на blank, и _NON_MATCHABLE_ROLES сразу уводит слайд в приложение вместо безрезультатного поиска доказательств по пустому payload.
Порог коллизии evidence снижен с >2 до >1: пара слайдов на одной реплике транскрипта остаётся verified только если они связаны как progressive_build или exact_duplicate — иначе как минимум один из них привязан неверно. Участники exact_duplicate теперь исключаются из подсчёта коллизий наравне с progressive_build. Закрыт последний открытый вопрос по этому механизму в OPEN-QUESTIONS.md.
Литерал "1. " в проверке атомарности блока покрывал только первый пункт списка — пункты 2., 3. и далее считались обычными абзацами и годились под якорь маркера слайда. Экспортёр заменяет маркер на строку с картинкой, поэтому список резался надвое. Заменили проверку на регулярное выражение, покрывающее нумерацию любым числом и оба разделителя (. и )).
Промежуточный шаг progressive build теряет каталожную запись: все его строки повторяются на последующих слайдах, detect_boilerplate_lines считает их колонтитулом, native_text_fallback отфильтровывает всё до пустого списка и возвращает unresolved. Если фильтрация стирает страницу целиком, берём её нативный текст без фильтрации — колонтитул всё ещё отсекается там, где у страницы остаётся собственный текст.
Четыре красных теста второго прохода охоты: коллизия по слайдам вместо пар, обнуление страницы с пустым visible_text, колонтитул как доказательство, судья не сверяет раздел. Три из них — регрессии задач 2, 3 и 12.
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MipicZifr8GCcvPE9XZdzi
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MipicZifr8GCcvPE9XZdzi
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MipicZifr8GCcvPE9XZdzi
Сделаны задачи 1-3 и 11-19 (19 задач плана, 14 коммитов), OPEN-QUESTIONS исчерпан. Осталось слияние: задачи 4-10, задача 10 ждёт видео-лекцию для гейта.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Что изменено
legacy,shadowиv2;temperature=0только для matcher-вызовов;Зачем
Legacy-алгоритм распределял приложенные документные слайды по разделам без достаточного подтверждения транскриптом. Это давало уверенные, но тематически неверные привязки, коллапс нескольких слайдов в один раздел и слабую позиционную точность.
Новый matcher требует проверяемое evidence и предпочитает безопасный gallery/appendix fallback сомнительной inline-вставке.
Совместимость
structure.jsonне изменены;DOCUMENT_SLIDE_ALIGNMENT_MODE=legacy, поэтому после merge поведение не переключается автоматически;shadowпозволяет собирать диагностику без изменения результата;v2меняет только размещение документных слайдов, используя существующиеslide_nums,slide_keys,content_mdи<!-- slide:N -->.Проверка
543 passed, 1 warningruff check lecturelog tests scriptsruff format --check lecturelog tests scriptsРеальные аудио/PDF и секреты не входят в репозиторий.
Что ещё проверить перед включением v2
После восстановления Gemini-квот прогнать те же реальные лекции на более сильных моделях, затем unseen-кейс 2026-03-12. Известные оставшиеся случаи для проверки: выбор более специфичного соседнего раздела и recovery явного упоминания слайда, которое не прошло первичную схему.