Stabilize Athena Talk audio delivery

This commit is contained in:
Mikei386
2026-09-03 23:11:44 +02:00
parent df41175960
commit 65ce9642cc
5 changed files with 42 additions and 27 deletions
+20 -14
View File
@@ -120,6 +120,7 @@ class AthenaTalkBridge {
private prefixBytes = 0;
private silenceTimer?: NodeJS.Timeout;
private active?: AbortController;
private transcribing = false;
private generation = 0;
constructor(private readonly req: any, private readonly cfg: Required<ProviderConfig>) {}
@@ -136,6 +137,11 @@ class AthenaTalkBridge {
sendAudio(chunk: Buffer): void {
if (!this.isConnected() || chunk.length === 0) return;
// Half-duplex by design: while Athena is transcribing, consulting the
// agent, synthesizing, or playing a reply, microphone input is ignored.
// This prevents speaker feedback and background noise from cancelling the
// response that is currently being delivered.
if (this.transcribing || this.active) return;
const voiced = pcmRms(chunk) >= this.cfg.vadThreshold;
const maxPrefix = Math.round(24000 * 2 * this.cfg.prefixPaddingMs / 1000);
@@ -146,12 +152,6 @@ class AthenaTalkBridge {
this.prefixBytes -= this.prefix.shift()!.length;
}
if (!voiced) return;
if (this.active) {
this.active.abort();
this.active = undefined;
this.generation += 1;
this.req.onClearAudio?.("barge-in");
}
this.speaking = true;
this.speech = this.prefix;
this.prefix = [];
@@ -201,13 +201,17 @@ class AthenaTalkBridge {
this.speech = [];
if (pcm.length < 24000 * 2 * 0.25) return;
this.transcribing = true;
try {
const text = await this.transcribe(pcm);
if (!text || !this.isConnected()) return;
this.req.onTranscript?.("user", text, true);
this.transcribing = false;
await this.answer(text);
} catch (error) {
if ((error as Error).name !== "AbortError") this.req.onError?.(error as Error);
} finally {
this.transcribing = false;
}
}
@@ -228,7 +232,7 @@ class AthenaTalkBridge {
private async answer(prompt: string): Promise<void> {
if (!this.req.runAgentConsult) throw new Error("OpenClaw agent-consult is unavailable");
this.active?.abort();
if (this.active) return;
const controller = new AbortController();
this.active = controller;
const generation = ++this.generation;
@@ -238,19 +242,21 @@ class AthenaTalkBridge {
if (generation !== this.generation || !this.isConnected()) return;
const text = String(result?.text || "").trim();
if (!text) return;
this.req.onEvent?.({ direction: "server", type: "response.created", responseId });
this.req.onTranscript?.("assistant", text, true);
const pcm = await this.synthesize(text, controller.signal);
if (generation !== this.generation || !this.isConnected()) return;
const frameBytes = 24000 * 2 / 50;
// Feed the relay 100 ms batches slightly faster than realtime. OpenClaw
// splits these into its native 20 ms frames. The small lead gives the
// client a jitter buffer and avoids the crackling caused by timer and
// WebSocket scheduling gaps at exact 20 ms pacing.
const frameBytes = 24000 * 2 / 10;
for (let offset = 0; offset < pcm.length; offset += frameBytes) {
if (controller.signal.aborted || generation !== this.generation || !this.isConnected()) {
return;
}
this.req.onAudio?.(pcm.subarray(offset, Math.min(offset + frameBytes, pcm.length)));
// Gateway relay drops audio frames when its WebSocket queue is full.
// Pace 20 ms PCM frames in realtime instead of flooding the complete
// synthesized response into the socket in a few milliseconds.
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => setTimeout(resolve, 80));
}
this.req.onResponseDone?.({ status: "completed", responseId });
this.req.onEvent?.({ direction: "server", type: "response.done", responseId });
@@ -299,8 +305,8 @@ export default definePluginEntry({
transports: ["gateway-relay"],
inputAudioFormats: [AUDIO_FORMAT],
outputAudioFormats: [AUDIO_FORMAT],
supportsBargeIn: true,
handlesInputAudioBargeIn: true,
supportsBargeIn: false,
handlesInputAudioBargeIn: false,
supportsToolCalls: true,
supportsSessionResumption: false,
},