[backend][B] STT: распознавание голоса через OpenRouter (+mock), POST /me/{token}/stt #116
Notifications
Due Date
No due date set.
Blocks
#115 [epic] Грант ВШЭ и пилот «Династия»: голосовой ввод + сад (геймификация)
MedITron/PTSRaiAgent
#117 [web] Голосовой ввод в чате: запись → проверка распознанного текста → отправка
MedITron/PTSRaiAgent
Reference: MedITron/PTSRaiAgent#116
Reference in New Issue
Block a user
Описание
Часть эпика «Демо к гранту ВШЭ и пилоту „Династия“». Слайд 11 презентации: пациент рассказывает голосом, видит распознанный текст и может его исправить. Нужен серверный шаг «аудио → текст».
Образец — vobot (
backend/app/core/openrouter_llm.py): httpx-клиент к OpenAI-совместимому/chat/completions, ретраи на 429/5xx, HTTP-прокси, потому что OpenRouter режет РФ-IP. В нашем репо такой же клиент уже есть:app/llm/agent/providers.py::OpenRouterProvider. Ключ, прокси и base_url переиспользуем (OPENROUTER_*вapp/config.py).OpenRouter принимает аудио в chat completions как часть сообщения:
{"type":"input_audio","input_audio":{"data":"<base64>","format":"wav"}}. Модель берётся с аудиовходом, по умолчаниюgoogle/gemini-2.5-flash. В промпте только «дословно расшифруй русскую речь, не отвечай на неё».Желаемый результат
app/llm/stt.pyс протоколомSpeechToText.transcribe(audio: bytes, fmt: str) -> strи провайдерами:OpenRouterSTT: input_audio, temperature 0, таймаут, прокси, ретраи по образцу vobot;MockSTT: офлайн, возвращает фиксированную демо-фразу изconfig/texts/(ADR-004: демо работает без сети).STT_PROVIDER=off|mock|openrouter(по умолчаниюmock),STT_OPENROUTER_MODEL,STT_MAX_SECONDS=60,STT_MAX_BYTES.POST /api/v1/me/{token}/sttпринимает multipart с аудио (WAV 16 кГц mono) и возвращает{"text": "...", "provider": "openrouter"}. Приoffручка отвечает 404, а/patient/capabilitiesотдаётvoice_input: false, по этому флагу web скрывает кнопку./dialog/{token}/events, где работают safety иscrub().Подзадачи
app/llm/stt.py: протокол,OpenRouterSTT,MockSTT, фабрика по настройкеapp/config.pyи.env.example: STT_* (контрактные файлы, мелкий PR с пингом)app/api/patient_web.py: ручка/me/{token}/sttи флагvoice_inputв capabilitiesdocs/DECISIONS.md: ADR-014 «STT через OpenRouter только на синтетике, в пилоте РФ-провайдер»tests/test_llm_stt.py,tests/test_api_stt.py, OpenRouter мокается черезhttpx.MockTransportАнализ
Затрагиваемые файлы
app/llm/stt.pyapp/config.py,.env.exampleapp/api/patient_web.pydocs/DECISIONS.mdОграничения и риски
scrub(), оно уходит за рубеж целиком. Разрешено только на синтетике (демо, жюри, грант). Реальный пациент получает голос только через РФ-провайдера (задача SaluteSpeech). Это фиксируется в ADR-014 и в комментарии к провайдеру, как уже сделано дляOpenRouterProvider.Архитектурный выбор
Формат аудио принимает только WAV 16 кГц mono, кодирует его браузер. Альтернатива: принимать webm/opus и mp4 и перекодировать на сервере через ffmpeg. Отказались, потому что это лишняя зависимость в образе, а webm OpenRouter поддерживает не у всех моделей. Запись в 60 с весит около 1,9 МБ, это приемлемо.
Как проверить
STT_PROVIDER=mock make up→curl -F audio=@sample.wav localhost:8000/api/v1/me/<token>/stt→ демо-фраза. СSTT_PROVIDER=openrouterи ключом: реальная расшифровка синтетической записи.make lint test.2026-10-31
app/llm/stt.py(OpenRouterinput_audio+ mock), ручка/me/{token}/stt, capabilityvoice_input,config/texts/stt.yaml, ADR-014, тесты..envотвечает 401User not found; пользователь заменит ключ.