diff --git a/CHANGELOG.md b/CHANGELOG.md index 4256fca..5658a6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to the OpenClaw Home Assistant Integration will be documented in this file. +## [0.1.42] - 2026-02-21 + +### Added +- Added integration option `browser_voice_language` (shown when `voice_provider` is `browser`) to explicitly control browser STT/TTS language. + +### Fixed +- Browser voice provider now applies integration-configured browser voice language for both listening and spoken replies. +- Voice replies are no longer spoken for one-shot/manual voice sends unless continuous voice mode is actively running. + ## [0.1.41] - 2026-02-20 ### Fixed diff --git a/custom_components/openclaw/__init__.py b/custom_components/openclaw/__init__.py index bddc0d9..b207b15 100644 --- a/custom_components/openclaw/__init__.py +++ b/custom_components/openclaw/__init__.py @@ -58,6 +58,7 @@ from .const import ( CONF_WAKE_WORD, CONF_WAKE_WORD_ENABLED, CONF_ALLOW_BRAVE_WEBSPEECH, + CONF_BROWSER_VOICE_LANGUAGE, CONF_VOICE_PROVIDER, CONTEXT_STRATEGY_TRUNCATE, DEFAULT_CONTEXT_MAX_CHARS, @@ -67,6 +68,7 @@ from .const import ( DEFAULT_WAKE_WORD, DEFAULT_WAKE_WORD_ENABLED, DEFAULT_ALLOW_BRAVE_WEBSPEECH, + DEFAULT_BROWSER_VOICE_LANGUAGE, DEFAULT_VOICE_PROVIDER, DOMAIN, EVENT_MESSAGE_RECEIVED, @@ -90,7 +92,7 @@ _CARD_PATH = Path(__file__).parent / "www" / _CARD_FILENAME # URL at which the card JS is served (registered via register_static_path) _CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}" # Versioned URL used for Lovelace resource registration to avoid stale browser cache -_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.41" +_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.42" OpenClawConfigEntry = ConfigEntry @@ -808,6 +810,10 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None: CONF_VOICE_PROVIDER, DEFAULT_VOICE_PROVIDER, ), + CONF_BROWSER_VOICE_LANGUAGE: options.get( + CONF_BROWSER_VOICE_LANGUAGE, + DEFAULT_BROWSER_VOICE_LANGUAGE, + ), "language": hass.config.language, }, ) diff --git a/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc b/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc index 4a5a231..a531c08 100644 Binary files a/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc and b/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc differ diff --git a/custom_components/openclaw/__pycache__/config_flow.cpython-312.pyc b/custom_components/openclaw/__pycache__/config_flow.cpython-312.pyc index c5b61b7..943f45b 100644 Binary files a/custom_components/openclaw/__pycache__/config_flow.cpython-312.pyc and b/custom_components/openclaw/__pycache__/config_flow.cpython-312.pyc differ diff --git a/custom_components/openclaw/__pycache__/const.cpython-312.pyc b/custom_components/openclaw/__pycache__/const.cpython-312.pyc index 231a41e..be114a0 100644 Binary files a/custom_components/openclaw/__pycache__/const.cpython-312.pyc and b/custom_components/openclaw/__pycache__/const.cpython-312.pyc differ diff --git a/custom_components/openclaw/config_flow.py b/custom_components/openclaw/config_flow.py index 0c17b31..d5ca526 100644 --- a/custom_components/openclaw/config_flow.py +++ b/custom_components/openclaw/config_flow.py @@ -45,7 +45,9 @@ from .const import ( CONF_WAKE_WORD, CONF_WAKE_WORD_ENABLED, CONF_ALLOW_BRAVE_WEBSPEECH, + CONF_BROWSER_VOICE_LANGUAGE, CONF_VOICE_PROVIDER, + BROWSER_VOICE_LANGUAGES, CONTEXT_STRATEGY_CLEAR, CONTEXT_STRATEGY_TRUNCATE, DEFAULT_GATEWAY_HOST, @@ -57,6 +59,7 @@ from .const import ( DEFAULT_WAKE_WORD, DEFAULT_WAKE_WORD_ENABLED, DEFAULT_ALLOW_BRAVE_WEBSPEECH, + DEFAULT_BROWSER_VOICE_LANGUAGE, DEFAULT_VOICE_PROVIDER, DOMAIN, OPENCLAW_CONFIG_REL_PATH, @@ -414,66 +417,73 @@ class OpenClawOptionsFlow(OptionsFlow): return self.async_create_entry(title="", data=user_input) options = self._config_entry.options - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Optional( - CONF_INCLUDE_EXPOSED_CONTEXT, - default=options.get( - CONF_INCLUDE_EXPOSED_CONTEXT, - DEFAULT_INCLUDE_EXPOSED_CONTEXT, - ), - ): bool, - vol.Optional( - CONF_CONTEXT_MAX_CHARS, - default=options.get( - CONF_CONTEXT_MAX_CHARS, - DEFAULT_CONTEXT_MAX_CHARS, - ), - ): vol.All(int, vol.Range(min=1000, max=200000)), - vol.Optional( - CONF_CONTEXT_STRATEGY, - default=options.get( - CONF_CONTEXT_STRATEGY, - DEFAULT_CONTEXT_STRATEGY, - ), - ): vol.In([CONTEXT_STRATEGY_TRUNCATE, CONTEXT_STRATEGY_CLEAR]), - vol.Optional( - CONF_ENABLE_TOOL_CALLS, - default=options.get( - CONF_ENABLE_TOOL_CALLS, - DEFAULT_ENABLE_TOOL_CALLS, - ), - ): bool, - vol.Optional( - CONF_WAKE_WORD_ENABLED, - default=options.get( - CONF_WAKE_WORD_ENABLED, - DEFAULT_WAKE_WORD_ENABLED, - ), - ): bool, - vol.Optional( - CONF_WAKE_WORD, - default=options.get( - CONF_WAKE_WORD, - DEFAULT_WAKE_WORD, - ), - ): str, - vol.Optional( - CONF_ALLOW_BRAVE_WEBSPEECH, - default=options.get( - CONF_ALLOW_BRAVE_WEBSPEECH, - DEFAULT_ALLOW_BRAVE_WEBSPEECH, - ), - ): bool, - vol.Optional( - CONF_VOICE_PROVIDER, - default=options.get( - CONF_VOICE_PROVIDER, - DEFAULT_VOICE_PROVIDER, - ), - ): vol.In(["browser", "assist_stt"]), - } - ), - ) + selected_provider = options.get(CONF_VOICE_PROVIDER, DEFAULT_VOICE_PROVIDER) + + schema: dict[Any, Any] = { + vol.Optional( + CONF_INCLUDE_EXPOSED_CONTEXT, + default=options.get( + CONF_INCLUDE_EXPOSED_CONTEXT, + DEFAULT_INCLUDE_EXPOSED_CONTEXT, + ), + ): bool, + vol.Optional( + CONF_CONTEXT_MAX_CHARS, + default=options.get( + CONF_CONTEXT_MAX_CHARS, + DEFAULT_CONTEXT_MAX_CHARS, + ), + ): vol.All(int, vol.Range(min=1000, max=200000)), + vol.Optional( + CONF_CONTEXT_STRATEGY, + default=options.get( + CONF_CONTEXT_STRATEGY, + DEFAULT_CONTEXT_STRATEGY, + ), + ): vol.In([CONTEXT_STRATEGY_TRUNCATE, CONTEXT_STRATEGY_CLEAR]), + vol.Optional( + CONF_ENABLE_TOOL_CALLS, + default=options.get( + CONF_ENABLE_TOOL_CALLS, + DEFAULT_ENABLE_TOOL_CALLS, + ), + ): bool, + vol.Optional( + CONF_WAKE_WORD_ENABLED, + default=options.get( + CONF_WAKE_WORD_ENABLED, + DEFAULT_WAKE_WORD_ENABLED, + ), + ): bool, + vol.Optional( + CONF_WAKE_WORD, + default=options.get( + CONF_WAKE_WORD, + DEFAULT_WAKE_WORD, + ), + ): str, + vol.Optional( + CONF_ALLOW_BRAVE_WEBSPEECH, + default=options.get( + CONF_ALLOW_BRAVE_WEBSPEECH, + DEFAULT_ALLOW_BRAVE_WEBSPEECH, + ), + ): bool, + vol.Optional( + CONF_VOICE_PROVIDER, + default=selected_provider, + ): vol.In(["browser", "assist_stt"]), + } + + if selected_provider == "browser": + schema[ + vol.Optional( + CONF_BROWSER_VOICE_LANGUAGE, + default=options.get( + CONF_BROWSER_VOICE_LANGUAGE, + DEFAULT_BROWSER_VOICE_LANGUAGE, + ), + ) + ] = vol.In(BROWSER_VOICE_LANGUAGES) + + return self.async_show_form(step_id="init", data_schema=vol.Schema(schema)) diff --git a/custom_components/openclaw/const.py b/custom_components/openclaw/const.py index b92b1b1..24150e0 100644 --- a/custom_components/openclaw/const.py +++ b/custom_components/openclaw/const.py @@ -32,6 +32,7 @@ CONF_WAKE_WORD_ENABLED = "wake_word_enabled" CONF_WAKE_WORD = "wake_word" CONF_ALLOW_BRAVE_WEBSPEECH = "allow_brave_webspeech" CONF_VOICE_PROVIDER = "voice_provider" +CONF_BROWSER_VOICE_LANGUAGE = "browser_voice_language" DEFAULT_INCLUDE_EXPOSED_CONTEXT = True DEFAULT_CONTEXT_MAX_CHARS = 13000 @@ -41,6 +42,44 @@ DEFAULT_WAKE_WORD_ENABLED = False DEFAULT_WAKE_WORD = "hey openclaw" DEFAULT_ALLOW_BRAVE_WEBSPEECH = False DEFAULT_VOICE_PROVIDER = "browser" +DEFAULT_BROWSER_VOICE_LANGUAGE = "auto" + +BROWSER_VOICE_LANGUAGES: tuple[str, ...] = ( + "auto", + "bg-BG", + "en-US", + "en-GB", + "de-DE", + "fr-FR", + "es-ES", + "it-IT", + "pt-PT", + "pt-BR", + "ru-RU", + "nl-NL", + "pl-PL", + "tr-TR", + "uk-UA", + "cs-CZ", + "ro-RO", + "el-GR", + "sv-SE", + "no-NO", + "da-DK", + "fi-FI", + "hu-HU", + "sk-SK", + "sl-SI", + "hr-HR", + "sr-RS", + "ja-JP", + "ko-KR", + "zh-CN", + "zh-TW", + "ar-SA", + "he-IL", + "hi-IN", +) CONTEXT_STRATEGY_TRUNCATE = "truncate" CONTEXT_STRATEGY_CLEAR = "clear" diff --git a/custom_components/openclaw/manifest.json b/custom_components/openclaw/manifest.json index df68a06..442652a 100644 --- a/custom_components/openclaw/manifest.json +++ b/custom_components/openclaw/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues", "requirements": [], - "version": "0.1.41", + "version": "0.1.42", "dependencies": ["conversation"], "after_dependencies": ["hassio", "lovelace"] } diff --git a/custom_components/openclaw/strings.json b/custom_components/openclaw/strings.json index dab5a33..9c4175c 100644 --- a/custom_components/openclaw/strings.json +++ b/custom_components/openclaw/strings.json @@ -44,7 +44,8 @@ "wake_word_enabled": "Require wake word", "wake_word": "Wake word", "allow_brave_webspeech": "Allow Web Speech in Brave (experimental)", - "voice_provider": "Voice input provider (browser or HA STT)" + "voice_provider": "Voice input provider (browser or HA STT)", + "browser_voice_language": "Browser voice language" } } } diff --git a/custom_components/openclaw/translations/en.json b/custom_components/openclaw/translations/en.json index dab5a33..9c4175c 100644 --- a/custom_components/openclaw/translations/en.json +++ b/custom_components/openclaw/translations/en.json @@ -44,7 +44,8 @@ "wake_word_enabled": "Require wake word", "wake_word": "Wake word", "allow_brave_webspeech": "Allow Web Speech in Brave (experimental)", - "voice_provider": "Voice input provider (browser or HA STT)" + "voice_provider": "Voice input provider (browser or HA STT)", + "browser_voice_language": "Browser voice language" } } } diff --git a/custom_components/openclaw/www/openclaw-chat-card.js b/custom_components/openclaw/www/openclaw-chat-card.js index 8c26ce1..eab0ff9 100644 --- a/custom_components/openclaw/www/openclaw-chat-card.js +++ b/custom_components/openclaw/www/openclaw-chat-card.js @@ -13,7 +13,7 @@ * + subscribes to openclaw_message_received events. */ -const CARD_VERSION = "0.3.5"; +const CARD_VERSION = "0.3.6"; // Max time (ms) to show the thinking indicator before falling back to an error const THINKING_TIMEOUT_MS = 120_000; @@ -68,6 +68,7 @@ class OpenClawChatCard extends HTMLElement { this._voiceNetworkErrorCount = 0; this._pendingResponses = 0; this._speechLangOverride = null; + this._integrationBrowserVoiceLanguage = null; this._integrationVoiceLanguage = null; this._integrationTtsLanguage = null; this._allowBraveWebSpeechIntegration = false; @@ -229,7 +230,7 @@ class OpenClawChatCard extends HTMLElement { this._scrollToBottom(); // In voice mode, speak the response - if (this._isVoiceMode && data.message) { + if (this._isVoiceMode && this._recognition && data.message) { this._speak(data.message); } } @@ -354,6 +355,10 @@ class OpenClawChatCard extends HTMLElement { this._allowBraveWebSpeechIntegration = !!result?.allow_brave_webspeech; this._voiceProviderIntegration = result?.voice_provider === "assist_stt" ? "assist_stt" : "browser"; + this._integrationBrowserVoiceLanguage = + result?.browser_voice_language && result?.browser_voice_language !== "auto" + ? this._normalizeSpeechLanguage(result.browser_voice_language) + : null; this._integrationVoiceLanguage = result?.language ? this._normalizeSpeechLanguage(result.language) : null; @@ -544,6 +549,13 @@ class OpenClawChatCard extends HTMLElement { } const configuredLang = this._config.voice_language; + if (configuredLang) { + return this._normalizeSpeechLanguage(configuredLang); + } + const provider = this._getVoiceProvider(); + if (provider === "browser" && this._integrationBrowserVoiceLanguage) { + return this._normalizeSpeechLanguage(this._integrationBrowserVoiceLanguage); + } const integrationLang = this._integrationVoiceLanguage; const hassLang = this._hass?.locale?.language || this._hass?.selectedLanguage || this._hass?.language; @@ -554,6 +566,13 @@ class OpenClawChatCard extends HTMLElement { _getSpeechSynthesisLanguage() { const configuredLang = this._config.voice_language; + if (configuredLang) { + return this._normalizeSpeechLanguage(configuredLang); + } + const provider = this._getVoiceProvider(); + if (provider === "browser" && this._integrationBrowserVoiceLanguage) { + return this._normalizeSpeechLanguage(this._integrationBrowserVoiceLanguage); + } const preferred = configuredLang || this._integrationTtsLanguage || diff --git a/www/openclaw-chat-card.js b/www/openclaw-chat-card.js index cb2245b..3685489 100644 --- a/www/openclaw-chat-card.js +++ b/www/openclaw-chat-card.js @@ -1,7 +1,7 @@ (async () => { try { if (!customElements.get("openclaw-chat-card")) { - const src = "/openclaw/openclaw-chat-card.js?v=0.1.41"; + const src = "/openclaw/openclaw-chat-card.js?v=0.1.42"; console.info("OpenClaw loader importing", src); await import(src); }