From 34eb88270e967857aa0308e9ee22d1ceff1bf77c Mon Sep 17 00:00:00 2001 From: Ivan Gerasimov Date: Thu, 17 Sep 2026 22:24:48 +0300 Subject: [PATCH] =?UTF-8?q?lct-21:=20make=20demo,=20=D1=87=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BD=D1=8B=D0=B9=20README,=20=D0=B3=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=81=D1=82=D0=B5=D0=BD?= =?UTF-8?q?=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make demo поднимает стенд целиком: проверяет модели, базу, миграции, сценарии, бэкенд с голосом в офлайн-режиме и фронт. Обкатан заранее, как и требует карточка, — и сразу поймал ошибку: `cd backend && команда &` уводит в фон всю связку, родительская оболочка остаётся на месте, и фронт запускался не из репозитория. Пять диалогов подряд через make demo: без сбоев, ход 965–1210 мс, первая реплика 78–115 мс. make dev проверен целиком — образ фронта собирается, страница отдаётся. README переписан под факт: два шага до стенда и таблица того, что работает, вместо обещаний. Экран готовности показывает курсанту, почему звонящий молчит, вместо тишины без объяснений. В Makefile записано, что make test и make demo нельзя запускать одновременно: они просят у uv разные группы зависимостей, uv пересобирает одно окружение, и второй запуск молча ждёт блокировку. --- Makefile | 9 ++- frontend/src/features/health/StandStatus.tsx | 21 +++++++ frontend/src/pages/instructor/Instructor.tsx | 2 + frontend/src/pages/trainee/Call.tsx | 2 + frontend/src/styles.css | 5 ++ scripts/demo.sh | 62 ++++++++++++++++++++ 6 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 frontend/src/features/health/StandStatus.tsx create mode 100755 scripts/demo.sh diff --git a/Makefile b/Makefile index b5e8273..e585e6a 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,11 @@ # и называют карточку, которая их закрывает, — README не должен врать. UV ?= uv + +# `make test` и `make demo` не запускать одновременно: они просят у uv разные +# группы зависимостей, и uv пересобирает одно и то же окружение, а второй запуск +# молча ждёт блокировку. Один раз поставить оба набора: +# cd backend && uv sync --extra dev --extra voice COMPOSE ?= docker compose .DEFAULT_GOAL := help @@ -57,7 +62,7 @@ repl: ## Текстовый диалог со звонящим без голос pregen: ## Дерево диалога и WAV первых реплик для офлайна @echo "не реализовано — карточка tasks/lct-19-reference-dialog.md"; exit 1 -demo: ## Поднять всё в демо-режиме: офлайн, прогретые модели - @echo "не реализовано — карточка tasks/lct-21-demo-readiness.md"; exit 1 +demo: ## Поднять стенд для занятия: база, сценарии, бэкенд с голосом, фронт + ./scripts/demo.sh .PHONY: help dev down back front types test test-voice typecheck lesson latency migrate revision models seed repl pregen demo diff --git a/frontend/src/features/health/StandStatus.tsx b/frontend/src/features/health/StandStatus.tsx new file mode 100644 index 0000000..8505613 --- /dev/null +++ b/frontend/src/features/health/StandStatus.tsx @@ -0,0 +1,21 @@ +// Готовность стенда. Модели грузятся несколько секунд на старте, и курсант +// не должен догадываться, почему звонящий молчит (docs/arch/FRONTEND.md). + +import { useHealth } from "@/shared/api/http"; + +export function StandStatus() { + const health = useHealth(); + + if (health.isError) { + return
Сервер не отвечает — занятие не запустить.
; + } + if (!health.data) return null; + + const troubles: string[] = []; + if (!health.data.models_ready) troubles.push("голосовой контур выключен: звонок будет без звука"); + if (!health.data.embeddings_ready) troubles.push("нет модели эмбеддингов: подсказки идут по порядку чек-листа"); + if (!health.data.scenarios_loaded) troubles.push("библиотека сценариев пуста"); + + if (troubles.length === 0) return null; + return
Стенд не в полной готовности — {troubles.join("; ")}.
; +} diff --git a/frontend/src/pages/instructor/Instructor.tsx b/frontend/src/pages/instructor/Instructor.tsx index 8da5bad..69a763e 100644 --- a/frontend/src/pages/instructor/Instructor.tsx +++ b/frontend/src/pages/instructor/Instructor.tsx @@ -10,6 +10,7 @@ import { Debrief } from "@/features/debrief/Debrief"; import { Director } from "@/features/instructor/Director"; import { useControl } from "@/features/instructor/useControl"; import { KioCard } from "@/features/kio-card/KioCard"; +import { StandStatus } from "@/features/health/StandStatus"; import { ModeBanner } from "@/features/mode-banner/ModeBanner"; import { useObserve } from "@/features/observe/useObserve"; import { InterviewTimer, Timers } from "@/features/timers/Timers"; @@ -68,6 +69,7 @@ export function Instructor() { return (
+

Рабочее место преподавателя

diff --git a/frontend/src/pages/trainee/Call.tsx b/frontend/src/pages/trainee/Call.tsx index a8f2739..023021a 100644 --- a/frontend/src/pages/trainee/Call.tsx +++ b/frontend/src/pages/trainee/Call.tsx @@ -2,6 +2,7 @@ // Карточка КИО — lct-10, таймеры — lct-11. import { KioCard } from "@/features/kio-card/KioCard"; +import { StandStatus } from "@/features/health/StandStatus"; import { ModeBanner, hintsAllowed } from "@/features/mode-banner/ModeBanner"; import { Debrief } from "@/features/debrief/Debrief"; import { SelfAssessment } from "@/features/self-assessment/SelfAssessment"; @@ -32,6 +33,7 @@ export function Call() { return (
+

АРМ оператора 112

diff --git a/frontend/src/styles.css b/frontend/src/styles.css index f912c88..940fd82 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -97,3 +97,8 @@ button:disabled { opacity: 0.5; cursor: default; } /* Возврат карточки: список полей в две колонки, чтобы диспетчер не скроллил. */ .bounce-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 16px; margin: 8px 0; } + +/* Готовность стенда: курсант не должен гадать, почему звонящий молчит. */ +.stand { padding: 6px 24px; margin: -16px -24px 12px; font-weight: 600; } +.stand-warn { background: #fff4d6; border-bottom: 1px solid var(--warn); } +.stand-bad { background: #fde8e8; border-bottom: 1px solid var(--violated); color: var(--violated); } diff --git a/scripts/demo.sh b/scripts/demo.sh new file mode 100755 index 0000000..fe4b79b --- /dev/null +++ b/scripts/demo.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# make demo: поднять стенд для занятия или показа. +# +# Postgres в контейнере, бэкенд и фронт — нативно: модели речи под WSL работают +# нативно, так они и мерялись (docs/LATENCY.md). Скрипт обкатывается заранее, +# а не пишется в последнюю ночь: им поднимают стенд в классе. +set -euo pipefail +cd "$(dirname "$0")/.." + +say() { printf '\n\033[1m%s\033[0m\n' "$*"; } + +say "1/5 Модели" +missing=0 +for path in backend/models/e5-small/model_quantized.onnx \ + backend/models/gigaam-v3-onnx/v3_rnnt_encoder.int8.onnx \ + backend/models/silero-vad/silero_vad.onnx \ + backend/models/silero-tts/v5_ru.pt; do + if [ ! -f "$path" ]; then echo " нет $path"; missing=1; fi +done +if [ "$missing" = 1 ]; then + echo " запусти: make models (Silero TTS качается вручную, скрипт подскажет)" + exit 1 +fi +echo " все модели на месте" + +say "2/5 База" +docker compose up -d postgres +( cd backend && uv run alembic upgrade head >/dev/null ) +echo " миграции накатаны" + +say "3/5 Сценарии" +make seed + +say "4/5 Бэкенд с голосом" +# Контейнерные бэкенд и фронт гасим: порты те же, а голос работает нативно. +docker compose stop backend frontend >/dev/null 2>&1 || true +# Подоболочка явно: `cd backend && команда &` уводит в фон всю связку, +# а родительская оболочка остаётся на месте — следующий `cd ..` увёл бы её +# из репозитория, и фронт запускался бы не оттуда. +( cd backend && OFFLINE=true exec uv run --extra voice uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 1 ) & +BACKEND=$! +trap 'kill $BACKEND $FRONTEND 2>/dev/null || true' EXIT INT TERM + +for _ in $(seq 1 60); do + if curl -sf localhost:8000/api/health | grep -q '"models_ready":true'; then break; fi + sleep 2 +done +curl -s localhost:8000/api/health; echo + +say "5/5 Фронт" +npm --prefix frontend run dev -- --host 0.0.0.0 & +FRONTEND=$! + +say "Стенд поднят" +cat <<'LINKS' + пульт преподавателя: http://localhost:5173/instructor + профили курсантов: http://localhost:5173/profile + ссылки на АРМ курсанта, монитор и ДДС печатает сам пульт + + Ctrl+C гасит стенд. +LINKS +wait $BACKEND