breve webspeech config, type animation fixes, voice language fixes
This commit is contained in:
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
All notable changes to the OpenClaw Home Assistant Integration will be documented in this file.
|
All notable changes to the OpenClaw Home Assistant Integration will be documented in this file.
|
||||||
|
|
||||||
|
## [0.1.26] - 2026-02-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added integration option `allow_brave_webspeech` to **Settings → Devices & Services → OpenClaw → Configure**.
|
||||||
|
- Frontend card now reads this option via `openclaw/get_settings` and applies it automatically.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Card version bumped to `0.2.6` and cache-busting URL updated to `v=0.1.26`.
|
||||||
|
|
||||||
|
## [0.1.25] - 2026-02-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Voice input language now prioritizes integration/HA locale settings more reliably (including frontend locale fallback), reducing unwanted fallback to English.
|
||||||
|
- Voice-mode assistant replies now use improved speech synthesis voice selection for the active language and better voice-loading handling.
|
||||||
|
- Reworked chat pending-response tracking to support multiple in-flight messages without leaving stuck typing indicators.
|
||||||
|
|
||||||
## [0.1.24] - 2026-02-20
|
## [0.1.24] - 2026-02-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from .const import (
|
|||||||
CONF_WAKE_WORD,
|
CONF_WAKE_WORD,
|
||||||
CONF_WAKE_WORD_ENABLED,
|
CONF_WAKE_WORD_ENABLED,
|
||||||
CONF_ALWAYS_VOICE_MODE,
|
CONF_ALWAYS_VOICE_MODE,
|
||||||
|
CONF_ALLOW_BRAVE_WEBSPEECH,
|
||||||
CONTEXT_STRATEGY_TRUNCATE,
|
CONTEXT_STRATEGY_TRUNCATE,
|
||||||
DEFAULT_CONTEXT_MAX_CHARS,
|
DEFAULT_CONTEXT_MAX_CHARS,
|
||||||
DEFAULT_CONTEXT_STRATEGY,
|
DEFAULT_CONTEXT_STRATEGY,
|
||||||
@@ -54,6 +55,7 @@ from .const import (
|
|||||||
DEFAULT_WAKE_WORD,
|
DEFAULT_WAKE_WORD,
|
||||||
DEFAULT_WAKE_WORD_ENABLED,
|
DEFAULT_WAKE_WORD_ENABLED,
|
||||||
DEFAULT_ALWAYS_VOICE_MODE,
|
DEFAULT_ALWAYS_VOICE_MODE,
|
||||||
|
DEFAULT_ALLOW_BRAVE_WEBSPEECH,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_MESSAGE_RECEIVED,
|
EVENT_MESSAGE_RECEIVED,
|
||||||
OPENCLAW_CONFIG_REL_PATH,
|
OPENCLAW_CONFIG_REL_PATH,
|
||||||
@@ -74,7 +76,7 @@ _CARD_PATH = Path(__file__).parent / "www" / _CARD_FILENAME
|
|||||||
# URL at which the card JS is served (registered via register_static_path)
|
# URL at which the card JS is served (registered via register_static_path)
|
||||||
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
|
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
|
||||||
# Versioned URL used for Lovelace resource registration to avoid stale browser cache
|
# Versioned URL used for Lovelace resource registration to avoid stale browser cache
|
||||||
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.24"
|
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.26"
|
||||||
|
|
||||||
type OpenClawConfigEntry = ConfigEntry
|
type OpenClawConfigEntry = ConfigEntry
|
||||||
|
|
||||||
@@ -683,6 +685,11 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
|
|||||||
CONF_ALWAYS_VOICE_MODE,
|
CONF_ALWAYS_VOICE_MODE,
|
||||||
DEFAULT_ALWAYS_VOICE_MODE,
|
DEFAULT_ALWAYS_VOICE_MODE,
|
||||||
),
|
),
|
||||||
|
CONF_ALLOW_BRAVE_WEBSPEECH: options.get(
|
||||||
|
CONF_ALLOW_BRAVE_WEBSPEECH,
|
||||||
|
DEFAULT_ALLOW_BRAVE_WEBSPEECH,
|
||||||
|
),
|
||||||
|
"language": hass.config.language,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from .const import (
|
|||||||
CONF_WAKE_WORD,
|
CONF_WAKE_WORD,
|
||||||
CONF_WAKE_WORD_ENABLED,
|
CONF_WAKE_WORD_ENABLED,
|
||||||
CONF_ALWAYS_VOICE_MODE,
|
CONF_ALWAYS_VOICE_MODE,
|
||||||
|
CONF_ALLOW_BRAVE_WEBSPEECH,
|
||||||
CONTEXT_STRATEGY_CLEAR,
|
CONTEXT_STRATEGY_CLEAR,
|
||||||
CONTEXT_STRATEGY_TRUNCATE,
|
CONTEXT_STRATEGY_TRUNCATE,
|
||||||
DEFAULT_GATEWAY_HOST,
|
DEFAULT_GATEWAY_HOST,
|
||||||
@@ -46,6 +47,7 @@ from .const import (
|
|||||||
DEFAULT_WAKE_WORD,
|
DEFAULT_WAKE_WORD,
|
||||||
DEFAULT_WAKE_WORD_ENABLED,
|
DEFAULT_WAKE_WORD_ENABLED,
|
||||||
DEFAULT_ALWAYS_VOICE_MODE,
|
DEFAULT_ALWAYS_VOICE_MODE,
|
||||||
|
DEFAULT_ALLOW_BRAVE_WEBSPEECH,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
OPENCLAW_CONFIG_REL_PATH,
|
OPENCLAW_CONFIG_REL_PATH,
|
||||||
)
|
)
|
||||||
@@ -455,6 +457,13 @@ class OpenClawOptionsFlow(OptionsFlow):
|
|||||||
DEFAULT_ALWAYS_VOICE_MODE,
|
DEFAULT_ALWAYS_VOICE_MODE,
|
||||||
),
|
),
|
||||||
): bool,
|
): bool,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_ALLOW_BRAVE_WEBSPEECH,
|
||||||
|
default=options.get(
|
||||||
|
CONF_ALLOW_BRAVE_WEBSPEECH,
|
||||||
|
DEFAULT_ALLOW_BRAVE_WEBSPEECH,
|
||||||
|
),
|
||||||
|
): bool,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ CONF_ENABLE_TOOL_CALLS = "enable_tool_calls"
|
|||||||
CONF_WAKE_WORD_ENABLED = "wake_word_enabled"
|
CONF_WAKE_WORD_ENABLED = "wake_word_enabled"
|
||||||
CONF_WAKE_WORD = "wake_word"
|
CONF_WAKE_WORD = "wake_word"
|
||||||
CONF_ALWAYS_VOICE_MODE = "always_voice_mode"
|
CONF_ALWAYS_VOICE_MODE = "always_voice_mode"
|
||||||
|
CONF_ALLOW_BRAVE_WEBSPEECH = "allow_brave_webspeech"
|
||||||
|
|
||||||
DEFAULT_INCLUDE_EXPOSED_CONTEXT = True
|
DEFAULT_INCLUDE_EXPOSED_CONTEXT = True
|
||||||
DEFAULT_CONTEXT_MAX_CHARS = 13000
|
DEFAULT_CONTEXT_MAX_CHARS = 13000
|
||||||
@@ -39,6 +40,7 @@ DEFAULT_ENABLE_TOOL_CALLS = False
|
|||||||
DEFAULT_WAKE_WORD_ENABLED = False
|
DEFAULT_WAKE_WORD_ENABLED = False
|
||||||
DEFAULT_WAKE_WORD = "hey openclaw"
|
DEFAULT_WAKE_WORD = "hey openclaw"
|
||||||
DEFAULT_ALWAYS_VOICE_MODE = False
|
DEFAULT_ALWAYS_VOICE_MODE = False
|
||||||
|
DEFAULT_ALLOW_BRAVE_WEBSPEECH = False
|
||||||
|
|
||||||
CONTEXT_STRATEGY_TRUNCATE = "truncate"
|
CONTEXT_STRATEGY_TRUNCATE = "truncate"
|
||||||
CONTEXT_STRATEGY_CLEAR = "clear"
|
CONTEXT_STRATEGY_CLEAR = "clear"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
|
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"version": "0.1.24",
|
"version": "0.1.26",
|
||||||
"dependencies": ["conversation"],
|
"dependencies": ["conversation"],
|
||||||
"after_dependencies": ["hassio", "lovelace"]
|
"after_dependencies": ["hassio", "lovelace"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@
|
|||||||
"enable_tool_calls": "Enable tool calls (execute services)",
|
"enable_tool_calls": "Enable tool calls (execute services)",
|
||||||
"wake_word_enabled": "Require wake word",
|
"wake_word_enabled": "Require wake word",
|
||||||
"wake_word": "Wake word",
|
"wake_word": "Wake word",
|
||||||
"always_voice_mode": "Always-on voice mode"
|
"always_voice_mode": "Always-on voice mode",
|
||||||
|
"allow_brave_webspeech": "Allow Web Speech in Brave (experimental)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@
|
|||||||
"enable_tool_calls": "Enable tool calls (execute services)",
|
"enable_tool_calls": "Enable tool calls (execute services)",
|
||||||
"wake_word_enabled": "Require wake word",
|
"wake_word_enabled": "Require wake word",
|
||||||
"wake_word": "Wake word",
|
"wake_word": "Wake word",
|
||||||
"always_voice_mode": "Always-on voice mode"
|
"always_voice_mode": "Always-on voice mode",
|
||||||
|
"allow_brave_webspeech": "Allow Web Speech in Brave (experimental)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* + subscribes to openclaw_message_received events.
|
* + subscribes to openclaw_message_received events.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CARD_VERSION = "0.2.4";
|
const CARD_VERSION = "0.2.6";
|
||||||
|
|
||||||
// Max time (ms) to show the thinking indicator before falling back to an error
|
// Max time (ms) to show the thinking indicator before falling back to an error
|
||||||
const THINKING_TIMEOUT_MS = 120_000;
|
const THINKING_TIMEOUT_MS = 120_000;
|
||||||
@@ -67,7 +67,10 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
this._voiceRetryTimer = null;
|
this._voiceRetryTimer = null;
|
||||||
this._voiceRetryCount = 0;
|
this._voiceRetryCount = 0;
|
||||||
this._voiceNetworkErrorCount = 0;
|
this._voiceNetworkErrorCount = 0;
|
||||||
|
this._pendingResponses = 0;
|
||||||
this._speechLangOverride = null;
|
this._speechLangOverride = null;
|
||||||
|
this._integrationVoiceLanguage = null;
|
||||||
|
this._allowBraveWebSpeechIntegration = false;
|
||||||
this._voiceBackendBlocked = false;
|
this._voiceBackendBlocked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,11 +165,6 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
const sessionId = this._getSessionId();
|
const sessionId = this._getSessionId();
|
||||||
if (data.session_id && data.session_id !== sessionId) return;
|
if (data.session_id && data.session_id !== sessionId) return;
|
||||||
|
|
||||||
// If we're waiting for a response, update the last "thinking" message
|
|
||||||
if (this._isProcessing) {
|
|
||||||
this._isProcessing = false;
|
|
||||||
this._clearThinkingTimer();
|
|
||||||
// Replace the thinking indicator with the actual response
|
|
||||||
const thinkingIdx = this._messages.findIndex((m) => m._thinking);
|
const thinkingIdx = this._messages.findIndex((m) => m._thinking);
|
||||||
if (thinkingIdx >= 0) {
|
if (thinkingIdx >= 0) {
|
||||||
this._messages[thinkingIdx] = {
|
this._messages[thinkingIdx] = {
|
||||||
@@ -174,13 +172,20 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
content: data.message,
|
content: data.message,
|
||||||
timestamp: data.timestamp || new Date().toISOString(),
|
timestamp: data.timestamp || new Date().toISOString(),
|
||||||
};
|
};
|
||||||
} else {
|
if (this._pendingResponses > 0) {
|
||||||
this._addMessage("assistant", data.message);
|
this._pendingResponses -= 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._addMessage("assistant", data.message);
|
this._addMessage("assistant", data.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._isProcessing = this._pendingResponses > 0;
|
||||||
|
if (!this._isProcessing) {
|
||||||
|
this._clearThinkingTimer();
|
||||||
|
} else {
|
||||||
|
this._startThinkingTimer();
|
||||||
|
}
|
||||||
|
|
||||||
this._persistMessages();
|
this._persistMessages();
|
||||||
this._render();
|
this._render();
|
||||||
this._scrollToBottom();
|
this._scrollToBottom();
|
||||||
@@ -194,6 +199,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
_clearChat() {
|
_clearChat() {
|
||||||
this._messages = [];
|
this._messages = [];
|
||||||
this._isProcessing = false;
|
this._isProcessing = false;
|
||||||
|
this._pendingResponses = 0;
|
||||||
this._clearThinkingTimer();
|
this._clearThinkingTimer();
|
||||||
this._persistMessages();
|
this._persistMessages();
|
||||||
this._render();
|
this._render();
|
||||||
@@ -273,6 +279,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
if (shouldReplace) {
|
if (shouldReplace) {
|
||||||
this._messages = validMessages;
|
this._messages = validMessages;
|
||||||
this._isProcessing = false;
|
this._isProcessing = false;
|
||||||
|
this._pendingResponses = 0;
|
||||||
this._clearThinkingTimer();
|
this._clearThinkingTimer();
|
||||||
this._persistMessages();
|
this._persistMessages();
|
||||||
this._render();
|
this._render();
|
||||||
@@ -307,6 +314,10 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
this._wakeWordEnabled = !!result?.wake_word_enabled;
|
this._wakeWordEnabled = !!result?.wake_word_enabled;
|
||||||
this._wakeWord = (result?.wake_word || "hey openclaw").toString().trim().toLowerCase();
|
this._wakeWord = (result?.wake_word || "hey openclaw").toString().trim().toLowerCase();
|
||||||
this._alwaysVoiceMode = !!result?.always_voice_mode;
|
this._alwaysVoiceMode = !!result?.always_voice_mode;
|
||||||
|
this._allowBraveWebSpeechIntegration = !!result?.allow_brave_webspeech;
|
||||||
|
this._integrationVoiceLanguage = result?.language
|
||||||
|
? this._normalizeSpeechLanguage(result.language)
|
||||||
|
: null;
|
||||||
|
|
||||||
if (this._alwaysVoiceMode && !this._isVoiceMode) {
|
if (this._alwaysVoiceMode && !this._isVoiceMode) {
|
||||||
this._isVoiceMode = true;
|
this._isVoiceMode = true;
|
||||||
@@ -321,9 +332,10 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
// ── Thinking timer ──────────────────────────────────────────────────
|
// ── Thinking timer ──────────────────────────────────────────────────
|
||||||
|
|
||||||
_startThinkingTimer() {
|
_startThinkingTimer() {
|
||||||
this._clearThinkingTimer();
|
if (this._thinkingTimer) return;
|
||||||
this._thinkingTimer = setTimeout(() => {
|
this._thinkingTimer = setTimeout(() => {
|
||||||
if (!this._isProcessing) return;
|
this._thinkingTimer = null;
|
||||||
|
if (this._pendingResponses <= 0) return;
|
||||||
const idx = this._messages.findIndex((m) => m._thinking);
|
const idx = this._messages.findIndex((m) => m._thinking);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
this._messages[idx] = {
|
this._messages[idx] = {
|
||||||
@@ -332,8 +344,13 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
_error: true,
|
_error: true,
|
||||||
};
|
};
|
||||||
|
this._pendingResponses = Math.max(0, this._pendingResponses - 1);
|
||||||
}
|
}
|
||||||
this._isProcessing = false;
|
this._isProcessing = this._pendingResponses > 0;
|
||||||
|
if (this._isProcessing) {
|
||||||
|
this._startThinkingTimer();
|
||||||
|
}
|
||||||
|
this._persistMessages();
|
||||||
this._render();
|
this._render();
|
||||||
}, THINKING_TIMEOUT_MS);
|
}, THINKING_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
@@ -364,6 +381,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
|
|
||||||
// Add thinking indicator with timeout safeguard
|
// Add thinking indicator with timeout safeguard
|
||||||
this._isProcessing = true;
|
this._isProcessing = true;
|
||||||
|
this._pendingResponses += 1;
|
||||||
this._messages.push({
|
this._messages.push({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "",
|
content: "",
|
||||||
@@ -383,7 +401,13 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("OpenClaw: Failed to send message:", err);
|
console.error("OpenClaw: Failed to send message:", err);
|
||||||
// Replace thinking with error
|
// Replace thinking with error
|
||||||
const thinkingIdx = this._messages.findIndex((m) => m._thinking);
|
let thinkingIdx = -1;
|
||||||
|
for (let idx = this._messages.length - 1; idx >= 0; idx -= 1) {
|
||||||
|
if (this._messages[idx]?._thinking) {
|
||||||
|
thinkingIdx = idx;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (thinkingIdx >= 0) {
|
if (thinkingIdx >= 0) {
|
||||||
this._messages[thinkingIdx] = {
|
this._messages[thinkingIdx] = {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
@@ -392,7 +416,14 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
_error: true,
|
_error: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this._isProcessing = false;
|
if (this._pendingResponses > 0) {
|
||||||
|
this._pendingResponses -= 1;
|
||||||
|
}
|
||||||
|
this._isProcessing = this._pendingResponses > 0;
|
||||||
|
if (!this._isProcessing) {
|
||||||
|
this._clearThinkingTimer();
|
||||||
|
}
|
||||||
|
this._persistMessages();
|
||||||
this._render();
|
this._render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,9 +470,11 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const configuredLang = this._config.voice_language;
|
const configuredLang = this._config.voice_language;
|
||||||
const hassLang = this._hass?.selectedLanguage || this._hass?.language;
|
const integrationLang = this._integrationVoiceLanguage;
|
||||||
|
const hassLang =
|
||||||
|
this._hass?.locale?.language || this._hass?.selectedLanguage || this._hass?.language;
|
||||||
const browserLang = navigator.language;
|
const browserLang = navigator.language;
|
||||||
const preferred = configuredLang || hassLang || browserLang || "en-US";
|
const preferred = configuredLang || integrationLang || hassLang || browserLang || "en-US";
|
||||||
return this._normalizeSpeechLanguage(preferred);
|
return this._normalizeSpeechLanguage(preferred);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,8 +491,10 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
|
|
||||||
_startVoiceRecognition() {
|
_startVoiceRecognition() {
|
||||||
this._voiceBackendBlocked = false;
|
this._voiceBackendBlocked = false;
|
||||||
|
const allowBraveWebSpeech =
|
||||||
|
this._config.allow_brave_webspeech || this._allowBraveWebSpeechIntegration;
|
||||||
|
|
||||||
if (this._isLikelyBraveBrowser() && !this._config.allow_brave_webspeech) {
|
if (this._isLikelyBraveBrowser() && !allowBraveWebSpeech) {
|
||||||
this._voiceStatus =
|
this._voiceStatus =
|
||||||
"Voice input disabled on Brave by default due browser SpeechRecognition network failures. Use Chrome/Edge, or set allow_brave_webspeech: true in card config to force-enable experimental mode.";
|
"Voice input disabled on Brave by default due browser SpeechRecognition network failures. Use Chrome/Edge, or set allow_brave_webspeech: true in card config to force-enable experimental mode.";
|
||||||
this._render();
|
this._render();
|
||||||
@@ -631,9 +666,54 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
if (!("speechSynthesis" in window)) return;
|
if (!("speechSynthesis" in window)) return;
|
||||||
// Strip markdown for TTS
|
// Strip markdown for TTS
|
||||||
const plain = text.replace(/[*_`#\[\]()]/g, "");
|
const plain = text.replace(/[*_`#\[\]()]/g, "");
|
||||||
|
const language = this._getSpeechRecognitionLanguage();
|
||||||
|
|
||||||
|
const speakNow = () => {
|
||||||
const utterance = new SpeechSynthesisUtterance(plain);
|
const utterance = new SpeechSynthesisUtterance(plain);
|
||||||
utterance.lang = this._hass?.language || "en-US";
|
utterance.lang = language;
|
||||||
|
|
||||||
|
const voices = speechSynthesis.getVoices() || [];
|
||||||
|
if (voices.length) {
|
||||||
|
const exactVoice = voices.find(
|
||||||
|
(voice) => String(voice.lang || "").toLowerCase() === language.toLowerCase()
|
||||||
|
);
|
||||||
|
const prefix = language.split("-")[0]?.toLowerCase();
|
||||||
|
const languageVoice =
|
||||||
|
exactVoice ||
|
||||||
|
voices.find((voice) => String(voice.lang || "").toLowerCase().startsWith(`${prefix}-`));
|
||||||
|
if (languageVoice) {
|
||||||
|
utterance.voice = languageVoice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utterance.onerror = () => {
|
||||||
|
this._voiceStatus = `TTS error for language ${language}`;
|
||||||
|
this._render();
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
speechSynthesis.cancel();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
speechSynthesis.speak(utterance);
|
speechSynthesis.speak(utterance);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadedVoices = speechSynthesis.getVoices() || [];
|
||||||
|
if (loadedVoices.length) {
|
||||||
|
speakNow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVoicesChanged = () => {
|
||||||
|
speechSynthesis.removeEventListener("voiceschanged", handleVoicesChanged);
|
||||||
|
speakNow();
|
||||||
|
};
|
||||||
|
speechSynthesis.addEventListener("voiceschanged", handleVoicesChanged);
|
||||||
|
setTimeout(() => {
|
||||||
|
speechSynthesis.removeEventListener("voiceschanged", handleVoicesChanged);
|
||||||
|
speakNow();
|
||||||
|
}, 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── File attachments ────────────────────────────────────────────────
|
// ── File attachments ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
if (!customElements.get("openclaw-chat-card")) {
|
if (!customElements.get("openclaw-chat-card")) {
|
||||||
const src = "/openclaw/openclaw-chat-card.js?v=0.1.24";
|
const src = "/openclaw/openclaw-chat-card.js?v=0.1.26";
|
||||||
console.info("OpenClaw loader importing", src);
|
console.info("OpenClaw loader importing", src);
|
||||||
await import(src);
|
await import(src);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user