import type { SpeechRecognitionResult } from "./types.js" export class SpeechRecognitionTask { isRecording = false isProcessing = false private recorder: MediaRecorder | null = null private stream: MediaStream | null = null private chunks: Blob[] = [] private cancelled = false private endpoint: string private ready: Promise private onComplete: () => void constructor(endpoint: string, onComplete: () => void) { this.endpoint = endpoint this.onComplete = onComplete this.ready = this.begin() } private async begin(): Promise { this.stream = await navigator.mediaDevices.getUserMedia({ audio: true }) this.recorder = new MediaRecorder(this.stream, { mimeType: "audio/webm;codecs=opus" }) this.chunks = [] this.recorder.ondataavailable = (e) => { if (e.data.size > 0) this.chunks.push(e.data) } this.recorder.start() this.isRecording = true } async finish(): Promise { await this.ready if (!this.recorder || this.cancelled) { this.onComplete() return { text: "", isFinal: true } } const blob = await new Promise((resolve) => { this.recorder!.onstop = () => { resolve(new Blob(this.chunks, { type: "audio/webm" })) } this.recorder!.stop() }) this.teardown() this.isRecording = false this.isProcessing = true try { const fd = new FormData() fd.append("audio", blob, "recording.webm") const r = await fetch(this.endpoint, { method: "POST", body: fd, signal: AbortSignal.timeout(30000), }) if (!r.ok) throw new Error("HTTP " + r.status) const data = await r.json() this.isProcessing = false this.onComplete() return { text: (data.text || "").trim(), isFinal: true } } catch (err) { this.isProcessing = false this.onComplete() throw err } } cancel(): void { this.cancelled = true if (this.recorder?.state === "recording") this.recorder.stop() this.teardown() this.isRecording = false this.isProcessing = false this.onComplete() } private teardown(): void { this.stream?.getTracks().forEach((t) => t.stop()) this.recorder = null this.stream = null this.chunks = [] } }