Smooth Talk playback and gate audio tail
This commit is contained in:
+6
-2
@@ -237,14 +237,18 @@ class AthenaTalkBridge {
|
|||||||
const pcm = await this.synthesize(text, controller.signal);
|
const pcm = await this.synthesize(text, controller.signal);
|
||||||
if (generation !== this.generation || !this.isConnected())
|
if (generation !== this.generation || !this.isConnected())
|
||||||
return;
|
return;
|
||||||
const frameBytes = 24000 * 2 / 10;
|
const frameBytes = 24000 * 2 / 50;
|
||||||
|
const playbackStartedAt = Date.now();
|
||||||
for (let offset = 0; offset < pcm.length; offset += frameBytes) {
|
for (let offset = 0; offset < pcm.length; offset += frameBytes) {
|
||||||
if (controller.signal.aborted || generation !== this.generation || !this.isConnected()) {
|
if (controller.signal.aborted || generation !== this.generation || !this.isConnected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.req.onAudio?.(pcm.subarray(offset, Math.min(offset + frameBytes, pcm.length)));
|
this.req.onAudio?.(pcm.subarray(offset, Math.min(offset + frameBytes, pcm.length)));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 80));
|
await new Promise((resolve) => setTimeout(resolve, 18));
|
||||||
}
|
}
|
||||||
|
const audioDurationMs = pcm.length / (24000 * 2) * 1000;
|
||||||
|
const remainingPlaybackMs = Math.max(0, audioDurationMs - (Date.now() - playbackStartedAt));
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, remainingPlaybackMs + 300));
|
||||||
this.req.onResponseDone?.({ status: "completed", responseId });
|
this.req.onResponseDone?.({ status: "completed", responseId });
|
||||||
this.req.onEvent?.({ direction: "server", type: "response.done", responseId });
|
this.req.onEvent?.({ direction: "server", type: "response.done", responseId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,18 +246,26 @@ class AthenaTalkBridge {
|
|||||||
this.req.onTranscript?.("assistant", text, true);
|
this.req.onTranscript?.("assistant", text, true);
|
||||||
const pcm = await this.synthesize(text, controller.signal);
|
const pcm = await this.synthesize(text, controller.signal);
|
||||||
if (generation !== this.generation || !this.isConnected()) return;
|
if (generation !== this.generation || !this.isConnected()) return;
|
||||||
// Feed the relay 100 ms batches slightly faster than realtime. OpenClaw
|
// Feed OpenClaw in its native 20 ms frame size. Sending larger bursts
|
||||||
// splits these into its native 20 ms frames. The small lead gives the
|
// made the relay split several frames at once; slow-client protection
|
||||||
// client a jitter buffer and avoids the crackling caused by timer and
|
// could then drop individual frames and produce audible holes.
|
||||||
// WebSocket scheduling gaps at exact 20 ms pacing.
|
const frameBytes = 24000 * 2 / 50;
|
||||||
const frameBytes = 24000 * 2 / 10;
|
const playbackStartedAt = Date.now();
|
||||||
for (let offset = 0; offset < pcm.length; offset += frameBytes) {
|
for (let offset = 0; offset < pcm.length; offset += frameBytes) {
|
||||||
if (controller.signal.aborted || generation !== this.generation || !this.isConnected()) {
|
if (controller.signal.aborted || generation !== this.generation || !this.isConnected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.req.onAudio?.(pcm.subarray(offset, Math.min(offset + frameBytes, pcm.length)));
|
this.req.onAudio?.(pcm.subarray(offset, Math.min(offset + frameBytes, pcm.length)));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 80));
|
// Run just ahead of realtime so the client builds a small jitter
|
||||||
|
// buffer without flooding the WebSocket queue.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 18));
|
||||||
}
|
}
|
||||||
|
// Transmission is intentionally a little faster than playback. Keep the
|
||||||
|
// microphone gated until the client has consumed that buffered tail;
|
||||||
|
// otherwise the last spoken words are transcribed again as user input.
|
||||||
|
const audioDurationMs = pcm.length / (24000 * 2) * 1000;
|
||||||
|
const remainingPlaybackMs = Math.max(0, audioDurationMs - (Date.now() - playbackStartedAt));
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, remainingPlaybackMs + 300));
|
||||||
this.req.onResponseDone?.({ status: "completed", responseId });
|
this.req.onResponseDone?.({ status: "completed", responseId });
|
||||||
this.req.onEvent?.({ direction: "server", type: "response.done", responseId });
|
this.req.onEvent?.({ direction: "server", type: "response.done", responseId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "athena-talk",
|
"id": "athena-talk",
|
||||||
"name": "Athena Local Talk",
|
"name": "Athena Local Talk",
|
||||||
"description": "Local German voice conversations through Athena without a public speech provider.",
|
"description": "Local German voice conversations through Athena without a public speech provider.",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"enabledByDefault": true,
|
"enabledByDefault": true,
|
||||||
"activation": {
|
"activation": {
|
||||||
"onStartup": true,
|
"onStartup": true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "openclaw-plugin-athena-talk",
|
"name": "openclaw-plugin-athena-talk",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"description": "Private realtime Talk bridge for Athena STT, OpenClaw agent consult, and Athena TTS.",
|
"description": "Private realtime Talk bridge for Athena STT, OpenClaw agent consult, and Athena TTS.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user