diff --git a/docs/backlog/stt-providers-and-async.md b/docs/backlog/stt-providers-and-async.md new file mode 100644 index 00000000..58da2879 --- /dev/null +++ b/docs/backlog/stt-providers-and-async.md @@ -0,0 +1,192 @@ +# STT: дополнительные провайдеры и переход на асинхронную схему + +> Статус: беклог / план развития. Контекст — фича «голосовая диктовка» (STT, +> speech-to-text): кнопка-микрофон в чате агента и в редакторе, аудио +> распознаётся на сервере через AI-провайдер воркспейса. Документ фиксирует +> (1) какие ещё форматы STT-API имеет смысл поддержать и как, и (2) как в +> будущем перейти с текущей синхронной схемы (push-to-talk) на асинхронную. + +## 1. Где мы сейчас + +Распознавание построено как **синхронный запрос-ответ**: + +- Клиент пишет звук (`MediaRecorder`), POST-ит blob → сервер распознаёт → + возвращает `{ text }`, который вставляется в ввод. Никакого состояния задачи нет. +- Клиентская часть: `apps/client/src/features/dictation/` (`hooks/use-dictation.ts`, + `components/mic-button.tsx`, `services/dictation-service.ts`). +- Эндпоинт: `POST /ai-chat/transcribe` + (`apps/server/src/core/ai-chat/ai-chat.controller.ts`) — фича-гейт + `settings.ai.dictation`, throttle, лимит 25 МБ, whitelist mime, вывод реальной + ошибки провайдера (`describeProviderError`), формат контейнера выводится из mime. +- Тонкая обёртка: `apps/server/src/core/ai-chat/ai-transcription.service.ts` → + делегирует в `AiService.transcribe(workspaceId, audio, format)`. +- Выбор кодировки запроса — **явное** поле `sttApiStyle` + (`apps/server/src/integrations/ai/ai.types.ts`, `SttApiStyle`, + `STT_API_STYLES`): + - `multipart` — OpenAI-совместимый `POST /v1/audio/transcriptions` (form-data) + через AI SDK (`createOpenAI(...).transcription()` + `experimental_transcribe`); + - `json` — OpenRouter-стиль: `POST {baseURL}/audio/transcriptions`, + `Content-Type: application/json`, тело `{ model, input_audio: { data:, format } }`, + ответ `{ text }` (`AiService.transcribeJsonBase64`). +- Поле прокладывается как любой не-секрет: `resolve()` / `getMasked()` / + whitelist в `AiSettingsService.update` + (`apps/server/src/integrations/ai/ai-settings.service.ts`) **и** массив + `ALLOWED` в `WorkspaceRepo.updateAiProviderSettings` + (`apps/server/src/database/repos/workspace/workspace.repo.ts`). +- UI: селектор «Request format» на карточке Voice / STT + (`apps/client/.../settings/components/ai-provider-settings.tsx`) + + кнопка «Test endpoint» (бэкенд-проба — тихий WAV через тот же `transcribe`). + +**Важно:** `multipart` уже покрывает почти всю экосистему — её реализуют OpenAI, +Azure OpenAI (Whisper), Groq, Together, Fireworks, DeepInfra, vLLM, LM Studio, +whisper.cpp/llama.cpp server, `speaches`, `faster-whisper-server`, WhisperX. +Для них **новый формат не нужен**, достаточно base URL + модель + ключ. +`json` покрывает OpenRouter. Ось `sttApiStyle` — это абстракция над +*контрактом запроса/ответа*: каждый реально иной контракт = одно значение enum ++ одна ветка-энкодер. + +### Точки расширения для нового СИНХРОННОГО формата (чек-лист) + +1. `ai.types.ts` — добавить значение в `SttApiStyle` и `STT_API_STYLES`. +2. `dto/update-ai-settings.dto.ts` — `@IsIn(STT_API_STYLES)` подхватит автоматически. +3. `ai.service.ts` — ветка в `transcribe()` + приватный энкодер + (по образцу `transcribeJsonBase64`): сборка запроса, заголовок авторизации, + `!res.ok` → бросок со статусом+телом (без утечки ключа), парс ответа в `text`. +4. Клиент: `ai-settings-service.ts` (тип `SttApiStyle`), опция в `