///
import { DICTATION_MODEL_VERSION } from "@shared/dictationModel";
import { AssetDownloader, ModelArch, Transcriber } from "./moonshine";
import type {
WorkerRequest,
WorkerResponse,
} from "./speechToTextWorkerProtocol";
function post(message: WorkerResponse, transfer: Transferable[] = []) {
self.postMessage(message, transfer);
}
/**
* Lines keyed by their opaque id, in the order they first appeared, so the
* transcript is the join of the map's values. Completed lines keep their final
* text; only the in-progress line changes.
*/
const lines = new Map();
function emitTranscript() {
post({ type: "transcript", text: [...lines.values()].join(" ").trim() });
}
let transcriber: Transcriber | null = null;
async function load(modelFiles: Record): Promise {
const downloader = new AssetDownloader({
// Versioned, so a model bump does not leave ~51 MB of unreachable entries
// behind in every user's Cache Storage.
cacheName: `minisearch-dictation-${DICTATION_MODEL_VERSION}`,
onProgress: (loaded, total) => post({ type: "progress", loaded, total }),
});
transcriber = await Transcriber.loadFromUrls(modelFiles, {
modelArch: ModelArch.TinyStreaming,
downloader,
});
transcriber.addListener({
onLineStarted: (event) => {
lines.set(event.line.id, event.line.text);
emitTranscript();
},
onLineTextChanged: (event) => {
lines.set(event.line.id, event.line.text);
emitTranscript();
},
onLineCompleted: (event) => {
lines.set(event.line.id, event.line.text);
emitTranscript();
},
onError: (event) => {
post({
type: "error",
message: event.error?.message ?? "Dictation failed",
});
},
});
transcriber.start();
post({ type: "loaded" });
}
self.onmessage = async ({ data }: MessageEvent) => {
try {
if (data.type === "load") {
await load(data.modelFiles);
return;
}
if (data.type === "audio") {
transcriber?.addAudio(
new Float32Array(data.buffer),
data.sampleRate,
undefined,
);
return;
}
try {
transcriber?.stop();
} finally {
// Always acknowledged: without this the main thread waits out its whole
// timeout before terminating a worker that already gave up.
post({ type: "stopped" });
}
} catch (error) {
post({
type: "error",
message: error instanceof Error ? error.message : String(error),
});
}
};