Update frontend debug drawer
This commit is contained in:
@@ -1329,6 +1329,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
onProtocolEvent,
|
onProtocolEvent,
|
||||||
}) => {
|
}) => {
|
||||||
const TARGET_SAMPLE_RATE = 16000;
|
const TARGET_SAMPLE_RATE = 16000;
|
||||||
|
const PCM_FRAME_BYTES = 640; // WS v1 fixed 20ms frame for 16k mono pcm_s16le
|
||||||
const downsampleTo16k = (input: Float32Array, inputSampleRate: number): Float32Array => {
|
const downsampleTo16k = (input: Float32Array, inputSampleRate: number): Float32Array => {
|
||||||
if (inputSampleRate === TARGET_SAMPLE_RATE) return input;
|
if (inputSampleRate === TARGET_SAMPLE_RATE) return input;
|
||||||
if (inputSampleRate < TARGET_SAMPLE_RATE) return input;
|
if (inputSampleRate < TARGET_SAMPLE_RATE) return input;
|
||||||
@@ -1367,6 +1368,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
const [textSessionStarted, setTextSessionStarted] = useState(false);
|
const [textSessionStarted, setTextSessionStarted] = useState(false);
|
||||||
const [wsStatus, setWsStatus] = useState<'disconnected' | 'connecting' | 'ready' | 'error'>('disconnected');
|
const [wsStatus, setWsStatus] = useState<'disconnected' | 'connecting' | 'ready' | 'error'>('disconnected');
|
||||||
const [wsError, setWsError] = useState('');
|
const [wsError, setWsError] = useState('');
|
||||||
|
const wsStatusRef = useRef<'disconnected' | 'connecting' | 'ready' | 'error'>('disconnected');
|
||||||
const [resolvedConfigOpen, setResolvedConfigOpen] = useState(false);
|
const [resolvedConfigOpen, setResolvedConfigOpen] = useState(false);
|
||||||
const [resolvedConfigView, setResolvedConfigView] = useState<string>('');
|
const [resolvedConfigView, setResolvedConfigView] = useState<string>('');
|
||||||
const [captureConfigOpen, setCaptureConfigOpen] = useState(false);
|
const [captureConfigOpen, setCaptureConfigOpen] = useState(false);
|
||||||
@@ -1410,6 +1412,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
const micSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
const micSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
||||||
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 micFrameBufferRef = useRef<Uint8Array>(new Uint8Array(0));
|
||||||
const userDraftIndexRef = useRef<number | null>(null);
|
const userDraftIndexRef = useRef<number | null>(null);
|
||||||
const lastUserFinalRef = useRef<string>('');
|
const lastUserFinalRef = useRef<string>('');
|
||||||
const selectedToolSchemas = useMemo(() => {
|
const selectedToolSchemas = useMemo(() => {
|
||||||
@@ -1461,6 +1464,10 @@ export const DebugDrawer: React.FC<{
|
|||||||
localStorage.setItem('debug_ws_url', wsUrl);
|
localStorage.setItem('debug_ws_url', wsUrl);
|
||||||
}, [wsUrl]);
|
}, [wsUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
wsStatusRef.current = wsStatus;
|
||||||
|
}, [wsStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('debug_audio_aec', aecEnabled ? '1' : '0');
|
localStorage.setItem('debug_audio_aec', aecEnabled ? '1' : '0');
|
||||||
}, [aecEnabled]);
|
}, [aecEnabled]);
|
||||||
@@ -1540,10 +1547,31 @@ export const DebugDrawer: React.FC<{
|
|||||||
void micAudioCtxRef.current.close();
|
void micAudioCtxRef.current.close();
|
||||||
micAudioCtxRef.current = null;
|
micAudioCtxRef.current = null;
|
||||||
}
|
}
|
||||||
|
micFrameBufferRef.current = new Uint8Array(0);
|
||||||
setCaptureConfigView('');
|
setCaptureConfigView('');
|
||||||
stopMedia();
|
stopMedia();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendFramedMicAudio = (pcm16: Int16Array) => {
|
||||||
|
const ws = wsRef.current;
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN || !wsReadyRef.current) return;
|
||||||
|
if (pcm16.byteLength <= 0) return;
|
||||||
|
|
||||||
|
const chunkBytes = new Uint8Array(pcm16.buffer, pcm16.byteOffset, pcm16.byteLength);
|
||||||
|
const pending = micFrameBufferRef.current;
|
||||||
|
const merged = new Uint8Array(pending.length + chunkBytes.length);
|
||||||
|
merged.set(pending, 0);
|
||||||
|
merged.set(chunkBytes, pending.length);
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
while (merged.length - offset >= PCM_FRAME_BYTES) {
|
||||||
|
ws.send(merged.subarray(offset, offset + PCM_FRAME_BYTES));
|
||||||
|
offset += PCM_FRAME_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
micFrameBufferRef.current = offset >= merged.length ? new Uint8Array(0) : merged.slice(offset);
|
||||||
|
};
|
||||||
|
|
||||||
const buildMicConstraints = (): MediaTrackConstraints => ({
|
const buildMicConstraints = (): MediaTrackConstraints => ({
|
||||||
deviceId: selectedMic ? { exact: selectedMic } : undefined,
|
deviceId: selectedMic ? { exact: selectedMic } : undefined,
|
||||||
echoCancellation: aecEnabled,
|
echoCancellation: aecEnabled,
|
||||||
@@ -1596,7 +1624,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
const inChannel = event.inputBuffer.getChannelData(0);
|
const inChannel = event.inputBuffer.getChannelData(0);
|
||||||
const downsampled = downsampleTo16k(inChannel, event.inputBuffer.sampleRate);
|
const downsampled = downsampleTo16k(inChannel, event.inputBuffer.sampleRate);
|
||||||
const pcm16 = float32ToPcm16(downsampled);
|
const pcm16 = float32ToPcm16(downsampled);
|
||||||
wsRef.current.send(pcm16.buffer);
|
sendFramedMicAudio(pcm16);
|
||||||
};
|
};
|
||||||
|
|
||||||
micSourceRef.current = source;
|
micSourceRef.current = source;
|
||||||
@@ -1949,6 +1977,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
assistantDraftIndexRef.current = null;
|
assistantDraftIndexRef.current = null;
|
||||||
userDraftIndexRef.current = null;
|
userDraftIndexRef.current = null;
|
||||||
lastUserFinalRef.current = '';
|
lastUserFinalRef.current = '';
|
||||||
|
micFrameBufferRef.current = new Uint8Array(0);
|
||||||
setTextSessionStarted(false);
|
setTextSessionStarted(false);
|
||||||
stopPlaybackImmediately();
|
stopPlaybackImmediately();
|
||||||
if (isOpen) setWsStatus('disconnected');
|
if (isOpen) setWsStatus('disconnected');
|
||||||
@@ -2096,6 +2125,14 @@ export const DebugDrawer: React.FC<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'config.resolved') {
|
||||||
|
const resolved = payload?.config || payload?.data?.config;
|
||||||
|
if (resolved) {
|
||||||
|
setResolvedConfigView(JSON.stringify({ config: resolved }, null, 2));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'input.speech_started') {
|
if (type === 'input.speech_started') {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
return;
|
return;
|
||||||
@@ -2225,7 +2262,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'error') {
|
if (type === 'error') {
|
||||||
const message = String(payload.message || 'Unknown error');
|
const message = String(payload?.message || payload?.data?.error?.message || 'Unknown error');
|
||||||
setWsStatus('error');
|
setWsStatus('error');
|
||||||
setWsError(message);
|
setWsError(message);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -2251,7 +2288,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
setTextSessionStarted(false);
|
setTextSessionStarted(false);
|
||||||
userDraftIndexRef.current = null;
|
userDraftIndexRef.current = null;
|
||||||
stopPlaybackImmediately();
|
stopPlaybackImmediately();
|
||||||
if (wsStatus !== 'error') setWsStatus('disconnected');
|
if (wsStatusRef.current !== 'error') setWsStatus('disconnected');
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user