-
Notifications
You must be signed in to change notification settings - Fork 17
Add mic-inactivity detection UX and related state fixes #230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2c2afd5
f68f257
55a6100
012e619
c057765
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,8 @@ export class WhisperRecognizer implements Recognizer { | |
| private num_threads: number; | ||
|
|
||
| private transcribed_callback: ((newFinalBlocks: Array<TranscriptBlock>, newInProgressBlock: TranscriptBlock) => void) | null = null; | ||
| private lastAudioTimestamp: number | null = null; | ||
| private inactivityInterval: any = null; | ||
|
|
||
| /** | ||
| * Creates an Whisper recognizer instance that listens to the default microphone | ||
|
|
@@ -135,6 +137,20 @@ export class WhisperRecognizer implements Recognizer { | |
| pcm_data = Float32Concat(last_suffix, pcm_data); | ||
| last_suffix = pcm_data.slice(-(pcm_data.length % 128)) | ||
|
|
||
| // update last audio timestamp and mark that we've received at least one audio chunk | ||
| this.lastAudioTimestamp = Date.now(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Date.now() is subject to the user's device time changing (not monotonic). Perhaps performance.now() would make sense instead? |
||
| try { (window as any).__lastAudioTimestamp = this.lastAudioTimestamp; } catch (e) {} | ||
| try { (window as any).__hasReceivedAudio = true; if ((window as any).__initialAudioTimer) { clearTimeout((window as any).__initialAudioTimer); (window as any).__initialAudioTimer = null; } } catch (e) {} | ||
| try { | ||
| const { store } = require('../../../store'); | ||
| const controlState = (store.getState() as any).ControlReducer; | ||
| if (controlState?.micNoAudio === true) { | ||
| store.dispatch({ type: 'SET_MIC_INACTIVITY', payload: false }); | ||
| } | ||
| } catch (e) { | ||
| console.warn('Failed to clear mic inactivity (whisper)', e); | ||
| } | ||
|
|
||
| // Feed process_recorder_message audio in 128 sample chunks | ||
| for (let i = 0; i < pcm_data.length - 127; i+= 128) { | ||
| const audio_chunk = pcm_data.subarray(i, i + 128) | ||
|
|
@@ -149,6 +165,29 @@ export class WhisperRecognizer implements Recognizer { | |
|
|
||
| this.recorder.startRecording(); | ||
| console.log("Whisper: Done setting up audio context"); | ||
|
|
||
| const thresholdMs = 3000; | ||
| if (this.inactivityInterval == null) { | ||
| const { store } = require('../../../store'); | ||
| this.inactivityInterval = setInterval(() => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels to me like a timeout would be more natural here rather than an interval. e.g. Set timeout of Semantically, |
||
| try { | ||
| const state: any = store.getState(); | ||
| const listening = state.ControlReducer?.listening === true; | ||
| const micNoAudio = state.ControlReducer?.micNoAudio === true; | ||
| if (listening) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this checked before showing the snackbar? Is it necessary to fetch state from redux here? We generally want to limit the use of getState() outside of redux. |
||
| if (!this.lastAudioTimestamp || (Date.now() - this.lastAudioTimestamp > thresholdMs)) { | ||
| if (!micNoAudio) store.dispatch({ type: 'SET_MIC_INACTIVITY', payload: true }); | ||
| } else { | ||
| if (micNoAudio) store.dispatch({ type: 'SET_MIC_INACTIVITY', payload: false }); | ||
| } | ||
| } else { | ||
| if (micNoAudio) store.dispatch({ type: 'SET_MIC_INACTIVITY', payload: false }); | ||
| } | ||
| } catch (e) { | ||
| console.warn('Error in whisper mic inactivity interval', e); | ||
| } | ||
| }, 1000); | ||
| } | ||
| } | ||
|
|
||
| private async load_model(model: string) { | ||
|
|
@@ -257,6 +296,17 @@ export class WhisperRecognizer implements Recognizer { | |
| this.whisper.set_status("paused"); | ||
| this.context.suspend(); | ||
| this.recorder?.stopRecording(); | ||
|
|
||
| if (this.inactivityInterval) { | ||
| clearInterval(this.inactivityInterval); | ||
| this.inactivityInterval = null; | ||
| } | ||
| try { | ||
| const { store } = require('../../../store'); | ||
| store.dispatch({ type: 'SET_MIC_INACTIVITY', payload: false }); | ||
| } catch (e) { | ||
| console.warn('Failed to clear mic inactivity on whisper stop', e); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,4 +16,5 @@ export type ControlStatus = { | |
| showMFCC: boolean | ||
| showSpeaker: boolean | ||
| showIntent: boolean | ||
| micNoAudio?: boolean | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hesitate to pollute to
windownamespace with additional props. I don't think it is necessary to do so in order to implement mic activity.It seems to me that the logic for setting the
micNoAudioflag can be fully encapsulated within the recognizers themselves. This portion here would simply be fetchinglisteningandmicNoAudiofrom redux and showing/hiding the snackbar if both are true inuseEffect. Am I missing something?