@happyvertical/smrt-svelte 0.55.5 → 0.55.7

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 (134) hide show
  1. package/AGENTS.md +17 -2
  2. package/README.md +83 -4
  3. package/agents/workspace.md +202 -5
  4. package/dist/browser-ai/adapters/stt/dictation-source.d.ts.map +1 -1
  5. package/dist/browser-ai/adapters/stt/dictation-source.js +8 -2
  6. package/dist/browser-ai/adapters/stt/factory.d.ts +6 -3
  7. package/dist/browser-ai/adapters/stt/factory.d.ts.map +1 -1
  8. package/dist/browser-ai/adapters/stt/factory.js +19 -9
  9. package/dist/browser-ai/adapters/stt/index.d.ts +3 -1
  10. package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -1
  11. package/dist/browser-ai/adapters/stt/index.js +3 -1
  12. package/dist/browser-ai/adapters/stt/local-speech-model.d.ts +111 -0
  13. package/dist/browser-ai/adapters/stt/local-speech-model.d.ts.map +1 -0
  14. package/dist/browser-ai/adapters/stt/local-speech-model.js +377 -0
  15. package/dist/browser-ai/adapters/stt/local-speech.d.ts +82 -0
  16. package/dist/browser-ai/adapters/stt/local-speech.d.ts.map +1 -0
  17. package/dist/browser-ai/adapters/stt/local-speech.js +317 -0
  18. package/dist/browser-ai/adapters/stt/local-speech.worker.d.ts +2 -0
  19. package/dist/browser-ai/adapters/stt/local-speech.worker.d.ts.map +1 -0
  20. package/dist/browser-ai/adapters/stt/local-speech.worker.js +32 -0
  21. package/dist/browser-ai/adapters/stt/transformers-env.d.ts +8 -0
  22. package/dist/browser-ai/adapters/stt/transformers-env.d.ts.map +1 -0
  23. package/dist/browser-ai/adapters/stt/transformers-env.js +17 -0
  24. package/dist/browser-ai/adapters/stt/types.d.ts +54 -9
  25. package/dist/browser-ai/adapters/stt/types.d.ts.map +1 -1
  26. package/dist/browser-ai/capabilities/speech-probe.d.ts +29 -0
  27. package/dist/browser-ai/capabilities/speech-probe.d.ts.map +1 -0
  28. package/dist/browser-ai/capabilities/speech-probe.js +19 -0
  29. package/dist/browser-ai/core/errors.d.ts +9 -0
  30. package/dist/browser-ai/core/errors.d.ts.map +1 -1
  31. package/dist/browser-ai/core/errors.js +12 -0
  32. package/dist/browser-ai/index.d.ts +1 -0
  33. package/dist/browser-ai/index.d.ts.map +1 -1
  34. package/dist/browser-ai/index.js +1 -0
  35. package/dist/components/app/AppShell.svelte +587 -33
  36. package/dist/components/app/AppShell.svelte.d.ts +75 -4
  37. package/dist/components/app/AppShell.svelte.d.ts.map +1 -1
  38. package/dist/components/app/DockToggles.svelte +0 -1
  39. package/dist/components/app/dock-toggle.d.ts +7 -1
  40. package/dist/components/app/dock-toggle.d.ts.map +1 -1
  41. package/dist/components/app/index.d.ts +2 -1
  42. package/dist/components/app/index.d.ts.map +1 -1
  43. package/dist/components/app/slot-item.d.ts +21 -0
  44. package/dist/components/app/slot-item.d.ts.map +1 -0
  45. package/dist/components/app/slot-item.js +1 -0
  46. package/dist/components/audit/AuditList.svelte +51 -10
  47. package/dist/components/audit/AuditList.svelte.d.ts +13 -1
  48. package/dist/components/audit/AuditList.svelte.d.ts.map +1 -1
  49. package/dist/components/audit/RecordHistory.svelte +168 -10
  50. package/dist/components/audit/RecordHistory.svelte.d.ts +11 -1
  51. package/dist/components/audit/RecordHistory.svelte.d.ts.map +1 -1
  52. package/dist/components/audit/index.d.ts +1 -1
  53. package/dist/components/audit/index.d.ts.map +1 -1
  54. package/dist/components/audit/types.d.ts +18 -0
  55. package/dist/components/audit/types.d.ts.map +1 -1
  56. package/dist/components/forms/AddressInput.svelte +2 -15
  57. package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -1
  58. package/dist/components/forms/DateRangeInput.svelte +2 -15
  59. package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
  60. package/dist/components/forms/DateTimeInput.svelte +8 -15
  61. package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
  62. package/dist/components/forms/MeasurementInput.svelte +2 -15
  63. package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -1
  64. package/dist/components/forms/MoneyInput.svelte +8 -25
  65. package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -1
  66. package/dist/components/forms/NumberInput.svelte +7 -15
  67. package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -1
  68. package/dist/components/forms/PhoneInput.svelte +7 -15
  69. package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
  70. package/dist/components/forms/SelectInput.svelte +34 -40
  71. package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -1
  72. package/dist/components/forms/TextInput.svelte +20 -37
  73. package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
  74. package/dist/components/forms/TextareaInput.svelte +19 -35
  75. package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
  76. package/dist/components/sortable/Sortable.svelte +6 -33
  77. package/dist/components/sortable/Sortable.svelte.d.ts.map +1 -1
  78. package/dist/components/sortable/announce.d.ts +5 -0
  79. package/dist/components/sortable/announce.d.ts.map +1 -0
  80. package/dist/components/sortable/announce.js +33 -0
  81. package/dist/components/workspace/admin-shell/AdminShell.svelte +424 -58
  82. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +27 -2
  83. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
  84. package/dist/components/workspace/admin-shell/ShellBrand.svelte +86 -0
  85. package/dist/components/workspace/admin-shell/ShellBrand.svelte.d.ts +18 -0
  86. package/dist/components/workspace/admin-shell/ShellBrand.svelte.d.ts.map +1 -0
  87. package/dist/components/workspace/admin-shell/ShellIconButton.svelte +77 -0
  88. package/dist/components/workspace/admin-shell/ShellIconButton.svelte.d.ts +26 -0
  89. package/dist/components/workspace/admin-shell/ShellIconButton.svelte.d.ts.map +1 -0
  90. package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte +88 -16
  91. package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte.d.ts.map +1 -1
  92. package/dist/components/workspace/admin-shell/ShellNavEditor.svelte +531 -0
  93. package/dist/components/workspace/admin-shell/ShellNavEditor.svelte.d.ts +26 -0
  94. package/dist/components/workspace/admin-shell/ShellNavEditor.svelte.d.ts.map +1 -0
  95. package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte +57 -0
  96. package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte.d.ts +22 -0
  97. package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte.d.ts.map +1 -0
  98. package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte +290 -0
  99. package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte.d.ts +32 -0
  100. package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte.d.ts.map +1 -0
  101. package/dist/components/workspace/admin-shell/TenantNav.svelte +67 -2
  102. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts +8 -0
  103. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
  104. package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts +67 -0
  105. package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts.map +1 -1
  106. package/dist/components/workspace/admin-shell/layout-controller.svelte.js +129 -1
  107. package/dist/components/workspace/admin-shell/layout.d.ts +122 -0
  108. package/dist/components/workspace/admin-shell/layout.d.ts.map +1 -1
  109. package/dist/components/workspace/admin-shell/layout.js +410 -10
  110. package/dist/components/workspace/admin-shell/settings.js +4 -4
  111. package/dist/components/workspace/admin-shell/shell-icons.d.ts +45 -0
  112. package/dist/components/workspace/admin-shell/shell-icons.d.ts.map +1 -0
  113. package/dist/components/workspace/admin-shell/shell-icons.js +66 -0
  114. package/dist/components/workspace/admin-shell/slot-labels.d.ts +33 -0
  115. package/dist/components/workspace/admin-shell/slot-labels.d.ts.map +1 -0
  116. package/dist/components/workspace/admin-shell/slot-labels.js +33 -0
  117. package/dist/components/workspace/admin-shell/slots.d.ts +50 -0
  118. package/dist/components/workspace/admin-shell/slots.d.ts.map +1 -0
  119. package/dist/components/workspace/admin-shell/slots.js +77 -0
  120. package/dist/components/workspace/admin-shell/state.svelte.d.ts +35 -0
  121. package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
  122. package/dist/components/workspace/admin-shell/state.svelte.js +111 -0
  123. package/dist/components/workspace/admin-shell/types.d.ts +101 -1
  124. package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
  125. package/dist/components/workspace/index.d.ts +9 -2
  126. package/dist/components/workspace/index.d.ts.map +1 -1
  127. package/dist/components/workspace/index.js +6 -0
  128. package/dist/i18n/strings.workspace.d.ts +49 -0
  129. package/dist/i18n/strings.workspace.d.ts.map +1 -1
  130. package/dist/i18n/strings.workspace.js +49 -0
  131. package/package.json +30 -19
  132. package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts +0 -51
  133. package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts.map +0 -1
  134. package/dist/browser-ai/adapters/stt/whisper-wasm.js +0 -380
@@ -0,0 +1,111 @@
1
+ import type { DownloadProgressInfo } from '../../core/types.js';
2
+ /** Where the speech model comes from by default (kept for `whisper-local`). */
3
+ export declare const WHISPER_LOCAL_DEFAULT_MODEL = "onnx-community/whisper-tiny.en";
4
+ /**
5
+ * Bytes a first download of the default model takes: the 8-bit encoder
6
+ * (~10 MB), merged decoder (~31 MB) and tokenizer/config files (~4 MB).
7
+ */
8
+ export declare const WHISPER_LOCAL_DEFAULT_SIZE_BYTES = 45100000;
9
+ /** A model this package knows by a short name. */
10
+ export interface LocalSpeechModelPreset {
11
+ /** Hugging Face model id. */
12
+ id: string;
13
+ /** Approximate first-download size in bytes (8-bit weights + tokenizer). */
14
+ bytes: number;
15
+ /** Transcribes English only (its decoder takes no `language`). */
16
+ englishOnly: boolean;
17
+ label: string;
18
+ }
19
+ /**
20
+ * Short names for `model`. Moonshine is the fastest for live dictation;
21
+ * Whisper `.en` is the longer-established English model.
22
+ */
23
+ export declare const LOCAL_SPEECH_MODELS: Readonly<Record<string, LocalSpeechModelPreset>>;
24
+ /** Resolve a short name (`moonshine-tiny`) or a Hugging Face id. */
25
+ export declare function resolveLocalSpeechModelId(model?: string): string;
26
+ /** Whether a model id only transcribes English. */
27
+ export declare function isEnglishOnlyLocalSpeechModel(modelId: string): boolean;
28
+ export type LocalSpeechDevice = 'webgpu' | 'wasm';
29
+ /** @deprecated Use {@link LocalSpeechDevice}. */
30
+ export type WhisperLocalDevice = LocalSpeechDevice;
31
+ export interface LocalSpeechModelOptions {
32
+ /**
33
+ * A short name (`'moonshine-tiny'`, `'moonshine-base'`, `'whisper-tiny.en'`,
34
+ * ...) or a Hugging Face model id. Default `onnx-community/whisper-tiny.en`.
35
+ */
36
+ model?: string;
37
+ /** Alias of `model`, kept from `createWhisperLocalModel`. */
38
+ modelId?: string;
39
+ /**
40
+ * `'auto'` (default): WebGPU when available, else single-thread WASM.
41
+ * With a worker, the worker entry decides (see `local-speech.worker.ts`).
42
+ */
43
+ device?: 'auto' | LocalSpeechDevice;
44
+ /** Weight quantisation. Default `'q8'`. Page-thread only, as `device`. */
45
+ dtype?: string;
46
+ /** Make the Web Worker (see `local-speech.worker.ts`). */
47
+ createWorker?: () => Worker;
48
+ /**
49
+ * Load `@happyvertical/speech/local`, e.g.
50
+ * `() => import('@happyvertical/speech/local')`. A bundled browser app
51
+ * needs it; plain `import()` of the bare name only works where Node (or
52
+ * an import map) resolves it.
53
+ */
54
+ loadSpeech?: () => Promise<unknown>;
55
+ /**
56
+ * Load `@huggingface/transformers` when running on the page's thread,
57
+ * e.g. `() => import('@huggingface/transformers')`.
58
+ */
59
+ loadModule?: () => Promise<unknown>;
60
+ allowLocalModels?: boolean;
61
+ }
62
+ /** @deprecated Use {@link LocalSpeechModelOptions}. */
63
+ export type WhisperLocalModelOptions = LocalSpeechModelOptions;
64
+ export interface LocalSpeechLoadOptions {
65
+ onProgress?: (progress: DownloadProgressInfo) => void;
66
+ /**
67
+ * Cancel the download. With a worker the download stops (the worker is
68
+ * ended); on the page's own thread the promise rejects and the files
69
+ * already fetched stay cached.
70
+ */
71
+ signal?: AbortSignal;
72
+ }
73
+ /** @deprecated Use {@link LocalSpeechLoadOptions}. */
74
+ export type WhisperLocalLoadOptions = LocalSpeechLoadOptions;
75
+ export type LocalSpeechModelState = 'idle' | 'loading' | 'ready' | 'error';
76
+ /** @deprecated Use {@link LocalSpeechModelState}. */
77
+ export type WhisperLocalModelState = LocalSpeechModelState;
78
+ export declare class LocalSpeechModel {
79
+ private readonly options;
80
+ readonly modelId: string;
81
+ private backend;
82
+ private loadPromise;
83
+ private listeners;
84
+ private _state;
85
+ constructor(options?: LocalSpeechModelOptions);
86
+ get state(): LocalSpeechModelState;
87
+ get ready(): boolean;
88
+ /** Transcribes English only (no `language` is sent to the engine). */
89
+ get englishOnly(): boolean;
90
+ /** Bytes a first download takes (an estimate; cached models take none). */
91
+ estimateSize(): number;
92
+ private emit;
93
+ private onEngineProgress;
94
+ private getBackend;
95
+ /** Whether the model files are already stored on this device. */
96
+ isCached(): Promise<boolean>;
97
+ /**
98
+ * Download (if needed) and prepare the model. Safe to call again: a ready
99
+ * model returns at once and a load in flight is shared.
100
+ */
101
+ load(options?: LocalSpeechLoadOptions): Promise<void>;
102
+ /** Transcribe 16 kHz mono PCM. The model must be loaded. */
103
+ transcribe(audio: Float32Array, language?: string): Promise<string>;
104
+ dispose(): void;
105
+ }
106
+ /** @deprecated Use {@link LocalSpeechModel}. */
107
+ export { LocalSpeechModel as WhisperLocalModel };
108
+ export declare function createLocalSpeechModel(options?: LocalSpeechModelOptions): LocalSpeechModel;
109
+ /** @deprecated Use {@link createLocalSpeechModel}. */
110
+ export declare const createWhisperLocalModel: typeof createLocalSpeechModel;
111
+ //# sourceMappingURL=local-speech-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-speech-model.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/stt/local-speech-model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,377 @@
1
+ /**
2
+ * The downloadable on-device speech model behind `whisper-local` and
3
+ * `moonshine`, as a handle a consent UI can drive before anything is spoken:
4
+ *
5
+ * ```ts
6
+ * const model = createLocalSpeechModel({ model: 'moonshine-tiny', createWorker });
7
+ * const bytes = model.estimateSize(); // "Download 32 MB?"
8
+ * if (await model.isCached()) { ... } // already on this device
9
+ * await model.load({ onProgress, signal }); // downloads, then ready
10
+ * createSttDictationSource({ type: 'moonshine', modelHandle: model });
11
+ * ```
12
+ *
13
+ * The engine is `@happyvertical/speech/local` (`LocalTranscriber`, or its
14
+ * worker host `serveLocalTranscriber` when `createWorker` is given). The
15
+ * files live in Cache Storage, so a later `load()` (even in a new tab) is
16
+ * quick. Passing the handle to an adapter shares the one loaded pipeline
17
+ * rather than loading a second copy.
18
+ *
19
+ * `@happyvertical/speech` and `@huggingface/transformers` are OPTIONAL
20
+ * peers. This module never imports them statically: a host supplies
21
+ * `loadSpeech` (and, without a worker, `loadModule`), or builds the worker
22
+ * from `@happyvertical/smrt-svelte/browser-ai/whisper-worker?worker`.
23
+ */
24
+ import { importOptional } from '@happyvertical/smrt-ui/utils/import-optional.js';
25
+ import { InitializationError } from '../../core/errors.js';
26
+ import { configureTransformersEnv } from './transformers-env.js';
27
+ /** Where the speech model comes from by default (kept for `whisper-local`). */
28
+ export const WHISPER_LOCAL_DEFAULT_MODEL = 'onnx-community/whisper-tiny.en';
29
+ /**
30
+ * Bytes a first download of the default model takes: the 8-bit encoder
31
+ * (~10 MB), merged decoder (~31 MB) and tokenizer/config files (~4 MB).
32
+ */
33
+ export const WHISPER_LOCAL_DEFAULT_SIZE_BYTES = 45_100_000;
34
+ /**
35
+ * Short names for `model`. Moonshine is the fastest for live dictation;
36
+ * Whisper `.en` is the longer-established English model.
37
+ */
38
+ export const LOCAL_SPEECH_MODELS = {
39
+ 'moonshine-tiny': {
40
+ id: 'onnx-community/moonshine-tiny-ONNX',
41
+ bytes: 32_200_000,
42
+ englishOnly: true,
43
+ label: 'Moonshine tiny',
44
+ },
45
+ 'moonshine-base': {
46
+ id: 'onnx-community/moonshine-base-ONNX',
47
+ bytes: 67_000_000,
48
+ englishOnly: true,
49
+ label: 'Moonshine base',
50
+ },
51
+ 'whisper-tiny.en': {
52
+ id: WHISPER_LOCAL_DEFAULT_MODEL,
53
+ bytes: WHISPER_LOCAL_DEFAULT_SIZE_BYTES,
54
+ englishOnly: true,
55
+ label: 'Whisper tiny (English)',
56
+ },
57
+ 'whisper-base.en': {
58
+ id: 'onnx-community/whisper-base.en',
59
+ bytes: 85_000_000,
60
+ englishOnly: true,
61
+ label: 'Whisper base (English)',
62
+ },
63
+ 'whisper-small.en': {
64
+ id: 'onnx-community/whisper-small.en',
65
+ bytes: 260_000_000,
66
+ englishOnly: true,
67
+ label: 'Whisper small (English)',
68
+ },
69
+ };
70
+ /** Resolve a short name (`moonshine-tiny`) or a Hugging Face id. */
71
+ export function resolveLocalSpeechModelId(model) {
72
+ if (!model)
73
+ return WHISPER_LOCAL_DEFAULT_MODEL;
74
+ return LOCAL_SPEECH_MODELS[model]?.id ?? model;
75
+ }
76
+ /** Whether a model id only transcribes English. */
77
+ export function isEnglishOnlyLocalSpeechModel(modelId) {
78
+ return (Object.values(LOCAL_SPEECH_MODELS).some((preset) => preset.id === modelId && preset.englishOnly) || /\.en$|moonshine/i.test(modelId));
79
+ }
80
+ const PCM_MIME = 'audio/pcm;rate=16000;channels=1;encoding=f32le';
81
+ function abortError() {
82
+ const error = new Error('Download cancelled');
83
+ error.name = 'AbortError';
84
+ return error;
85
+ }
86
+ /** `en-CA` -> `en`; Whisper wants the two-letter code. */
87
+ function languageCode(language) {
88
+ return language?.split(/[-_]/)[0]?.toLowerCase() || undefined;
89
+ }
90
+ function pcmRequest(pcm) {
91
+ // The worker takes ownership of what it is sent: hand over a copy.
92
+ const bytes = new Uint8Array(pcm.slice().buffer);
93
+ return { data: bytes, mimeType: PCM_MIME };
94
+ }
95
+ async function loadSpeechModule(loadSpeech) {
96
+ try {
97
+ return (await (loadSpeech
98
+ ? loadSpeech()
99
+ : importOptional('@happyvertical/speech/local')));
100
+ }
101
+ catch (error) {
102
+ throw new InitializationError('local-speech', '@happyvertical/speech could not be loaded. Install it (and ' +
103
+ '@huggingface/transformers) and pass ' +
104
+ '`loadSpeech: () => import("@happyvertical/speech/local")`.');
105
+ }
106
+ }
107
+ class WorkerBackend {
108
+ createWorker;
109
+ loadSpeech;
110
+ progress;
111
+ worker = null;
112
+ client = null;
113
+ constructor(createWorker, loadSpeech, progress) {
114
+ this.createWorker = createWorker;
115
+ this.loadSpeech = loadSpeech;
116
+ this.progress = progress;
117
+ }
118
+ ensure() {
119
+ this.client ??= loadSpeechModule(this.loadSpeech).then((speech) => {
120
+ const worker = this.createWorker();
121
+ this.worker = worker;
122
+ return new speech.LocalTranscriberWorkerClient(worker, {
123
+ onProgress: this.progress,
124
+ });
125
+ });
126
+ return this.client;
127
+ }
128
+ async isCached(model) {
129
+ const client = await this.ensure();
130
+ return client.isCached(model);
131
+ }
132
+ async preload(model, signal) {
133
+ if (signal?.aborted)
134
+ throw abortError();
135
+ const client = await this.ensure();
136
+ if (!signal)
137
+ return client.preload(model);
138
+ // The only way to stop a download in a worker is to end the worker.
139
+ const onAbort = () => this.dispose();
140
+ signal.addEventListener('abort', onAbort, { once: true });
141
+ try {
142
+ await Promise.race([
143
+ client.preload(model),
144
+ new Promise((_, reject) => signal.addEventListener('abort', () => reject(abortError()), {
145
+ once: true,
146
+ })),
147
+ ]);
148
+ }
149
+ finally {
150
+ signal.removeEventListener('abort', onAbort);
151
+ }
152
+ }
153
+ async transcribe(pcm, model, language) {
154
+ const client = await this.ensure();
155
+ const result = await client.transcribe({
156
+ audio: pcmRequest(pcm),
157
+ model,
158
+ language: languageCode(language),
159
+ });
160
+ return result.text.trim();
161
+ }
162
+ dispose() {
163
+ this.client?.then((client) => client.close()).catch(() => undefined);
164
+ this.worker?.terminate();
165
+ this.worker = null;
166
+ this.client = null;
167
+ }
168
+ }
169
+ class InPageBackend {
170
+ options;
171
+ model;
172
+ progress;
173
+ transcriber = null;
174
+ constructor(options, model, progress) {
175
+ this.options = options;
176
+ this.model = model;
177
+ this.progress = progress;
178
+ }
179
+ ensure() {
180
+ this.transcriber ??= loadSpeechModule(this.options.loadSpeech).then((speech) => new speech.LocalTranscriber({
181
+ model: this.model,
182
+ device: this.options.device ?? 'auto',
183
+ dtype: this.options.dtype ?? 'q8',
184
+ ...(this.options.loadModule
185
+ ? { transformers: this.options.loadModule }
186
+ : {}),
187
+ onProgress: this.progress,
188
+ configureEnv: configureTransformersEnv(this.options.allowLocalModels ?? false),
189
+ }));
190
+ return this.transcriber;
191
+ }
192
+ async isCached(model) {
193
+ const transcriber = await this.ensure();
194
+ return transcriber.isCached(model);
195
+ }
196
+ async preload(model, signal) {
197
+ if (signal?.aborted)
198
+ throw abortError();
199
+ const transcriber = await this.ensure();
200
+ // `preload` stops waiting on abort; the files fetched stay cached.
201
+ await transcriber.preload(model, signal).catch((error) => {
202
+ if (signal?.aborted)
203
+ throw abortError();
204
+ throw error;
205
+ });
206
+ }
207
+ async transcribe(pcm, model, language) {
208
+ const transcriber = await this.ensure();
209
+ const result = await transcriber.transcribe({
210
+ audio: pcmRequest(pcm),
211
+ model,
212
+ language: languageCode(language),
213
+ });
214
+ return result.text.trim();
215
+ }
216
+ dispose() {
217
+ this.transcriber?.then((t) => t.dispose()).catch(() => undefined);
218
+ this.transcriber = null;
219
+ }
220
+ }
221
+ export class LocalSpeechModel {
222
+ options;
223
+ modelId;
224
+ backend = null;
225
+ loadPromise = null;
226
+ listeners = new Set();
227
+ _state = 'idle';
228
+ constructor(options = {}) {
229
+ this.options = options;
230
+ this.modelId = resolveLocalSpeechModelId(options.model ?? options.modelId);
231
+ }
232
+ get state() {
233
+ return this._state;
234
+ }
235
+ get ready() {
236
+ return this._state === 'ready';
237
+ }
238
+ /** Transcribes English only (no `language` is sent to the engine). */
239
+ get englishOnly() {
240
+ return isEnglishOnlyLocalSpeechModel(this.modelId);
241
+ }
242
+ /** Bytes a first download takes (an estimate; cached models take none). */
243
+ estimateSize() {
244
+ return (Object.values(LOCAL_SPEECH_MODELS).find((p) => p.id === this.modelId)
245
+ ?.bytes ?? WHISPER_LOCAL_DEFAULT_SIZE_BYTES);
246
+ }
247
+ emit(progress) {
248
+ for (const listener of this.listeners)
249
+ listener(progress);
250
+ }
251
+ onEngineProgress = (event) => {
252
+ if (event.status !== 'progress_total' ||
253
+ typeof event.loaded !== 'number' ||
254
+ typeof event.total !== 'number') {
255
+ return;
256
+ }
257
+ const total = this.estimateSize();
258
+ const bytesTotal = event.total || total;
259
+ // All bytes are in: what remains is compiling the model ("Getting ready").
260
+ const finished = event.total > 0 && event.loaded >= event.total;
261
+ this.emit({
262
+ state: finished ? 'extracting' : 'downloading',
263
+ bytesLoaded: event.loaded,
264
+ bytesTotal,
265
+ percent: event.total ? Math.round((event.loaded / event.total) * 100) : 0,
266
+ currentFile: event.file,
267
+ });
268
+ };
269
+ getBackend() {
270
+ if (this.backend)
271
+ return this.backend;
272
+ const { createWorker, loadSpeech } = this.options;
273
+ this.backend = createWorker
274
+ ? new WorkerBackend(createWorker, loadSpeech, this.onEngineProgress)
275
+ : new InPageBackend(this.options, this.modelId, this.onEngineProgress);
276
+ return this.backend;
277
+ }
278
+ /** Whether the model files are already stored on this device. */
279
+ async isCached() {
280
+ if (this._state === 'ready')
281
+ return true;
282
+ try {
283
+ return await this.getBackend().isCached(this.modelId);
284
+ }
285
+ catch {
286
+ return false;
287
+ }
288
+ }
289
+ /**
290
+ * Download (if needed) and prepare the model. Safe to call again: a ready
291
+ * model returns at once and a load in flight is shared.
292
+ */
293
+ load(options = {}) {
294
+ const { onProgress, signal } = options;
295
+ if (this._state === 'ready')
296
+ return Promise.resolve();
297
+ if (onProgress)
298
+ this.listeners.add(onProgress);
299
+ if (!this.loadPromise) {
300
+ this._state = 'loading';
301
+ const total = this.estimateSize();
302
+ this.emit({
303
+ state: 'downloading',
304
+ bytesLoaded: 0,
305
+ bytesTotal: total,
306
+ percent: 0,
307
+ });
308
+ this.loadPromise = this.getBackend()
309
+ .preload(this.modelId, signal)
310
+ .then(() => {
311
+ this._state = 'ready';
312
+ this.emit({
313
+ state: 'complete',
314
+ bytesLoaded: total,
315
+ bytesTotal: total,
316
+ percent: 100,
317
+ });
318
+ })
319
+ .catch((error) => {
320
+ this._state = 'idle';
321
+ // A cancelled worker is gone; start fresh next time.
322
+ this.backend?.dispose();
323
+ this.backend = null;
324
+ const err = error instanceof Error ? error : new Error(String(error));
325
+ if (err.name !== 'AbortError') {
326
+ this._state = 'error';
327
+ this.emit({
328
+ state: 'error',
329
+ bytesLoaded: 0,
330
+ bytesTotal: total,
331
+ percent: 0,
332
+ error: err.message,
333
+ });
334
+ }
335
+ throw err;
336
+ })
337
+ .finally(() => {
338
+ this.loadPromise = null;
339
+ this.listeners.clear();
340
+ });
341
+ }
342
+ const shared = this.loadPromise;
343
+ if (!signal)
344
+ return shared;
345
+ // Each caller's own signal rejects its own promise; the load stops only
346
+ // when the backend can (worker) and the call that owns it aborts.
347
+ return new Promise((resolve, reject) => {
348
+ if (signal.aborted)
349
+ return reject(abortError());
350
+ signal.addEventListener('abort', () => reject(abortError()), {
351
+ once: true,
352
+ });
353
+ shared.then(resolve, reject);
354
+ });
355
+ }
356
+ /** Transcribe 16 kHz mono PCM. The model must be loaded. */
357
+ transcribe(audio, language) {
358
+ if (this._state !== 'ready') {
359
+ return Promise.reject(new Error('The speech model is not loaded'));
360
+ }
361
+ return this.getBackend().transcribe(audio, this.modelId, language);
362
+ }
363
+ dispose() {
364
+ this.backend?.dispose();
365
+ this.backend = null;
366
+ this.loadPromise = null;
367
+ this.listeners.clear();
368
+ this._state = 'idle';
369
+ }
370
+ }
371
+ /** @deprecated Use {@link LocalSpeechModel}. */
372
+ export { LocalSpeechModel as WhisperLocalModel };
373
+ export function createLocalSpeechModel(options = {}) {
374
+ return new LocalSpeechModel(options);
375
+ }
376
+ /** @deprecated Use {@link createLocalSpeechModel}. */
377
+ export const createWhisperLocalModel = createLocalSpeechModel;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * On-device speech-to-text (`whisper-local`, alias `whisper-wasm`, and
3
+ * `moonshine`).
4
+ *
5
+ * For browsers whose own speech recognition is missing or unreliable
6
+ * (Firefox, Brave). It is push-to-talk: `start()` records from the
7
+ * microphone, `stop()` ends the recording and transcribes it with a Whisper
8
+ * or Moonshine model running in the browser (WebGPU when available,
9
+ * single-thread WASM otherwise, in a Web Worker when the host supplies one),
10
+ * then emits one final result. `transcribePcm()` transcribes one finished
11
+ * utterance directly, which smrt-ui's hands-free dictation uses. The engine
12
+ * is `@happyvertical/speech/local`; the model is a one-time download kept in
13
+ * Cache Storage; see `LocalSpeechModel` for the consent/progress API.
14
+ */
15
+ import { type DictationAudioCaptureFactory } from '@happyvertical/smrt-ui/forms';
16
+ import type { InitState, OnProgress } from '../../core/types.js';
17
+ import { type LocalSpeechLoadOptions, type LocalSpeechModel } from './local-speech-model.js';
18
+ import type { LocalSpeechSTTOptions, STTAdapter, STTCapabilities, STTOptions, STTResult } from './types.js';
19
+ export declare class LocalSpeechSTTAdapter implements STTAdapter {
20
+ readonly type: 'whisper-local' | 'whisper-wasm' | 'moonshine';
21
+ /**
22
+ * `Dictation` waits this long for the text after the person stops.
23
+ */
24
+ readonly stopTimeoutMs = 90000;
25
+ private readonly options;
26
+ private readonly _model;
27
+ private readonly makeCapture;
28
+ private capture;
29
+ private captureOff;
30
+ private _isListening;
31
+ private language;
32
+ private session;
33
+ private starting;
34
+ private stopping;
35
+ private resultListeners;
36
+ private errorListeners;
37
+ private startListeners;
38
+ private endListeners;
39
+ constructor(options?: Partial<LocalSpeechSTTOptions> & {
40
+ /** Test seam: the microphone recorder. */
41
+ capture?: DictationAudioCaptureFactory;
42
+ });
43
+ /** The downloadable model, for consent UIs. */
44
+ get model(): LocalSpeechModel;
45
+ get initState(): InitState;
46
+ estimateSize(): number;
47
+ isCached(): Promise<boolean>;
48
+ load(options?: LocalSpeechLoadOptions): Promise<void>;
49
+ ensureInitialized(onProgress?: OnProgress): Promise<void>;
50
+ /**
51
+ * Make the model ready for dictation without ever downloading it: a model
52
+ * already loaded is used, one stored on this device is loaded from there,
53
+ * and anything else rejects with `ModelNotDownloadedError`. The host asks
54
+ * for consent and calls `load()` itself.
55
+ */
56
+ prepare(): Promise<void>;
57
+ getCapabilities(): STTCapabilities;
58
+ /**
59
+ * Transcribe one finished utterance (16 kHz mono PCM). Loads the model
60
+ * first if it is not ready. This is what hands-free dictation calls for
61
+ * each utterance the voice activity detector cuts out.
62
+ */
63
+ transcribePcm(pcm: Float32Array, options?: {
64
+ language?: string;
65
+ }): Promise<string>;
66
+ start(options?: STTOptions): Promise<void>;
67
+ /** End the recording, transcribe it, emit the final result, then end. */
68
+ stop(): Promise<void>;
69
+ private finish;
70
+ private emitError;
71
+ abort(): void;
72
+ isListening(): boolean;
73
+ onResult(callback: (result: STTResult) => void): () => void;
74
+ onError(callback: (error: Error) => void): () => void;
75
+ onStart(callback: () => void): () => void;
76
+ onEnd(callback: () => void): () => void;
77
+ dispose(): Promise<void>;
78
+ }
79
+ /** @deprecated Use {@link LocalSpeechSTTAdapter}. */
80
+ /** @deprecated Use {@link LocalSpeechSTTAdapter}. */
81
+ export { LocalSpeechSTTAdapter as WhisperLocalSTTAdapter, LocalSpeechSTTAdapter as WhisperWasmSTTAdapter, };
82
+ //# sourceMappingURL=local-speech.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-speech.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/stt/local-speech.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAGL,KAAK,4BAA4B,EAClC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EACV,qBAAqB,EACrB,UAAU,EACV,eAAe,EACf,UAAU,EACV,SAAS,EACV,MAAM,YAAY,CAAC;AAgCpB,qBAAa,qBAAsB,YAAW,UAAU;IACtD,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,cAAc,GAAG,WAAW,CAAC;IAC9D;;OAEG;IACH,QAAQ,CAAC,aAAa,SAAmB;IAEzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAA8B;IAE9C,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,YAAY,CAAyB;gBAG3C,OAAO,GAAE,OAAO,CAAC,qBAAqB,CAAC,GAAG;QACxC,0CAA0C;QAC1C,OAAO,CAAC,EAAE,4BAA4B,CAAC;KACnC;IA+BR,+CAA+C;IAC/C,IAAI,KAAK,IAAI,gBAAgB,CAE5B;IAED,IAAI,SAAS,IAAI,SAAS,CAWzB;IAED,YAAY,IAAI,MAAM;IAItB,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IAI5B,IAAI,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C,iBAAiB,CAAC,UAAU,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAM/D;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B,eAAe,IAAI,eAAe;IA0BlC;;;;OAIG;IACG,aAAa,CACjB,GAAG,EAAE,YAAY,EACjB,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAClC,OAAO,CAAC,MAAM,CAAC;IAQZ,KAAK,CAAC,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAwCpD,yEAAyE;IACzE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAsBP,MAAM;IAwBpB,OAAO,CAAC,SAAS;IAIjB,KAAK,IAAI,IAAI;IAab,WAAW,IAAI,OAAO;IAItB,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GAAG,MAAM,IAAI;IAK3D,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI;IAKrD,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKzC,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAS/B;AAED,qDAAqD;AACrD,qDAAqD;AACrD,OAAO,EACL,qBAAqB,IAAI,sBAAsB,EAC/C,qBAAqB,IAAI,qBAAqB,GAC/C,CAAC"}