Контур замкнут без 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.
78 lines
3.4 KiB
Python
78 lines
3.4 KiB
Python
"""Сборка приложения. Роутеры подключаются по мере готовности — см. tasks/."""
|
||
|
||
import logging
|
||
from contextlib import asynccontextmanager
|
||
|
||
from fastapi import FastAPI
|
||
|
||
from pathlib import Path
|
||
|
||
from app.api.http import scenarios as scenarios_api
|
||
from app.api.http import sessions
|
||
from app.api.ws import call as call_ws
|
||
from app.api.ws import control as control_ws
|
||
from app.api.ws import observe as observe_ws
|
||
from app.config import get_settings
|
||
from app.db.base import get_sessionmaker
|
||
from app.dialog.runtime import get_embedder
|
||
from app.voice.models import get_voice_models
|
||
from app.scenarios import store
|
||
from app.session.hub import hub
|
||
from app.session.journal import DbJournal
|
||
from app.scenarios.loader import ScenarioError
|
||
|
||
|
||
LIBRARY = Path(__file__).resolve().parents[2] / "scenarios"
|
||
|
||
# uvicorn настраивает только собственные логгеры: без этого INFO из модулей
|
||
# приложения («получено N кадров», замеры задержки голоса) молча теряется,
|
||
# а до лога доходят одни предупреждения. Чужие библиотеки — от WARNING.
|
||
logging.basicConfig(level=logging.WARNING, format="%(levelname)-8s %(name)s: %(message)s")
|
||
logging.getLogger("app").setLevel(logging.INFO)
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
# Библиотека проверяется на старте целиком: сломанный сценарий, найденный
|
||
# посреди занятия, — сценарий, которого не должно случиться.
|
||
try:
|
||
loaded = store.load_from_disk(LIBRARY)
|
||
except ScenarioError as exc:
|
||
raise RuntimeError(f"библиотека сценариев не прошла проверку: {exc}") from exc
|
||
app.state.scenarios_loaded = len(loaded)
|
||
|
||
# Журнал: всё, что не записано, для оценки не существует.
|
||
hub.journal = DbJournal(get_sessionmaker())
|
||
|
||
# Эмбеддинги для слот-автомата — грузятся один раз, до первого занятия.
|
||
app.state.embeddings_ready = get_embedder() is not None
|
||
|
||
# Модели речи: ~5 секунд на старте стенда вместо паузы на первом звонке.
|
||
app.state.models_ready = get_voice_models() is not None
|
||
yield
|
||
|
||
await hub.shutdown()
|
||
for state in list(hub._sessions.values()):
|
||
if state.voice is not None:
|
||
await state.voice.close()
|
||
|
||
|
||
app = FastAPI(title="Учебный симулятор занятия для системы 112", lifespan=lifespan)
|
||
app.include_router(sessions.router)
|
||
app.include_router(scenarios_api.router)
|
||
app.include_router(call_ws.router)
|
||
app.include_router(observe_ws.router)
|
||
app.include_router(control_ws.router)
|
||
|
||
|
||
@app.get("/api/health")
|
||
async def health() -> dict:
|
||
"""Готовность стенда. Фронт показывает экран «модели прогреваются»."""
|
||
settings = get_settings()
|
||
return {
|
||
"status": "ok",
|
||
"models_ready": getattr(app.state, "models_ready", False),
|
||
"scenarios_loaded": getattr(app.state, "scenarios_loaded", 0),
|
||
"embeddings_ready": getattr(app.state, "embeddings_ready", False),
|
||
"offline": settings.offline,
|
||
}
|