feat(voice): live dictation with browser pause restart and server audio chunking

This commit is contained in:
sandrodach committed 2026-10-30 09:27:30 +01:00
1 parent 446f8e4649
commit 6795edbf4b
5 files changed
+415 -77

No files matched your search

+44 -13
View File
@@ -86,19 +86,7 @@ export class VoiceRecorder {
this.stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
});
const type = recordingType();
this.recorder = new MediaRecorder(this.stream, type ? { mimeType: type } : undefined);
this.chunks = [];
this.recorder.ondataavailable = (e) => {
if (e.data.size > 0) this.chunks.push(e.data);
};
this.recorder.onstop = () => {
const blob = this.chunks.length ? new Blob(this.chunks, { type: this.recorder?.mimeType || type || 'audio/webm' }) : null;
this.release();
this.done?.(blob);
this.done = null;
};
this.recorder.start(1000);
this.startSegmentRecorder();
try {
const Ctx = (window as any).AudioContext || (window as any).webkitAudioContext;
this.audioCtx = new Ctx();
@@ -113,6 +101,25 @@ export class VoiceRecorder {
}
}
private startSegmentRecorder(): void {
if (!this.stream) return;
const type = recordingType();
const rec = new MediaRecorder(this.stream, type ? { mimeType: type } : undefined);
const chunks: Blob[] = [];
rec.ondataavailable = (e) => {
if (e.data.size > 0) chunks.push(e.data);
};
rec.onstop = () => {
const blob = chunks.length ? new Blob(chunks, { type: rec.mimeType || type || 'audio/webm' }) : null;
this.release();
this.done?.(blob);
this.done = null;
};
this.recorder = rec;
this.chunks = chunks;
rec.start(1000);
}
/** Input level 0..1 (roughly loudness), or null when unknown. */
level(): number | null {
if (!this.analyser || !this.samples) return null;
@@ -137,6 +144,30 @@ export class VoiceRecorder {
});
}
/**
* Stops the current MediaRecorder segment and immediately starts a new one on the same MediaStream.
* Resolves with the finished segment's audio Blob while recording continues seamlessly.
*/
cutSegment(): Promise<Blob | null> {
if (!this.stream || !this.recorder || this.recorder.state === 'inactive') {
return Promise.resolve(null);
}
const oldRecorder = this.recorder;
const oldChunks = this.chunks;
this.startSegmentRecorder();
return new Promise((resolve) => {
oldRecorder.onstop = () => {
const blob = oldChunks.length
? new Blob(oldChunks, { type: oldRecorder.mimeType || 'audio/webm' })
: null;
resolve(blob);
};
oldRecorder.stop();
});
}
cancel(): void {
this.done = null;
if (this.recorder && this.recorder.state !== 'inactive') {