каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
[project]
|
|
|
|
|
|
name = "lct-backend"
|
|
|
|
|
|
version = "0.1.0"
|
|
|
|
|
|
description = "Учебный симулятор занятия для системы 112 — бэкенд"
|
|
|
|
|
|
requires-python = ">=3.11,<3.12"
|
|
|
|
|
|
dependencies = [
|
|
|
|
|
|
"fastapi>=0.115",
|
|
|
|
|
|
"uvicorn[standard]>=0.30",
|
|
|
|
|
|
"pydantic>=2.7",
|
|
|
|
|
|
"pydantic-settings>=2.3",
|
|
|
|
|
|
"sqlalchemy[asyncio]>=2.0",
|
|
|
|
|
|
"alembic>=1.13",
|
|
|
|
|
|
"asyncpg>=0.29",
|
|
|
|
|
|
"httpx>=0.27",
|
2026-09-24 01:10:49 +03:00
|
|
|
|
"certifi>=2024",
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
"pyyaml>=6.0",
|
2026-09-21 17:40:54 +03:00
|
|
|
|
"reportlab>=4.0",
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
"structlog>=24.1",
|
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
|
|
|
|
# Эмбеддинги для слот-автомата: матчинг вопроса к чек-листу. Нужны ядру,
|
|
|
|
|
|
# а не только голосу, поэтому не в группе voice.
|
|
|
|
|
|
"onnxruntime>=1.18",
|
|
|
|
|
|
"tokenizers>=0.19",
|
|
|
|
|
|
"numpy>=1.26",
|
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
|
|
|
|
# Числа словами перед синтезом: Silero молча выбрасывает цифры, и звонящий
|
|
|
|
|
|
# произносит «улица Ленина, дом… квартира…» без номеров.
|
|
|
|
|
|
"num2words>=0.5.13",
|
feat: вход, роли и аудит действий (lct-23)
Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.
- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
call и station — обучающемуся и преподавателю. Отказ приходит событием
error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.
Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.
Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.
181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
|
|
|
|
# Вход и роли (lct-23): argon2 — современная рекомендация по хешированию
|
|
|
|
|
|
# паролей, itsdangerous нужен SessionMiddleware из starlette.
|
|
|
|
|
|
"argon2-cffi>=23.1",
|
|
|
|
|
|
"itsdangerous>=2.1",
|
2026-09-26 18:12:27 +03:00
|
|
|
|
"ldap3>=2.9.1,<3",
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
[project.optional-dependencies]
|
|
|
|
|
|
# Тяжёлый слой моделей — ставится отдельно: uv sync --extra voice
|
|
|
|
|
|
voice = [
|
|
|
|
|
|
"onnx-asr>=0.6",
|
|
|
|
|
|
"torch>=2.2",
|
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
|
|
|
|
# Нужен пакету Silero TTS: импорт зашит внутри v5_ru.pt.
|
|
|
|
|
|
"scipy>=1.11",
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
]
|
|
|
|
|
|
dev = [
|
|
|
|
|
|
"pytest>=8.2",
|
|
|
|
|
|
"pytest-asyncio>=0.23",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
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
|
|
|
|
# torch — только CPU-сборка. Из обычного PyPI он тянет CUDA-библиотеки
|
|
|
|
|
|
# на несколько гигабайт, а GPU на стенде нет (docs/arch/STACK.md).
|
|
|
|
|
|
[tool.uv.sources]
|
|
|
|
|
|
torch = { index = "pytorch-cpu" }
|
|
|
|
|
|
|
|
|
|
|
|
[[tool.uv.index]]
|
|
|
|
|
|
name = "pytorch-cpu"
|
|
|
|
|
|
url = "https://download.pytorch.org/whl/cpu"
|
|
|
|
|
|
explicit = true
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
[build-system]
|
|
|
|
|
|
requires = ["hatchling"]
|
|
|
|
|
|
build-backend = "hatchling.build"
|
|
|
|
|
|
|
|
|
|
|
|
[tool.hatch.build.targets.wheel]
|
|
|
|
|
|
packages = ["app"]
|
|
|
|
|
|
|
|
|
|
|
|
[tool.pytest.ini_options]
|
|
|
|
|
|
asyncio_mode = "auto"
|
2026-09-26 18:12:27 +03:00
|
|
|
|
# Живые запросы идут отдельно (`make test-llm-local`) к loopback-модели;
|
|
|
|
|
|
# локальный инференс медленный и не должен запускаться в каждом unit-прогоне.
|
|
|
|
|
|
markers = ["llm: live запрос к локальной LLM"]
|
lct-07 и lct-19: звонящий на LLM, офлайн-таблица, предгенерация
Что можно сказать, решает слот-автомат, а не модель: в промпт попадают
только раскрытые факты. Иначе LLM услужливо назовёт адрес, которого не
спрашивали. Промпты лежат файлами — их правит тот, кто ведёт занятия.
Отказ провайдера не роняет занятие: звонящий откатывается на заготовки.
Молчащий звонящий хуже шаблонной фразы.
Офлайн — таблица, а не локальная модель. Ветвление уже делает слот-автомат,
поэтому таблица индексируется парой «событие × настроение» и выходит
небольшой: для пожарного сценария 62 реплики, около 36 секунд генерации.
Формулировки в ней от облачной модели, а задержка на занятии нулевая —
локальная 3–4B дала бы формулировки хуже и за 2.5 секунды.
Найдено на бесплатной модели OpenRouter: рассуждающие модели тратят
сотни токенов на размышление и при малом бюджете возвращают пустой
content с finish_reason=length. Пустой ответ теперь честный отказ,
бюджет токенов поднят, рассуждение из ответа исключается.
Живые запросы к LLM вынесены из общего прогона: они требуют сети,
а рассуждающая модель отвечает десятками секунд.
2026-09-17 23:50:19 +03:00
|
|
|
|
addopts = "-m 'not llm'"
|