Qurany · Recite · технический отчёт для ML Senior

Qurany Recite - Protocol 1.7: Boundary & Continuous Reading

ML: 06c752b на research/recite-boundary-diagnostics-20260925 (GitHub, запушено) APP: 9ea8a2b + 6d2d3bd на integration/app-ml (GitLab, запушено) Дата отчёта: 12.10.2026
Research Реализовано и сохранено в Git; не слито в основную ветку, не в production Тесты 1.7 повторно прогнаны 12.10.2026 и зелёные. Последнее подтверждение работы тестовых серверов — 06.10.2026; текущее состояние GPU-хоста неизвестно.
VERIFIED проверено в коде, тестах или логах в ходе этого аудита HISTORICAL записанное ранее наблюдение (лог, заметка); с указанием даты INFERENCE вывод из фактов, напрямую не проверен UNKNOWN данных нет
server поведение ML-сервиса app поведение приложения diag экспериментальная диагностика, не часть контракта

01Executive Summary

226 / 226
тестов boundary на ML 06c752b проходят (повторный прогон 12.10)
1166 / 1166
тестов Recite ML в ридере APP 6d2d3bd проходят (12.10)
16 + 47
срабатываний continuation_split + settled_close в логах 8722 за 23–24.09 (часть — скриптовые пробы)
0
live-проверок сервера, где boundary 1.7 и Mistake Evidence 1.6 объединены

Простыми словами

Раньше один аят = одна сессия. Когда человек читал аяты подряд, сервер узнавал о конце аята N только после паузы, а приложение — ещё на один сетевой шаг позже. К этому моменту чтец уже начинал аят N+1.

Начало N+1 при этом терялось дважды. В сессии N оно засчитывалось как лишние звуки на последнем слове N. В сессию N+1 оно не попадало вовсе, поэтому первое слово N+1 становилось пропущенным или ошибочным.

Protocol 1.7 решает это втроём:

  • Сервер называет точную границу аята в сэмплах.
  • Приложение хранит последние 20 секунд записи и переотправляет звук с этой границы в сессию следующего аята.
  • Сервер не оценивает соединительный гласный: он считается контекстом, а не началом N+1.

Что важно для интеграции

  • VERIFIED Весь 1.7 сохранён на remote. ML: 06c752b; на него не указывает отдельный ref, он предок 7872427. APP: 6d2d3bd.
  • VERIFIED Mistake Evidence от ML Senior (4e7c7e2) тоже называет себя «1.6», но по смыслу это другая 1.6. Boundary-кода в ней нет.
  • VERIFIED Пробное слияние 06c752b + 4e7c7e2 даёт конфликты в 9 файлах.
  • INFERENCE Механизмы независимы по назначению: evidence объясняет почему слово ошибочно, boundary решает, какому аяту принадлежит звук. Но они правят одни и те же функции, поэтому при слиянии boundary может незаметно потеряться.
Отчёт не утверждает, что распознавание стало точнее на какой-то процент, и что «исправлены все переходы». Доказано: механизм границ работает в коде и тестах и срабатывал на реальном сервере. Качество оценки слов после перехода измерено только на малом числе реальных стыков (раздел 6).

02Исходная проблема

Как было до 1.6/1.7 serverapp

  • VERIFIED «One session covers one verse». Следующий аят — новая сессия (контракт на базе f8d5650, docs/service-contract.md).
  • VERIFIED verse_complete приходит только после закрывающей свёртки:
    • после измеренной тишины SILENCE_EVIDENCE_SECONDS = 0.45;
    • либо после waqf WAQF_SECONDS = 1.0 (streaming.py).
  • VERIFIED Старый переход в приложении: новая сессия получает только очередь звука с момента переключения (handoff frontier F). Звук N+1 до F в неё не попадает.

Что ломалось при непрерывном чтении

  • VERIFIED Фонемы начала N+1 попадали в окно N как хвостовые вставки на последнем слове N. По комментарию в continuation.py, такие вставки «damning», то есть делают слово ошибкой.
  • VERIFIED Пример из кода: потеряно начало следующего аята, «eleven phones of ٱلْحَمْدُ لِ in one capture» (streaming.py).
  • VERIFIED Сегмент мог «проглотить» следующий аят: конец сегмента 14.08 s при последней фонеме N на 9.42 s.
  • VERIFIED Соединительный гласный (wasl), который переотправлялся вместе со звуком, без 1.7 превращался во вставку на первом слове N+1.

Что наблюдали вживую

ДатаНаблюдениеМетка
22.09Первый раунд на телефоне. 3 перехода, все с boundary_fallback_reason=no_boundary, то есть все через старый переход. Время перехода 560.7 / 646.3 / 565.4 ms.HISTORICAL
22.09Live-захват на 8722 (1.6, до B2): 17 сессий, 12 final. 12 из 12 — boundary=None: обычное закрытие по тишине «выигрывало гонку» у split.VERIFIED пересчёт лога
22.09Офлайн-переигрывание 7 реальных стыков из этого захвата: базовая версия ошибается на 5 из 7 (الحمد — skipped, الرحمن — mistake). Следующая сессия начиналась с середины аята.HISTORICAL
Расхождение в исторической записи. В заметке от 22.09 сказано, что last_split_reason=gate стоит у 12 из 12 final. Пересчёт лога даёт: gate 6, e1_placement 4, e3_no_split 2. Вывод «12/12 без boundary» верен; формулировка «всегда gate» — нет.
Почему старого перехода недостаточно. Он не знает, где на самом деле кончился аят N. Звук между концом N и моментом переключения достаётся не тому аяту или теряется. Ни пауза, ни таймер, ни эвристика на клиенте этого не исправляют: координату границы знает только сервер.

03История разработки

Нумерация. В нашей линии boundary-поля появились под номером 1.6 (22.09), а context_prefix_samples — под номером 1.7 (23.09). Позже, 10–11.10, ML Senior выпустил Mistake Evidence тоже как 1.6. Подробно — раздел 8.
  1. 15–17.09
    Protocol 1.5: open.prior_tempo_baseline
    База boundary-работы — f8d5650 (Merge PR #8, тег phase1-end). В неё уже входит 84545d0 (frozen-слова неизменяемы).
  2. 21.09
    A0–A2: исследование и дизайн HISTORICAL
    Офлайн на реальных захватах Аль-Фатихи. Точка split A стабильна (±8–53 ms). Окно переотправки — 150–200 ms до A (±40 ms); только оно декодирует первую фонему N+1 на всех трёх стыках.
  3. 22.09
    B1 = «protocol 1.6 boundary» на 8722
    capabilities, объект boundary, audio_samples. Тестовый APK собран. Первый раунд на телефоне. Включено тестовое логирование RECITE_SPLIT_DIAG. Live-захват: 12/12 без boundary.
  4. 22–23.09
    B2: settled_close
    Граница появляется и при обычном закрытии по тишине. Бандл и APK — 23.09.
  5. 23.09
    A3 = protocol 1.7: context_prefix_samples
    8722 отвечает /health 1.7 (13:15Z). Хостовые тесты: Ran 997, errors=2 (нет jsonschema), skipped=99. Первые split'ы на 1.7, APK A3, прогоны на телефоне.
  6. 24.09
    Захват settled-boundary #2
    26 сессий, 23 final: 15 settled_close, 6 continuation_split. Тест рекордера диагностики — PASS.
  7. 25.09
    Сохранено в Git
    ML 06c752b → 8f69fc8 → 7872427 запушены в research-ветку. APP 9ea8a2b + 6d2d3bd запушены в integration/app-ml. 8722 перезапущен (PID с 11:30Z, причина неизвестна).
  8. 06.10
    Дерево 8722 сверено, поднят 8732
    Владелец хоста сверил дерево: 128/128 файлов по манифесту. 8732 = 8722 + отключение проверки мадда. A/B — PASS.
  9. 10–11.10
    Mistake Evidence «1.6» от ML Senior
    da5b7c8, e9094db → phase1/eval-harness @ 4e7c7e2.
  10. 12.10
    Этот аудит
    Тесты повторены, логи пересчитаны. GPU-хост недоступен из текущей сети.

Стадии в коммите 06c752b

СтадияЧто делаетWire
A0Кадры CTC-эмиссии, модуль continuation.py, офлайн-калибратор. Только данные для решений.нет
A1Issue continuation: начало N+1 не засчитывается как вставка на последнем слове N.нет
A2Закрытие N на стыке с N+1 (continuation_split).через B1
B1Gate boundary_replay, объект boundary, audio_samples.«1.6»
B2Граница при обычном закрытии (settled_close).значение source
A3context_prefix_samples: префикс переотправки — контекст, а не звук N+1.«1.7»
Коммит 06c752b сам пишет: «Not run locally… these tests have not been validated by this commit». Позже тесты прошли на хосте 23.09 и локально 12.10 (раздел 6).

Коммиты

  • 06c752b — весь протокол 1.7: 23 файла, +9063/−194.
  • 8f69fc8 — поле issues в debug_record. Только отладка, на 8722 не развёрнуто.
  • 7872427 — shadow vowel-rescue. RESEARCH ONLY, не для деплоя, на 8722 нет.
  • APP 9ea8a2b — клиент: 23 файла, +6717/−54. 6d2d3bd — штамп reader-бандла.

04Architecture

схема, не скриншот
← прокрутите схему вбок →
Микрофон PCM16 16 kHz, кольцо 20 s Распознаватель слышит весь сегмент Выравнивание placement, cursor, слова Свёртка (fold) сегмента N есть next_reference и capability? continuation_split (A2) E1–E4 пройдены во время чтения A = кадр 1-й фонемы N+1; B ≤ A settled_close (B2) обычное закрытие / waqf A = B = конец посл. фонемы да иначе boundary = null нет capability, последний аят суры, разрыв звука, ничего не размещено verse_complete { boundary } → final { boundary, audio_samples } одна и та же запись границы в обоих кадрах Старый переход новая сессия только с F Приложение: проверка та же граница в final, audio_samples = отправлено, поток без разрывов Сессия N+1 open + context_prefix = A − B переотправка [G, F), затем живой звук с F проверка не прошла следующий аят
Жизненный цикл: звук → выравнивание → граница (continuation_split или settled_close) → проверка в приложении → переотправка → следующий аят. Если проверка не прошла или границы нет, используется прежний переход.
схема координат
← прокрутите схему вбок →
Сессия N (сэмпл 0 = начало звука этой сессии) речь аята N (оценивается для N) wasl начало N+1 (для N не оценивается) B next_replay_start_sample (включительно) A current_judgement_end_sample (не включительно) F frontierпереключения Сессия N+1 (сэмпл 0 = B, то есть G = origin + B в координатах записи) оценивается как N+1 живой звук контекст context_prefix_samples = A − B (≤ 16000) переотправка [G, F) из кольца,потом живой звук с F settled_close: A = B → префикс 0
Две координаты. B — откуда приложение переотправляет звук. A — где закончилась оценка аята N. Интервал [B, A) — соединительный гласный: распознаватель N+1 его слышит, но фонемы в нём не размещаются. Масштаб условный.
последовательность кадров
← прокрутите схему вбок →
APP (reader-web) ML service open { N, protocol_version "1.1", capabilities: ["boundary_replay"] } ready { words, context_prefix_samples: 0 } PCM16 16 kHz (каждый блок пишется и в кольцо истории 20 s) word_update / position_update verse_complete { boundary: { source, A, B } } G = origin + B; кольцо закреплено (pin) с G end_audio frontier F; копия точных байтов [G, F) final { boundary (та же), audio_samples } проверка: та же граница, audio_samples = samplesSent, поток линеен новая WebSocket-сессия (две сессии одновременно не живут) open { N+1, capabilities, context_prefix_samples: A − B } ready { context_prefix_samples: принятое значение } переотправка [G, F) → затем живой звук с F word_update … (префикс [0, A−B) не оценивается)
Кадры через границу. Значение ready.context_prefix_samples приложение 6d2d3bd не проверяет (раздел 9). Ключ context_prefix_samples отправляется только при continuation_split; при settled_close префикс равен 0 и ключ не отправляется.

05Реализованные механизмы

Пути ML указаны относительно корня репозитория на 06c752b, пути APP — относительно reader-web/src/app/ на 6d2d3bd. Числа тестов — это количество def test_ / it( в файле.

capability negotiation · boundary_replayVERIFIED
Проблема
Закрыть аят на стыке безопасно только для клиента, который хранит запись и переотправит её. Иначе начало N+1 «досталось бы аяту, который только что закончился».
server
  • open.capabilities разбирает contract.parse_capabilities. Неизвестные строки игнорируются; не-массив строк вызывает protocol_violation.
  • service.stream_continuation без capability возвращает (None, False). Тогда A1, A2 и B2 выключены, а клиент получает поведение до 1.6, плюс boundary: null и audio_samples.
  • Версия проверяется только по мажору: check_protocol_version. Минор не сравнивается, функции включаются только через capability.
  • Переменных окружения для этого нет.
app
ML_PROTOCOL_VERSION = '1.1' намеренно не повышен. capabilities: ["boundary_replay"] отправляется в каждом open, где у захвата есть кольцо истории; в production-роутере кольцо есть всегда (lib/recite-ml-verse-session.ts, lib/recite-ml-wire.ts).
Тесты
  • ML: tests/test_boundary_protocol.py — 33 теста, в том числе OldClientTests, GatePolicyTests, test_the_same_audio_without_the_capability_is_the_legacy_session.
  • APP: recite-ml-boundary-replay.test.ts — «NEW APP + OLD SERVER», «OLD APP + NEW SERVER».
  • APP: useReciteMlTeacherController.continuous.test.ts — «every verse opens with the 1.1 handshake plus the boundary_replay capability».
boundary · current_judgement_end_sample · next_replay_start_sampleVERIFIED
Проблема
Граница — это две разные координаты: конец оценки N (A) и начало переотправки (B). Одной точки недостаточно.
Поля
boundary = { current_judgement_end_sample, next_replay_start_sample, source } или null в verse_complete и final.
  • A — не включительно: ничто на A и после не участвовало в оценке N.
  • B — включительно: следующая сессия получает запись с этого сэмпла.
  • Единицы: сэмплы PCM16 моно 16 kHz этой сессии, отсчёт от первого принятого байта.
  • Всегда B ≤ A.
server
  • contract.boundary_payload.
  • Запись Boundary пишется один раз (streaming.py), и оба кадра строятся из неё, поэтому они равны по построению.
  • Калибровочные поля (matched_phones, evidence, …) наружу не отправляются.
app
  • parseBoundary: обе координаты — безопасные неотрицательные целые; при B > A — проблема; при A = B — допустимо.
  • Неизвестный source даёт boundary: null и запись о проблеме. Кадр не отклоняется.
  • Сервер до 1.6 не присылает ключа, и приложение уходит в старый переход (no_boundary).
Тесты
ML: WireTests и NullBoundaryTests. APP: «every way a boundary can be malformed is a reason, never a throw», «10b. an unknown settlement is refused; both known ones are not».
settled_close (B2)VERIFIED
Проблема
Обычное закрытие по тишине корректно, но приложение узнаёт о нём поздно, и начало N+1 теряется. Live 22.09: 12 из 12 переходов шли без границы.
server
  • _verse_complete вызывает _settled_close_boundary, и A = B = конец последней размещённой фонемы N.
  • Это намеренно не конец сегмента: он может лежать внутри следующего аята.
  • Сброс end_audio без завершения аята границу не создаёт.
app
Исполняется так же, как continuation_split, только с префиксом 0. Отдельный describe: «a settled_close boundary is executed exactly like a continuation_split one».
Тесты
ML: tests/test_ordinary_close_boundary.py — 23 теста (обычное закрытие, waqf, независимость от сегмента, null-случаи, инварианты).
Наблюдения
HISTORICAL 22.09: 8 реальных стыков переиграны с выдаваемой координаты.
  • Первое слово N+1 декодируется с реальной уверенностью (0.25–0.79) на 8 из 8; cursor доходит до конца аята.
  • Вердикт correct — на 6 из 8. Более раннее «8/8» было артефактом округления и отозвано.
  • Вариант «A − 200 ms» опровергнут.
continuation_split (A2)VERIFIED
Проблема
Закрыть аят N прямо на стыке, пока чтец продолжает, не дожидаясь паузы. Звук N+1 не должен портить N.
server
streaming._split_at_join вызывается из _fold:
  • Gate: есть capability, звук без разрывов, есть следующий аят, аят ещё не завершён.
  • E1: размещение доходит до конца N; между N и кандидатом — только wasl.
  • E2: политика защищённых фонем.
  • E3: точка split = кадр CTC-эмиссии первой фонемы N+1 (continuation.split_candidate).
  • E4: пробная оценка на логитах того же прогона, обрезанных по split. Она должна довести cursor до конца без ошибки на последнем слове.
  • A = sample split, B = min(конец последней фонемы N в пробе, split).
  • Коммитится только [start, A); звук после A для N не оценивается.
Пороги
CONTINUATION_MIN_PHONES = 6, MAX_COST_RATIO = 0.25, MAX_UNHEARD = 1, SPLIT_CONTEXT_SECONDS = 2.0.
  • В коде все помечены как PROVISIONAL; SPLIT_CONTEXT_SECONDS — «an empirical bound from three real joins».
  • Калибратор scripts/calibrate_continuation.py работает только офлайн.
app
Отдельной логики нет: тот же путь переотправки, что и для B2, плюс context_prefix_samples = A − B.
Тесты
ML:
  • test_continuation_split.py — 28 тестов на реальных стыках 1:3 и 1:6.
  • test_continuation.py — 47 тестов (A0).
  • test_continuation_issue.py — 32 теста (A1).
  • test_emission_frames.py — 14 тестов.
  • Тест test_verse_complete_arrives_three_passes_earlier_than_the_silence_close фиксирует: на реальном стыке 1:3 split завершает аят на проходе 2 (3.84 s), закрытие по тишине — на проходе 5 (7.68 s).
context_prefix_samples (A3 · «1.7»)VERIFIED
Проблема
Приложение переотправляет с B, поэтому первые A − B сэмплов новой сессии — это wasl. До 1.7 сервер оценивал их как звук N+1, и wasl становился вставкой на первом слове.
server
  • contract.parse_context_prefix_samples: целое от 0 до 16000 (1.0 s, длина waqf). Ненулевое значение требует boundary_replay.
  • Нарушение вызывает protocol_violation; значение не обрезается.
  • Принятое значение возвращается в ready.context_prefix_samples.
  • assess.measure_segment: распознаватель слышит весь сегмент, но размещение фонем начинается с A − B.
  • Отсчёт времени, audio_samples и новая граница по-прежнему от сэмпла 0 = B.
app
contextPrefixOf = max(0, A − B) по границе из final. Ключ отправляется, только если есть переотправка и префикс > 0. Эхо в ready приложение не читает.
Тесты
  • ML: tests/test_context_prefix.py — 49 тестов, в том числе test_the_emitted_boundary_satisfies_the_clients_formula.
  • APP: recite-ml-wire.test.ts — «a settled_close boundary declares no prefix, a continuation_split does».
Live
VERIFIED по логам 8722 за 23–24.09:
  • принятые префиксы — от 1280 до 8950 сэмплов (80–559 ms);
  • один реальный отказ: got 19203 (≈ 1.2 s > потолка 1.0 s), следующий open завершился ошибкой;
  • ещё 5 отказов — намеренные негативные пробы (−1, 16001, 1920.5, True, без capability).
next_referenceVERIFIED
Что это
Поля next_reference на проводе нет. Это внутреннее значение сервера: фонемы аята (сура, аят+1) из кэша эталонов. Через суру оно не переходит.
server
stream_continuation → StreamSession(next_reference=…) → SessionState.next_reference. Используется только A1 (защита вердикта) и A2 (поиск стыка). Не двигает cursor, live-позицию и verse_complete.
app
Ничего не отправляет. Сервер выводит следующий аят из ключа текущего.
replay audio · audio_samples · provenanceVERIFIED
Проблема
Координаты имеют смысл, только если приложение отправило ровно те сэмплы, которые сервер посчитал.
server
  • final.audio_samples — число принятых сэмплов; отклонённый кусок не считается.
  • Отброшенный звук помечает поток как разорванный, и новая граница после этого не выдаётся.
  • Звук между сессиями сервер не хранит.
app
  • Кольцо PCM16 на 20 s (320 000 сэмплов): lib/recite-ml-capture-history.ts.
  • На verse_complete оно закрепляется с G, на переключении копируется [G, F).
  • На final проверяются: та же граница, audio_samples === samplesSent, линейный поток.
  • Переотправка идёт после ready и до живого звука.
  • 14 причин отката к старому переходу: no_boundary, audio_samples_mismatch, history_evicted, …
Тесты
APP:
  • recite-ml-boundary-replay.test.ts — 33;
  • recite-ml-capture-history.test.ts — 19;
  • recite-ml-capture-provenance.test.ts — 32;
  • useReciteMlTeacherController.continuous.test.ts — 31.
ML: ProvenanceTests.
frozen / revisionVERIFIED
Правило
Слово, уже отправленное как frozen, больше не отправляется, что бы сервер ни пересчитал (84545d0, WordDiffer). Boundary-работа DIFF_FIELDS и WordDiffer не меняет.
server
  • Принятый split проходит обычным путём закрытия по [start, A): слова N, включая последнее, замораживаются.
  • Split возможен только до завершения аята, поэтому он не может пересмотреть уже замороженное слово.
  • INFERENCE Звук N+1 после A в N не сворачивается, и вердикта для N не появляется.
app
  • applyMlWordDiff не перезаписывает frozen-слово даже при большей revision.
  • Состояние слов привязано к одной сессии и одному аяту и через границу не переносится.
  • Тест «ayah 1 stays retained, unrevised, while ayah 2 runs».
RECITE_SPLIT_DIAG · RECITE_TRANSITIONdiagVERIFIED
Что это
Экспериментальная диагностика, не часть контракта.
  • Сервер: строки DIAG OPEN/SPLIT/FINAL (почему split принят или отклонён). Включается переменной RECITE_SPLIT_DIAG=1. Этого кода нет в Git: только локальный патч и дерево 8722.
  • APP: трасса RECITE_TRANSITION, только при import.meta.env.DEV.
Gate в APP
Весь путь ML Teacher, включая непрерывное чтение, есть только в тестовой сборке (EXPO_PUBLIC_RECITE_ML_TEST=1 → vite --mode development). В production-бандле его нет.

06Результаты тестирования

Повторный прогон 12.10.2026 VERIFIED

ЧтоГде и какРезультат
ML, 7 модулей boundaryЧистый git archive 06c752b; Python 3.13.15, torch 2.13; PYTHONPATH=src, без сети226 / 226 OK
ML, полный наборто же1010: 9 fail/error, skipped 2
ML, полный набор базыgit archive f8d5650 (1.5)784: те же 9
APP, Recite ML + контроллер + TeacherModeДерево reader-web, по содержимому равное 6d2d3bd (23/23 blob совпали); vitest24 файла, 1166 / 1166
APP, весь ридерто же, параллельно с ML-прогоном1695 / 1702

Исторические прогоны на GPU-хосте

Live-срабатывания границ на 8722 VERIFIED пересчёт логов

ЗахватПротоколopenИтоги finalПрефиксы
22.09«1.6» (B1, до B2)1712 × None—
23.091.7468 × split, 29 × settled_close, 4 × None7 приняты (80–200 ms); 1 реальный отказ 19203; 5 негативных проб
24.09 (утро)1.782 × split, 3 × settled_close8950, 7031 (559 / 439 ms)
24.09 захват #21.7266 × split, 15 × settled_close, 2 × None6 приняты (160–241 ms)
Чего эти цифры не доказывают. Они показывают, что механизм срабатывает на реальном сервере. Правильность оценки слов они не доказывают: разметки «как должно быть» для этих сессий нет. Устройство и сборка — INFERENCE: APK A3, один тестовый телефон; часть сессий 23.09 — скриптовые пробы. Тексты — в основном Аль-Фатиха.

Офлайн-исследования HISTORICAL 21–22.09

8722 против 8732, 06.10

Ограничения

07Compatibility Matrix

Код серверов — по последнему подтверждению 06.10.2026, кроме 8723. Сегодня (12.10) хост недоступен, поэтому текущее состояние UNKNOWN.

Функция8720872287328723
Назначениепрежний серверboundary / контрольboundary + madd-offMistake Evidence
/health protocol_version1.51.71.71.6 не проверено нами
КодUNKNOWN точный коммит; H f8d5650f8d5650 + незакоммиченные файлы = 06c752b + SPLIT_DIAG H 06.108722 + MADD_OFF_ON_8722.diff H 06.10H b43e1c9; на хосте UNKNOWN
capabilities: boundary_replayигнор (нет поля)дадаигнор (нет поля)
boundary в verse_complete / finalнетдаданет
continuation_split (A1/A2)нетдаданет
settled_close (B2)нетдаданет
context_prefix_samplesнетда, ≤ 16000данет
final.audio_samplesнетдаданет
reason, letter_errors, heard_text, extra_wordsнетнетнетда
word_update с words: []нетнетнеттолько клиенту, объявившему ≥ 1.6
Проверка маддавкл.вкл.выкл. (RECITE_TEST_DISABLE_MADD_CHECK=1)вкл.
RECITE_SPLIT_DIAG diagнетвкл.вкл.UNKNOWN
Что получает APP 6d2d3bdстарый переходпереотправка границпереотправка + madd-offстарый переход (no_boundary); evidence не разбирается
Что получает APP c4609fb (Phase 1, локально)старый переходпереотправка, без evidenceпереотправка, без evidenceevidence, старый переход
Последнее подтверждение06.1006.1006.10ни одной live-сессии
Все четыре сервера принимают любой минор 1.x: check_protocol_version проверяет только мажор. Поэтому номер версии в open ничего не говорит о функциях. Функции boundary включает capability, а пустые кадры evidence — объявленный минор ≥ 1.6.

08Integration With Senior Phase 1

Две независимые функции

Mistake Evidence (ML Senior)Boundary / replay (наша)
ВопросПочему слово ошибочноКакому аяту принадлежит звук
Меняетсодержимое слова: reason, letter_errors, heard_text; extra_wordsжизненный цикл сессии, границы, владение звуком
Gateобъявленный минор ≥ 1.6 (только пустые кадры)capability boundary_replay
Remotephase1/eval-harness @ 4e7c7e206c752b (предок 7872427)

INFERENCE Без boundary evidence на непрерывном чтении работает хуже: начало N+1 снова будет теряться или помечаться ошибкой. Только теперь с «объяснением», то есть с убедительно выглядящей неверной причиной.

Где они пересекаются в коде VERIFIED

  • Обе линии меняют одни и те же файлы относительно f8d5650: contract.py, streaming.py, decisions.py, api.py, service.py, assess.py, документацию, schema, stub и test_streaming.py.
  • Пробное слияние без записи ref'ов (git merge-tree 06c752b 4e7c7e2) даёт конфликты в 9 файлах; assess.py сливается автоматически.
  • Самое важное место INFERENCE:
    • в evidence вставка на границе слова превращается в extra_words;
    • в boundary начало N+1 на последнем слове N защищено issue continuation (A1).
    После слияния нужно проверить, что начало N+1 не попадёт в extra_words аята N.
  • DIFF_FIELDS в evidence расширены (letter_errors, heard_text, reason). Правило frozen-слов должно сохраниться и для пробной оценки E4.

Конфликт нумерации BLOCKER для слияния

НомерВ нашей линии (8722/8732)В линии ML Senior (8723, remote)
1.5общий: open.prior_tempo_baseline (f8d5650)
1.6capabilities, boundary, audio_samplesreason, letter_errors, heard_text, extra_words, пустые word_update
1.7context_prefix_samples—

Что уже живёт с этими номерами:

  • /health 8722 и 8732 отвечает «1.7».
  • APP 6d2d3bd объявляет «1.1».
  • APP Phase 1 (c4609fb, локально) объявляет «1.6», чтобы получать пустые кадры evidence.

Варианты объединения (решение за ML Senior)

A. Remote-история главная

Evidence остаётся 1.6. Boundary (B1 + B2) становится 1.7, context_prefix_samples — 1.8 (или вместе 1.7). В контракт добавляется таблица соответствия research-номеров.

B. Один общий минор

Объединённый сервер объявляет новый минор, который включает evidence и boundary. Минус: на время перехода «1.7» на /health 8722 будет значить другое.

C. Функции по capability

Минор остаётся монотонным, а сервер явно сообщает поддерживаемые функции (например, эхо capabilities в ready). Это дополнение контракта. Нужно сохранить смысл gate «минор ≥ 1.6» для пустых кадров evidence.

Мы не меняли ML-код и не выбираем окончательную версию.

09Open Questions / Risks

РискФактСерьёзность
Коллизия версийДве несовместимые «1.6». Gate evidence зависит от минора, gate boundary — от capability.BLOCKER для слияния
Нет live-проверки объединённого сервераОбъединённого кода нет. Взаимодействие A1 ↔ extra_words и prefix ↔ evidence не проверено.IMPORTANT
Регрессия непрерывного чтенияСлияние конфликтует в 9 файлах. Если boundary-код потеряется, сломается только непрерывное чтение, а одиночные аяты продолжат работать — это легко пропустить. 226 тестов boundary должны остаться зелёными.IMPORTANT
Незакоммиченная диагностика на GPUНа 8722 код лежит незакоммиченным поверх f8d5650. RECITE_SPLIT_DIAG (api +69, streaming +113/−10) есть только там и в локальном бандле.IMPORTANT
Сохранность test-only патчейdiag-8722.patch, MADD_OFF_ON_8722.diff и бандлы лежат вне Git на одной машине. На 06c752b нет собственного ref.IMPORTANT
Неизвестное текущее состояние GPUПоследнее подтверждение — 06.10; 12.10 хост недоступен. Был ли перезапуск или изменения — UNKNOWN.IMPORTANT
Потолок префикса 1.0 sLive 23.09: split с A − B ≈ 1.2 s, сервер отказал, следующая сессия не открылась. Приложение префикс не ограничивает.IMPORTANT
Эхо префикса не проверяетсяAPP не читает ready.context_prefix_samples. Сервер, молча игнорирующий префикс (8720/8723), неотличим от 1.7.MINOR
Устаревшие места в контракте
  • stub server объявляет 1.6;
  • текст schema говорит «1.6»;
  • §8 не упоминает 1.7;
  • source без enum;
  • документация говорит «null при закрытии по тишине», хотя B2 там выдаёт settled_close.
MINOR
Повторное распознавание без префиксаINFERENCE На свёртке с префиксом ограниченный повтор в A2 (хвост длиннее 2 s) распознаёт звук без контекста префикса.MINOR

10References

ML — GitHub Galamat-IT/qurany-recite

  • research/recite-boundary-diagnostics-20260925 @ 7872427 (remote, проверено 12.10)
  • 06c752b — A0–A3, B1–B2, контракт, schema, stub, тесты
  • 8f69fc8 — debug_record issues (только отладка)
  • 7872427 — shadow vowel-rescue, RESEARCH ONLY
  • База f8d5650 (тег phase1-end, protocol 1.5), включает 84545d0
  • Evidence: phase1/eval-harness @ 4e7c7e2 (da5b7c8, e9094db, PR #10/#11); feat/phase1-mistake-evidence @ b43e1c9

Файлы

  • src/obadx_pipeline/contract.py — история версий, boundary_payload, parse_capabilities, parse_context_prefix_samples
  • src/obadx_pipeline/streaming.py — _split_at_join, _settled_close_boundary, WordDiffer
  • src/obadx_pipeline/continuation.py, decisions.py, assess.py, service.py, recognizer.py
  • docs/service-contract.md — §4.1, §4.2, §4.6, §4.9, §8; docs/service-contract-v1.schema.json
  • scripts/calibrate_continuation.py, scripts/contract_stub_server.py
  • Тесты: test_boundary_protocol (33), test_context_prefix (49), test_continuation (47), test_continuation_issue (32), test_continuation_split (28), test_emission_frames (14), test_ordinary_close_boundary (23)

APP — GitLab eidillum/qurany-rn

  • integration/app-ml @ 6d2d3bd (remote, проверено 12.10)
  • 9ea8a2b — feat(recite): replay ayah boundaries with capture provenance
  • База 15fab36; локальная ветка Phase 1 feature/recite-phase1-mistake-evidence @ c4609fb (на remote нет)

Файлы (reader-web/src/app/)

  • lib/recite-ml-wire.ts — parseBoundary, buildOpenFrame, contextPrefixOf
  • lib/recite-ml-verse-session.ts — candidate, verify, replayBoundary
  • lib/recite-ml-capture-history.ts, lib/recite-ml-provenance.ts, lib/recite-ml-capture-router.ts, lib/recite-ml-verse-sequencer.ts
  • lib/recite-ml-transition-trace.ts (DEV-only)
  • components/reader/TeacherMode.tsx — resolveTeacherMlActiveAyah
  • Тесты: boundary-replay (33), capture-history (19), capture-provenance (32), transition-trace (18), controller .continuous (31)

Связанные материалы

  • Отчёт «Mobile Phase 1 Implementation» — отдельная страница
  • Документ APP docs/recite-phase1-mistake-evidence.md (на c4609fb) — раздел о двух «1.6»

Артефакты вне Git

  • Бандлы A0–B1 и diag-8722.patch (22.09), B2 и A3 (23.09), диагностика тестов A3 — на машине разработки, вне Git
  • Логи и записи захватов 22–24.09 — внутренние, не публикуются
  • Манифест дерева 8722 (baseline-8722.sha256), MADD_OFF_ON_8722.diff и выводы шагов развёртывания 8732 (06.10)
  • GPU: рабочие деревья 8722 и 8732 на внутреннем тестовом хосте

11Action Items For ML Senior

  • 1
    Выбрать нумерацию (раздел 8: A, B или C). Обновить §8 Versioning и сохранить смысл gate «минор ≥ 1.6» для пустых word_update.
  • 2
    Выбрать способ слияния: перенос 06c752b на 4e7c7e2 или merge. Конфликтующие файлы: contract.py, streaming.py, decisions.py, api.py, service.py, контракт, schema, stub, test_streaming.py.
  • 3
    A1 против extra_words: при boundary_replay начало N+1 на последнем слове N не должно попадать в extra_words или letter_errors аята N. Без capability — решить, что ожидается.
  • 4
    Префикс против evidence: в [0, A − B) не должны появляться extra_words, вставки и heard_text; after_index и время должны считаться от сэмпла 0 = B.
  • 5
    E4 и frozen: пробная оценка split должна выдавать evidence-поля, согласованные с обычным закрытием. Правило 84545d0 должно сохраниться с расширенными DIFF_FIELDS.
  • 6
    Кадр final: boundary, audio_samples и extra_words вместе; schema (required, enum для source), stub-сервер, примеры.
  • 7
    Потолок префикса: на live встречался A − B ≈ 1.2 s. Решить: поднять потолок, обрезать на сервере, ограничить A − B при split, или клиент откатывается к префиксу 0.
  • 8
    Пороги A2 (6 фонем, 0.25, 1 unheard, 2.0 s) помечены PROVISIONAL и подобраны на 3 стыках. Нужен план калибровки на большем наборе.
  • 9
    Обязательные тесты после слияния: все 7 модулей boundary (226) и test_streaming (224) плюс тесты evidence. На хосте поставить jsonschema (сейчас 99 пропусков).
  • 10
    Live-проверка объединённого сервера на отдельном порту, без замены 8722 и 8723. Одиночный аят, стык с split, стык с settled_close, последний аят суры, старый клиент «1.1», клиент Phase 1 «1.6».
  • 11
    Сохранность: поставить ref на 06c752b; решить судьбу RECITE_SPLIT_DIAG (отдельный TEST-ONLY коммит или отказ); сверить дерево 8722 с манифестом и подтвердить коммит 8723.
  • 12
    Мелкие правки контракта: stub 1.6 → актуальная версия; текст schema; §4.9 «null при тишине»; §8 без 1.7; устаревший комментарий в assess.py про emission_frames.