Commit graph

20 commits

Author SHA1 Message Date
Ivan Gerasimov
57f4c858b6 lct-22: пульт директив — преподаватель ведёт ситуацию
Мягкие директивы перекрывают дугу сценария и применяются со следующей
реплики: разговор не дёргается от того, что преподаватель что-то нажал.
Жёсткие правят ситуацию — обрыв связи рвёт звук тем же механизмом, что
перебивание, и запускает норматив обратного дозвона; второй пострадавший
правит эталон; неточный адрес снимает раскрытый факт, и оператор обязан
переспросить.

Своя копия сценария на занятие: директивы правят факты и эталон, а
сценарий был общим на библиотеку — правка в одной группе протекла бы во
все остальные. Тест проверяет, что библиотека не изменилась.

Свободный текст честно отказывает: офлайн-дерево предгенерировано,
произвольную фразу взять неоткуда, и преподаватель видит это на пульте.

Ни одна директива не трогает карточку курсанта — он управляет ситуацией,
а не работой обучаемого.

Занятие теперь собирается целиком и только потом регистрируется:
наблюдатель мог увидеть его без слот-автомата и звонящего.
2026-09-17 21:38:43 +03:00
Ivan Gerasimov
978d7dc1aa lct-17: пульт преподавателя — сценарий, наблюдение, пометки, ссылки
Компоновка от того, по чему пульт оценит действующий преподаватель:
задать сценарий и не вмешиваться, видеть работу курсанта вживую,
получить готовый разбор, показать группе. Кнопка запуска одна и крупная,
ссылки на АРМ курсанта и монитор печатаются рядом.

Разделение прав архитектурное и здесь: пульт смотрит через observe,
пишет через control, карточка курсанта у него только для чтения.

Две ловушки запуска: scenario.start уходил из эффекта и мог повториться
при перерисовке, а открытая из истории ссылка на идущее занятие
запустила бы его заново и сбросила. Запускается только занятие,
созданное кнопкой.
2026-09-17 21:34:01 +03:00
Ivan Gerasimov
fadfa3e479 lct-16 и половина lct-19: разбор, отчёт, внешний монитор, эталон
Эталонный диалог собирается кодом из фактов и чек-листа: написанный
руками, он разошёлся бы с фактами при первой же правке сценария,
и курсанта оштрафовали бы за правильный ответ.

Отчёт: метрики фактом против норматива со ссылкой, отметка E1 на каждый
недобытый факт с эталонным вопросом, расхождение самооценки — что
курсант заметил сам, чего не заметил, что отметил зря. Не заметил —
самое ценное для разбора.

Внешний монитор — не отдельное приложение, а другой режим отрисовки тех
же событий: крупный таймер опроса, ход разговора, карточка, после оценки
разбор на весь экран.

Коррекция преподавателем сохраняет автооценку рядом: видно, что
скорректировано и кем.

Найдено: все метрики весили одинаково, и курсант, не задавший ни одного
вопроса, но заполнивший карточку руками, получал 87 из 100. Предварительные
веса (полнота опроса — 4) дают 74; окончательные утверждает методист,
вопрос записан в DEBRIEF.md.
2026-09-17 21:32:12 +03:00
Ivan Gerasimov
25143645d3 lct-15: самооценка курсанта до показа автооценки
Правило сервера, а не интерфейса: score.ready курсанту уходит только
после self_assessment.submit, обойти его через DevTools нельзя.
Преподаватель и монитор получают оценку сразу — им ждать нечего.

Чек-лист для самооценки отдаётся отдельной точкой и только после конца
звонка: во время разговора это содержимое подсказок, и открыть его
значило бы выдать в контрольном режиме то, чего там быть не должно.

Попутно подключён расчёт оценки при завершении звонка — детерминированный
слой был написан в lct-12, но его никто не вызывал.

Тест на отсутствие события смотрит очередь курсанта, а не ждёт из сокета:
ожидание того, чего не будет, вешает прогон навсегда.
2026-09-17 21:24:34 +03:00
Ivan Gerasimov
78f66cf3b5 lct-11: таймеры ГОСТ и баннер режима на АРМ курсанта
Опрос (75 с) — крупным индикатором: это центральная метрика продукта,
и она обязана быть видна курсанту, преподавателю и залу одновременно.
Три состояния норматива — единственное место, где цвет несёт смысл.

Фронт время не считает и рисует ровно те цифры, что прислал сервер:
метрика по часам браузера недоказуема.

Баннер режима определяет доступность подсказки: в контрольном режиме
кнопки нет, и это часть нормы контроля.
2026-09-17 21:18:28 +03:00
Ivan Gerasimov
48c936ea6e lct-10: карточка КИО — форма, слияние правок, read-only
Фронт не хранит карточку как истину: локально живут только правки
курсанта, ещё не подтверждённые сервером. Распознанное сервером значение
перекрывает набранное, эхо своей правки её подтверждает. Иначе
автозаполнение и ручной ввод дерутся за одно поле — худший класс багов
для отладки в последнюю ночь.

Описание полей отдельно от отрисовки: по нему же карточка рисуется
read-only на мониторе, пульте и АРМ ДДС.

Сверка формы с контрактом встроена в make typecheck: все 40 полей
домена на форме, а поле, добавленное в контракт и забытое в форме,
теперь поймается само. Это пропущенный факт по вине интерфейса,
а не курсанта.

Открытый вопрос в карточке: нужно ли автозаполнение карточки из речи
вообще — полнота КИО оценивается кодом E5, и если сервер впишет адрес
сам, курсант перестанет за неё отвечать.
2026-09-17 21:16:51 +03:00
Ivan Gerasimov
750ded2991 lct-09: звук звонящего на фронте, перебивание, аудио-фон
Очередь AudioBufferSourceNode, а не <audio>: перебивание требует
оборвать звук мгновенно и выбросить очередь. Планирование встык по
currentTime с упреждением 50 мс — иначе между чанками щелчок.

Перебивание двухслойное: фронт гасит звук по громкости микрофона за два
кадра (40 мс), сервер подтверждает по VAD за 88–90 мс и присылает
tts.cancel. Сервер — авторитет, фронт — скорость.

Логика вынесена из WebAudio и проверена в Node: очередь планируется без
щелей и наложений, сброс останавливает все чанки, гейт не срабатывает на
шуме и ловит начало речи ровно один раз.

Нет файла фона — звонок идёт без фона с предупреждением в консоли, а не
падает: записи это контент, их кладёт методист.

Путь через браузер с живым микрофоном не проверен: шаги записаны
в карточке.
2026-09-17 21:13:41 +03:00
Ivan Gerasimov
456f3cd34d lct-06: голосовой контур VAD → STT → звонящий → TTS, с перебиванием
Контур замкнут без LLM: звонящий отвечает заготовками через слот-автомат.
Живой диалог через WebSocket прошёл 5 раз подряд без сбоев: первая
реплика после снятия трубки — 87–154 мс, ход — 1.2–1.4 с, перебивание —
88–90 мс. На LLM до цели 1.5 с остаётся 100–300 мс.

Задача ответа живёт, пока у курсанта доигрывает звук: перебивание —
это отмена одной задачи, и tts.end приходит, когда звонящий
действительно замолчал.

Что нашлось при сборке:
- Silero молча выбрасывает цифры: «улица Ленина, дом… квартира…» без
  номеров. Числа и сокращения разворачиваются в слова до синтеза.
- onnxruntime брал все 16 ядер и вытеснял синтез, первый вызов был
  холодным: распознавание 576 мс вместо ~210. Прогрев на старте и по
  4 потока на модель.
- Весь адрес одним предложением — ~455 мс синтеза до первого звука.
  Длинное предложение режется по запятым, номер от улицы не отрывается:
  ~250 мс.
- Первая реплика «Алло! Помогите!» запускалась мимо задачи ответа и не
  перебивалась вовсе.

Отдельно: сломал make dev — num2words попал в pyproject, а Dockerfile
ставил зависимости ручным списком, и бэкенд в контейнере упал на
импорте. Образ теперь ставит зависимости из pyproject.toml.

Голоса в контейнере нет: под WSL модели работают нативно, make back.
2026-09-17 15:04:17 +03:00
Ivan Gerasimov
dcdec56779 lct-02: замер задержки голосового контура, make latency
Замер на машине разработки (Ryzen 7 8845HS, WSL2), не на демо-машине —
там make latency надо прогнать трижды, разброс между прогонами до 20%.

Распознавание — GigaAM v3 RNNT int8: фраза на 2 с за ~285 мс, WER 0%
на 99 словах живой речи Golos. CTC не быстрее ни в одном из двух прогонов
и ошибается чаще: время съедает общий энкодер, поэтому ступень деградации
«RNNT не успевает → CTC» по скорости ничего не даёт.

Синтез — Silero v5: в 10 раз быстрее реального времени, а не в 40, как
заложено в STACK.md; первая фраза 100–250 мс в зависимости от длины.
Частота синтеза на скорость не влияет, без профилирующего компилятора —
на 13% быстрее.

Сквозной бюджет: 600 мс endpointing + ~285 STT + ~150 TTS = ~1 с без LLM.
Цель ≤ 1.5 с держится, только если LLM отдаёт первое предложение быстрее
~465 мс. Вопрос, считается ли филлер «алло?..» первым звуком, записан
в LATENCY.md для людей.

make models качает GigaAM и Silero VAD с Hugging Face. Silero TTS лежит
на российском хосте, недоступном из-под VPN: скрипт не лезет на зеркала
(.pt грузится через pickle — это чужой код) и честно говорит, откуда
скачать вручную.
2026-09-17 14:44:02 +03:00
Ivan Gerasimov
f24031b5b2 lct-08: каркас фронта, типизированный сокет, захват микрофона
Направление канала зашито в тип: у наблюдателя Out = never, отправить
нечего и нельзя; у преподавателя нет входящих. Разделение прав
архитектурное и на фронте тоже. Пока соединения нет, события не копятся:
истина на сервере, после переподключения придёт актуальное состояние.

Ресемплинг 48 → 16 кГц усредняет по окну выходного сэмпла: без фильтра
всё выше 8 кГц наложилось бы на речь и испортило распознавание. Проверено
числами: секунда звука — ровно 50 кадров по 320 сэмплов из 48 и 44.1 кГц,
помеха 15 кГц подавлена до 7.8%. Эхоподавление и автоусиление браузера
выключены: гарнитура эхо не даёт, а обработка портит речь для STT.

Бэкенд принимает бинарные кадры по тому же сокету, что и события; кадр
не того размера отбрасывается с одним предупреждением, а не 50 в секунду.

Логирование приложения не было настроено: uvicorn настраивает только
свои логгеры, и весь INFO модулей app.* молча терялся — нашлось, когда
в логе не оказалось строки о принятых кадрах.

make lesson запускает занятие и печатает ссылки на экраны — пульта
преподавателя ещё нет, а открыть АРМ курсанта без занятия не с чем.

Путь из браузера с живым микрофоном не проверен: нужен человек, шаги
записаны в карточке.
2026-09-17 14:19:38 +03:00
Ivan Gerasimov
f01f2dab76 lct-12: детерминированная оценка по ГОСТ, таксономия, радар
Каждая метрика — факт против норматива со ссылкой: «94 с при ≤ 75 с,
ГОСТ Р 22.7.03-2021», а не балл. По недобытому факту — отдельная отметка
E1 с эталонным вопросом: в разборе нужен конкретный вопрос, не процент.
Радар — проекция тех же метрик без пересчёта весов, коммуникация без
судьи не рисуется нулём.

Таймер, не остановленный событием, — провал, а не зачёт: время
недоказуемо, операция не завершена. Метрика, которую нечем посчитать
(полнота опроса без модели эмбеддингов), видна как «не посчитано» —
молча выброшенная выглядела бы пройденной.

Найдено противоречие: таймеры опроса (75 с) и оповещения ДДС (60 с)
стартовали на ответе и останавливались передачей в ДДС — один отрезок,
два лимита. Опрос за законные 70 с давал E3, а на экране курсанта
краснел таймер посреди нормального разговора. События «опрос закончен»
в контракте нет, поэтому dds_notify снят с учёта, вопрос записан в
CONTRACT.md.

KIO проверяет присваивание: card.dds = "03" клало в карточку сырую
строку вместо кода ДДС, и падала уже оценка, далеко от места ошибки.
2026-09-17 14:06:16 +03:00
Ivan Gerasimov
7c97310938 lct-07 (без LLM): слот-автомат, персона, звонящий на заготовках, make repl
Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и
отдаёт звонящему только их. Тест прогоняет длинную серию реплик и
проверяет, что ни один нераскрытый факт не прозвучал.

Главная находка — матчинг по порогу близости не работает. На
multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь
на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт
адрес на «успокойтесь», либо не слышит «где вы находитесь?».

Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько
формулировок (examples), рядом общий список не-вопросов
(checklists/common.yaml), и реплика засчитывается пункту, только если она
ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20
вопросов, 0 из 8 ложных срабатываний.

Реплика режется только по границам предложений, со знаком: «?» для e5 —
сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам.

Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат
начнёт слышать оператора, когда голосовой контур передаст ему stt.final.

Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими
словами, dialog/llm.py с кэшем.
2026-09-17 13:57:43 +03:00
Ivan Gerasimov
4f5a8d1400 конфиг: адрес OpenAI-совместимого провайдера LLM
Добавлен LLM_BASE_URL — без него не подключить Cloud.ru Foundation Models
и любой другой OpenAI-совместимый шлюз. Имена COMPAT_MODEL_* принимаются
тоже, так их выставляет командный сниппет. Ключ в repr настроек скрыт.
2026-09-17 13:43:15 +03:00
Ivan Gerasimov
e7a0798ba7 lct-05: состояние сессии, таймеры по событиям, четыре канала
Таймеры объявлены таблицей «событие → старт/стоп», а не разбросаны
по обработчикам: так видно целиком, что чем запускается, и норматив
нельзя потерять по дороге. Опрос стартует на ответе курсанта и
останавливается передачей в ДДС — событием, а не таймаутом.

Канал наблюдателя без единого обработчика входящих: кадры читаются
и выбрасываются, не разбираясь, только чтобы заметить разрыв. Без
чтения задача сокета висела бы на очереди до первой отправки, а
закрытая вкладка монитора оставляла бы подписку. Тест проверяет, что
наблюдатель не может ни изменить карточку, ни завершить занятие.

Пульт теперь заводит сессию в журнале: запущенное с него занятие жило
только в памяти, и реплики с подсказками уходили в нарушение внешнего
ключа — журнал об этом честно сообщал в лог.

Тикер таймеров гасится при остановке приложения, иначе задачи переживают
выключение и держат событийный цикл.

Проверено вживую через uvicorn: монитор, подключённый посреди занятия,
получает снимок с заполненной карточкой; такт таймера идёт раз в секунду.
2026-09-16 02:07:27 +03:00
Ivan Gerasimov
6b2c96bab6 lct-03 и lct-04: журнал сессий и библиотека сценариев
БД: 11 таблиц, первая миграция. Группы и связь trainee → group заложены
сразу, даже пустыми — размечать накопленные сессии задним числом значит
делать лишнюю миграцию. Номер попытки живёт в сессии, отдельной таблицы
попыток нет: дельта считается запросом по (trainee_id, scenario_id).

Сценарии: строгая схема — опечатка в имени поля падает на старте, а не
игнорируется молча. ground_truth собирается кодом, попытка задать
incident_type, dds или required_facts в YAML отвергается: иначе генератор
разведёт факты и эталон и курсанта оштрафуют за правильный ответ. Руками
задаются только нормализованные адрес и число пострадавших — из фразы
«улица Ленина, 14, квартира 47, 5-й этаж» кодом «улица Ленина, 14»
не достать.

GET /api/scenarios/{id} больше не отдаёт чек-лист. Это содержимое
подсказок: отдать его целиком значит выдать в контрольном режиме то,
чего там быть не должно, в обход выдачи по одному пункту.

Тесты базы поднимают свой движок на каждый тест: глобальный кэшируется
и привязывается к первому событийному циклу.
2026-09-15 20:10:05 +03:00
Ivan Gerasimov
59d5a6a26d lct-01: контракт закрыт — координаты, обязательные поля, коды ошибок
Координаты были представлены дважды: кортежем в КИО и объектом в данных
ЭРА-ГЛОНАСС. Теперь Coords {lat, lon} везде — в кортеже не видно, где
широта, и ошибка всплывает на карте у диспетчера, а не в типах.

required_fields едет в call.incoming и session.snapshot: обязательность
полей задаёт сценарий, без списка АРМ не подсветит незаполненное поле,
и курсант узнаёт о неполноте карточки только из разбора.

Канал error получил коды (ErrorKind, восемь значений) — фронт разбирает
код, а не текст сообщения. Имя не ErrorCode: оно занято таксономией E1–E6,
два одинаковых имени дали бы коллизию в generated.ts.

Добавлены поля, которые спрашивает чек-лист, а записать было некуда:
куда идёт дым, уехал ли нарушитель, код домофона.

Документы догнали код: mkh → gkh, payload transcript.append с якорем ref,
причина director у tts.cancel, attempt и stopped у таймеров.
2026-09-15 19:48:56 +03:00
Ivan Gerasimov
5844b374ad сборка: LF во всём репозитории
На машине с core.autocrlf=true Makefile и скрипты приезжают с CRLF
и перестают запускаться.
2026-09-15 19:35:23 +03:00
Ivan Gerasimov
7b7dd38e9f каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.

Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.

frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
andreysk0304
4425c7d4ca docs: переработка документации — фокус на учебную часть и симуляцию звонков
- Новая структура: product/ (продукт и методика), spec/ (ТЗ, ГОСТ, формат сценария), arch/ (контракт, бэк, фронт, стек), hackathon/ (архив стратегии)
- Учебное ядро: чек-листы опроса, подсказки по запросу, таксономия ошибок E1-E6, эталонный диалог с автопроигрыванием, самооценка, режимы тренировки/контроля, профиль курсанта и дельта попыток
- Симуляция: персоны evasive/child/foreigner, ЭРА-ГЛОНАСС, эмоциональная дуга, память звонящего, аудио-фон
- Контракт дополнен событиями подсказок, фона, самооценки, пометок
- Убраны спринты, календарь и хакатон-повествование из технических документов
2026-09-15 13:06:24 +03:00
MIkhail Khorokhorin
98a212e199 Initial commit 2026-09-09 08:07:58 +03:00