Voice debug drawer can select device and asr no duplicate

This commit is contained in:
Xin Wang
2026-02-09 16:47:58 +08:00
parent 38e20052f7
commit ab90b7c7df

View File

@@ -1076,6 +1076,7 @@ export const DebugDrawer: React.FC<{
const micProcessorRef = useRef<ScriptProcessorNode | null>(null); const micProcessorRef = useRef<ScriptProcessorNode | null>(null);
const micGainRef = useRef<GainNode | null>(null); const micGainRef = useRef<GainNode | null>(null);
const userDraftIndexRef = useRef<number | null>(null); const userDraftIndexRef = useRef<number | null>(null);
const lastUserFinalRef = useRef<string>('');
// Initialize // Initialize
useEffect(() => { useEffect(() => {
@@ -1382,6 +1383,7 @@ export const DebugDrawer: React.FC<{
try { try {
setCallStatus('calling'); setCallStatus('calling');
setMessages([]); setMessages([]);
lastUserFinalRef.current = '';
setWsError(''); setWsError('');
closeWs(); closeWs();
if (textTtsEnabled) await ensureAudioContext(); if (textTtsEnabled) await ensureAudioContext();
@@ -1406,6 +1408,7 @@ export const DebugDrawer: React.FC<{
closeWs(); closeWs();
setCallStatus('idle'); setCallStatus('idle');
setMessages([]); setMessages([]);
lastUserFinalRef.current = '';
setIsLoading(false); setIsLoading(false);
}; };
@@ -1450,6 +1453,7 @@ export const DebugDrawer: React.FC<{
setWsError(''); setWsError('');
// Start every text debug run as a fresh session transcript. // Start every text debug run as a fresh session transcript.
setMessages([]); setMessages([]);
lastUserFinalRef.current = '';
assistantDraftIndexRef.current = null; assistantDraftIndexRef.current = null;
// Force a fresh WS session so updated assistant runtime config // Force a fresh WS session so updated assistant runtime config
// (voice/model/provider/speed) is applied on session.start. // (voice/model/provider/speed) is applied on session.start.
@@ -1566,6 +1570,7 @@ export const DebugDrawer: React.FC<{
pendingRejectRef.current = null; pendingRejectRef.current = null;
assistantDraftIndexRef.current = null; assistantDraftIndexRef.current = null;
userDraftIndexRef.current = null; userDraftIndexRef.current = null;
lastUserFinalRef.current = '';
setTextSessionStarted(false); setTextSessionStarted(false);
stopPlaybackImmediately(); stopPlaybackImmediately();
if (isOpen) setWsStatus('disconnected'); if (isOpen) setWsStatus('disconnected');
@@ -1670,7 +1675,8 @@ export const DebugDrawer: React.FC<{
return next; return next;
} }
const next = [...prev]; const next = [...prev];
next[idx] = { ...next[idx], text: next[idx].text + delta }; // ASR interim is typically the latest partial text, not a true text delta.
next[idx] = { ...next[idx], text: delta };
return next; return next;
}); });
return; return;
@@ -1678,15 +1684,37 @@ export const DebugDrawer: React.FC<{
if (type === 'transcript.final') { if (type === 'transcript.final') {
const finalText = String(payload.text || ''); const finalText = String(payload.text || '');
if (!finalText) {
userDraftIndexRef.current = null;
return;
}
if (lastUserFinalRef.current === finalText) {
userDraftIndexRef.current = null;
return;
}
setMessages((prev) => { setMessages((prev) => {
const idx = userDraftIndexRef.current; const idx = userDraftIndexRef.current;
userDraftIndexRef.current = null; userDraftIndexRef.current = null;
if (idx !== null && prev[idx] && prev[idx].role === 'user') { if (idx !== null && prev[idx] && prev[idx].role === 'user') {
const next = [...prev]; const next = [...prev];
next[idx] = { ...next[idx], text: finalText || next[idx].text }; next[idx] = { ...next[idx], text: finalText || next[idx].text };
lastUserFinalRef.current = finalText;
return next; return next;
} }
if (!finalText) return prev; const last = prev[prev.length - 1];
if (last?.role === 'user') {
if (last.text === finalText) {
lastUserFinalRef.current = finalText;
return prev;
}
if (finalText.startsWith(last.text) || last.text.startsWith(finalText)) {
const next = [...prev];
next[next.length - 1] = { ...last, text: finalText };
lastUserFinalRef.current = finalText;
return next;
}
}
lastUserFinalRef.current = finalText;
return [...prev, { role: 'user', text: finalText }]; return [...prev, { role: 'user', text: finalText }];
}); });
return; return;
@@ -1986,6 +2014,17 @@ export const DebugDrawer: React.FC<{
<div className="flex-1 flex flex-col min-h-0 space-y-2"> <div className="flex-1 flex flex-col min-h-0 space-y-2">
{mode === 'voice' ? ( {mode === 'voice' ? (
<div className="flex flex-col h-full min-h-0 animate-in fade-in"> <div className="flex flex-col h-full min-h-0 animate-in fade-in">
<div className="mb-2">
<select
className="w-full text-xs bg-white/5 border border-white/10 rounded px-2 py-1 text-foreground"
value={selectedMic}
onChange={(e) => setSelectedMic(e.target.value)}
>
{devices.filter(d => d.kind === 'audioinput').map(d => (
<option key={d.deviceId} value={d.deviceId}>{d.label || 'Mic'}</option>
))}
</select>
</div>
<div className="h-1/3 min-h-[150px] shrink-0 border border-white/5 rounded-md bg-black/20 flex flex-col items-center justify-center text-muted-foreground space-y-4 mb-2 relative overflow-hidden"> <div className="h-1/3 min-h-[150px] shrink-0 border border-white/5 rounded-md bg-black/20 flex flex-col items-center justify-center text-muted-foreground space-y-4 mb-2 relative overflow-hidden">
<div className="h-24 w-24 rounded-full bg-primary/10 flex items-center justify-center animate-pulse relative z-10"> <div className="h-24 w-24 rounded-full bg-primary/10 flex items-center justify-center animate-pulse relative z-10">
<Mic className="h-10 w-10 text-primary" /> <Mic className="h-10 w-10 text-primary" />