Refactor STT
This commit is contained in:
@@ -4,12 +4,12 @@
|
|||||||
* decides what to do with the result.
|
* decides what to do with the result.
|
||||||
*
|
*
|
||||||
* Recording lifecycle: acquire() requests microphone access (async,
|
* Recording lifecycle: acquire() requests microphone access (async,
|
||||||
* may prompt) and creates a fresh MediaRecorder; start() begins
|
* may prompt) and creates the MediaRecorder; start() begins capturing
|
||||||
* capturing (synchronous); stop() resolves the audio blob and
|
* (synchronous); stop() resolves the audio blob. The stream and
|
||||||
* releases the stream. A fresh stream is acquired per recording and
|
* recorder are reused across recordings to avoid codec warmup delay.
|
||||||
* torn down on every stop() so iOS/WebKit never reuses a capture
|
* Before each acquire(), the track state is checked; if iOS/WebKit
|
||||||
* track that TTS playback or an audio-session interruption has
|
* has killed the track due to TTS playback or an audio-session
|
||||||
* quietly killed.
|
* interruption, a fresh stream is obtained.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,19 +41,29 @@ export class STT {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* acquire requests microphone access and creates a fresh
|
* acquire requests microphone access and creates the MediaRecorder.
|
||||||
* MediaRecorder. Always starts clean so iOS never reuses a dead
|
* If an existing recorder has a live track, it is reused to avoid
|
||||||
* capture track. Must be called before start.
|
* codec warmup delay. If the track is dead (e.g., killed by iOS
|
||||||
|
* after TTS playback), a fresh stream and recorder are created.
|
||||||
|
* Must be called before start.
|
||||||
*/
|
*/
|
||||||
async acquire() {
|
async acquire() {
|
||||||
|
// Reuse if the existing track is still live.
|
||||||
|
const track = this.mediaRecorder?.stream?.getAudioTracks()[0];
|
||||||
|
if (track?.readyState === 'live') {
|
||||||
|
this.audioChunks = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track is dead or doesn't exist — acquire fresh.
|
||||||
this._release();
|
this._release();
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: true,
|
audio: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [track] = stream.getAudioTracks();
|
const [newTrack] = stream.getAudioTracks();
|
||||||
if (track?.readyState !== 'live') {
|
if (newTrack?.readyState !== 'live') {
|
||||||
for (const t of stream.getTracks()) t.stop();
|
for (const t of stream.getTracks()) t.stop();
|
||||||
throw new Error('microphone unavailable');
|
throw new Error('microphone unavailable');
|
||||||
}
|
}
|
||||||
@@ -93,6 +103,7 @@ export class STT {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* stop stops recording, transcribes the audio, and returns the result.
|
* stop stops recording, transcribes the audio, and returns the result.
|
||||||
|
* The stream and recorder are kept alive for reuse.
|
||||||
* @returns {Promise<{text: string, detectedLanguage: string}>}
|
* @returns {Promise<{text: string, detectedLanguage: string}>}
|
||||||
*/
|
*/
|
||||||
async stop() {
|
async stop() {
|
||||||
@@ -118,10 +129,10 @@ export class STT {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* _stopRecording stops the MediaRecorder and resolves with the
|
* _stopRecording stops the MediaRecorder and resolves with the
|
||||||
* assembled audio blob, releasing the stream. Returns an empty blob
|
* assembled audio blob. Does not release the stream or recorder,
|
||||||
* if there is no active recording, so it is safe to call before
|
* so they can be reused. Returns an empty blob if there is no
|
||||||
* start has run (e.g. if the user released while acquire was still
|
* active recording, so it is safe to call before start has run
|
||||||
* pending).
|
* (e.g., if the user released while acquire was still pending).
|
||||||
* @returns {Promise<Blob>}
|
* @returns {Promise<Blob>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
@@ -129,7 +140,6 @@ export class STT {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const mr = this.mediaRecorder;
|
const mr = this.mediaRecorder;
|
||||||
if (!mr || mr.state === 'inactive') {
|
if (!mr || mr.state === 'inactive') {
|
||||||
this._release();
|
|
||||||
resolve(new Blob([]));
|
resolve(new Blob([]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -142,7 +152,7 @@ export class STT {
|
|||||||
if (type?.includes('mp4') || type?.includes('m4a')) {
|
if (type?.includes('mp4') || type?.includes('m4a')) {
|
||||||
this._ext = 'm4a';
|
this._ext = 'm4a';
|
||||||
}
|
}
|
||||||
this._release();
|
this.audioChunks = [];
|
||||||
resolve(blob);
|
resolve(blob);
|
||||||
},
|
},
|
||||||
{ once: true },
|
{ once: true },
|
||||||
@@ -151,7 +161,7 @@ export class STT {
|
|||||||
mr.addEventListener(
|
mr.addEventListener(
|
||||||
'error',
|
'error',
|
||||||
(event) => {
|
(event) => {
|
||||||
this._release();
|
this.audioChunks = [];
|
||||||
reject(event.error);
|
reject(event.error);
|
||||||
},
|
},
|
||||||
{ once: true },
|
{ once: true },
|
||||||
|
|||||||
Reference in New Issue
Block a user