@corenel/tools-web 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/core/protocol.d.ts +8 -0
  2. package/dist/core/protocol.d.ts.map +1 -1
  3. package/dist/runSpans.d.ts +21 -0
  4. package/dist/runSpans.d.ts.map +1 -0
  5. package/dist/runSpans.js +85 -0
  6. package/dist/runSpans.js.map +1 -0
  7. package/dist/runtime.d.ts.map +1 -1
  8. package/dist/runtime.js +42 -19
  9. package/dist/runtime.js.map +1 -1
  10. package/dist/speech/engine.d.ts +16 -0
  11. package/dist/speech/engine.d.ts.map +1 -0
  12. package/dist/speech/engine.js +115 -0
  13. package/dist/speech/engine.js.map +1 -0
  14. package/dist/speech/model.d.ts +40 -0
  15. package/dist/speech/model.d.ts.map +1 -0
  16. package/dist/speech/model.js +135 -0
  17. package/dist/speech/model.js.map +1 -0
  18. package/dist/speech/progress.d.ts +16 -0
  19. package/dist/speech/progress.d.ts.map +1 -0
  20. package/dist/speech/progress.js +46 -0
  21. package/dist/speech/progress.js.map +1 -0
  22. package/dist/speech/protocol.d.ts +38 -0
  23. package/dist/speech/protocol.d.ts.map +1 -0
  24. package/dist/speech/protocol.js +14 -0
  25. package/dist/speech/protocol.js.map +1 -0
  26. package/dist/speech/session.d.ts +34 -0
  27. package/dist/speech/session.d.ts.map +1 -0
  28. package/dist/speech/session.js +254 -0
  29. package/dist/speech/session.js.map +1 -0
  30. package/dist/speech/speechWorker.d.ts +2 -0
  31. package/dist/speech/speechWorker.d.ts.map +1 -0
  32. package/dist/speech/speechWorker.js +107 -0
  33. package/dist/speech/speechWorker.js.map +1 -0
  34. package/dist/workerClient.d.ts +14 -0
  35. package/dist/workerClient.d.ts.map +1 -1
  36. package/dist/workerClient.js +45 -3
  37. package/dist/workerClient.js.map +1 -1
  38. package/dist/workerHandler.d.ts.map +1 -1
  39. package/dist/workerHandler.js +19 -8
  40. package/dist/workerHandler.js.map +1 -1
  41. package/package.json +5 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../speech/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { SPEECH_MODEL_ID } from './protocol';
2
+ export { SPEECH_MODEL_ID };
3
+ /** Where the user's consent is remembered. */
4
+ export declare const INTENT_KEY = "prompd.web.speech.enabled";
5
+ /** Loads the ASR pipeline, reporting whole-percent progress. Injected so tests
6
+ * never download 50MB and never touch ONNX. */
7
+ export type LoadPipeline = (onProgress?: (pct: number) => void) => Promise<unknown>;
8
+ export interface ModelHostOpts {
9
+ caches: CacheStorage;
10
+ storage: Storage;
11
+ loadPipeline: LoadPipeline;
12
+ /** Extra teardown on evict -- the browser host uses it to drop the warm
13
+ * speech engine, since clearing the model has to actually clear it rather
14
+ * than leave a loaded pipeline running against deleted weights. */
15
+ onEvict?: () => void;
16
+ }
17
+ export interface ModelHost {
18
+ /** Whether the user has consented to the download. Survives eviction. */
19
+ isEnabled(): boolean;
20
+ setEnabled(on: boolean): void;
21
+ /** Whether the bytes are on this device RIGHT NOW. Always a real probe. */
22
+ isCached(): Promise<boolean>;
23
+ /** Download (if needed) and warm the pipeline. Safe to call concurrently. */
24
+ ensureModel(onProgress?: (pct: number) => void): Promise<void>;
25
+ /** Remove this model's bytes. Leaves intent alone. */
26
+ evict(): Promise<void>;
27
+ }
28
+ export declare function createModelHost({ caches, storage, loadPipeline, onEvict }: ModelHostOpts): ModelHost;
29
+ export declare function browserModelHost(): ModelHost;
30
+ /** The browser's real host.
31
+ *
32
+ * WARMS THE WORKER, NOT THE MAIN THREAD. An earlier version built a pipeline
33
+ * here, on the main thread, purely so it could watch a download -- then threw it
34
+ * away and let the worker build its own. That duplicated the expensive part
35
+ * (ONNX instantiation, graph compilation) and did it in the one place the worker
36
+ * exists to protect. Warming the shared engine means the thing that gets warmed
37
+ * is the thing that transcribes, and the Settings prefetch now genuinely makes
38
+ * the first dictation fast rather than only pre-filling the HTTP cache. */
39
+ export declare function createBrowserModelHost(): ModelHost;
40
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../speech/model.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAG7C,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,8CAA8C;AAC9C,eAAO,MAAM,UAAU,8BAA8B,CAAC;AAKtD;+CAC+C;AAC/C,MAAM,MAAM,YAAY,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpF,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B;;uEAEmE;IACnE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,yEAAyE;IACzE,SAAS,IAAI,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,2EAA2E;IAC3E,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,6EAA6E;IAC7E,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,sDAAsD;IACtD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,aAAa,GAAG,SAAS,CA4EpG;AAeD,wBAAgB,gBAAgB,IAAI,SAAS,CAG5C;AAED;;;;;;;;2EAQ2E;AAC3E,wBAAgB,sBAAsB,IAAI,SAAS,CAOlD"}
@@ -0,0 +1,135 @@
1
+ /* The dictation model's lifecycle: is it here, get it here, remove it.
2
+ *
3
+ * Deliberately separate from session.ts because it has a SECOND CONSUMER that
4
+ * never opens a microphone -- the Settings row prefetches on wifi, reports what
5
+ * is stored, and clears it. Folding this into the session would make that row
6
+ * reach through a dictation object it has no use for.
7
+ *
8
+ * The two pieces of state here are different things and are stored differently:
9
+ *
10
+ * INTENT (localStorage) -- the user said yes to the download.
11
+ * PRESENCE (Cache Storage) -- the bytes are actually on this device.
12
+ *
13
+ * Intent deliberately outlives presence. Browsers evict Cache Storage under
14
+ * memory pressure without telling the page, and someone who has already
15
+ * consented must not be asked again because the browser reclaimed some space --
16
+ * the model just re-downloads on next use. */
17
+ import { SPEECH_MODEL_ID } from './protocol';
18
+ import { sharedSpeechEngine } from './engine';
19
+ export { SPEECH_MODEL_ID };
20
+ /** Where the user's consent is remembered. */
21
+ export const INTENT_KEY = 'prompd.web.speech.enabled';
22
+ /** transformers.js keeps downloaded weights here. */
23
+ const CACHE_NAME = 'transformers-cache';
24
+ export function createModelHost({ caches, storage, loadPipeline, onEvict }) {
25
+ /* The in-flight load, so three surfaces asking at once download once. Cleared
26
+ * on failure -- a rejected promise cached here would turn one offline blip
27
+ * into a permanently broken button. */
28
+ let loading = null;
29
+ /* The warmed pipeline. Presence in memory is NOT presence on disk: isCached()
30
+ * still probes, because this says nothing about a later eviction. */
31
+ let warmed = null;
32
+ const modelUrls = async () => {
33
+ const cache = await caches.open(CACHE_NAME);
34
+ const keys = await cache.keys();
35
+ return keys.map((k) => k.url).filter((u) => u.includes(SPEECH_MODEL_ID));
36
+ };
37
+ return {
38
+ isEnabled: () => storage.getItem(INTENT_KEY) === '1',
39
+ setEnabled: (on) => {
40
+ if (on)
41
+ storage.setItem(INTENT_KEY, '1');
42
+ else
43
+ storage.removeItem(INTENT_KEY);
44
+ },
45
+ /* A REAL PROBE EVERY TIME. The tempting optimisation -- remember the answer
46
+ * -- is the bug this module exists to avoid: a remembered `true` after an
47
+ * eviction makes the caller skip the download and then wait forever for a
48
+ * model that is not there. */
49
+ isCached: async () => {
50
+ try {
51
+ /* THE WEIGHTS, not merely "something for this model".
52
+ *
53
+ * Matching any cached URL containing the model id counted config.json,
54
+ * tokenizer_config.json and friends -- so a download that died after the
55
+ * small metadata files reported "Ready on this site", skipped the
56
+ * consent prompt, and left the button waiting on weights that were never
57
+ * there. Observed in a real cache: the metadata lands in milliseconds,
58
+ * the two .onnx files are ~28MB and take far longer, so the window where
59
+ * this lies is the entire download. */
60
+ const urls = await modelUrls();
61
+ const onnx = urls.filter((u) => u.endsWith('.onnx'));
62
+ return onnx.some((u) => /encoder/i.test(u)) && onnx.some((u) => /decoder/i.test(u));
63
+ }
64
+ catch {
65
+ // No Cache Storage (private mode, blocked site data): treat as absent
66
+ // rather than throwing. The model simply downloads again.
67
+ return false;
68
+ }
69
+ },
70
+ ensureModel: (onProgress) => {
71
+ if (warmed)
72
+ return Promise.resolve();
73
+ if (loading)
74
+ return loading;
75
+ loading = loadPipeline(onProgress)
76
+ .then((p) => { warmed = p; })
77
+ .catch((e) => {
78
+ // Let the next caller try again rather than inheriting this failure.
79
+ loading = null;
80
+ throw e;
81
+ });
82
+ return loading;
83
+ },
84
+ /* Deletes only OUR entries. The transformers cache is shared -- nuking the
85
+ * whole thing would evict any other model the app has downloaded. */
86
+ evict: async () => {
87
+ try {
88
+ const cache = await caches.open(CACHE_NAME);
89
+ const urls = await modelUrls();
90
+ await Promise.all(urls.map((u) => cache.delete(u)));
91
+ }
92
+ catch {
93
+ // Nothing to remove if the cache is unavailable.
94
+ }
95
+ warmed = null;
96
+ loading = null;
97
+ onEvict?.();
98
+ },
99
+ };
100
+ }
101
+ /* ONE HOST PER PAGE.
102
+ *
103
+ * The composer's mic button and the Settings row are two views of the same
104
+ * thing. Given separate hosts they would disagree: Settings clears the model,
105
+ * the button's own instance still holds a warmed pipeline and happily records;
106
+ * or Settings prefetches and the button still believes nothing is downloaded.
107
+ *
108
+ * Created LAZILY rather than at module scope -- touching `caches` or
109
+ * `localStorage` at import time runs in every test and every worker that pulls
110
+ * this module in transitively, which is exactly how import-time side effects
111
+ * poison things that never asked for them. */
112
+ let shared = null;
113
+ export function browserModelHost() {
114
+ if (!shared)
115
+ shared = createBrowserModelHost();
116
+ return shared;
117
+ }
118
+ /** The browser's real host.
119
+ *
120
+ * WARMS THE WORKER, NOT THE MAIN THREAD. An earlier version built a pipeline
121
+ * here, on the main thread, purely so it could watch a download -- then threw it
122
+ * away and let the worker build its own. That duplicated the expensive part
123
+ * (ONNX instantiation, graph compilation) and did it in the one place the worker
124
+ * exists to protect. Warming the shared engine means the thing that gets warmed
125
+ * is the thing that transcribes, and the Settings prefetch now genuinely makes
126
+ * the first dictation fast rather than only pre-filling the HTTP cache. */
127
+ export function createBrowserModelHost() {
128
+ return createModelHost({
129
+ caches: globalThis.caches,
130
+ storage: globalThis.localStorage,
131
+ loadPipeline: (onProgress) => sharedSpeechEngine().load(onProgress),
132
+ onEvict: () => sharedSpeechEngine().dispose(),
133
+ });
134
+ }
135
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../speech/model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /** One event from transformers' progress_callback. Only the fields this reads
2
+ * are declared; the real event carries more (name, loaded, total). */
3
+ export interface ProgressEvent {
4
+ status?: string;
5
+ file?: string;
6
+ progress?: number;
7
+ }
8
+ export interface ProgressReporterOpts {
9
+ /** Injected so tests drive time instead of sleeping through the throttle. */
10
+ now?: () => number;
11
+ /** Minimum gap between reports. ~10/s is far below what a progress bar needs
12
+ * to look smooth, and far above what 200 messages in 440ms costs. */
13
+ intervalMs?: number;
14
+ }
15
+ export declare function createProgressReporter(sink: ((pct: number) => void) | undefined, opts?: ProgressReporterOpts): (e: ProgressEvent) => void;
16
+ //# sourceMappingURL=progress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../../speech/progress.ts"],"names":[],"mappings":"AAcA;sEACsE;AACtE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB;yEACqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,EACzC,IAAI,GAAE,oBAAyB,GAC9B,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAgC5B"}
@@ -0,0 +1,46 @@
1
+ /* Download progress, as one number reported sparingly.
2
+ *
3
+ * transformers reports progress PER FILE. Forwarding those straight across the
4
+ * worker boundary produced 200 messages in ~440ms on a real load, each one a
5
+ * React setState on the main thread -- and because every file restarts at zero,
6
+ * the percentage jumped backwards as often as forwards.
7
+ *
8
+ * pipeline() already wraps the callback in DefaultProgressCallback, which emits
9
+ * `progress_total` events aggregating bytes across all files. So this does not
10
+ * reconstruct that number: it drops the per-file events and throttles the
11
+ * aggregate one. The saving is postMessage traffic as well as renders, because
12
+ * the filtering happens inside the worker, before anything crosses.
13
+ */
14
+ export function createProgressReporter(sink, opts = {}) {
15
+ const now = opts.now ?? (() => Date.now());
16
+ const intervalMs = opts.intervalMs ?? 100;
17
+ let lastAt = -Infinity;
18
+ let lastPct = null;
19
+ return (e) => {
20
+ // Per-file events (`progress`), and the lifecycle ones (`initiate`, `done`,
21
+ // `ready`), are not this number.
22
+ if (!e || e.status !== 'progress_total')
23
+ return;
24
+ const raw = e.progress;
25
+ if (typeof raw !== 'number' || Number.isNaN(raw))
26
+ return;
27
+ // Clamped rather than trusted: a percentage outside 0..100 would render as a
28
+ // bar past its own end, and it is not worth propagating an upstream oddity
29
+ // into the UI to find out.
30
+ const pct = Math.max(0, Math.min(100, Math.round(raw)));
31
+ // Already showing this number. Re-rendering to say the same thing is the
32
+ // cost this module exists to remove.
33
+ if (pct === lastPct)
34
+ return;
35
+ /* COMPLETION IS NEVER THROTTLED. The bar otherwise stops at whatever the
36
+ * last permitted tick was -- 97%, say -- and stays there while the engine
37
+ * goes on to instantiate ONNX, which takes seconds and reads as a hang. */
38
+ const t = now();
39
+ if (pct < 100 && t - lastAt < intervalMs)
40
+ return;
41
+ lastAt = t;
42
+ lastPct = pct;
43
+ sink?.(pct);
44
+ };
45
+ }
46
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.js","sourceRoot":"","sources":["../../speech/progress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAkBH,MAAM,UAAU,sBAAsB,CACpC,IAAyC,EACzC,OAA6B,EAAE;IAE/B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC;IAC1C,IAAI,MAAM,GAAG,CAAC,QAAQ,CAAC;IACvB,IAAI,OAAO,GAAkB,IAAI,CAAC;IAElC,OAAO,CAAC,CAAC,EAAE,EAAE;QACX,4EAA4E;QAC5E,iCAAiC;QACjC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,gBAAgB;YAAE,OAAO;QAEhD,MAAM,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC;QACvB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,OAAO;QACzD,6EAA6E;QAC7E,2EAA2E;QAC3E,2BAA2B;QAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAExD,yEAAyE;QACzE,qCAAqC;QACrC,IAAI,GAAG,KAAK,OAAO;YAAE,OAAO;QAE5B;;mFAE2E;QAC3E,MAAM,CAAC,GAAG,GAAG,EAAE,CAAC;QAChB,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,UAAU;YAAE,OAAO;QAEjD,MAAM,GAAG,CAAC,CAAC;QACX,OAAO,GAAG,GAAG,CAAC;QACd,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC;IACd,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,38 @@
1
+ /** The model the browser downloads and runs. English-only by design: Moonshine's
2
+ * other languages are separate monolingual models, i.e. separate downloads. */
3
+ export declare const SPEECH_MODEL_ID = "onnx-community/moonshine-tiny-ONNX";
4
+ /** The pipeline's input contract: mono PCM at this rate. Both the capture and
5
+ * the worker resample to it, so it lives where both can see it. */
6
+ export declare const SAMPLE_RATE = 16000;
7
+ export type MainToWorker = {
8
+ kind: 'load';
9
+ } | {
10
+ kind: 'audio';
11
+ pcm: Float32Array;
12
+ } | {
13
+ kind: 'flush';
14
+ };
15
+ export type WorkerToMain = {
16
+ kind: 'ready';
17
+ }
18
+ /** Download progress, whole percent. Reported from the WORKER because the
19
+ * worker is what fetches the weights -- the main thread no longer builds a
20
+ * pipeline of its own just to observe a download. */
21
+ | {
22
+ kind: 'progress';
23
+ pct: number;
24
+ }
25
+ /** A revisable guess. Never reaches the draft -- see session.ts. */
26
+ | {
27
+ kind: 'interim';
28
+ text: string;
29
+ }
30
+ /** Committed text, safe to append to what the user is writing. */
31
+ | {
32
+ kind: 'final';
33
+ text: string;
34
+ } | {
35
+ kind: 'error';
36
+ message: string;
37
+ };
38
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../speech/protocol.ts"],"names":[],"mappings":"AAQA;+EAC+E;AAC/E,eAAO,MAAM,eAAe,uCAAuC,CAAC;AAEpE;mEACmE;AACnE,eAAO,MAAM,WAAW,QAAQ,CAAC;AAEjC,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,YAAY,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE;AACnB;;qDAEqD;GACnD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AACnC,oEAAoE;GAClE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACnC,kEAAkE;GAChE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC"}
@@ -0,0 +1,14 @@
1
+ /* The wire between the main thread and the dictation worker.
2
+ *
3
+ * Its own module because BOTH sides import it and neither may import the other:
4
+ * the worker must not drag the session's DOM code (getUserMedia, MediaStream)
5
+ * into a worker scope that has none, and the session must not drag the
6
+ * transformers pipeline onto the main thread, which is the whole reason the
7
+ * worker exists. */
8
+ /** The model the browser downloads and runs. English-only by design: Moonshine's
9
+ * other languages are separate monolingual models, i.e. separate downloads. */
10
+ export const SPEECH_MODEL_ID = 'onnx-community/moonshine-tiny-ONNX';
11
+ /** The pipeline's input contract: mono PCM at this rate. Both the capture and
12
+ * the worker resample to it, so it lives where both can see it. */
13
+ export const SAMPLE_RATE = 16000;
14
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../speech/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;oBAMoB;AAEpB;+EAC+E;AAC/E,MAAM,CAAC,MAAM,eAAe,GAAG,oCAAoC,CAAC;AAEpE;mEACmE;AACnE,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { type SpeechEngine } from './engine';
2
+ export type SpeechErrorKind = 'permission' | 'unsupported' | 'inference';
3
+ export interface SpeechError {
4
+ kind: SpeechErrorKind;
5
+ message: string;
6
+ }
7
+ /** Pulls PCM off a live stream. Returned handle stops the pull. */
8
+ export interface Capture {
9
+ stop(): void;
10
+ }
11
+ export type CreateCapture = (stream: MediaStream, onChunk: (pcm: Float32Array) => void) => Capture;
12
+ export interface DictationOpts {
13
+ onInterim(text: string): void;
14
+ onFinal(text: string): void;
15
+ onError(e: SpeechError): void;
16
+ /** Injected in tests; defaults to the page's shared engine. */
17
+ engine?: SpeechEngine;
18
+ getMedia?: () => Promise<MediaStream>;
19
+ createCapture?: CreateCapture;
20
+ /** How long stop() waits for the tail transcript before giving up. A dead
21
+ * worker must not be able to wedge stop(); the microphone is already released
22
+ * by then, so the only thing at risk is the last sentence. */
23
+ flushTimeoutMs?: number;
24
+ }
25
+ export interface Dictation {
26
+ /** Acquire the mic, warm the engine, begin listening. Never rejects: a denial
27
+ * is a state the UI renders, not a crash it has to catch. */
28
+ start(): Promise<void>;
29
+ /** Flush the tail, release the mic. Idempotent, safe before start(). */
30
+ stop(): Promise<void>;
31
+ }
32
+ export declare const defaultCreateCapture: CreateCapture;
33
+ export declare function createDictation(opts: DictationOpts): Dictation;
34
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../speech/session.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAEjE,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,aAAa,GAAG,WAAW,CAAC;AACzE,MAAM,WAAW,WAAW;IAAG,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAEvE,mEAAmE;AACnE,MAAM,WAAW,OAAO;IAAG,IAAI,IAAI,IAAI,CAAA;CAAE;AACzC,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,KAAK,OAAO,CAAC;AAEnG,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC9B,+DAA+D;IAC/D,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;IACtC,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;kEAE8D;IAC9D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACxB;iEAC6D;IAC7D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,wEAAwE;IACxE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAyBD,eAAO,MAAM,oBAAoB,EAAE,aAsBlC,CAAC;AAcF,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,CAgK9D"}
@@ -0,0 +1,254 @@
1
+ /* One dictation session: microphone in, text out.
2
+ *
3
+ * THE INVARIANT: the microphone is released on every terminal path. Stop,
4
+ * permission denial, a failed load, a worker crash, an inference error -- all of
5
+ * them run the same teardown. On-device dictation's whole claim is that nothing
6
+ * leaves the machine, and a mic left hot after a crash makes that claim false in
7
+ * a way the user cannot see.
8
+ *
9
+ * THE SECOND RULE: interim text never reaches the caller's draft. The worker
10
+ * revises its guesses as more audio arrives; only `final` is committed.
11
+ *
12
+ * WHAT THIS MODULE DOES NOT OWN: the worker. It borrows the page's shared
13
+ * SpeechEngine and hands it back on stop. An earlier version built and
14
+ * terminated a worker per dictation, which re-instantiated the ONNX runtime on
15
+ * every single click. Runs are cheap now; only the first one pays. */
16
+ import { SAMPLE_RATE } from './protocol';
17
+ import { sharedSpeechEngine } from './engine';
18
+ const defaultGetMedia = () => navigator.mediaDevices.getUserMedia({ audio: true });
19
+ /* Real capture: resample to the pipeline's contract (mono, 16kHz) and hand up
20
+ * Float32 frames. ScriptProcessor is deprecated but is the only node available
21
+ * without shipping a separate worklet file; the buffer is small and the work per
22
+ * frame is a copy.
23
+ *
24
+ * Exported so the two things below can actually be tested. Both were silent
25
+ * failures found from use, not from the suite.
26
+ *
27
+ * RESUME, ALWAYS. This context is constructed after two awaits -- the permission
28
+ * prompt and the engine warm-up -- so it is created well outside the user
29
+ * gesture that started dictation, and Chrome hands back a SUSPENDED context.
30
+ * A suspended context never fires onaudioprocess, so not one sample reaches the
31
+ * worker: the button says "Stop dictating", the microphone light is on, and
32
+ * nothing is ever transcribed.
33
+ *
34
+ * AND THE GRAPH MUST NOT END AT THE SPEAKERS. A ScriptProcessor only pulls while
35
+ * it is connected to something live, but connecting it to ctx.destination pipes
36
+ * the microphone straight back out of the speakers -- you hear yourself, with
37
+ * feedback if you are not on headphones. A zero-gain node keeps the graph pulling
38
+ * and outputs silence. */
39
+ export const defaultCreateCapture = (stream, onChunk) => {
40
+ const ctx = new AudioContext({ sampleRate: SAMPLE_RATE });
41
+ // Fire-and-forget: resume() rejects only if the context is already closed,
42
+ // and a failure here must not take down a dictation that may still work.
43
+ if (ctx.state === 'suspended')
44
+ void ctx.resume().catch(() => { });
45
+ const source = ctx.createMediaStreamSource(stream);
46
+ const node = ctx.createScriptProcessor(4096, 1, 1);
47
+ const mute = ctx.createGain();
48
+ mute.gain.value = 0;
49
+ node.onaudioprocess = (e) => { onChunk(new Float32Array(e.inputBuffer.getChannelData(0))); };
50
+ source.connect(node);
51
+ node.connect(mute);
52
+ mute.connect(ctx.destination);
53
+ return {
54
+ stop: () => {
55
+ node.onaudioprocess = null;
56
+ node.disconnect();
57
+ mute.disconnect();
58
+ source.disconnect();
59
+ void ctx.close().catch(() => { });
60
+ },
61
+ };
62
+ };
63
+ /** Map a getUserMedia rejection onto something the UI can explain. */
64
+ function mediaError(e) {
65
+ const name = e.name;
66
+ if (name === 'NotAllowedError' || name === 'PermissionDeniedError') {
67
+ return { kind: 'permission', message: 'Microphone permission was denied.' };
68
+ }
69
+ if (name === 'NotFoundError' || name === 'DevicesNotFoundError') {
70
+ return { kind: 'unsupported', message: 'No microphone was found.' };
71
+ }
72
+ return { kind: 'unsupported', message: 'The microphone is unavailable in this browser.' };
73
+ }
74
+ export function createDictation(opts) {
75
+ const engine = opts.engine ?? sharedSpeechEngine();
76
+ const getMedia = opts.getMedia ?? defaultGetMedia;
77
+ const createCapture = opts.createCapture ?? defaultCreateCapture;
78
+ const flushTimeoutMs = opts.flushTimeoutMs ?? 4000;
79
+ let stream = null;
80
+ let capture = null;
81
+ let unsubscribe = null;
82
+ let listening = false;
83
+ let running = false;
84
+ /* THE RUN GENERATION -- and the reason it is a counter rather than a boolean.
85
+ *
86
+ * A `disposed` flag cannot express "a stop arrived while start() was still
87
+ * awaiting the permission prompt". In that window there is nothing to tear
88
+ * down AND no way to say "do not finish starting either", so start() would
89
+ * resume after the user answered, open the microphone, and leave it live with
90
+ * no reachable handle to close it -- clicking again would begin a second,
91
+ * independent session rather than reaching the orphan.
92
+ *
93
+ * So every run claims a generation, every await in start() re-checks it, and
94
+ * stop() bumps it UNCONDITIONALLY. */
95
+ let gen = 0;
96
+ /** Release whatever a run holds. Never throws. Safe with partial state. */
97
+ function release(s, c) {
98
+ try {
99
+ c?.stop();
100
+ }
101
+ catch { /* releasing anyway */ }
102
+ // The microphone itself. This is the line that matters.
103
+ try {
104
+ s?.getTracks().forEach((t) => t.stop());
105
+ }
106
+ catch { /* releasing anyway */ }
107
+ }
108
+ /** The one teardown every terminal path runs. Bumping `gen` also cancels a
109
+ * start() sitting between its awaits. The ENGINE IS NOT DISPOSED -- it belongs
110
+ * to the page, and the next dictation reuses it warm. */
111
+ function teardown() {
112
+ gen += 1;
113
+ listening = false;
114
+ running = false;
115
+ unsubscribe?.();
116
+ unsubscribe = null;
117
+ const s = stream;
118
+ const c = capture;
119
+ stream = null;
120
+ capture = null;
121
+ release(s, c);
122
+ }
123
+ return {
124
+ async start() {
125
+ if (running)
126
+ return;
127
+ running = true;
128
+ const myGen = ++gen;
129
+ const stale = () => myGen !== gen;
130
+ // Mic FIRST. A denial must not warm an engine for a run that never starts.
131
+ let s;
132
+ try {
133
+ s = await getMedia();
134
+ }
135
+ catch (e) {
136
+ running = false;
137
+ opts.onError(mediaError(e));
138
+ return;
139
+ }
140
+ /* The window the generation exists for: stop() ran while the permission
141
+ * prompt was up. This run owns `s` and nothing else can see it. */
142
+ if (stale()) {
143
+ release(s, null);
144
+ return;
145
+ }
146
+ stream = s;
147
+ // Warm on the first dictation only; later ones resolve immediately.
148
+ try {
149
+ await engine.load();
150
+ }
151
+ catch (e) {
152
+ if (stale())
153
+ return;
154
+ opts.onError({ kind: 'inference', message: e.message });
155
+ teardown();
156
+ return;
157
+ }
158
+ if (stale())
159
+ return;
160
+ unsubscribe = engine.subscribe((m) => {
161
+ if (myGen !== gen)
162
+ return;
163
+ if (m.kind === 'interim') {
164
+ opts.onInterim(m.text);
165
+ return;
166
+ }
167
+ if (m.kind === 'final') {
168
+ opts.onFinal(m.text);
169
+ return;
170
+ }
171
+ if (m.kind === 'error') {
172
+ opts.onError({ kind: 'inference', message: m.message });
173
+ teardown();
174
+ }
175
+ }, () => {
176
+ if (myGen !== gen)
177
+ return;
178
+ opts.onError({ kind: 'inference', message: 'The speech engine stopped unexpectedly.' });
179
+ teardown();
180
+ });
181
+ listening = true;
182
+ capture = createCapture(s, (pcm) => {
183
+ if (!listening || myGen !== gen)
184
+ return;
185
+ engine.post({ kind: 'audio', pcm });
186
+ });
187
+ },
188
+ async stop() {
189
+ /* Nothing listening -- but STILL tear down, because this is what cancels a
190
+ * start() stuck at the permission prompt. A bare `return` here is what once
191
+ * left a microphone open with no handle to close it. */
192
+ if (!listening) {
193
+ teardown();
194
+ return;
195
+ }
196
+ // Stop feeding first, so the flush covers a fixed buffer rather than
197
+ // racing new frames into it.
198
+ listening = false;
199
+ try {
200
+ capture?.stop();
201
+ }
202
+ catch { /* tearing down anyway */ }
203
+ capture = null;
204
+ /* THE MICROPHONE GOES NOW, not after the tail arrives. Transcribing the
205
+ * last sentence takes a moment, and leaving the OS recording indicator lit
206
+ * for that long after the user pressed stop is the one thing this feature
207
+ * cannot do. The tail is computed from audio already captured; it does not
208
+ * need the device. */
209
+ const s = stream;
210
+ stream = null;
211
+ try {
212
+ s?.getTracks().forEach((t) => t.stop());
213
+ }
214
+ catch { /* releasing anyway */ }
215
+ /* Hand the tail to a DEDICATED subscriber. The run's own subscriber is
216
+ * dropped first so the final cannot be delivered twice, and this one
217
+ * unsubscribes the moment it has what it came for.
218
+ *
219
+ * stop() used to post the flush and tear down in the same tick, so the
220
+ * final came back to nobody -- every dictation silently discarded the last
221
+ * thing you said. */
222
+ await new Promise((resolve) => {
223
+ unsubscribe?.();
224
+ unsubscribe = null;
225
+ let settled = false;
226
+ let off = null;
227
+ let timer = null;
228
+ const finish = () => {
229
+ if (settled)
230
+ return;
231
+ settled = true;
232
+ if (timer)
233
+ clearTimeout(timer);
234
+ off?.();
235
+ resolve();
236
+ };
237
+ off = engine.subscribe((m) => {
238
+ if (m.kind !== 'final')
239
+ return;
240
+ // An empty flush is what a stop with no speech in it produces; it is
241
+ // not a transcript and must not reach the draft.
242
+ const text = m.text.trim();
243
+ if (text)
244
+ opts.onFinal(text);
245
+ finish();
246
+ }, () => finish());
247
+ timer = setTimeout(finish, flushTimeoutMs);
248
+ engine.post({ kind: 'flush' });
249
+ });
250
+ teardown();
251
+ },
252
+ };
253
+ }
254
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../speech/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=speechWorker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speechWorker.d.ts","sourceRoot":"","sources":["../../speech/speechWorker.ts"],"names":[],"mappings":""}