diff --git a/.github/assets/dds-card.png b/.github/assets/dds-card.png new file mode 100644 index 0000000..c294709 Binary files /dev/null and b/.github/assets/dds-card.png differ diff --git a/.github/assets/dds-registry.png b/.github/assets/dds-registry.png new file mode 100644 index 0000000..ac163d9 Binary files /dev/null and b/.github/assets/dds-registry.png differ diff --git a/.github/assets/instructor.png b/.github/assets/instructor.png new file mode 100644 index 0000000..2ec087b Binary files /dev/null and b/.github/assets/instructor.png differ diff --git a/.github/assets/kio-112.png b/.github/assets/kio-112.png new file mode 100644 index 0000000..6a5b83c Binary files /dev/null and b/.github/assets/kio-112.png differ diff --git a/.github/assets/report.png b/.github/assets/report.png new file mode 100644 index 0000000..c3036f8 Binary files /dev/null and b/.github/assets/report.png differ diff --git a/.github/assets/wordmark-dark.svg b/.github/assets/wordmark-dark.svg new file mode 100644 index 0000000..10fd303 --- /dev/null +++ b/.github/assets/wordmark-dark.svg @@ -0,0 +1,6 @@ + + + 112 + Тренажёр 112 и ДДС + карточка · 30 секунд · нормативы ГОСТ · офлайн + diff --git a/.github/assets/wordmark-light.svg b/.github/assets/wordmark-light.svg new file mode 100644 index 0000000..b7f127a --- /dev/null +++ b/.github/assets/wordmark-light.svg @@ -0,0 +1,6 @@ + + + 112 + Тренажёр 112 и ДДС + карточка · 30 секунд · нормативы ГОСТ · офлайн + diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b56635 --- /dev/null +++ b/README.md @@ -0,0 +1,288 @@ +

+ + + Тренажёр 112 и ДДС + +

+ +

+ Тренажёр занятия для диспетчеров ДДС и операторов 112: карточка, 30 секунд на решение,
оценка по нормативам, которую считает код.

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

+ +

+ Запуск: docker compose up + Для жюри: критерии ТЗ +

+ +

+ Идея + Три решения + Как устроено + Посмотреть за 2 минуты + Что измерено + Критерии ТЗ + Запуск + Ограничения +

+ +

+ backend FastAPI · Python + frontend React · TypeScript + БД PostgreSQL + телефония Asterisk · WebRTC + LLM Qwen3 1.7B · Vikhr 1B + голос GigaAM v3 · Silero + режим офлайн, без облака +

+ +

+ АРМ ДДС: карточка происшествия, таймеры открытия и первой записи остановлены в норме, служба МВД ведёт статусы хода работ с основаниями +

+ +

АРМ ДДС на карточке из билета 25: оба таймера остановлены в норме, служба МВД ведёт статусы «Принята → Начало реагирования → Прибытие → Проведение работ» с основанием и адресатом.

+ +--- +## Идея + +Диспетчеров ДДС и операторов 112 сейчас готовят по памятке и билетам вручную: преподаватель +зачитывает вводную, курсант заполняет бумажную карточку, время засекают на глаз. Специального +ПО для этого нет, российских открытых аналогов мы не нашли. + +Мы строим **тренажёр занятия, а не одного звонка**. Преподаватель выдаёт готовые карточки из +96 вызовов 32 билетов заказчика. Курсант-диспетчер за 30 секунд принимает карточку или +мотивированно отказывает, выбирает бригаду и ведёт статусы хода работ своей службы. Система +засекает время, находит ошибки D1–D6 из памятки АРМ-112 и ставит числовую оценку, где у каждого +снятого балла есть факт, норматив и рекомендация. + +> **Нормативы — из первоисточников, а не собственных шкал.** 30 с на открытие карточки ДДС — +> ПП РФ № 1931 и памятка АРМ-112. Ответ на вызов 8 с, опрос 75 с, оповещение ДДС 60 с — +> ГОСТ Р 22.7.03-2021. Список оповещения строится по классификатору ЕКП v046.24 (1283 кода). + +Второе упражнение — **оператор 112 по текстовой вводной**: опросная карта → карточка +информационного обмена (КИО) → список оповещения по ЕКП → передача в ДДС. Живой голосовой +заявитель — дополнительный режим поверх, он не подменяет цикл ДДС. + +## Три решения + + + + + + + +
+ +**Баллы считает код** + +Оценка — детерминированные правила: факт против норматива, код ошибки, обоснование. LLM-судьи +нет. Qwen играет заявителя и пишет черновики сценариев, Vikhr — рекомендации к уже проваленным +критериям. Итог преподаватель может поправить, только с обоснованием и записью в аудит. + + + +**Таймер останавливает событие** + +Таймер ГОСТ останавливается действием курсанта — открытием карточки, первой записью, статусом, +— а не таймаутом клиента. Время фиксирует сервер в UTC, поэтому каждую просрочку в отчёте можно +предъявить: когда началось, чем закончилось, какой предел. + + + +**Всё локально** + +Модели, телефония и база живут на одной машине. В офлайн-режиме нелокальный адрес модели +отвергается, а не молча подменяется заготовкой. Основной путь ДДС вообще не требует моделей. +После скачивания образов стенду сеть не нужна. + +
+ +## Как устроено + +```mermaid +flowchart LR + I["Преподаватель
билет · упражнение · курсант"] --> Q["Очередь карточек
интервал 0–300 с"] + Q --> R["Реестр АРМ ДДС
у каждой карточки свой таймер"] + R --> D{"Решение ≤ 30 с"} + D -- "Принята" --> B["Бригада
учебная связь"] + D -- "Не принята" --> W["Отказ только
с причиной"] + B --> S["Статусы хода работ
основание · адресат"] + S --> E["Правила D1–D6
и таймеры ГОСТ"] + W --> E + E --> O["Оценка 0–100
разбор · CSV · PDF"] +``` + +| Код | Ошибка из памятки АРМ-112 | +|---|---| +| D1 | Статус реагирования не проставлен, карточка ушла в «Не оповещено»; карточка открыта позже 30 с | +| D2 | Статус не соответствует факту: «Принята» там, где работы не ведутся, и наоборот | +| D3 | Отказ от профильного происшествия вопреки эталону сценария | +| D4 | Отказ без комментария | +| D5 | Неполный комментарий: не назван получатель переданных сведений | +| D6 | Нет статусов хода работ: начало, прибытие, завершение | + +Контракт фронта и бэка — Pydantic-модели в [`backend/app/domain/`](backend/app/domain/): +КИО, события, таймеры, статусы ДДС, ЕКП, таксономия ошибок. TypeScript-типы фронта генерируются +из них (`make types`), расхождение ловит тест. Курсант не получает ни фактов сценария, ни +эталона: их не видно даже в DevTools. + +## Посмотреть за 2 минуты + +После [запуска](#запуск) войдите преподавателем на `http://localhost:5173` (`teacher` / `teacher`). + +| Шаг | Экран | Что увидите | +|---|---|---| +| 1 | `/instructor` — пульт преподавателя | Фильтры по билету, категории ЕКП и сложности; упражнение «Диспетчер ДДС»; «Начать занятие» выдаёт ссылку на АРМ курсанта | +| 2 | `/dds` — реестр АРМ ДДС | Карточки поступают по одной, у каждой идёт свой таймер 30 с и видно, сколько карточек осталось | +| 3 | Карточка ДДС | «Принята» или «Не принята»; отказ без причины не сохраняется, бригаду выбирает сам курсант | +| 4 | Ход работ | Статусы с основанием и адресатом; «Связь» открывает учебный звонок старшему бригады через Asterisk | +| 5 | Результаты занятия | Итог, балл, «на чём сосредоточиться», разбор по каждому критерию со ссылкой на норматив, профиль компетенций | + +

+ Пульт преподавателя: четыре шага до запуска — сценарий из билета 25, упражнение «Диспетчер ДДС», справа история группы +

+

Шаг 1. Пульт: занятие собирается из билета заказчика, упражнение выбирается явно.

+ +

+ Реестр АРМ ДДС: две карточки в работе, у первой статус «МВД: Принята, открыта за 3 с», у второй «не открыта 4 с» +

+

Шаг 2. Реестр ДДС: две карточки в работе одновременно, таймер каждой идёт независимо.

+ +

+ Результаты занятия: «Не зачтено», 68 из 100, разбор по критериям — открытие карточки за 123,7 с при нормативе 30 с по ГОСТ Р 22.7.03-2021 +

+

Шаг 5. Прогон с ошибками нарочно: система поймала открытие карточки через 123,7 с при нормативе +30 с и пропущенные статусы хода работ — каждая строка со ссылкой на источник.

+ +
+Упражнение 112: вводная → КИО → список оповещения +
+

+ АРМ 112: слева диалог с заявителем, справа карточка информационного обмена — адрес, код ЕКП 15220100, признаки происшествия и автоматически собранный список оповещения +

+

Курсант опрашивает заявителя, заполняет КИО, код ЕКП выбирает список служб для оповещения. +Реплики с пометкой «Qwen» написала локальная модель, остальные — из сценария.

+
+ +## Что измерено + +Все прогоны — на машине разработки. + +| Цифра | Что показывает | +|---|---| +| **746 passed**, 8 skipped | Полный прогон бэкенда на чистой PostgreSQL (`make test-db`): пропуски — голосовые тесты без моделей | +| **p95 946,74 мс**, 100/100 страниц | 20 занятий и 100 разных учётных записей за Nginx, подключение в течение 2 с, ошибок нет | +| **p95 1187,08 мс**, 100/100 | То же на двух backend-узлах за Nginx с привязкой сессии к узлу | +| **17,87 с**, 100/100 каналов | SIGKILL одного узла под нагрузкой 20×100: все 20 занятий переходят на второй узел, каналы восстановлены за 18,30 с | +| **11,19 с**, 27/27 проверок | Узел теряет связь с БД: второй узел забирает занятие, старый отстранён от записи (fencing epoch 1 → 2) | +| **97,9 и 99,0** из 100 | Карточка ДДС начата до отказа узла и закончена после; все пять статусов записаны ровно один раз | +| **2,25 с** после обрыва 30 с | Nginx недоступен 30 с; после возврата WSS переподключился, неподтверждённая правка КИО дошла до сервера | +| **13 288** записей/с | Запись в журнал аудита PostgreSQL при 20 соединениях; требование ТЗ — от 100 операций/с | +| **0,19 с** | Формирование PDF-отчёта после перезапуска backend; норматив ТЗ — 30 с | +| **p95 RTT 1,5 мс**, 29 пакетов | SIP/RTP на локальном Asterisk: REGISTER, Echo-вызов, BYE (`make sip-test`) | +| **2,57 с** до соединения | Два браузера 6101 ↔ 6102 по WebRTC, аудиотрек у обеих сторон, запись WAV (`make webrtc-test`) | +| **0 утечек** на 96 карточках | Провокационные вопросы локальному Qwen: скрытые факты сценария не выданы, 11 ответов из 96 заменены заготовкой | +| **0 переполнений** на 390 px | 7 экранов курсанта и преподавателя в мобильной ширине, нет контролов меньше 40 px и JS-ошибок | +| STT **≈ 0,29 с**, TTS **0,14–0,17 с** | GigaAM v3 на фразе 2 с и Silero на реплике; время LLM до первого предложения не измерено | + +## Критерии ТЗ + +
+Где закрыт каждый ключевой критерий — «стенд»: проверено на локальном стенде, «код»: есть код и тесты +
+ +| Требование | Чем закрыто | Статус | +|---|---|---| +| ДДС получает готовую карточку от преподавателя | Пульт `/instructor` → очередь → АРМ `/dds` | стенд | +| «Принята / Не принята» за 30 с, отказ только с причиной | Таймер открытия 30 с, отказ без комментария не сохраняется, ошибки D1 и D4 | стенд | +| Ручной выбор бригады и статусы хода работ | Карточка ДДС, автомат статусов, ошибка D6 | стенд | +| Несколько карточек одновременно | Интервал поступления 0–300 с, до 10 ожидающих, у каждой свой таймер | стенд | +| Упражнение 112: вводная → КИО → оповещение → передача в ДДС | АРМ `/trainee`, классификатор ЕКП на 1283 кода | стенд | +| Вызовы из билетов заказчика | 96 вызовов 32 билетов в `scenarios/tickets/`, сверка с каждой страницей скана | стенд | +| Числовая оценка, веса настраивает преподаватель | Правила в `backend/app/scoring/`, веса 0–10 на пульте | код | +| Корректировка оценки | Итог 0–100 с обязательным обоснованием и записью в аудит | код | +| Генерация сценариев ИИ | Локальный Qwen → черновик → детерминированная проверка → утверждение преподавателем | код | +| Аналитика группы и рекомендации | `/groups`, рекомендации по кодам ошибок, профиль курсанта | код | +| Работа без внешних сервисов | `OFFLINE=true`, облачный адрес модели отвергается | стенд | +| Роли и AD/LDAP | Администратор, преподаватель, курсант; LDAPS / StartTLS | роли — стенд, LDAP — код | +| Локальная IP-телефония | Asterisk: SIP по UDP/TCP/TLS и WebRTC, запись разговоров | стенд | +| 20 занятий, 100 пользователей, отклик ≤ 2 с | Nginx, один или два backend-узла | стенд при подключении за 2 с | +| Запись ≥ 100 оп/с, отчёт ≤ 30 с, сбой сети до 30 с | PostgreSQL, отчёты JSON/CSV/PDF, клиентский буфер 35 с | стенд | +| Мобильный интерфейс | Одна колонка на 390 px, сенсорные цели от 40 px | стенд | + +
+ +## Запуск + +Нужен Docker на x86-64. Образы собраны заранее, `.env` не обязателен: + +```bash +docker compose up # или make dev: база, backend, фронт, Asterisk, Qwen и Vikhr +``` + +Учётки заводятся при старте, пароль совпадает с логином: `admin`, `teacher`, `trainee`. +Стенд открывается на `http://localhost:5173`, порты опубликованы на всех интерфейсах — курсанты +подключаются из локальной сети. Когда образы скачаны, `make offline` поднимает его без обращения к сети. + +
+Показ без Docker, базы и моделей +
+ +```bash +make demo-lite +VITE_DEMO_MODE=true VITE_BACKEND_URL=http://127.0.0.1:8112 VITE_WS_BACKEND_URL=ws://127.0.0.1:8112 \ + npm --prefix frontend run dev -- --host 127.0.0.1 --port 5173 +``` + +Вход — «Войти в демо одним нажатием». Данные живут в памяти до остановки backend; история, +аналитика группы и голос в этом режиме недоступны. + +
+ +
+Разработка +
+ +``` +make demo база в Docker, backend и фронт нативно, готовые карточки +make test тесты бэкенда make test-db полный прогон на временной PostgreSQL +make typecheck проверка фронта make types Pydantic → TypeScript +make sip только Asterisk make sip-test / make webrtc-test — приёмка связи +make images собрать 4 образа стенда (docker-bake.hcl) +``` + +``` +backend/app/domain/ контракт: КИО, события, таймеры, статусы ДДС, ЕКП, таксономия ошибок +backend/app/scoring/ правила оценки и нормативы +frontend/ АРМ ДДС, АРМ 112, пульт, монитор, профиль +scenarios/tickets/ 96 вызовов из билетов заказчика +sip/ локальный Asterisk: SIP и WebRTC +deploy/ образ LLM и лицензии весов +``` + +
+ +## Ограничения + +- **Отклик ≤ 2 с при одновременном старте 100 интерфейсов не выполнен.** Порог держится, когда + подключение растянуто на 2 с. При старте всех сразу p95 — от 2,3 до 3,3 с в разных прогонах. +- **Целевой сервер 6 ядер / 32 ГБ не испытывался.** Все цифры выше сняты на машине разработки. +- **Windows 10/11 не проверялся**, установки на ПК заказчика не было. +- **AD/LDAP проверен только на заглушках**, подключения к настоящему каталогу не было. +- **Телефония проверена только на localhost** с синтетическим звуком браузера: учебная сеть, + гарнитуры и качество звука — нет. Речь в SIP-звонке не распознаётся и не оценивается. +- **Голосовой заявитель — дополнительный режим:** время LLM до первой фразы не измерено, + сквозной голосовой прогон на целевом железе не проводился. +- **Методику не утверждал заказчик.** Веса критериев, вклад времени в балл и эталонные отказы по + 96 билетам — допущения команды. Из 15 условий списка оповещения ЕКП из КИО выводятся 5. +- **Двухузловый режим не закрыт до конца:** отстранение старого узла при свежей авторизации не + прогонялось отдельно, доставка «ровно один раз» для всех команд канала не заявляется. + +--- + +

+Веса моделей не изменялись, лицензии — в deploy/licenses/: +Qwen3 — Apache 2.0, GigaAM и Silero VAD — MIT, Silero TTS — CC BY-NC-SA 4.0 (только некоммерческое использование).
+Built with Llama. Vikhr-Llama-3.2-1B-Instruct — дообучение Llama 3.2, Llama 3.2 Community License, © Meta Platforms, Inc.
+Хакатон ЛЦТ 2026, задача № 9. +