update calling tts after chatbot response
Browse files- src/app/components/Main.tsx +19 -48
src/app/components/Main.tsx
CHANGED
|
@@ -11,8 +11,8 @@ import {
|
|
| 11 |
streamChat,
|
| 12 |
type ChatSource,
|
| 13 |
} from "../../services/api";
|
| 14 |
-
import {
|
| 15 |
-
import {
|
| 16 |
import ChatLayout from "./chat/ChatLayout";
|
| 17 |
import Sidebar from "./chat/Sidebar";
|
| 18 |
import ChatWindow from "./chat/ChatWindow";
|
|
@@ -401,59 +401,30 @@ export default function Main() {
|
|
| 401 |
}
|
| 402 |
}, [user, ensureRoom]);
|
| 403 |
|
|
|
|
|
|
|
| 404 |
const playTtsAudio = useCallback(async (
|
| 405 |
ttsText: string,
|
| 406 |
onStarted?: () => void,
|
| 407 |
): Promise<{ chunks: ArrayBuffer[]; sampleRate: number }> => {
|
| 408 |
-
|
|
|
|
|
|
|
| 409 |
try {
|
| 410 |
-
const {
|
| 411 |
-
const
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
audioStartedAt = Date.now();
|
| 422 |
-
onStarted?.();
|
| 423 |
-
}
|
| 424 |
-
};
|
| 425 |
-
while (true) {
|
| 426 |
-
const { done, value } = await reader.read();
|
| 427 |
-
if (done) break;
|
| 428 |
-
if (value && value.byteLength > 0) {
|
| 429 |
-
const pcm = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer;
|
| 430 |
-
chunks.push(pcm);
|
| 431 |
-
player.enqueue(pcm, notifyStarted);
|
| 432 |
-
}
|
| 433 |
-
}
|
| 434 |
-
// Play any remaining buffered audio that didn't reach the threshold (short responses)
|
| 435 |
-
player.flush(notifyStarted);
|
| 436 |
-
const totalBytes = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
| 437 |
-
const durationMs = (totalBytes / 2 / sampleRate) * 1000;
|
| 438 |
-
// Wait until audio actually starts, then wait for the full playback duration.
|
| 439 |
-
// Polling in 10ms steps avoids a fixed large buffer while staying responsive.
|
| 440 |
-
await new Promise<void>((resolve) => {
|
| 441 |
-
const check = () => {
|
| 442 |
-
if (audioStartedAt !== null) {
|
| 443 |
-
const elapsed = Date.now() - audioStartedAt;
|
| 444 |
-
const remaining = Math.max(0, durationMs - elapsed) + 150;
|
| 445 |
-
setTimeout(resolve, remaining);
|
| 446 |
-
} else {
|
| 447 |
-
setTimeout(check, 10);
|
| 448 |
-
}
|
| 449 |
-
};
|
| 450 |
-
check();
|
| 451 |
-
});
|
| 452 |
-
player.stopImmediately();
|
| 453 |
-
return { chunks, sampleRate };
|
| 454 |
} catch {
|
| 455 |
// TTS failure is non-fatal
|
| 456 |
-
return { chunks, sampleRate: 24000 };
|
| 457 |
}
|
| 458 |
}, []);
|
| 459 |
|
|
|
|
| 11 |
streamChat,
|
| 12 |
type ChatSource,
|
| 13 |
} from "../../services/api";
|
| 14 |
+
import { textToSpeech } from "../../services/voiceApi";
|
| 15 |
+
import { replayAudio } from "../../audio/AudioPlayer";
|
| 16 |
import ChatLayout from "./chat/ChatLayout";
|
| 17 |
import Sidebar from "./chat/Sidebar";
|
| 18 |
import ChatWindow from "./chat/ChatWindow";
|
|
|
|
| 401 |
}
|
| 402 |
}, [user, ensureRoom]);
|
| 403 |
|
| 404 |
+
const cancelTtsRef = useRef<(() => void) | null>(null);
|
| 405 |
+
|
| 406 |
const playTtsAudio = useCallback(async (
|
| 407 |
ttsText: string,
|
| 408 |
onStarted?: () => void,
|
| 409 |
): Promise<{ chunks: ArrayBuffer[]; sampleRate: number }> => {
|
| 410 |
+
// Stop any in-flight TTS before starting a new one (e.g. rapid re-query).
|
| 411 |
+
cancelTtsRef.current?.();
|
| 412 |
+
cancelTtsRef.current = null;
|
| 413 |
try {
|
| 414 |
+
const { pcm, sampleRate } = await textToSpeech(ttsText);
|
| 415 |
+
const durationMs = (pcm.byteLength / 2 / sampleRate) * 1000;
|
| 416 |
+
// Use the same proven playback path as the speaker button: schedule the
|
| 417 |
+
// full response at once via replayAudio, avoiding all streaming complexity.
|
| 418 |
+
const cancel = replayAudio([pcm], sampleRate);
|
| 419 |
+
cancelTtsRef.current = cancel;
|
| 420 |
+
onStarted?.();
|
| 421 |
+
await new Promise<void>((resolve) => setTimeout(resolve, durationMs + 150));
|
| 422 |
+
cancel();
|
| 423 |
+
cancelTtsRef.current = null;
|
| 424 |
+
return { chunks: [pcm], sampleRate };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
} catch {
|
| 426 |
// TTS failure is non-fatal
|
| 427 |
+
return { chunks: [], sampleRate: 24000 };
|
| 428 |
}
|
| 429 |
}, []);
|
| 430 |
|