iobroker.assistant 1.4.5 → 1.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -235,6 +235,9 @@ train it twice.
235
235
  Placeholder for the next version (at the beginning of the line):
236
236
  ### **WORK IN PROGRESS**
237
237
  -->
238
+ ### 1.4.6 (2026-11-03)
239
+ * (@GermanBluefox) Speech recognition: own OpenAI-compatible server (Voice tab, provider OpenAI, "Speech recognition server"), e.g. a local whisper-large-v3-turbo in speaches, faster-whisper-server or LocalAI. It needs no OpenAI key and never gets it; an own key for the server is optional
240
+
238
241
  ### 1.4.5 (2026-11-03)
239
242
  * (@GermanBluefox) ESPHome satellites: spoken replies go out in the format the device's media player announces (FLAC at its sample rate, as Home Assistant does; MP3/Opus via ffmpeg). Since ESPHome 2026.10 the player is built with only the codecs its pipelines use, and a WAV reply did not play at all. Devices that announce no formats keep getting WAV
240
243
  * (@GermanBluefox) ESPHome satellites: when the device ends the conversation itself (button, or the wake word said again), it now gets its RUN_END. It stayed in the conversation, and every further wake word only sent another stop
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "ESPHome-Satelliten",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "ESPHome-Satelliten bekommen die Antwort nicht über ihren API-Socket — sie holen sie als URL ab. Das ist der Port, auf dem dieser kleine HTTP-Server lauscht.",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "Leer = tts-1. Auch: tts-1-hd, gpt-4o-mini-tts.",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "Leer = whisper-1. Auch: gpt-4o-transcribe, gpt-4o-mini-transcribe.",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "Leer = whisper-1. Auch: gpt-4o-transcribe, gpt-4o-mini-transcribe. Mit eigenem Server: das Modell so, wie der Server es nennt, z. B. Systran/faster-whisper-large-v3-turbo.",
62
62
  "Enable voice (STT/TTS for satellites)": "Sprache aktivieren (STT/TTS für Satelliten)",
63
63
  "Enable voice server (satellites)": "Sprach-Server aktivieren (Satelliten)",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "Aktiviert Spracherkennung/-synthese. ioBroker-native Satelliten erreichen sie über den Nachrichtenbus — kein Port nötig. Den UDP-Server unten nur für ESP-/Hannah-Satelliten einschalten.",
@@ -222,5 +222,9 @@
222
222
  "Person": "Person",
223
223
  "Guest": "Gast",
224
224
  "Pet": "Haustier",
225
- "Group": "Gruppe"
225
+ "Group": "Gruppe",
226
+ "Speech recognition server (optional)": "Server für die Spracherkennung (optional)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "OpenAI-kompatibler Server statt OpenAI, z. B. ein lokales Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Leer = OpenAI.",
228
+ "Key of the speech recognition server": "Schlüssel des Spracherkennungs-Servers",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Nur, wenn der Server einen verlangt. Der OpenAI-Schlüssel wird nie dorthin geschickt."
226
230
  }
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "ESPHome satellites",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.",
62
62
  "Enable voice (STT/TTS for satellites)": "Enable voice (STT/TTS for satellites)",
63
63
  "Enable voice server (satellites)": "Enable voice server (satellites)",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.",
@@ -222,5 +222,9 @@
222
222
  "Person": "Person",
223
223
  "Guest": "Guest",
224
224
  "Pet": "Pet",
225
- "Group": "Group"
225
+ "Group": "Group",
226
+ "Speech recognition server (optional)": "Speech recognition server (optional)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.",
228
+ "Key of the speech recognition server": "Key of the speech recognition server",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Only if the server wants one. The OpenAI key is never sent to it."
226
230
  }
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "Satélites ESPHome",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "Los satélites ESPHome no reciben la respuesta por su socket de API: la descargan como URL. Este es el puerto en el que escucha ese pequeño servidor HTTP.",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "Vacío = tts-1. También: tts-1-hd, gpt-4o-mini-tts.",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "Vacío = whisper-1. También: gpt-4o-transcribe, gpt-4o-mini-transcribe.",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "Vacío = whisper-1. También: gpt-4o-transcribe, gpt-4o-mini-transcribe. Con un servidor propio: el modelo tal como lo llama el servidor, p. ej. Systran/faster-whisper-large-v3-turbo.",
62
62
  "Enable voice (STT/TTS for satellites)": "Habilitar la voz (STT/TTS para satélites)",
63
63
  "Enable voice server (satellites)": "Habilitar servidor de voz (satélites)",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "Permite el reconocimiento/síntesis de voz. Los satélites nativos de ioBroker se conectan a través del bus de mensajes; no se necesita ningún puerto. Active el servidor UDP que se indica a continuación solo para los satélites ESP/Hannah.",
@@ -222,5 +222,9 @@
222
222
  "Person": "Persona",
223
223
  "Guest": "Invitado",
224
224
  "Pet": "Mascota",
225
- "Group": "Grupo"
225
+ "Group": "Grupo",
226
+ "Speech recognition server (optional)": "Servidor de reconocimiento de voz (opcional)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "Servidor compatible con OpenAI en lugar de OpenAI, p. ej. un Whisper local: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Vacío = OpenAI.",
228
+ "Key of the speech recognition server": "Clave del servidor de reconocimiento de voz",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Solo si el servidor la pide. La clave de OpenAI nunca se envía allí."
226
230
  }
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "Satellites ESPHome",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "Les satellites ESPHome ne reçoivent pas la réponse par leur socket API : ils la récupèrent via une URL. C’est le port sur lequel écoute ce petit serveur HTTP.",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "Vide = tts-1. Également : tts-1-hd, gpt-4o-mini-tts.",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "Vide = whisper-1. Également : gpt-4o-transcribe, gpt-4o-mini-transcribe.",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "Vide = whisper-1. Également : gpt-4o-transcribe, gpt-4o-mini-transcribe. Avec un serveur propre : le modèle tel que le serveur le nomme, p. ex. Systran/faster-whisper-large-v3-turbo.",
62
62
  "Enable voice (STT/TTS for satellites)": "Activer la voix (STT/TTS pour les satellites)",
63
63
  "Enable voice server (satellites)": "Activer le serveur vocal (satellites)",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "Permet la reconnaissance et la synthèse vocales. Les satellites natifs d'ioBroker y accèdent via le bus de messages ; aucun port n'est nécessaire. Activez le serveur UDP ci-dessous uniquement pour les satellites ESP/Hannah.",
@@ -222,5 +222,9 @@
222
222
  "Person": "Personne",
223
223
  "Guest": "Invité",
224
224
  "Pet": "Animal",
225
- "Group": "Groupe"
225
+ "Group": "Groupe",
226
+ "Speech recognition server (optional)": "Serveur de reconnaissance vocale (facultatif)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "Serveur compatible OpenAI à la place d'OpenAI, p. ex. un Whisper local : http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Vide = OpenAI.",
228
+ "Key of the speech recognition server": "Clé du serveur de reconnaissance vocale",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Seulement si le serveur en demande une. La clé OpenAI n'y est jamais envoyée."
226
230
  }
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "Satelliti ESPHome",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "I satelliti ESPHome non ricevono la risposta dal loro socket API: la scaricano come URL. Questa è la porta su cui ascolta quel piccolo server HTTP.",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "Vuoto = tts-1. Anche: tts-1-hd, gpt-4o-mini-tts.",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "Vuoto = whisper-1. Anche: gpt-4o-transcribe, gpt-4o-mini-transcribe.",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "Vuoto = whisper-1. Anche: gpt-4o-transcribe, gpt-4o-mini-transcribe. Con un server proprio: il modello come lo chiama il server, ad es. Systran/faster-whisper-large-v3-turbo.",
62
62
  "Enable voice (STT/TTS for satellites)": "Abilita la voce (STT/TTS per i satelliti)",
63
63
  "Enable voice server (satellites)": "Abilita il server vocale (satelliti)",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "Abilita il riconoscimento/sintesi vocale. I satelliti nativi di ioBroker lo raggiungono tramite il bus di messaggi, senza bisogno di porte. Attivare il server UDP qui sotto solo per i satelliti ESP/Hannah.",
@@ -222,5 +222,9 @@
222
222
  "Person": "Persona",
223
223
  "Guest": "Ospite",
224
224
  "Pet": "Animale",
225
- "Group": "Gruppo"
225
+ "Group": "Gruppo",
226
+ "Speech recognition server (optional)": "Server di riconoscimento vocale (facoltativo)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "Server compatibile con OpenAI al posto di OpenAI, ad es. un Whisper locale: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Vuoto = OpenAI.",
228
+ "Key of the speech recognition server": "Chiave del server di riconoscimento vocale",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Solo se il server ne richiede una. La chiave OpenAI non viene mai inviata lì."
226
230
  }
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "ESPHome-satellieten",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "ESPHome-satellieten krijgen het antwoord niet via hun API-socket — ze halen het op als URL. Dit is de poort waarop die kleine HTTP-server luistert.",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "Leeg = tts-1. Ook: tts-1-hd, gpt-4o-mini-tts.",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "Leeg = whisper-1. Ook: gpt-4o-transcribe, gpt-4o-mini-transcribe.",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "Leeg = whisper-1. Ook: gpt-4o-transcribe, gpt-4o-mini-transcribe. Met een eigen server: het model zoals de server het noemt, bijv. Systran/faster-whisper-large-v3-turbo.",
62
62
  "Enable voice (STT/TTS for satellites)": "Schakel spraak in (STT/TTS voor satellieten)",
63
63
  "Enable voice server (satellites)": "Schakel de spraakserver (satellieten) in.",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "Maakt spraakherkenning/synthese mogelijk. ioBroker-native satellieten bereiken deze via de berichtenbus — er is geen poort nodig. Schakel de onderstaande UDP-server alleen in voor ESP/Hannah-satellieten.",
@@ -222,5 +222,9 @@
222
222
  "Person": "Persoon",
223
223
  "Guest": "Gast",
224
224
  "Pet": "Huisdier",
225
- "Group": "Groep"
225
+ "Group": "Groep",
226
+ "Speech recognition server (optional)": "Server voor spraakherkenning (optioneel)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "OpenAI-compatibele server in plaats van OpenAI, bijv. een lokale Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Leeg = OpenAI.",
228
+ "Key of the speech recognition server": "Sleutel van de spraakherkenningsserver",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Alleen als de server er een vraagt. De OpenAI-sleutel wordt er nooit naartoe gestuurd."
226
230
  }
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "Satelity ESPHome",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "Satelity ESPHome nie odbierają odpowiedzi przez swoje gniazdo API — pobierają ją jako URL. To port, na którym nasłuchuje ten mały serwer HTTP.",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "Pusty = tts-1. Również: tts-1-hd, gpt-4o-mini-tts.",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "Pusty = szept-1. Również: gpt-4o-transcribe, gpt-4o-mini-transcribe.",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "Puste = whisper-1. Również: gpt-4o-transcribe, gpt-4o-mini-transcribe. Z własnym serwerem: model tak, jak nazywa go serwer, np. Systran/faster-whisper-large-v3-turbo.",
62
62
  "Enable voice (STT/TTS for satellites)": "Włącz głos (STT/TTS dla satelitów)",
63
63
  "Enable voice server (satellites)": "Włącz serwer głosowy (satelity)",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "Umożliwia rozpoznawanie/syntezę mowy. Satelity natywne dla ioBroker osiągają go przez magistralę komunikatów — nie jest potrzebny żaden port. Włącz serwer UDP poniżej tylko dla satelitów ESP/Hannah.",
@@ -222,5 +222,9 @@
222
222
  "Person": "Osoba",
223
223
  "Guest": "Gość",
224
224
  "Pet": "Zwierzę",
225
- "Group": "Grupa"
225
+ "Group": "Grupa",
226
+ "Speech recognition server (optional)": "Serwer rozpoznawania mowy (opcjonalnie)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "Serwer zgodny z OpenAI zamiast OpenAI, np. lokalny Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Puste = OpenAI.",
228
+ "Key of the speech recognition server": "Klucz serwera rozpoznawania mowy",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Tylko jeśli serwer go wymaga. Klucz OpenAI nigdy nie jest tam wysyłany."
226
230
  }
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "Satélites ESPHome",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "Os satélites ESPHome não recebem a resposta pelo seu socket de API — buscam-na como URL. Esta é a porta em que esse pequeno servidor HTTP escuta.",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "Vazio = tts-1. Também: tts-1-hd, gpt-4o-mini-tts.",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "Vazio = whisper-1. Também: gpt-4o-transcribe, gpt-4o-mini-transcribe.",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "Vazio = whisper-1. Também: gpt-4o-transcribe, gpt-4o-mini-transcribe. Com servidor próprio: o modelo como o servidor o chama, p. ex. Systran/faster-whisper-large-v3-turbo.",
62
62
  "Enable voice (STT/TTS for satellites)": "Ativar voz (STT/TTS para satélites)",
63
63
  "Enable voice server (satellites)": "Ativar servidor de voz (satélites)",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "Habilita o reconhecimento/síntese de voz. Os satélites nativos do ioBroker acessam o servidor através do barramento de mensagens — sem necessidade de porta. Ative o servidor UDP abaixo apenas para satélites ESP/Hannah.",
@@ -222,5 +222,9 @@
222
222
  "Person": "Pessoa",
223
223
  "Guest": "Convidado",
224
224
  "Pet": "Animal de estimação",
225
- "Group": "Grupo"
225
+ "Group": "Grupo",
226
+ "Speech recognition server (optional)": "Servidor de reconhecimento de voz (opcional)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "Servidor compatível com OpenAI em vez da OpenAI, p. ex. um Whisper local: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Vazio = OpenAI.",
228
+ "Key of the speech recognition server": "Chave do servidor de reconhecimento de voz",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Só se o servidor pedir uma. A chave da OpenAI nunca é enviada para lá."
226
230
  }
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "Сателлиты ESPHome",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "Сателлиты ESPHome получают ответ не через свой сокет API — они забирают его по URL. Это порт, на котором слушает этот небольшой HTTP-сервер.",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "Пусто = tts-1. Также: tts-1-hd, gpt-4o-mini-tts.",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "Пусто = whisper-1. Также: gpt-4o-transcribe, gpt-4o-mini-transcribe.",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "Пусто = whisper-1. Также: gpt-4o-transcribe, gpt-4o-mini-transcribe. Со своим сервером: модель так, как её называет сервер, напр. Systran/faster-whisper-large-v3-turbo.",
62
62
  "Enable voice (STT/TTS for satellites)": "Включить голосовую связь (STT/TTS для спутниковой связи)",
63
63
  "Enable voice server (satellites)": "Включить голосовой сервер (спутники)",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "Включает распознавание/синтез речи. Спутники, использующие ioBroker, получают к нему доступ через шину сообщений — порт не требуется. Включите UDP-сервер, указанный ниже, только для спутников ESP/Hannah.",
@@ -222,5 +222,9 @@
222
222
  "Person": "Человек",
223
223
  "Guest": "Гость",
224
224
  "Pet": "Питомец",
225
- "Group": "Группа"
225
+ "Group": "Группа",
226
+ "Speech recognition server (optional)": "Сервер распознавания речи (опционально)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "OpenAI-совместимый сервер вместо OpenAI, напр. локальный Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Пусто = OpenAI.",
228
+ "Key of the speech recognition server": "Ключ сервера распознавания речи",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Только если сервер его требует. Ключ OpenAI туда никогда не отправляется."
226
230
  }
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "Сателіти ESPHome",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "Сателіти ESPHome отримують відповідь не через свій сокет API — вони забирають її за URL. Це порт, на якому слухає цей невеликий HTTP-сервер.",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "Порожнє = tts-1. Також: tts-1-hd, gpt-4o-mini-tts.",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "Порожнє = шепіт-1. Також: gpt-4o-trancribe, gpt-4o-mini-trancribe.",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "Порожньо = whisper-1. Також: gpt-4o-transcribe, gpt-4o-mini-transcribe. З власним сервером: модель так, як її називає сервер, напр. Systran/faster-whisper-large-v3-turbo.",
62
62
  "Enable voice (STT/TTS for satellites)": "Увімкнути голосовий зв'язок (STT/TTS для супутників)",
63
63
  "Enable voice server (satellites)": "Увімкнути голосовий сервер (супутники)",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "Дозволяє розпізнавання/синтез мовлення. Супутники, сумісні з ioBroker, досягають його через шину повідомлень — порт не потрібен. Увімкніть UDP-сервер нижче лише для супутників ESP / Hannah.",
@@ -222,5 +222,9 @@
222
222
  "Person": "Людина",
223
223
  "Guest": "Гість",
224
224
  "Pet": "Улюбленець",
225
- "Group": "Група"
225
+ "Group": "Група",
226
+ "Speech recognition server (optional)": "Сервер розпізнавання мовлення (необов'язково)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "OpenAI-сумісний сервер замість OpenAI, напр. локальний Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Порожньо = OpenAI.",
228
+ "Key of the speech recognition server": "Ключ сервера розпізнавання мовлення",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Лише якщо сервер його вимагає. Ключ OpenAI туди ніколи не надсилається."
226
230
  }
@@ -58,7 +58,7 @@
58
58
  "ESPHome satellites": "ESPHome 卫星",
59
59
  "ESPHome satellites do not get the reply over their API socket — they fetch it as a URL. This is the port that small HTTP server listens on.": "ESPHome 卫星不会通过其 API 套接字接收回复,而是按 URL 获取。这里就是那个小型 HTTP 服务器监听的端口。",
60
60
  "Empty = tts-1. Also: tts-1-hd, gpt-4o-mini-tts.": "空 = tts-1。另见:tts-1-hd、gpt-4o-mini-tts。",
61
- "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.": "空 = whisper-1。另见:gpt-4o-transcribe、gpt-4o-mini-transcribe。",
61
+ "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.": "空 = whisper-1。另见:gpt-4o-transcribe、gpt-4o-mini-transcribe。使用自己的服务器时:按服务器的名称填写模型,例如 Systran/faster-whisper-large-v3-turbo。",
62
62
  "Enable voice (STT/TTS for satellites)": "启用语音功能(卫星语音转语音/文本转语音)",
63
63
  "Enable voice server (satellites)": "启用语音服务器(卫星)",
64
64
  "Enables speech recognition/synthesis. ioBroker-native satellites reach it over the message bus — no port needed. Turn on the UDP server below only for ESP / Hannah satellites.": "启用语音识别/合成功能。ioBroker 原生卫星通过消息总线与其通信,无需端口。仅当 ESP/Hannah 卫星连接时,才需要启用下方的 UDP 服务器。",
@@ -222,5 +222,9 @@
222
222
  "Person": "人员",
223
223
  "Guest": "访客",
224
224
  "Pet": "宠物",
225
- "Group": "组"
225
+ "Group": "组",
226
+ "Speech recognition server (optional)": "语音识别服务器(可选)",
227
+ "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "代替 OpenAI 的 OpenAI 兼容服务器,例如本地 Whisper:http://192.168.1.10:8000/v1(speaches、faster-whisper-server、LocalAI)。空 = OpenAI。",
228
+ "Key of the speech recognition server": "语音识别服务器的密钥",
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "仅当服务器需要时填写。OpenAI 密钥永远不会发送到该服务器。"
226
230
  }
@@ -1184,8 +1184,30 @@
1184
1184
  "newLine": true,
1185
1185
  "type": "text",
1186
1186
  "label": "OpenAI STT model (optional)",
1187
- "help": "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe.",
1188
- "hidden": "!data.voiceEnabled || data.sttProvider !== 'openai'",
1187
+ "help": "Empty = whisper-1. Also: gpt-4o-transcribe, gpt-4o-mini-transcribe. With an own server: the model as that server names it, e.g. Systran/faster-whisper-large-v3-turbo.",
1188
+ "hidden": "!data.voiceEnabled || (data.sttProvider !== 'openai' && data.sttFallback !== 'openai')",
1189
+ "xs": 12,
1190
+ "sm": 6,
1191
+ "md": 4,
1192
+ "lg": 4,
1193
+ "xl": 4
1194
+ },
1195
+ "sttBaseUrl": {
1196
+ "type": "text",
1197
+ "label": "Speech recognition server (optional)",
1198
+ "help": "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.",
1199
+ "hidden": "!data.voiceEnabled || (data.sttProvider !== 'openai' && data.sttFallback !== 'openai')",
1200
+ "xs": 12,
1201
+ "sm": 6,
1202
+ "md": 4,
1203
+ "lg": 4,
1204
+ "xl": 4
1205
+ },
1206
+ "sttServerKey": {
1207
+ "type": "password",
1208
+ "label": "Key of the speech recognition server",
1209
+ "help": "Only if the server wants one. The OpenAI key is never sent to it.",
1210
+ "hidden": "!data.voiceEnabled || (data.sttProvider !== 'openai' && data.sttFallback !== 'openai') || !data.sttBaseUrl",
1189
1211
  "xs": 12,
1190
1212
  "sm": 6,
1191
1213
  "md": 4,
@@ -104,6 +104,9 @@ function buildSttEngine(provider, ctx) {
104
104
  case 'piper':
105
105
  throw new Error('Piper is a text-to-speech engine, not speech-to-text');
106
106
  default:
107
+ if (ctx.sttBaseUrl?.trim()) {
108
+ return new stt_1.OpenAiStt(ctx.sttServerKey || '', ctx.sttModel || undefined, ctx.sttBaseUrl);
109
+ }
107
110
  requireOpenAi(ctx.creds);
108
111
  return new stt_1.OpenAiStt(ctx.creds.openaiKey, ctx.sttModel || undefined);
109
112
  }
@@ -1 +1 @@
1
- {"version":3,"file":"engines.js","sourceRoot":"","sources":["../../../src/lib/voice/engines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Factory that builds the configured STT/TTS engine. Cloud engines (OpenAI/Azure/AWS) use resolved\n * credentials; local engines (Vosk/Piper) download their models on demand into the data dir.\n * Adding a provider = one new engine class + one `case` here.\n */\nimport { OpenAiStt, type SttEngine } from './stt';\nimport { OpenAiTts, type OpenAiVoice, type TtsEngine } from './tts';\nimport { AzureStt, AzureTts, listAzureVoices } from './azure';\nimport { AwsStt, AwsTts, listPollyVoices, type AwsCreds } from './aws';\nimport { VoskStt, listVoskModels } from './vosk';\nimport { PiperTts, listPiperVoices } from './piper';\nimport {\n OpenRouterStt,\n OpenRouterTts,\n listOpenRouterSpeechModels,\n voicesForLanguage,\n DEFAULT_OPENROUTER_TTS_MODEL,\n type AudioDecoder,\n} from './openrouter';\nimport * as path from 'node:path';\n\nimport { CachedTts } from './ttsCache';\nimport { FallbackStt, FallbackTts, type NamedEngine } from './fallback';\nimport { isoToLocale } from './lang';\nimport type { VoiceLogger } from './download';\n\nexport type SpeechProvider = 'openai' | 'openrouter' | 'azure' | 'aws' | 'vosk' | 'piper';\n\n/** All speech credentials, resolved from config (or the central credential store). */\nexport interface VoiceCredentials {\n openaiKey: string;\n openrouterKey: string;\n azureKey: string;\n azureRegion: string;\n aws: AwsCreds;\n}\n\n/** Provider-specific voice ids (each provider names voices differently). */\nexport interface VoiceNames {\n openai: string; // alloy/echo/…\n azure: string; // e.g. de-DE-KatjaNeural\n aws: string; // e.g. Vicki\n piper: string; // e.g. de_DE-thorsten-medium\n openrouter: string; // depends on the model, e.g. Kore, george\n}\n\n/** Everything the factory needs to build any engine. */\nexport interface EngineContext {\n creds: VoiceCredentials;\n voices: VoiceNames;\n /** Optional second engine, used when the primary one fails ('' = none). */\n sttFallback?: SpeechProvider | '';\n ttsFallback?: SpeechProvider | '';\n /** Writable instance data dir (for the local engines' downloads). */\n dataDir: string;\n log: VoiceLogger;\n /** Local model overrides ('' → default per language). */\n voskModel: string;\n /** OpenAI STT model ('' → 'whisper-1'; e.g. 'gpt-4o-transcribe'). */\n sttModel: string;\n /** OpenAI TTS model ('' → 'tts-1'; e.g. 'gpt-4o-mini-tts'). */\n ttsModel: string;\n /** OpenRouter models ('' → the defaults of openrouter.ts) */\n openrouterSttModel: string;\n openrouterTtsModel: string;\n /** Decoder for compressed speech (OpenRouter models that only return mp3); absent → those models fail */\n decodeAudio?: AudioDecoder;\n /** Longest text that is spoken, in characters; longer ones are cut at a sentence end. 0 = no limit, absent = 400. */\n ttsMaxChars?: number;\n}\n\nfunction requireAzure(creds: VoiceCredentials): void {\n if (!creds.azureKey || !creds.azureRegion) {\n throw new Error('Azure speech needs a subscription key and region');\n }\n}\n\nfunction requireAws(creds: VoiceCredentials): void {\n if (!creds.aws.accessKeyId || !creds.aws.secretAccessKey || !creds.aws.region) {\n throw new Error('AWS speech needs an access key id, secret access key and region');\n }\n}\n\nfunction requireOpenAi(creds: VoiceCredentials): void {\n if (!creds.openaiKey) {\n throw new Error('OpenAI speech needs an API key');\n }\n}\n\nfunction requireOpenRouter(creds: VoiceCredentials): void {\n if (!creds.openrouterKey) {\n throw new Error('OpenRouter speech needs an API key');\n }\n}\n\nexport function createSttEngine(provider: SpeechProvider, ctx: EngineContext): SttEngine {\n const primary: NamedEngine<SttEngine> = { name: provider, engine: buildSttEngine(provider, ctx) };\n const fallback = ctx.sttFallback && ctx.sttFallback !== provider ? ctx.sttFallback : '';\n if (!fallback) {\n return primary.engine;\n }\n try {\n return new FallbackStt([primary, { name: fallback, engine: buildSttEngine(fallback, ctx) }], ctx.log);\n } catch (e) {\n ctx.log.warn(`Speech-to-text fallback '${fallback}' unavailable: ${(e as Error).message}`);\n return primary.engine;\n }\n}\n\nfunction buildSttEngine(provider: SpeechProvider, ctx: EngineContext): SttEngine {\n switch (provider) {\n case 'azure':\n requireAzure(ctx.creds);\n return new AzureStt(ctx.creds.azureKey, ctx.creds.azureRegion);\n case 'aws':\n requireAws(ctx.creds);\n return new AwsStt(ctx.creds.aws);\n case 'vosk':\n return new VoskStt(ctx.dataDir, ctx.voskModel, ctx.log);\n case 'openrouter':\n requireOpenRouter(ctx.creds);\n return new OpenRouterStt(ctx.creds.openrouterKey, ctx.openrouterSttModel);\n case 'piper':\n throw new Error('Piper is a text-to-speech engine, not speech-to-text');\n default:\n requireOpenAi(ctx.creds);\n return new OpenAiStt(ctx.creds.openaiKey, ctx.sttModel || undefined);\n }\n}\n\nexport function createTtsEngine(provider: SpeechProvider, ctx: EngineContext): TtsEngine {\n // Each engine gets the shared behaviour (disk cache, length limit, SSML) and its own cache directory,\n // so a fallback engine's voice can never be served later while the primary one is healthy again.\n const primary: NamedEngine<TtsEngine> = { name: provider, engine: cachedTts(provider, ctx) };\n const fallback = ctx.ttsFallback && ctx.ttsFallback !== provider ? ctx.ttsFallback : '';\n if (!fallback) {\n return primary.engine;\n }\n try {\n return new FallbackTts([primary, { name: fallback, engine: cachedTts(fallback, ctx) }], ctx.log);\n } catch (e) {\n // A fallback that cannot even be built (missing key, wrong provider) must not take the main one down.\n ctx.log.warn(`Text-to-speech fallback '${fallback}' unavailable: ${(e as Error).message}`);\n return primary.engine;\n }\n}\n\n/**\n * One provider binding plus the shared cache/length/SSML layer. One cache directory per provider+voice —\n * a voice change must not serve audio in the old voice.\n */\nfunction cachedTts(provider: SpeechProvider, ctx: EngineContext): TtsEngine {\n const voice =\n provider === 'azure'\n ? ctx.voices.azure\n : provider === 'aws'\n ? ctx.voices.aws\n : provider === 'piper'\n ? ctx.voices.piper\n : provider === 'openrouter'\n ? `${ctx.openrouterTtsModel || DEFAULT_OPENROUTER_TTS_MODEL}-${ctx.voices.openrouter}`\n : ctx.voices.openai;\n const dir = path.join(ctx.dataDir, 'ttscache', `${provider}-${(voice || 'default').replace(/[^\\w.-]/g, '_')}`);\n return new CachedTts(buildTtsEngine(provider, ctx), { dir, log: ctx.log, maxChars: ctx.ttsMaxChars });\n}\n\nfunction buildTtsEngine(provider: SpeechProvider, ctx: EngineContext): TtsEngine {\n switch (provider) {\n case 'azure':\n requireAzure(ctx.creds);\n return new AzureTts(ctx.creds.azureKey, ctx.creds.azureRegion, ctx.voices.azure);\n case 'aws':\n requireAws(ctx.creds);\n return new AwsTts(ctx.creds.aws, ctx.voices.aws);\n case 'piper':\n return new PiperTts(ctx.dataDir, ctx.voices.piper, ctx.log);\n case 'vosk':\n throw new Error('Vosk is a speech-to-text engine, not text-to-speech');\n case 'openrouter':\n requireOpenRouter(ctx.creds);\n return new OpenRouterTts(\n ctx.creds.openrouterKey,\n ctx.openrouterTtsModel,\n ctx.voices.openrouter,\n ctx.decodeAudio,\n );\n default:\n requireOpenAi(ctx.creds);\n return new OpenAiTts(\n ctx.creds.openaiKey,\n (ctx.voices.openai || 'alloy') as OpenAiVoice,\n ctx.ttsModel || undefined,\n );\n }\n}\n\n/** OpenAI TTS voices are a fixed set (no list API). */\nconst OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];\n\n/** List the available TTS voices for a provider (for the settings voice dropdown). */\nexport async function listVoices(provider: SpeechProvider, ctx: EngineContext, language: string): Promise<string[]> {\n switch (provider) {\n case 'azure':\n requireAzure(ctx.creds);\n return listAzureVoices(ctx.creds.azureKey, ctx.creds.azureRegion, isoToLocale(language));\n case 'aws':\n requireAws(ctx.creds);\n return listPollyVoices(ctx.creds.aws, isoToLocale(language));\n case 'piper':\n return listPiperVoices();\n case 'openrouter': {\n // public list, no key needed: the voices of the chosen model\n const model = ctx.openrouterTtsModel || DEFAULT_OPENROUTER_TTS_MODEL;\n const models = await listOpenRouterSpeechModels('speech');\n return voicesForLanguage(models.find(m => m.id === model)?.voices || [], language);\n }\n default:\n return OPENAI_VOICES;\n }\n}\n\n/** Suggested STT model names for the settings dropdown: the local Vosk catalogue, or the models of OpenRouter. */\nexport async function listSttModels(provider: SpeechProvider, language: string): Promise<string[]> {\n if (provider === 'openrouter') {\n return (await listOpenRouterSpeechModels('transcription')).map(m => m.id);\n }\n return provider === 'vosk' ? listVoskModels(language) : [];\n}\n\n/** TTS model names for the settings dropdown — only OpenRouter has a choice worth listing. */\nexport async function listTtsModels(provider: SpeechProvider): Promise<string[]> {\n return provider === 'openrouter' ? (await listOpenRouterSpeechModels('speech')).map(m => m.id) : [];\n}\n"]}
1
+ {"version":3,"file":"engines.js","sourceRoot":"","sources":["../../../src/lib/voice/engines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Factory that builds the configured STT/TTS engine. Cloud engines (OpenAI/Azure/AWS) use resolved\n * credentials; local engines (Vosk/Piper) download their models on demand into the data dir.\n * Adding a provider = one new engine class + one `case` here.\n */\nimport { OpenAiStt, type SttEngine } from './stt';\nimport { OpenAiTts, type OpenAiVoice, type TtsEngine } from './tts';\nimport { AzureStt, AzureTts, listAzureVoices } from './azure';\nimport { AwsStt, AwsTts, listPollyVoices, type AwsCreds } from './aws';\nimport { VoskStt, listVoskModels } from './vosk';\nimport { PiperTts, listPiperVoices } from './piper';\nimport {\n OpenRouterStt,\n OpenRouterTts,\n listOpenRouterSpeechModels,\n voicesForLanguage,\n DEFAULT_OPENROUTER_TTS_MODEL,\n type AudioDecoder,\n} from './openrouter';\nimport * as path from 'node:path';\n\nimport { CachedTts } from './ttsCache';\nimport { FallbackStt, FallbackTts, type NamedEngine } from './fallback';\nimport { isoToLocale } from './lang';\nimport type { VoiceLogger } from './download';\n\nexport type SpeechProvider = 'openai' | 'openrouter' | 'azure' | 'aws' | 'vosk' | 'piper';\n\n/** All speech credentials, resolved from config (or the central credential store). */\nexport interface VoiceCredentials {\n openaiKey: string;\n openrouterKey: string;\n azureKey: string;\n azureRegion: string;\n aws: AwsCreds;\n}\n\n/** Provider-specific voice ids (each provider names voices differently). */\nexport interface VoiceNames {\n openai: string; // alloy/echo/…\n azure: string; // e.g. de-DE-KatjaNeural\n aws: string; // e.g. Vicki\n piper: string; // e.g. de_DE-thorsten-medium\n openrouter: string; // depends on the model, e.g. Kore, george\n}\n\n/** Everything the factory needs to build any engine. */\nexport interface EngineContext {\n creds: VoiceCredentials;\n voices: VoiceNames;\n /** Optional second engine, used when the primary one fails ('' = none). */\n sttFallback?: SpeechProvider | '';\n ttsFallback?: SpeechProvider | '';\n /** Writable instance data dir (for the local engines' downloads). */\n dataDir: string;\n log: VoiceLogger;\n /** Local model overrides ('' → default per language). */\n voskModel: string;\n /** OpenAI STT model ('' → 'whisper-1'; e.g. 'gpt-4o-transcribe'). */\n sttModel: string;\n /**\n * OpenAI-compatible transcription server instead of OpenAI ('' = OpenAI), e.g. a local whisper container.\n * It gets `sttServerKey`, never the OpenAI key — that one is only for OpenAI.\n */\n sttBaseUrl?: string;\n /** Key for `sttBaseUrl` ('' = the server needs none). */\n sttServerKey?: string;\n /** OpenAI TTS model ('' → 'tts-1'; e.g. 'gpt-4o-mini-tts'). */\n ttsModel: string;\n /** OpenRouter models ('' → the defaults of openrouter.ts) */\n openrouterSttModel: string;\n openrouterTtsModel: string;\n /** Decoder for compressed speech (OpenRouter models that only return mp3); absent → those models fail */\n decodeAudio?: AudioDecoder;\n /** Longest text that is spoken, in characters; longer ones are cut at a sentence end. 0 = no limit, absent = 400. */\n ttsMaxChars?: number;\n}\n\nfunction requireAzure(creds: VoiceCredentials): void {\n if (!creds.azureKey || !creds.azureRegion) {\n throw new Error('Azure speech needs a subscription key and region');\n }\n}\n\nfunction requireAws(creds: VoiceCredentials): void {\n if (!creds.aws.accessKeyId || !creds.aws.secretAccessKey || !creds.aws.region) {\n throw new Error('AWS speech needs an access key id, secret access key and region');\n }\n}\n\nfunction requireOpenAi(creds: VoiceCredentials): void {\n if (!creds.openaiKey) {\n throw new Error('OpenAI speech needs an API key');\n }\n}\n\nfunction requireOpenRouter(creds: VoiceCredentials): void {\n if (!creds.openrouterKey) {\n throw new Error('OpenRouter speech needs an API key');\n }\n}\n\nexport function createSttEngine(provider: SpeechProvider, ctx: EngineContext): SttEngine {\n const primary: NamedEngine<SttEngine> = { name: provider, engine: buildSttEngine(provider, ctx) };\n const fallback = ctx.sttFallback && ctx.sttFallback !== provider ? ctx.sttFallback : '';\n if (!fallback) {\n return primary.engine;\n }\n try {\n return new FallbackStt([primary, { name: fallback, engine: buildSttEngine(fallback, ctx) }], ctx.log);\n } catch (e) {\n ctx.log.warn(`Speech-to-text fallback '${fallback}' unavailable: ${(e as Error).message}`);\n return primary.engine;\n }\n}\n\nfunction buildSttEngine(provider: SpeechProvider, ctx: EngineContext): SttEngine {\n switch (provider) {\n case 'azure':\n requireAzure(ctx.creds);\n return new AzureStt(ctx.creds.azureKey, ctx.creds.azureRegion);\n case 'aws':\n requireAws(ctx.creds);\n return new AwsStt(ctx.creds.aws);\n case 'vosk':\n return new VoskStt(ctx.dataDir, ctx.voskModel, ctx.log);\n case 'openrouter':\n requireOpenRouter(ctx.creds);\n return new OpenRouterStt(ctx.creds.openrouterKey, ctx.openrouterSttModel);\n case 'piper':\n throw new Error('Piper is a text-to-speech engine, not speech-to-text');\n default:\n if (ctx.sttBaseUrl?.trim()) {\n return new OpenAiStt(ctx.sttServerKey || '', ctx.sttModel || undefined, ctx.sttBaseUrl);\n }\n requireOpenAi(ctx.creds);\n return new OpenAiStt(ctx.creds.openaiKey, ctx.sttModel || undefined);\n }\n}\n\nexport function createTtsEngine(provider: SpeechProvider, ctx: EngineContext): TtsEngine {\n // Each engine gets the shared behaviour (disk cache, length limit, SSML) and its own cache directory,\n // so a fallback engine's voice can never be served later while the primary one is healthy again.\n const primary: NamedEngine<TtsEngine> = { name: provider, engine: cachedTts(provider, ctx) };\n const fallback = ctx.ttsFallback && ctx.ttsFallback !== provider ? ctx.ttsFallback : '';\n if (!fallback) {\n return primary.engine;\n }\n try {\n return new FallbackTts([primary, { name: fallback, engine: cachedTts(fallback, ctx) }], ctx.log);\n } catch (e) {\n // A fallback that cannot even be built (missing key, wrong provider) must not take the main one down.\n ctx.log.warn(`Text-to-speech fallback '${fallback}' unavailable: ${(e as Error).message}`);\n return primary.engine;\n }\n}\n\n/**\n * One provider binding plus the shared cache/length/SSML layer. One cache directory per provider+voice —\n * a voice change must not serve audio in the old voice.\n */\nfunction cachedTts(provider: SpeechProvider, ctx: EngineContext): TtsEngine {\n const voice =\n provider === 'azure'\n ? ctx.voices.azure\n : provider === 'aws'\n ? ctx.voices.aws\n : provider === 'piper'\n ? ctx.voices.piper\n : provider === 'openrouter'\n ? `${ctx.openrouterTtsModel || DEFAULT_OPENROUTER_TTS_MODEL}-${ctx.voices.openrouter}`\n : ctx.voices.openai;\n const dir = path.join(ctx.dataDir, 'ttscache', `${provider}-${(voice || 'default').replace(/[^\\w.-]/g, '_')}`);\n return new CachedTts(buildTtsEngine(provider, ctx), { dir, log: ctx.log, maxChars: ctx.ttsMaxChars });\n}\n\nfunction buildTtsEngine(provider: SpeechProvider, ctx: EngineContext): TtsEngine {\n switch (provider) {\n case 'azure':\n requireAzure(ctx.creds);\n return new AzureTts(ctx.creds.azureKey, ctx.creds.azureRegion, ctx.voices.azure);\n case 'aws':\n requireAws(ctx.creds);\n return new AwsTts(ctx.creds.aws, ctx.voices.aws);\n case 'piper':\n return new PiperTts(ctx.dataDir, ctx.voices.piper, ctx.log);\n case 'vosk':\n throw new Error('Vosk is a speech-to-text engine, not text-to-speech');\n case 'openrouter':\n requireOpenRouter(ctx.creds);\n return new OpenRouterTts(\n ctx.creds.openrouterKey,\n ctx.openrouterTtsModel,\n ctx.voices.openrouter,\n ctx.decodeAudio,\n );\n default:\n requireOpenAi(ctx.creds);\n return new OpenAiTts(\n ctx.creds.openaiKey,\n (ctx.voices.openai || 'alloy') as OpenAiVoice,\n ctx.ttsModel || undefined,\n );\n }\n}\n\n/** OpenAI TTS voices are a fixed set (no list API). */\nconst OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];\n\n/** List the available TTS voices for a provider (for the settings voice dropdown). */\nexport async function listVoices(provider: SpeechProvider, ctx: EngineContext, language: string): Promise<string[]> {\n switch (provider) {\n case 'azure':\n requireAzure(ctx.creds);\n return listAzureVoices(ctx.creds.azureKey, ctx.creds.azureRegion, isoToLocale(language));\n case 'aws':\n requireAws(ctx.creds);\n return listPollyVoices(ctx.creds.aws, isoToLocale(language));\n case 'piper':\n return listPiperVoices();\n case 'openrouter': {\n // public list, no key needed: the voices of the chosen model\n const model = ctx.openrouterTtsModel || DEFAULT_OPENROUTER_TTS_MODEL;\n const models = await listOpenRouterSpeechModels('speech');\n return voicesForLanguage(models.find(m => m.id === model)?.voices || [], language);\n }\n default:\n return OPENAI_VOICES;\n }\n}\n\n/** Suggested STT model names for the settings dropdown: the local Vosk catalogue, or the models of OpenRouter. */\nexport async function listSttModels(provider: SpeechProvider, language: string): Promise<string[]> {\n if (provider === 'openrouter') {\n return (await listOpenRouterSpeechModels('transcription')).map(m => m.id);\n }\n return provider === 'vosk' ? listVoskModels(language) : [];\n}\n\n/** TTS model names for the settings dropdown — only OpenRouter has a choice worth listing. */\nexport async function listTtsModels(provider: SpeechProvider): Promise<string[]> {\n return provider === 'openrouter' ? (await listOpenRouterSpeechModels('speech')).map(m => m.id) : [];\n}\n"]}
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.OpenAiStt = void 0;
37
37
  exports.hintsToPrompt = hintsToPrompt;
38
38
  exports.pcmToWav = pcmToWav;
39
+ exports.normalizeSttBaseUrl = normalizeSttBaseUrl;
39
40
  /**
40
41
  * Speech-to-text engines. V1 ships the cloud engine (OpenAI Whisper); local engines
41
42
  * (Vosk / whisper.cpp / Wyoming) plug in behind the same `SttEngine` interface later.
@@ -88,12 +89,43 @@ function pcmToWav(pcm, sampleRate, numChannels = 1) {
88
89
  header.writeUInt32LE(pcm.length, 40);
89
90
  return Buffer.concat([header, pcm]);
90
91
  }
91
- /** Cloud STT via the OpenAI transcription API (Whisper). */
92
+ /**
93
+ * Base URL of an OpenAI-compatible transcription server as the SDK wants it. Practically every such server
94
+ * (speaches/faster-whisper-server, LocalAI, vLLM, Groq) serves `/v1/audio/transcriptions`, so an address typed
95
+ * without a path gets `/v1`; one with a path is taken as it is.
96
+ */
97
+ function normalizeSttBaseUrl(raw) {
98
+ const url = raw.trim().replace(/\/+$/, '');
99
+ if (!url) {
100
+ return '';
101
+ }
102
+ try {
103
+ const parsed = new URL(url);
104
+ return parsed.pathname === '/' || parsed.pathname === '' ? `${url}/v1` : url;
105
+ }
106
+ catch {
107
+ throw new Error(`speech recognition base URL "${raw}" is not a URL (e.g. http://192.168.1.10:8000/v1)`);
108
+ }
109
+ }
110
+ /**
111
+ * STT via the OpenAI transcription API (Whisper) — or any server that speaks it, e.g. a local
112
+ * whisper-large-v3-turbo next to ioBroker (`baseUrl`).
113
+ */
92
114
  class OpenAiStt {
93
115
  client;
94
116
  model;
95
- constructor(apiKey, model = 'whisper-1') {
96
- this.client = new openai_1.default({ apiKey });
117
+ /**
118
+ * @param apiKey OpenAI key, or the key of the server at `baseUrl` ('' for a server without one)
119
+ * @param model model id; on an own server as that server names it
120
+ * @param baseUrl OpenAI-compatible server instead of api.openai.com ('' = OpenAI)
121
+ */
122
+ constructor(apiKey, model = 'whisper-1', baseUrl = '') {
123
+ const baseURL = normalizeSttBaseUrl(baseUrl);
124
+ this.client = baseURL
125
+ ? // The SDK refuses an empty key; a local server ignores it. A server that hangs must not hold the
126
+ // satellite for the SDK's default ten minutes.
127
+ new openai_1.default({ apiKey: apiKey || 'none', baseURL, timeout: 60_000, maxRetries: 1 })
128
+ : new openai_1.default({ apiKey });
97
129
  this.model = model;
98
130
  }
99
131
  async transcribe(pcm, sampleRate, lang, hints) {
@@ -1 +1 @@
1
- {"version":3,"file":"stt.js","sourceRoot":"","sources":["../../../src/lib/voice/stt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Speech-to-text engines. V1 ships the cloud engine (OpenAI Whisper); local engines\n * (Vosk / whisper.cpp / Wyoming) plug in behind the same `SttEngine` interface later.\n */\nimport OpenAI, { toFile } from 'openai';\n\nexport interface SttEngine {\n /**\n * Transcribe raw mono 16-bit PCM. `lang` is an ISO-639-1 hint ('' = auto-detect).\n *\n * `hints` is an optional domain vocabulary (device / room / scene names …) to bias recognition toward,\n * so the engine better transcribes those proper nouns. Only engines that support *soft* biasing use it\n * (OpenAI Whisper `prompt`, Azure phrase list); engines where a word list would be a hard constraint or\n * needs pre-registration (Vosk grammar, AWS custom vocabulary) ignore it.\n */\n transcribe(pcm: Buffer, sampleRate: number, lang: string, hints?: string[]): Promise<string>;\n /** Optional warm-up (install/download engine + model for `lang`) so the first request isn't slow. */\n prepare?(lang: string): Promise<void>;\n}\n\n/**\n * Join vocabulary hints into a Whisper `prompt` string (a soft bias toward these terms), de-duplicated and\n * length-bounded to stay within the model's ~224-token prompt budget. Returns '' when there are no hints.\n */\nexport function hintsToPrompt(hints: string[] | undefined, maxChars = 800): string {\n if (!hints?.length) {\n return '';\n }\n const seen = new Set<string>();\n const parts: string[] = [];\n let len = 0;\n for (const h of hints) {\n const term = (h || '').trim();\n const key = term.toLowerCase();\n if (!term || seen.has(key)) {\n continue;\n }\n if (len + term.length + 2 > maxChars) {\n break;\n }\n seen.add(key);\n parts.push(term);\n len += term.length + 2;\n }\n return parts.join(', ');\n}\n\n/** Wrap raw 16-bit-signed-LE PCM (mono, or interleaved stereo) into a minimal 44-byte WAV container. */\nexport function pcmToWav(pcm: Buffer, sampleRate: number, numChannels = 1): Buffer {\n const bitsPerSample = 16;\n const blockAlign = (numChannels * bitsPerSample) / 8;\n const byteRate = sampleRate * blockAlign;\n const header = Buffer.alloc(44);\n header.write('RIFF', 0);\n header.writeUInt32LE(36 + pcm.length, 4);\n header.write('WAVE', 8);\n header.write('fmt ', 12);\n header.writeUInt32LE(16, 16); // PCM fmt chunk size\n header.writeUInt16LE(1, 20); // audio format = PCM\n header.writeUInt16LE(numChannels, 22);\n header.writeUInt32LE(sampleRate, 24);\n header.writeUInt32LE(byteRate, 28);\n header.writeUInt16LE(blockAlign, 32);\n header.writeUInt16LE(bitsPerSample, 34);\n header.write('data', 36);\n header.writeUInt32LE(pcm.length, 40);\n return Buffer.concat([header, pcm]);\n}\n\n/** Cloud STT via the OpenAI transcription API (Whisper). */\nexport class OpenAiStt implements SttEngine {\n private readonly client: OpenAI;\n private readonly model: string;\n\n constructor(apiKey: string, model = 'whisper-1') {\n this.client = new OpenAI({ apiKey });\n this.model = model;\n }\n\n async transcribe(pcm: Buffer, sampleRate: number, lang: string, hints?: string[]): Promise<string> {\n const file = await toFile(pcmToWav(pcm, sampleRate), 'audio.wav', { type: 'audio/wav' });\n // A prompt biases Whisper toward the given domain vocabulary (device/room names) without excluding\n // other words — improves recognition of proper nouns the NLU then needs to match.\n const prompt = hintsToPrompt(hints);\n const res = await this.client.audio.transcriptions.create({\n file,\n model: this.model,\n // Whisper wants the bare ISO-639-1 code; strip any region (de-DE → de). '' → auto-detect.\n language: (lang || '').split('-')[0] || undefined,\n ...(prompt ? { prompt } : {}),\n });\n return (res.text || '').trim();\n }\n}\n"]}
1
+ {"version":3,"file":"stt.js","sourceRoot":"","sources":["../../../src/lib/voice/stt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Speech-to-text engines. V1 ships the cloud engine (OpenAI Whisper); local engines\n * (Vosk / whisper.cpp / Wyoming) plug in behind the same `SttEngine` interface later.\n */\nimport OpenAI, { toFile } from 'openai';\n\nexport interface SttEngine {\n /**\n * Transcribe raw mono 16-bit PCM. `lang` is an ISO-639-1 hint ('' = auto-detect).\n *\n * `hints` is an optional domain vocabulary (device / room / scene names …) to bias recognition toward,\n * so the engine better transcribes those proper nouns. Only engines that support *soft* biasing use it\n * (OpenAI Whisper `prompt`, Azure phrase list); engines where a word list would be a hard constraint or\n * needs pre-registration (Vosk grammar, AWS custom vocabulary) ignore it.\n */\n transcribe(pcm: Buffer, sampleRate: number, lang: string, hints?: string[]): Promise<string>;\n /** Optional warm-up (install/download engine + model for `lang`) so the first request isn't slow. */\n prepare?(lang: string): Promise<void>;\n}\n\n/**\n * Join vocabulary hints into a Whisper `prompt` string (a soft bias toward these terms), de-duplicated and\n * length-bounded to stay within the model's ~224-token prompt budget. Returns '' when there are no hints.\n */\nexport function hintsToPrompt(hints: string[] | undefined, maxChars = 800): string {\n if (!hints?.length) {\n return '';\n }\n const seen = new Set<string>();\n const parts: string[] = [];\n let len = 0;\n for (const h of hints) {\n const term = (h || '').trim();\n const key = term.toLowerCase();\n if (!term || seen.has(key)) {\n continue;\n }\n if (len + term.length + 2 > maxChars) {\n break;\n }\n seen.add(key);\n parts.push(term);\n len += term.length + 2;\n }\n return parts.join(', ');\n}\n\n/** Wrap raw 16-bit-signed-LE PCM (mono, or interleaved stereo) into a minimal 44-byte WAV container. */\nexport function pcmToWav(pcm: Buffer, sampleRate: number, numChannels = 1): Buffer {\n const bitsPerSample = 16;\n const blockAlign = (numChannels * bitsPerSample) / 8;\n const byteRate = sampleRate * blockAlign;\n const header = Buffer.alloc(44);\n header.write('RIFF', 0);\n header.writeUInt32LE(36 + pcm.length, 4);\n header.write('WAVE', 8);\n header.write('fmt ', 12);\n header.writeUInt32LE(16, 16); // PCM fmt chunk size\n header.writeUInt16LE(1, 20); // audio format = PCM\n header.writeUInt16LE(numChannels, 22);\n header.writeUInt32LE(sampleRate, 24);\n header.writeUInt32LE(byteRate, 28);\n header.writeUInt16LE(blockAlign, 32);\n header.writeUInt16LE(bitsPerSample, 34);\n header.write('data', 36);\n header.writeUInt32LE(pcm.length, 40);\n return Buffer.concat([header, pcm]);\n}\n\n/**\n * Base URL of an OpenAI-compatible transcription server as the SDK wants it. Practically every such server\n * (speaches/faster-whisper-server, LocalAI, vLLM, Groq) serves `/v1/audio/transcriptions`, so an address typed\n * without a path gets `/v1`; one with a path is taken as it is.\n */\nexport function normalizeSttBaseUrl(raw: string): string {\n const url = raw.trim().replace(/\\/+$/, '');\n if (!url) {\n return '';\n }\n try {\n const parsed = new URL(url);\n return parsed.pathname === '/' || parsed.pathname === '' ? `${url}/v1` : url;\n } catch {\n throw new Error(`speech recognition base URL \"${raw}\" is not a URL (e.g. http://192.168.1.10:8000/v1)`);\n }\n}\n\n/**\n * STT via the OpenAI transcription API (Whisper) — or any server that speaks it, e.g. a local\n * whisper-large-v3-turbo next to ioBroker (`baseUrl`).\n */\nexport class OpenAiStt implements SttEngine {\n private readonly client: OpenAI;\n private readonly model: string;\n\n /**\n * @param apiKey OpenAI key, or the key of the server at `baseUrl` ('' for a server without one)\n * @param model model id; on an own server as that server names it\n * @param baseUrl OpenAI-compatible server instead of api.openai.com ('' = OpenAI)\n */\n constructor(apiKey: string, model = 'whisper-1', baseUrl = '') {\n const baseURL = normalizeSttBaseUrl(baseUrl);\n this.client = baseURL\n ? // The SDK refuses an empty key; a local server ignores it. A server that hangs must not hold the\n // satellite for the SDK's default ten minutes.\n new OpenAI({ apiKey: apiKey || 'none', baseURL, timeout: 60_000, maxRetries: 1 })\n : new OpenAI({ apiKey });\n this.model = model;\n }\n\n async transcribe(pcm: Buffer, sampleRate: number, lang: string, hints?: string[]): Promise<string> {\n const file = await toFile(pcmToWav(pcm, sampleRate), 'audio.wav', { type: 'audio/wav' });\n // A prompt biases Whisper toward the given domain vocabulary (device/room names) without excluding\n // other words — improves recognition of proper nouns the NLU then needs to match.\n const prompt = hintsToPrompt(hints);\n const res = await this.client.audio.transcriptions.create({\n file,\n model: this.model,\n // Whisper wants the bare ISO-639-1 code; strip any region (de-DE → de). '' → auto-detect.\n language: (lang || '').split('-')[0] || undefined,\n ...(prompt ? { prompt } : {}),\n });\n return (res.text || '').trim();\n }\n}\n"]}
package/build/main.js CHANGED
@@ -107,6 +107,8 @@ const VOICE_FORM_FIELDS = [
107
107
  'voiceLanguage',
108
108
  'sttProvider',
109
109
  'sttModel',
110
+ 'sttBaseUrl',
111
+ 'sttServerKey',
110
112
  'openrouterSttModel',
111
113
  'voskModel',
112
114
  'ttsProvider',
@@ -131,7 +133,13 @@ const VOICE_FORM_FIELDS = [
131
133
  'piperVoice',
132
134
  ];
133
135
  /** Of those, the protected ones: the browser never gets them, so an empty value there means "keep the saved one" */
134
- const VOICE_FORM_SECRETS = ['voiceApiKey', 'openrouterSpeechKey', 'azureSpeechKey', 'awsSecretAccessKey'];
136
+ const VOICE_FORM_SECRETS = [
137
+ 'voiceApiKey',
138
+ 'openrouterSpeechKey',
139
+ 'azureSpeechKey',
140
+ 'awsSecretAccessKey',
141
+ 'sttServerKey',
142
+ ];
135
143
  /** An empty `ask` from the same caller is reported at most this often. */
136
144
  const EMPTY_ASK_REPORT_MS = 60_000;
137
145
  /** How long to wait for a device to confirm a write, and how often to look. */
@@ -700,6 +708,8 @@ class Assistant extends adapter_core_1.Adapter {
700
708
  log: this.log,
701
709
  voskModel: (cfg.voskModel || '').trim(),
702
710
  sttModel: (cfg.sttModel || '').trim(),
711
+ sttBaseUrl: (cfg.sttBaseUrl || '').trim(),
712
+ sttServerKey: cfg.sttServerKey || '',
703
713
  ttsModel: (cfg.ttsModel || '').trim(),
704
714
  openrouterSttModel: (cfg.openrouterSttModel || '').trim(),
705
715
  openrouterTtsModel: (cfg.openrouterTtsModel || '').trim(),
@@ -2941,6 +2951,7 @@ class Assistant extends adapter_core_1.Adapter {
2941
2951
  fallback: cfg.sttFallback || undefined,
2942
2952
  model: (provider === 'openrouter' ? cfg.openrouterSttModel : cfg.sttModel || cfg.voskModel || '').trim() ||
2943
2953
  undefined,
2954
+ server: (provider === 'openai' && cfg.sttBaseUrl?.trim()) || undefined,
2944
2955
  language: cfg.voiceLanguage || this.language || '',
2945
2956
  audioMs: origin.audioMs,
2946
2957
  wakeWord: origin.wakeWord,