mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor: centralize talk silence timeout defaults
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package ai.openclaw.app.voice
|
||||
|
||||
internal object TalkDefaults {
|
||||
const val defaultSilenceTimeoutMs = 700L
|
||||
}
|
||||
@@ -60,7 +60,6 @@ class TalkModeManager(
|
||||
private const val defaultModelIdFallback = "eleven_v3"
|
||||
private const val defaultOutputFormatFallback = "pcm_24000"
|
||||
private const val defaultTalkProvider = "elevenlabs"
|
||||
private const val defaultSilenceTimeoutMs = 700L
|
||||
private const val listenWatchdogMs = 12_000L
|
||||
private const val chatFinalWaitWithSubscribeMs = 45_000L
|
||||
private const val chatFinalWaitWithoutSubscribeMs = 6_000L
|
||||
@@ -118,11 +117,12 @@ class TalkModeManager(
|
||||
}
|
||||
|
||||
internal fun resolvedSilenceTimeoutMs(talk: JsonObject?): Long {
|
||||
val primitive = talk?.get("silenceTimeoutMs") as? JsonPrimitive ?: return defaultSilenceTimeoutMs
|
||||
if (primitive.isString) return defaultSilenceTimeoutMs
|
||||
val timeout = primitive.content.toDoubleOrNull() ?: return defaultSilenceTimeoutMs
|
||||
val fallback = TalkDefaults.defaultSilenceTimeoutMs
|
||||
val primitive = talk?.get("silenceTimeoutMs") as? JsonPrimitive ?: return fallback
|
||||
if (primitive.isString) return fallback
|
||||
val timeout = primitive.content.toDoubleOrNull() ?: return fallback
|
||||
if (timeout <= 0 || timeout % 1.0 != 0.0 || timeout > Long.MAX_VALUE.toDouble()) {
|
||||
return defaultSilenceTimeoutMs
|
||||
return fallback
|
||||
}
|
||||
return timeout.toLong()
|
||||
}
|
||||
@@ -155,7 +155,7 @@ class TalkModeManager(
|
||||
private var listeningMode = false
|
||||
|
||||
private var silenceJob: Job? = null
|
||||
private var silenceWindowMs = defaultSilenceTimeoutMs
|
||||
private var silenceWindowMs = TalkDefaults.defaultSilenceTimeoutMs
|
||||
private var lastTranscript: String = ""
|
||||
private var lastHeardAtMs: Long? = null
|
||||
private var lastSpokenText: String? = null
|
||||
@@ -1467,7 +1467,7 @@ class TalkModeManager(
|
||||
}
|
||||
configLoaded = true
|
||||
} catch (_: Throwable) {
|
||||
silenceWindowMs = defaultSilenceTimeoutMs
|
||||
silenceWindowMs = TalkDefaults.defaultSilenceTimeoutMs
|
||||
defaultVoiceId = envVoice?.takeIf { it.isNotEmpty() } ?: sagVoice?.takeIf { it.isNotEmpty() }
|
||||
defaultModelId = defaultModelIdFallback
|
||||
if (!modelOverrideActive) currentModelId = defaultModelId
|
||||
|
||||
@@ -94,20 +94,20 @@ class TalkModeConfigParsingTest {
|
||||
|
||||
@Test
|
||||
fun defaultsSilenceTimeoutMsWhenMissing() {
|
||||
assertEquals(700L, TalkModeManager.resolvedSilenceTimeoutMs(null))
|
||||
assertEquals(TalkDefaults.defaultSilenceTimeoutMs, TalkModeManager.resolvedSilenceTimeoutMs(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultsSilenceTimeoutMsWhenInvalid() {
|
||||
val talk = buildJsonObject { put("silenceTimeoutMs", 0) }
|
||||
|
||||
assertEquals(700L, TalkModeManager.resolvedSilenceTimeoutMs(talk))
|
||||
assertEquals(TalkDefaults.defaultSilenceTimeoutMs, TalkModeManager.resolvedSilenceTimeoutMs(talk))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultsSilenceTimeoutMsWhenString() {
|
||||
val talk = buildJsonObject { put("silenceTimeoutMs", "1500") }
|
||||
|
||||
assertEquals(700L, TalkModeManager.resolvedSilenceTimeoutMs(talk))
|
||||
assertEquals(TalkDefaults.defaultSilenceTimeoutMs, TalkModeManager.resolvedSilenceTimeoutMs(talk))
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/ios/Sources/Voice/TalkDefaults.swift
Normal file
3
apps/ios/Sources/Voice/TalkDefaults.swift
Normal file
@@ -0,0 +1,3 @@
|
||||
enum TalkDefaults {
|
||||
static let silenceTimeoutMs = 900
|
||||
}
|
||||
@@ -34,7 +34,7 @@ final class TalkModeManager: NSObject {
|
||||
private typealias SpeechRequest = SFSpeechAudioBufferRecognitionRequest
|
||||
private static let defaultModelIdFallback = "eleven_v3"
|
||||
private static let defaultTalkProvider = "elevenlabs"
|
||||
private static let defaultSilenceTimeoutMs = 900
|
||||
private static let defaultSilenceTimeoutMs = TalkDefaults.silenceTimeoutMs
|
||||
private static let redactedConfigSentinel = "__OPENCLAW_REDACTED__"
|
||||
var isEnabled: Bool = false
|
||||
var isListening: Bool = false
|
||||
|
||||
@@ -60,7 +60,7 @@ import Testing
|
||||
}
|
||||
|
||||
@Test func defaultsSilenceTimeoutMsWhenMissing() {
|
||||
#expect(TalkModeManager.resolvedSilenceTimeoutMs(nil) == 900)
|
||||
#expect(TalkModeManager.resolvedSilenceTimeoutMs(nil) == TalkDefaults.silenceTimeoutMs)
|
||||
}
|
||||
|
||||
@Test func defaultsSilenceTimeoutMsWhenInvalid() {
|
||||
@@ -68,7 +68,7 @@ import Testing
|
||||
"silenceTimeoutMs": 0,
|
||||
]
|
||||
|
||||
#expect(TalkModeManager.resolvedSilenceTimeoutMs(TalkConfigParsing.bridgeFoundationDictionary(talk)) == 900)
|
||||
#expect(TalkModeManager.resolvedSilenceTimeoutMs(TalkConfigParsing.bridgeFoundationDictionary(talk)) == TalkDefaults.silenceTimeoutMs)
|
||||
}
|
||||
|
||||
@Test func defaultsSilenceTimeoutMsWhenBool() {
|
||||
@@ -76,6 +76,6 @@ import Testing
|
||||
"silenceTimeoutMs": true,
|
||||
]
|
||||
|
||||
#expect(TalkModeManager.resolvedSilenceTimeoutMs(TalkConfigParsing.bridgeFoundationDictionary(talk)) == 900)
|
||||
#expect(TalkModeManager.resolvedSilenceTimeoutMs(TalkConfigParsing.bridgeFoundationDictionary(talk)) == TalkDefaults.silenceTimeoutMs)
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/macos/Sources/OpenClaw/TalkDefaults.swift
Normal file
3
apps/macos/Sources/OpenClaw/TalkDefaults.swift
Normal file
@@ -0,0 +1,3 @@
|
||||
enum TalkDefaults {
|
||||
static let silenceTimeoutMs = 700
|
||||
}
|
||||
@@ -12,7 +12,7 @@ actor TalkModeRuntime {
|
||||
private let ttsLogger = Logger(subsystem: "ai.openclaw", category: "talk.tts")
|
||||
private static let defaultModelIdFallback = "eleven_v3"
|
||||
private static let defaultTalkProvider = "elevenlabs"
|
||||
private static let defaultSilenceTimeoutMs = 700
|
||||
private static let defaultSilenceTimeoutMs = TalkDefaults.silenceTimeoutMs
|
||||
|
||||
private final class RMSMeter: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
|
||||
@@ -33,7 +33,7 @@ struct TalkModeConfigParsingTests {
|
||||
#expect(selection?.config["apiKey"]?.stringValue == "legacy-key")
|
||||
}
|
||||
|
||||
@Test func readsConfiguredSilenceTimeoutMs() {
|
||||
@Test func `reads configured silence timeout ms`() {
|
||||
let talk: [String: AnyCodable] = [
|
||||
"silenceTimeoutMs": AnyCodable(1500),
|
||||
]
|
||||
@@ -41,15 +41,15 @@ struct TalkModeConfigParsingTests {
|
||||
#expect(TalkModeRuntime.resolvedSilenceTimeoutMs(talk) == 1500)
|
||||
}
|
||||
|
||||
@Test func defaultsSilenceTimeoutMsWhenMissing() {
|
||||
#expect(TalkModeRuntime.resolvedSilenceTimeoutMs(nil) == 700)
|
||||
@Test func `defaults silence timeout ms when missing`() {
|
||||
#expect(TalkModeRuntime.resolvedSilenceTimeoutMs(nil) == TalkDefaults.silenceTimeoutMs)
|
||||
}
|
||||
|
||||
@Test func defaultsSilenceTimeoutMsWhenInvalid() {
|
||||
@Test func `defaults silence timeout ms when invalid`() {
|
||||
let talk: [String: AnyCodable] = [
|
||||
"silenceTimeoutMs": AnyCodable(0),
|
||||
]
|
||||
|
||||
#expect(TalkModeRuntime.resolvedSilenceTimeoutMs(talk) == 700)
|
||||
#expect(TalkModeRuntime.resolvedSilenceTimeoutMs(talk) == TalkDefaults.silenceTimeoutMs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1669,7 +1669,7 @@ Defaults for Talk mode (macOS/iOS/Android).
|
||||
- `apiKey` and `providers.*.apiKey` accept plaintext strings or SecretRef objects.
|
||||
- `ELEVENLABS_API_KEY` fallback applies only when no Talk API key is configured.
|
||||
- `voiceAliases` lets Talk directives use friendly names.
|
||||
- `silenceTimeoutMs` controls how long Talk mode waits after user silence before it sends the transcript. Unset keeps the platform default pause window (`700` ms on macOS and Android, `900` ms on iOS).
|
||||
- `silenceTimeoutMs` controls how long Talk mode waits after user silence before it sends the transcript. Unset keeps the platform default pause window (`700 ms on macOS and Android, 900 ms on iOS`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ Supported keys:
|
||||
Defaults:
|
||||
|
||||
- `interruptOnSpeech`: true
|
||||
- `silenceTimeoutMs`: when unset, Talk keeps the platform default pause window before sending the transcript (`700` ms on macOS and Android, `900` ms on iOS)
|
||||
- `silenceTimeoutMs`: when unset, Talk keeps the platform default pause window before sending the transcript (`700 ms on macOS and Android, 900 ms on iOS`)
|
||||
- `voiceId`: falls back to `ELEVENLABS_VOICE_ID` / `SAG_VOICE_ID` (or first ElevenLabs voice when API key is available)
|
||||
- `modelId`: defaults to `eleven_v3` when unset
|
||||
- `apiKey`: falls back to `ELEVENLABS_API_KEY` (or gateway shell profile if available)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
} from "../discord/monitor/timeouts.js";
|
||||
import { MEDIA_AUDIO_FIELD_HELP } from "./media-audio-field-metadata.js";
|
||||
import { IRC_FIELD_HELP } from "./schema.irc.js";
|
||||
import { describeTalkSilenceTimeoutDefaults } from "./talk-defaults.js";
|
||||
|
||||
export const FIELD_HELP: Record<string, string> = {
|
||||
meta: "Metadata fields automatically maintained by OpenClaw to record write/version history for this config file. Keep these values system-managed and avoid manual edits unless debugging migration history.",
|
||||
@@ -163,8 +164,7 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Use this legacy ElevenLabs API key for Talk mode only during migration, and keep secrets in env-backed storage. Prefer talk.providers.elevenlabs.apiKey (fallback: ELEVENLABS_API_KEY).",
|
||||
"talk.interruptOnSpeech":
|
||||
"If true (default), stop assistant speech when the user starts speaking in Talk mode. Keep enabled for conversational turn-taking.",
|
||||
"talk.silenceTimeoutMs":
|
||||
"Milliseconds of user silence before Talk mode finalizes and sends the current transcript. Leave unset to keep the platform default pause window (700 ms on macOS and Android, 900 ms on iOS).",
|
||||
"talk.silenceTimeoutMs": `Milliseconds of user silence before Talk mode finalizes and sends the current transcript. Leave unset to keep the platform default pause window (${describeTalkSilenceTimeoutDefaults()}).`,
|
||||
acp: "ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.",
|
||||
"acp.enabled":
|
||||
"Global ACP feature gate. Keep disabled unless ACP runtime + policy are configured.",
|
||||
|
||||
43
src/config/talk-defaults.test.ts
Normal file
43
src/config/talk-defaults.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { FIELD_HELP } from "./schema.help.js";
|
||||
import {
|
||||
describeTalkSilenceTimeoutDefaults,
|
||||
TALK_SILENCE_TIMEOUT_MS_BY_PLATFORM,
|
||||
} from "./talk-defaults.js";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
function readRepoFile(relativePath: string): string {
|
||||
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
|
||||
}
|
||||
|
||||
describe("talk silence timeout defaults", () => {
|
||||
it("keeps help text and docs aligned with the policy", () => {
|
||||
const defaultsDescription = describeTalkSilenceTimeoutDefaults();
|
||||
|
||||
expect(FIELD_HELP["talk.silenceTimeoutMs"]).toContain(defaultsDescription);
|
||||
expect(readRepoFile("docs/gateway/configuration-reference.md")).toContain(defaultsDescription);
|
||||
expect(readRepoFile("docs/nodes/talk.md")).toContain(defaultsDescription);
|
||||
});
|
||||
|
||||
it("matches the Apple and Android runtime constants", () => {
|
||||
const macDefaults = readRepoFile("apps/macos/Sources/OpenClaw/TalkDefaults.swift");
|
||||
const iosDefaults = readRepoFile("apps/ios/Sources/Voice/TalkDefaults.swift");
|
||||
const androidDefaults = readRepoFile(
|
||||
"apps/android/app/src/main/java/ai/openclaw/app/voice/TalkDefaults.kt",
|
||||
);
|
||||
|
||||
expect(macDefaults).toContain(
|
||||
`static let silenceTimeoutMs = ${TALK_SILENCE_TIMEOUT_MS_BY_PLATFORM.macos}`,
|
||||
);
|
||||
expect(iosDefaults).toContain(
|
||||
`static let silenceTimeoutMs = ${TALK_SILENCE_TIMEOUT_MS_BY_PLATFORM.ios}`,
|
||||
);
|
||||
expect(androidDefaults).toContain(
|
||||
`const val defaultSilenceTimeoutMs = ${TALK_SILENCE_TIMEOUT_MS_BY_PLATFORM.android}L`,
|
||||
);
|
||||
});
|
||||
});
|
||||
11
src/config/talk-defaults.ts
Normal file
11
src/config/talk-defaults.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const TALK_SILENCE_TIMEOUT_MS_BY_PLATFORM = {
|
||||
macos: 700,
|
||||
android: 700,
|
||||
ios: 900,
|
||||
} as const;
|
||||
|
||||
export function describeTalkSilenceTimeoutDefaults(): string {
|
||||
const macos = TALK_SILENCE_TIMEOUT_MS_BY_PLATFORM.macos;
|
||||
const ios = TALK_SILENCE_TIMEOUT_MS_BY_PLATFORM.ios;
|
||||
return `${macos} ms on macOS and Android, ${ios} ms on iOS`;
|
||||
}
|
||||
Reference in New Issue
Block a user