From 9847ad67521a0aa73e1365c0edb8c8eb8d0707c2 Mon Sep 17 00:00:00 2001 From: chexit Date: Tue, 10 Nov 2026 23:02:27 +0300 Subject: [PATCH] Use Mini 2 firmware audio processing and Scribe v3 Live --- .env.example | 2 + README.md | 4 +- music/main.go | 2 +- server/providers.mjs | 7 +- server/public/index.html | 2 +- server/store.mjs | 5 +- server/turns.mjs | 10 +- station/README.md | 19 +++- station/capture.go | 165 +++++++++++++++++++++++++++ station/keyword.go | 25 +++- station/main.go | 207 +++++++++------------------------- station/main_test.go | 13 ++- station/station-supervisor.sh | 2 +- station/vqe-capture.sh | 11 ++ station/vqe/capture.c | 98 ++++++++++++++++ test/admin.test.mjs | 4 +- test/voice.test.mjs | 24 +++- 17 files changed, 416 insertions(+), 184 deletions(-) create mode 100644 station/capture.go create mode 100755 station/vqe-capture.sh create mode 100644 station/vqe/capture.c diff --git a/.env.example b/.env.example index 16deed9..c95460b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,8 @@ # Provide the actual values privately in .env. DEVICE_TOKEN= ELEVENLABS_API_KEY= +# Optional comma-separated names for Scribe Realtime (+20% STT cost when set). +ELEVEN_STT_KEYTERMS= OPENROUTER_API_KEY= TELEGRAM_BOT_TOKEN= TELEGRAM_OWNER_ID= diff --git a/README.md b/README.md index 5ee8bae..99f4782 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ Собственный голосовой клиент для рутированной Mini 2, сервер на Node.js и пульт в Telegram. На колонке работает русская модель ключевого слова «Алиса». Когда клиент готов принимать команду, верхнее кольцо загорается сине-фиолетовым. Произнеси команду после включения кольца. ```text -Mini 2 → WSS с PCM16 / 16 кГц → Scribe v2 Realtime +Mini 2 → VQE → WSS с PCM16 / 16 кГц → Scribe v3 Live ← WSS с PCM16 / 24 кГц ← Eleven v4 Turbo ← OpenRouter / Haiku 5.5 ``` -Scribe выдаёт промежуточный текст, а запрос к LLM начинается после подтверждения конца команды. Текстовые фрагменты из OpenRouter сразу идут в ElevenLabs, звуковые фрагменты сразу уходят на колонку. Запрос и ответ в текстовом виде сохраняются после успешного хода. Сервер держит 50 последних ходов и использует до 10 из них как контекст. Аудио сервер не пишет на диск. Облачные провайдеры обрабатывают отправленные им аудио и текст. +Модель Scribe можно переключить в пульте, включая Scribe v2 Realtime. Scribe выдаёт промежуточный текст, а запрос к LLM начинается после подтверждения конца команды. Текстовые фрагменты из OpenRouter сразу идут в ElevenLabs, звуковые фрагменты сразу уходят на колонку. Запрос и ответ в текстовом виде сохраняются после успешного хода. Сервер держит 50 последних ходов и использует до 10 из них как контекст. Аудио сервер не пишет на диск. Облачные провайдеры обрабатывают отправленные им аудио и текст. ## Запуск и проверка diff --git a/music/main.go b/music/main.go index 79bdba9..5f4d83a 100644 --- a/music/main.go +++ b/music/main.go @@ -32,7 +32,7 @@ func execute(req request) (map[string]interface{}, error) { if req.Name == "duck" { volume := "100%" if req.Arguments.Active { - volume = "0%" + volume = "75%" } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() diff --git a/server/providers.mjs b/server/providers.mjs index 28cc739..c1a6072 100644 --- a/server/providers.mjs +++ b/server/providers.mjs @@ -2,6 +2,7 @@ import WebSocket from 'ws'; import { tools, validateTool } from './tools.mjs'; const env = process.env; +const sttKeyterms = (env.ELEVEN_STT_KEYTERMS || '').split(',').map(term => term.trim()).filter(Boolean).slice(0, 50); const sttUrl = env.ELEVEN_STT_URL || 'wss://api.elevenlabs.io/v1/speech-to-text/realtime'; const ttsUrl = env.ELEVEN_TTS_URL || 'wss://api.elevenlabs.io/v1/text-to-dialogue/stream-input'; const flashUrl = env.ELEVEN_FLASH_URL || 'wss://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream-input'; @@ -9,7 +10,9 @@ const llmUrl = env.OPENROUTER_URL || 'https://openrouter.ai/api/v1/chat/completi function endpoint(base, params) { const url = new URL(base); - for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value); + for (const [key, value] of Object.entries(params)) { + for (const item of Array.isArray(value) ? value : [value]) url.searchParams.append(key, item); + } return url.toString(); } function connect(url, signal) { @@ -25,7 +28,7 @@ function connect(url, signal) { } export async function openStt(settings, signal, onPartial, onFinal, onFailure) { - const ws = await connect(endpoint(sttUrl, { model_id: settings.sttModel, audio_format: 'pcm_16000', language_code: 'ru', commit_strategy: 'manual', filter_background_audio: 'true' }), signal); + const ws = await connect(endpoint(sttUrl, { model_id: settings.sttModel, audio_format: 'pcm_16000', language_code: 'ru', commit_strategy: 'manual', filter_background_audio: 'true', keyterms: sttKeyterms }), signal); let final = false; ws.on('message', data => { try { diff --git a/server/public/index.html b/server/public/index.html index 263471b..ac81dae 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -29,7 +29,7 @@

Spotify

Проверяем подключение…

Голосовой тракт

- +