Stream Athena Talk replies incrementally
This commit is contained in:
+43
-10
@@ -48,6 +48,28 @@ function pcmRms(pcm) {
|
||||
}
|
||||
return Math.sqrt(sum / count);
|
||||
}
|
||||
function splitForIncrementalSpeech(text, limit = 80) {
|
||||
const words = text.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
||||
const chunks = [];
|
||||
let current = "";
|
||||
for (const word of words) {
|
||||
const candidate = current ? `${current} ${word}` : word;
|
||||
if (current && candidate.length > limit) {
|
||||
chunks.push(current);
|
||||
current = word;
|
||||
}
|
||||
else {
|
||||
current = candidate;
|
||||
}
|
||||
if (current.length >= 35 && /[.!?](?:["')\]_*]+)?$/.test(word)) {
|
||||
chunks.push(current);
|
||||
current = "";
|
||||
}
|
||||
}
|
||||
if (current)
|
||||
chunks.push(current);
|
||||
return chunks;
|
||||
}
|
||||
function readWavPcm24k(wav) {
|
||||
if (wav.length < 44 || wav.toString("ascii", 0, 4) !== "RIFF") {
|
||||
throw new Error("Athena TTS did not return PCM WAV audio");
|
||||
@@ -226,7 +248,8 @@ class AthenaTalkBridge {
|
||||
const generation = ++this.generation;
|
||||
const responseId = `athena-${randomUUID()}`;
|
||||
try {
|
||||
const result = await this.req.runAgentConsult({ prompt, signal: controller.signal });
|
||||
const voicePrompt = `${prompt}\n\nAntwortregeln für diese Sprachantwort: Antworte ausschließlich auf Deutsch, kurz und direkt. Keine Analyse, keine Meta-Kommentare, kein Markdown und keine Wiederholung der Anfrage. Gib nur den Text aus, der gesprochen werden soll.`;
|
||||
const result = await this.req.runAgentConsult({ prompt: voicePrompt, signal: controller.signal });
|
||||
if (generation !== this.generation || !this.isConnected())
|
||||
return;
|
||||
const text = String(result?.text || "").trim();
|
||||
@@ -234,20 +257,30 @@ class AthenaTalkBridge {
|
||||
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())
|
||||
const speechChunks = splitForIncrementalSpeech(text);
|
||||
if (speechChunks.length === 0)
|
||||
return;
|
||||
const frameBytes = 24000 * 2 / 50;
|
||||
const playbackStartedAt = Date.now();
|
||||
for (let offset = 0; offset < pcm.length; offset += frameBytes) {
|
||||
if (controller.signal.aborted || generation !== this.generation || !this.isConnected()) {
|
||||
let playbackEndsAt = 0;
|
||||
let nextAudio = this.synthesize(speechChunks[0], controller.signal);
|
||||
for (let index = 0; index < speechChunks.length; index += 1) {
|
||||
const pcm = await nextAudio;
|
||||
if (generation !== this.generation || !this.isConnected())
|
||||
return;
|
||||
if (index + 1 < speechChunks.length) {
|
||||
nextAudio = this.synthesize(speechChunks[index + 1], controller.signal);
|
||||
}
|
||||
const audioDurationMs = pcm.length / (24000 * 2) * 1000;
|
||||
playbackEndsAt = Math.max(playbackEndsAt, Date.now()) + audioDurationMs;
|
||||
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)));
|
||||
await new Promise((resolve) => setTimeout(resolve, 18));
|
||||
}
|
||||
this.req.onAudio?.(pcm.subarray(offset, Math.min(offset + frameBytes, pcm.length)));
|
||||
await new Promise((resolve) => setTimeout(resolve, 18));
|
||||
}
|
||||
const audioDurationMs = pcm.length / (24000 * 2) * 1000;
|
||||
const remainingPlaybackMs = Math.max(0, audioDurationMs - (Date.now() - playbackStartedAt));
|
||||
const remainingPlaybackMs = Math.max(0, playbackEndsAt - Date.now());
|
||||
await new Promise((resolve) => setTimeout(resolve, remainingPlaybackMs + 300));
|
||||
this.req.onResponseDone?.({ status: "completed", responseId });
|
||||
this.req.onEvent?.({ direction: "server", type: "response.done", responseId });
|
||||
|
||||
Reference in New Issue
Block a user