Adds an option to transcribe server STT audio when stopping voice input. (#1219)
* feat(voice): add transcribe-on-stop recognition option * fix(voice): tighten transcribe-on-stop cleanup * fix(voice): address transcribe-on-stop review feedback * fix(voice): scope transcribe-on-stop to server STT --------- Co-authored-by: Konstantin Zolin <kzolin@alfabank.ru> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Konstantin Zolin
Bohdan Triapitsyn
parent
9347f97a98
commit
c1948fe691
@@ -59,6 +59,7 @@ class AudioStreamService {
|
||||
private silenceSince: number | null = null;
|
||||
private onResult: SpeechResultCallback | null = null;
|
||||
private onError: ErrorCallback | null = null;
|
||||
private finishResolver: (() => void) | null = null;
|
||||
private lang = 'en';
|
||||
|
||||
// Configurable parameters
|
||||
@@ -125,16 +126,29 @@ class AudioStreamService {
|
||||
|
||||
/** Stop listening and clean up all resources. */
|
||||
stopListening(): void {
|
||||
this.isActive = false;
|
||||
this._stopVAD();
|
||||
this._stopRecorder();
|
||||
this._teardownAudioContext();
|
||||
this._releaseStream();
|
||||
this.chunks = [];
|
||||
this._cleanupAfterStop(true);
|
||||
}
|
||||
|
||||
async finishListening(): Promise<void> {
|
||||
if (!this.isActive) return;
|
||||
|
||||
this._stopVAD();
|
||||
this.isSpeaking = false;
|
||||
this.silenceSince = null;
|
||||
this.onResult = null;
|
||||
this.onError = null;
|
||||
|
||||
if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive') {
|
||||
this._cleanupAfterStop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
this.finishResolver = resolve;
|
||||
this._finaliseUtterance(false);
|
||||
});
|
||||
|
||||
this._cleanupAfterStop(true);
|
||||
}
|
||||
|
||||
/** Whether currently listening. */
|
||||
@@ -186,11 +200,18 @@ class AudioStreamService {
|
||||
this.mediaRecorder.onstop = () => {
|
||||
const blobs = this.chunks.splice(0);
|
||||
const durationMs = Date.now() - this.recordingStartMs;
|
||||
if (blobs.length === 0 || durationMs < MIN_UTTERANCE_MS) return;
|
||||
if (blobs.length === 0 || durationMs < MIN_UTTERANCE_MS) {
|
||||
this.finishResolver?.();
|
||||
this.finishResolver = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const mType = blobs[0].type || mimeType || 'audio/webm';
|
||||
const blob = new Blob(blobs, { type: mType });
|
||||
void this._upload(blob, mType);
|
||||
void this._upload(blob, mType).finally(() => {
|
||||
this.finishResolver?.();
|
||||
this.finishResolver = null;
|
||||
});
|
||||
};
|
||||
|
||||
// Collect data every 250 ms so we don't lose the tail on stop()
|
||||
@@ -202,6 +223,8 @@ class AudioStreamService {
|
||||
try {
|
||||
this.mediaRecorder.stop();
|
||||
} catch {
|
||||
this.finishResolver?.();
|
||||
this.finishResolver = null;
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
@@ -244,7 +267,7 @@ class AudioStreamService {
|
||||
// End of utterance — stop recorder (triggers onstop → upload)
|
||||
this.isSpeaking = false;
|
||||
this.silenceSince = null;
|
||||
this._finaliseUtterance();
|
||||
this._finaliseUtterance(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,12 +281,31 @@ class AudioStreamService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop the current recorder to flush the utterance, then restart it. */
|
||||
private _finaliseUtterance(): void {
|
||||
private _cleanupAfterStop(clearChunks: boolean): void {
|
||||
const pendingResolver = this.finishResolver;
|
||||
this.isActive = false;
|
||||
this.finishResolver = null;
|
||||
this.mediaRecorder = null;
|
||||
this._teardownAudioContext();
|
||||
this._releaseStream();
|
||||
if (clearChunks) {
|
||||
this.chunks = [];
|
||||
}
|
||||
this.isSpeaking = false;
|
||||
this.silenceSince = null;
|
||||
this.onResult = null;
|
||||
this.onError = null;
|
||||
pendingResolver?.();
|
||||
}
|
||||
|
||||
/** Stop the current recorder to flush the utterance, optionally restarting for the next one. */
|
||||
private _finaliseUtterance(restart: boolean): void {
|
||||
if (!this.isActive) return;
|
||||
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
|
||||
this.mediaRecorder.stop();
|
||||
}
|
||||
if (!restart) return;
|
||||
|
||||
// Restart recorder for the next utterance after a short delay
|
||||
// (MediaRecorder.onstop fires asynchronously; we wait for it to complete)
|
||||
setTimeout(() => {
|
||||
|
||||
Reference in New Issue
Block a user