File size: 2,575 Bytes
6c3af4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/// <reference lib="webworker" />
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<string, string>();

function emitTranscript() {
  post({ type: "transcript", text: [...lines.values()].join(" ").trim() });
}

let transcriber: Transcriber | null = null;

async function load(modelFiles: Record<string, string>): Promise<void> {
  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<WorkerRequest>) => {
  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),
    });
  }
};