mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(dictation): guard cross-composer cancel, stop engine on unmount, await flush before stop
Addresses 3 new review comments:
1. **Avoid cancelling another composer's dictation** — the draftKey effect
now checks isRecordingRef before calling cancelRecording(), so switching
channels in one composer won't kill a recording in another.
2. **Stop native dictation on unmount** — cleanup() now calls
invoke('stop_dictation') so the native SttEngine doesn't linger when
the hook unmounts mid-recording (navigation, composer close).
3. **Await final audio flush before stopping STT** — stopRecording() and
cleanup() now await the flushAudioBatch() promise before invoking
stop_dictation, ensuring the last batch of audio arrives at the native
engine before it shuts down and flushes its speech buffer.
This commit is contained in:
@@ -89,9 +89,15 @@ export function useComposerDictation({
|
||||
|
||||
// Cancel dictation when the channel/thread changes so that transcript events
|
||||
// from a stale local STT session don't leak into the wrong draft.
|
||||
// Only cancel if this instance is actually recording — avoids killing another
|
||||
// composer's session since the native engine is a singleton.
|
||||
const isRecordingRef = useRef(false);
|
||||
isRecordingRef.current = dictation.isRecording || dictation.isStarting;
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: draftKey is the sole trigger
|
||||
useEffect(() => {
|
||||
dictation.cancelRecording();
|
||||
if (isRecordingRef.current) {
|
||||
dictation.cancelRecording();
|
||||
}
|
||||
}, [draftKey]);
|
||||
|
||||
// Auto-cancel dictation when the composer becomes disabled mid-recording
|
||||
|
||||
@@ -111,10 +111,11 @@ export function useLocalDictation({
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** Flush accumulated audio batch to the native STT engine. */
|
||||
const flushAudioBatch = useCallback(() => {
|
||||
/** Flush accumulated audio batch to the native STT engine. Returns a promise
|
||||
* that resolves once the IPC call completes (or immediately if nothing to flush). */
|
||||
const flushAudioBatch = useCallback((): Promise<void> => {
|
||||
const batch = audioBatchRef.current;
|
||||
if (batch.length === 0) return;
|
||||
if (batch.length === 0) return Promise.resolve();
|
||||
|
||||
// Calculate total byte length and merge into a single buffer.
|
||||
let totalSamples = 0;
|
||||
@@ -134,12 +135,16 @@ export function useLocalDictation({
|
||||
merged.byteOffset,
|
||||
merged.byteLength,
|
||||
);
|
||||
invokeRawBinary("push_dictation_audio", bytes).catch(() => {});
|
||||
return invokeRawBinary("push_dictation_audio", bytes)
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
// Flush any remaining audio before teardown.
|
||||
flushAudioBatch();
|
||||
// Flush any remaining audio before teardown, then stop the native engine.
|
||||
void flushAudioBatch().then(() => {
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
});
|
||||
// Stop batch timer.
|
||||
if (batchTimerRef.current) {
|
||||
clearInterval(batchTimerRef.current);
|
||||
@@ -307,16 +312,12 @@ export function useLocalDictation({
|
||||
}, [cleanup, flushAudioBatch, isEnabled, isRecording, isStarting]);
|
||||
|
||||
const stopRecording = useCallback(() => {
|
||||
// Flush remaining audio so the native engine can transcribe the tail.
|
||||
flushAudioBatch();
|
||||
// Stop batch timer.
|
||||
// Stop batch timer immediately.
|
||||
if (batchTimerRef.current) {
|
||||
clearInterval(batchTimerRef.current);
|
||||
batchTimerRef.current = null;
|
||||
}
|
||||
// Stop mic and audio pipeline immediately so the user gets visual feedback,
|
||||
// but keep isTranscribing=true until the native `stopped` event arrives
|
||||
// (which fires only after the final transcript has been forwarded).
|
||||
// Stop mic and audio pipeline immediately so the user gets visual feedback.
|
||||
if (streamRef.current) {
|
||||
for (const track of streamRef.current.getTracks()) {
|
||||
track.stop();
|
||||
@@ -331,9 +332,13 @@ export function useLocalDictation({
|
||||
void audioContextRef.current.close();
|
||||
audioContextRef.current = null;
|
||||
}
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
setIsRecording(false);
|
||||
// Flush remaining audio and THEN stop the native engine, ensuring the
|
||||
// final batch arrives before the engine shuts down and flushes its buffer.
|
||||
// isTranscribing stays true — cleared when `dictation-state: stopped` arrives.
|
||||
void flushAudioBatch().then(() => {
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
});
|
||||
}, [flushAudioBatch]);
|
||||
|
||||
const cancelRecording = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user