breve webspeech config, type animation fixes, voice language fixes
This commit is contained in:
@@ -46,6 +46,7 @@ from .const import (
|
||||
CONF_WAKE_WORD,
|
||||
CONF_WAKE_WORD_ENABLED,
|
||||
CONF_ALWAYS_VOICE_MODE,
|
||||
CONF_ALLOW_BRAVE_WEBSPEECH,
|
||||
CONTEXT_STRATEGY_TRUNCATE,
|
||||
DEFAULT_CONTEXT_MAX_CHARS,
|
||||
DEFAULT_CONTEXT_STRATEGY,
|
||||
@@ -54,6 +55,7 @@ from .const import (
|
||||
DEFAULT_WAKE_WORD,
|
||||
DEFAULT_WAKE_WORD_ENABLED,
|
||||
DEFAULT_ALWAYS_VOICE_MODE,
|
||||
DEFAULT_ALLOW_BRAVE_WEBSPEECH,
|
||||
DOMAIN,
|
||||
EVENT_MESSAGE_RECEIVED,
|
||||
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)
|
||||
_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.24"
|
||||
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.26"
|
||||
|
||||
type OpenClawConfigEntry = ConfigEntry
|
||||
|
||||
@@ -683,6 +685,11 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
|
||||
CONF_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_ENABLED,
|
||||
CONF_ALWAYS_VOICE_MODE,
|
||||
CONF_ALLOW_BRAVE_WEBSPEECH,
|
||||
CONTEXT_STRATEGY_CLEAR,
|
||||
CONTEXT_STRATEGY_TRUNCATE,
|
||||
DEFAULT_GATEWAY_HOST,
|
||||
@@ -46,6 +47,7 @@ from .const import (
|
||||
DEFAULT_WAKE_WORD,
|
||||
DEFAULT_WAKE_WORD_ENABLED,
|
||||
DEFAULT_ALWAYS_VOICE_MODE,
|
||||
DEFAULT_ALLOW_BRAVE_WEBSPEECH,
|
||||
DOMAIN,
|
||||
OPENCLAW_CONFIG_REL_PATH,
|
||||
)
|
||||
@@ -455,6 +457,13 @@ class OpenClawOptionsFlow(OptionsFlow):
|
||||
DEFAULT_ALWAYS_VOICE_MODE,
|
||||
),
|
||||
): 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 = "wake_word"
|
||||
CONF_ALWAYS_VOICE_MODE = "always_voice_mode"
|
||||
CONF_ALLOW_BRAVE_WEBSPEECH = "allow_brave_webspeech"
|
||||
|
||||
DEFAULT_INCLUDE_EXPOSED_CONTEXT = True
|
||||
DEFAULT_CONTEXT_MAX_CHARS = 13000
|
||||
@@ -39,6 +40,7 @@ DEFAULT_ENABLE_TOOL_CALLS = False
|
||||
DEFAULT_WAKE_WORD_ENABLED = False
|
||||
DEFAULT_WAKE_WORD = "hey openclaw"
|
||||
DEFAULT_ALWAYS_VOICE_MODE = False
|
||||
DEFAULT_ALLOW_BRAVE_WEBSPEECH = False
|
||||
|
||||
CONTEXT_STRATEGY_TRUNCATE = "truncate"
|
||||
CONTEXT_STRATEGY_CLEAR = "clear"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
|
||||
"requirements": [],
|
||||
"version": "0.1.24",
|
||||
"version": "0.1.26",
|
||||
"dependencies": ["conversation"],
|
||||
"after_dependencies": ["hassio", "lovelace"]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"enable_tool_calls": "Enable tool calls (execute services)",
|
||||
"wake_word_enabled": "Require 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)",
|
||||
"wake_word_enabled": "Require 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.
|
||||
*/
|
||||
|
||||
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
|
||||
const THINKING_TIMEOUT_MS = 120_000;
|
||||
@@ -67,7 +67,10 @@ class OpenClawChatCard extends HTMLElement {
|
||||
this._voiceRetryTimer = null;
|
||||
this._voiceRetryCount = 0;
|
||||
this._voiceNetworkErrorCount = 0;
|
||||
this._pendingResponses = 0;
|
||||
this._speechLangOverride = null;
|
||||
this._integrationVoiceLanguage = null;
|
||||
this._allowBraveWebSpeechIntegration = false;
|
||||
this._voiceBackendBlocked = false;
|
||||
}
|
||||
|
||||
@@ -162,25 +165,27 @@ class OpenClawChatCard extends HTMLElement {
|
||||
const sessionId = this._getSessionId();
|
||||
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);
|
||||
if (thinkingIdx >= 0) {
|
||||
this._messages[thinkingIdx] = {
|
||||
role: "assistant",
|
||||
content: data.message,
|
||||
timestamp: data.timestamp || new Date().toISOString(),
|
||||
};
|
||||
} else {
|
||||
this._addMessage("assistant", data.message);
|
||||
const thinkingIdx = this._messages.findIndex((m) => m._thinking);
|
||||
if (thinkingIdx >= 0) {
|
||||
this._messages[thinkingIdx] = {
|
||||
role: "assistant",
|
||||
content: data.message,
|
||||
timestamp: data.timestamp || new Date().toISOString(),
|
||||
};
|
||||
if (this._pendingResponses > 0) {
|
||||
this._pendingResponses -= 1;
|
||||
}
|
||||
} else {
|
||||
this._addMessage("assistant", data.message);
|
||||
}
|
||||
|
||||
this._isProcessing = this._pendingResponses > 0;
|
||||
if (!this._isProcessing) {
|
||||
this._clearThinkingTimer();
|
||||
} else {
|
||||
this._startThinkingTimer();
|
||||
}
|
||||
|
||||
this._persistMessages();
|
||||
this._render();
|
||||
this._scrollToBottom();
|
||||
@@ -194,6 +199,7 @@ class OpenClawChatCard extends HTMLElement {
|
||||
_clearChat() {
|
||||
this._messages = [];
|
||||
this._isProcessing = false;
|
||||
this._pendingResponses = 0;
|
||||
this._clearThinkingTimer();
|
||||
this._persistMessages();
|
||||
this._render();
|
||||
@@ -273,6 +279,7 @@ class OpenClawChatCard extends HTMLElement {
|
||||
if (shouldReplace) {
|
||||
this._messages = validMessages;
|
||||
this._isProcessing = false;
|
||||
this._pendingResponses = 0;
|
||||
this._clearThinkingTimer();
|
||||
this._persistMessages();
|
||||
this._render();
|
||||
@@ -307,6 +314,10 @@ class OpenClawChatCard extends HTMLElement {
|
||||
this._wakeWordEnabled = !!result?.wake_word_enabled;
|
||||
this._wakeWord = (result?.wake_word || "hey openclaw").toString().trim().toLowerCase();
|
||||
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) {
|
||||
this._isVoiceMode = true;
|
||||
@@ -321,9 +332,10 @@ class OpenClawChatCard extends HTMLElement {
|
||||
// ── Thinking timer ──────────────────────────────────────────────────
|
||||
|
||||
_startThinkingTimer() {
|
||||
this._clearThinkingTimer();
|
||||
if (this._thinkingTimer) return;
|
||||
this._thinkingTimer = setTimeout(() => {
|
||||
if (!this._isProcessing) return;
|
||||
this._thinkingTimer = null;
|
||||
if (this._pendingResponses <= 0) return;
|
||||
const idx = this._messages.findIndex((m) => m._thinking);
|
||||
if (idx >= 0) {
|
||||
this._messages[idx] = {
|
||||
@@ -332,8 +344,13 @@ class OpenClawChatCard extends HTMLElement {
|
||||
timestamp: new Date().toISOString(),
|
||||
_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();
|
||||
}, THINKING_TIMEOUT_MS);
|
||||
}
|
||||
@@ -364,6 +381,7 @@ class OpenClawChatCard extends HTMLElement {
|
||||
|
||||
// Add thinking indicator with timeout safeguard
|
||||
this._isProcessing = true;
|
||||
this._pendingResponses += 1;
|
||||
this._messages.push({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
@@ -383,7 +401,13 @@ class OpenClawChatCard extends HTMLElement {
|
||||
} catch (err) {
|
||||
console.error("OpenClaw: Failed to send message:", err);
|
||||
// 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) {
|
||||
this._messages[thinkingIdx] = {
|
||||
role: "assistant",
|
||||
@@ -392,7 +416,14 @@ class OpenClawChatCard extends HTMLElement {
|
||||
_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();
|
||||
}
|
||||
}
|
||||
@@ -439,9 +470,11 @@ class OpenClawChatCard extends HTMLElement {
|
||||
}
|
||||
|
||||
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 preferred = configuredLang || hassLang || browserLang || "en-US";
|
||||
const preferred = configuredLang || integrationLang || hassLang || browserLang || "en-US";
|
||||
return this._normalizeSpeechLanguage(preferred);
|
||||
}
|
||||
|
||||
@@ -458,8 +491,10 @@ class OpenClawChatCard extends HTMLElement {
|
||||
|
||||
_startVoiceRecognition() {
|
||||
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 =
|
||||
"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();
|
||||
@@ -631,9 +666,54 @@ class OpenClawChatCard extends HTMLElement {
|
||||
if (!("speechSynthesis" in window)) return;
|
||||
// Strip markdown for TTS
|
||||
const plain = text.replace(/[*_`#\[\]()]/g, "");
|
||||
const utterance = new SpeechSynthesisUtterance(plain);
|
||||
utterance.lang = this._hass?.language || "en-US";
|
||||
speechSynthesis.speak(utterance);
|
||||
const language = this._getSpeechRecognitionLanguage();
|
||||
|
||||
const speakNow = () => {
|
||||
const utterance = new SpeechSynthesisUtterance(plain);
|
||||
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);
|
||||
};
|
||||
|
||||
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 ────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user