텍스트 응답 음성 재생 연결
This commit is contained in:
parent
64e06a1185
commit
d80e33da5e
9 changed files with 524 additions and 16 deletions
|
|
@ -886,6 +886,8 @@ export default function Session() {
|
|||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const ttsChunksRef = useRef<BlobPart[]>([]);
|
||||
const ttsPlaybackCleanupRef = useRef<(() => void) | null>(null);
|
||||
const ttsPlaybackRequestRef = useRef(0);
|
||||
const playTtsAudioRef = useRef<(() => Promise<void>) | null>(null);
|
||||
const pendingVoiceLearnerIdRef = useRef<number | null>(null);
|
||||
const pendingVoiceLearnerTextRef = useRef<string>("");
|
||||
const coachEvidenceCloseRef = useRef<HTMLButtonElement>(null);
|
||||
|
|
@ -930,6 +932,7 @@ export default function Session() {
|
|||
}, [ensureVoiceAudioContext]);
|
||||
|
||||
const stopTtsPlayback = useCallback(() => {
|
||||
ttsPlaybackRequestRef.current += 1;
|
||||
const cleanup = ttsPlaybackCleanupRef.current;
|
||||
ttsPlaybackCleanupRef.current = null;
|
||||
if (cleanup) cleanup();
|
||||
|
|
@ -1405,6 +1408,30 @@ export default function Session() {
|
|||
}
|
||||
}, [acceptConsent, consentChecked, pushSignal]);
|
||||
|
||||
const speakTextClientTurn = useCallback(
|
||||
async (sessionId: string, turnSeq: number) => {
|
||||
const requestId = ttsPlaybackRequestRef.current + 1;
|
||||
ttsPlaybackRequestRef.current = requestId;
|
||||
setVoiceStatus("speaking");
|
||||
setVoiceDetail("OpenAI 음성을 생성하고 있습니다.");
|
||||
try {
|
||||
const audio = await sessionApi.speakClientTurn(sessionId, turnSeq);
|
||||
if (ttsPlaybackRequestRef.current !== requestId) return;
|
||||
ttsChunksRef.current = [audio];
|
||||
const play = playTtsAudioRef.current;
|
||||
if (!play) throw new Error("voice playback is not ready");
|
||||
await play();
|
||||
} catch {
|
||||
if (ttsPlaybackRequestRef.current !== requestId) return;
|
||||
setAvatarState("listening");
|
||||
setVoiceStatus("degraded");
|
||||
setVoiceDetail("OpenAI 음성을 재생하지 못했습니다. 자막 응답은 화면에 남겼습니다.");
|
||||
pushSignal("warn", "AI 음성 재생 실패");
|
||||
}
|
||||
},
|
||||
[pushSignal],
|
||||
);
|
||||
|
||||
const appendServerClientReply = useCallback(
|
||||
(replyText: string | null, at = elapsed, options?: { suppressEmptyWarning?: boolean }) => {
|
||||
setClientReplyPending(false);
|
||||
|
|
@ -1450,6 +1477,8 @@ export default function Session() {
|
|||
setClientReplyPending(true);
|
||||
setAvatarState("thinking");
|
||||
setTurnError(null);
|
||||
stopTtsPlayback();
|
||||
void primeVoicePlayback();
|
||||
|
||||
let clientId: number | null = null;
|
||||
let clientReply = "";
|
||||
|
|
@ -1539,6 +1568,9 @@ export default function Session() {
|
|||
}
|
||||
setAvatarState("listening");
|
||||
if (!conversationStopped && !qualityRetryable) {
|
||||
if (clientReply && typeof done.turn_seq === "number") {
|
||||
void speakTextClientTurn(liveSessionId, done.turn_seq);
|
||||
}
|
||||
void requestLiveCoach({
|
||||
learnerText: text,
|
||||
clientReply,
|
||||
|
|
@ -1580,7 +1612,10 @@ export default function Session() {
|
|||
liveSessionId,
|
||||
elapsed,
|
||||
pushSignal,
|
||||
primeVoicePlayback,
|
||||
requestLiveCoach,
|
||||
speakTextClientTurn,
|
||||
stopTtsPlayback,
|
||||
]);
|
||||
|
||||
const onComposeKey = (e: ReactKeyboardEvent<HTMLTextAreaElement>) => {
|
||||
|
|
@ -1752,6 +1787,15 @@ export default function Session() {
|
|||
}
|
||||
}, [clientName, closeVoiceSocket, ensureVoiceAudioContext, stopTtsPlayback]);
|
||||
|
||||
useEffect(() => {
|
||||
playTtsAudioRef.current = playTtsAudio;
|
||||
return () => {
|
||||
if (playTtsAudioRef.current === playTtsAudio) {
|
||||
playTtsAudioRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [playTtsAudio]);
|
||||
|
||||
const startVoiceCapture = useCallback(async () => {
|
||||
if (!liveSessionId || paused || sending || sessionEnded) return;
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue