managarten/packages/local-stt/src/worker.ts
Till JS 3deee755b3 feat(web): PillNav bar mode, fullscreen, local STT + mic button
PillNav overhaul:
- Dropdown-as-bar: theme/AI/sync/user menus render as horizontal
  bars in the bottom stack (PillDropdownBar) instead of floating
  popovers. New onOpenBar/activeBarId props on PillNavigation.
- iconOnly pills: tags/search/workbench-tabs pills show only icons.
  Home pill removed. New iconOnly flag on PillNavItem.
- Segmented toggle groups: items sharing a `group` id render as a
  single segmented pill (e.g. Light/Dark/System triple).
- Fullscreen mode: press "f" to hide all bottom chrome, Esc to exit.
- QuickInputBar + bottom bar visibility toggles via new pills.
- Progress ring on AI trigger pill during model download
  (conic-gradient ::after, follows pill border-radius).

@mana/local-stt — new package for browser-local speech-to-text:
- Whisper models via transformers.js v4 (WebGPU + WASM fallback)
- Same Web Worker architecture as @mana/local-llm
- Two models: Whisper Tiny (150 MB) and Whisper Small (950 MB)
- Reactive Svelte 5 bindings (getLocalSttStatus, loadLocalStt, transcribe)

Voice-to-text integration:
- useLocalStt() composable: mic capture via AudioContext +
  ScriptProcessor, resample to 16kHz mono, feed into Whisper worker
- Mic button in QuickInputBar (leftAction slot) with
  recording/loading/transcribing states + pulse animation
- Transcribed text injected into InputBar via new injectedText prop
- STT model selector in AI bar alongside LLM tier controls

Also: vite.config.ts server.fs.allow expanded to monorepo root
so workspace package workers resolve in dev.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:05:43 +02:00

96 lines
2.9 KiB
TypeScript

/**
* Web Worker entry point for @mana/local-stt.
*
* Runs in a Dedicated Worker context, owns a single LocalSttEngineImpl
* instance, and exchanges messages with the main thread proxy (engine.ts).
*
* Protocol:
*
* Main → Worker (WorkerRequest):
* { id, type: 'load', modelKey: ModelKey }
* { id, type: 'unload' }
* { id, type: 'transcribe', opts: SerializableTranscribeOptions }
* { id, type: 'isReady' }
*
* Worker → Main (WorkerResponse):
* { id, type: 'result', data?: unknown }
* { id, type: 'error', message: string }
* { id, type: 'chunk', text: string } — streaming chunk
* { type: 'status', status: LoadingStatus } — broadcast, no id
*/
import { LocalSttEngineImpl } from './engine-impl';
import type { LoadingStatus, TranscribeOptions } from './types';
import type { ModelKey } from './models';
// ─── Protocol types (mirrored in engine.ts) ────────────────────
export type SerializableTranscribeOptions = Omit<TranscribeOptions, 'onChunk'>;
export type WorkerRequest =
| { id: string; type: 'load'; modelKey: ModelKey }
| { id: string; type: 'unload' }
| { id: string; type: 'transcribe'; opts: SerializableTranscribeOptions }
| { id: string; type: 'isReady' };
export type WorkerResponse =
| { id: string; type: 'result'; data?: unknown }
| { id: string; type: 'error'; message: string }
| { id: string; type: 'chunk'; text: string }
| { type: 'status'; status: LoadingStatus };
// ─── Worker setup ──────────────────────────────────────────────
const engine = new LocalSttEngineImpl();
// Forward all status changes to the main thread as broadcast messages.
engine.onStatusChange((status) => {
postMessage({ type: 'status', status } satisfies WorkerResponse);
});
self.addEventListener('message', async (event: MessageEvent<WorkerRequest>) => {
const req = event.data;
try {
switch (req.type) {
case 'load': {
await engine.load(req.modelKey);
postMessage({ id: req.id, type: 'result' } satisfies WorkerResponse);
break;
}
case 'unload': {
await engine.unload();
postMessage({ id: req.id, type: 'result' } satisfies WorkerResponse);
break;
}
case 'isReady': {
postMessage({
id: req.id,
type: 'result',
data: engine.isReady,
} satisfies WorkerResponse);
break;
}
case 'transcribe': {
const result = await engine.transcribe({
...req.opts,
onChunk: (text) => {
postMessage({ id: req.id, type: 'chunk', text } satisfies WorkerResponse);
},
});
postMessage({
id: req.id,
type: 'result',
data: result,
} satisfies WorkerResponse);
break;
}
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
postMessage({ id: req.id, type: 'error', message } satisfies WorkerResponse);
}
});