Skip to content

Commit ed49a6b

Browse files
committed
fix(mockingbird): split voice timeout into capture and AI phases to prevent false errors
1 parent 1914560 commit ed49a6b

File tree

1 file changed

+54
-18
lines changed

1 file changed

+54
-18
lines changed

src/mockingbird/ui/stores/VoiceStore.js

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
PLAY_INTENT,
1313
} from "../components/Listening/VoiceConfirmationIntents";
1414

15-
const RESPONSE_TIMEOUT = 15000;
15+
const CAPTURE_TIMEOUT = 10000;
16+
const AI_TIMEOUT = 30000;
1617
const TIMEOUT_BEFORE_CLOSING_LISTENING_MS = 7500;
1718
const OVERLAY_TRANSITION_DURATION_MS = 300;
1819

@@ -37,7 +38,8 @@ class VoiceStore {
3738
isMicMuted = localStorage.getItem("mockingbird_mic_muted") === "true";
3839
microphoneLevelsSlidingWindow = [];
3940
_wsCleanup = null;
40-
_responseTimeoutId = null;
41+
_captureTimeoutId = null;
42+
_aiTimeoutId = null;
4143
_closeTimeoutId = null;
4244
_micLevelIntervalId = null;
4345

@@ -46,7 +48,8 @@ class VoiceStore {
4648
makeAutoObservable(this, {
4749
rootStore: false,
4850
_wsCleanup: false,
49-
_responseTimeoutId: false,
51+
_captureTimeoutId: false,
52+
_aiTimeoutId: false,
5053
_closeTimeoutId: false,
5154
_micLevelIntervalId: false,
5255
});
@@ -110,28 +113,36 @@ class VoiceStore {
110113
if (this.isMicMuted) return;
111114
this.resetVoiceSessionState();
112115
overlayController.showVoice();
113-
this._startResponseTimeout();
116+
this._startCaptureTimeout();
114117
});
115118

116119
onTranscription = action((data) => {
117120
this.state.asr.transcript = data.transcript || "";
118121
this.state.asr.isFinal = !!data.is_final;
119122
if (data.is_final) {
120123
this.micLevelMovingAverage = 0;
124+
this._clearCaptureTimeout();
125+
this._startAITimeout();
126+
} else {
127+
this._startCaptureTimeout();
121128
}
122129
});
123130

124131
onAIState = action((data) => {
125132
const prevState = this.state.aiState;
126133
this.state.aiState = data.state || "idle";
134+
if (data.state === "thinking" || data.state === "executing_tool") {
135+
this._clearCaptureTimeout();
136+
this._startAITimeout();
137+
}
127138
if (data.state === "idle" && prevState === "speaking") {
128139
this._scheduleClose();
129140
}
130141
});
131142

132143
onAIResponse = action((data) => {
133144
this.state.aiResponse = data.text || "";
134-
this._clearResponseTimeout();
145+
this._clearAITimeout();
135146
this.micLevelMovingAverage = 0;
136147
});
137148

@@ -151,6 +162,7 @@ class VoiceStore {
151162
this.state.showingVoiceConfirmation = true;
152163
this.state.aiResponse = "";
153164
}
165+
this._clearAITimeout();
154166
});
155167

156168
_onMicLevel = action((data) => {
@@ -162,16 +174,18 @@ class VoiceStore {
162174
this.state.friendlyError = "";
163175
this.state.asr.transcript = "";
164176
this.state.asr.isFinal = false;
165-
this._clearResponseTimeout();
177+
this._clearCaptureTimeout();
178+
this._clearAITimeout();
166179
this._clearCloseTimeout();
167-
this._startResponseTimeout();
180+
this._startCaptureTimeout();
168181
sendNocturneWsRequest("audio.record.start", {});
169182
});
170183

171184
cancel = action(() => {
172185
sendNocturneWsRequest("audio.record.stop", {});
173186
this.rootStore.overlayController.hideVoice();
174-
this._clearResponseTimeout();
187+
this._clearCaptureTimeout();
188+
this._clearAITimeout();
175189
this._clearCloseTimeout();
176190
this._stopSyntheticMicLevel();
177191
setTimeout(
@@ -194,29 +208,50 @@ class VoiceStore {
194208

195209
resetVoiceSessionState = action(() => {
196210
this.state = getInitialVoiceSessionState();
197-
this._clearResponseTimeout();
211+
this._clearCaptureTimeout();
212+
this._clearAITimeout();
198213
this._clearCloseTimeout();
199214
this._stopSyntheticMicLevel();
200215
this.micLevelMovingAverage = 0;
201216
});
202217

203-
_startResponseTimeout() {
204-
this._clearResponseTimeout();
205-
this._responseTimeoutId = setTimeout(
218+
_startCaptureTimeout() {
219+
this._clearCaptureTimeout();
220+
this._captureTimeoutId = setTimeout(
221+
action(() => {
222+
this.state.error = "error";
223+
this.state.friendlyError = "Something went wrong. Tap to try again.";
224+
this._stopSyntheticMicLevel();
225+
this._scheduleClose();
226+
}),
227+
CAPTURE_TIMEOUT,
228+
);
229+
}
230+
231+
_clearCaptureTimeout() {
232+
if (this._captureTimeoutId) {
233+
clearTimeout(this._captureTimeoutId);
234+
this._captureTimeoutId = null;
235+
}
236+
}
237+
238+
_startAITimeout() {
239+
this._clearAITimeout();
240+
this._aiTimeoutId = setTimeout(
206241
action(() => {
207242
this.state.error = "error";
208243
this.state.friendlyError = "Something went wrong. Tap to try again.";
209244
this._stopSyntheticMicLevel();
210245
this._scheduleClose();
211246
}),
212-
RESPONSE_TIMEOUT,
247+
AI_TIMEOUT,
213248
);
214249
}
215250

216-
_clearResponseTimeout() {
217-
if (this._responseTimeoutId) {
218-
clearTimeout(this._responseTimeoutId);
219-
this._responseTimeoutId = null;
251+
_clearAITimeout() {
252+
if (this._aiTimeoutId) {
253+
clearTimeout(this._aiTimeoutId);
254+
this._aiTimeoutId = null;
220255
}
221256
}
222257

@@ -266,7 +301,8 @@ class VoiceStore {
266301
this._wsCleanup();
267302
this._wsCleanup = null;
268303
}
269-
this._clearResponseTimeout();
304+
this._clearCaptureTimeout();
305+
this._clearAITimeout();
270306
this._clearCloseTimeout();
271307
this._stopSyntheticMicLevel();
272308
}

0 commit comments

Comments
 (0)