@tryhamster/gerbil 1.0.0-rc.9 → 1.0.0
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.
- package/LICENSE +1 -1
- package/README.md +247 -84
- package/dist/architectures-C1I5V3Dt.mjs +6070 -0
- package/dist/architectures-C1I5V3Dt.mjs.map +1 -0
- package/dist/browser/index.d.ts +264 -588
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +585 -2334
- package/dist/browser/index.js.map +1 -1
- package/dist/cli.mjs +625 -1098
- package/dist/cli.mjs.map +1 -1
- package/dist/defaults-9komdrbY.mjs +24 -0
- package/dist/defaults-9komdrbY.mjs.map +1 -0
- package/dist/frameworks/express.d.mts +1 -3
- package/dist/frameworks/express.d.mts.map +1 -1
- package/dist/frameworks/express.mjs +7 -7
- package/dist/frameworks/express.mjs.map +1 -1
- package/dist/frameworks/fastify.d.mts +1 -1
- package/dist/frameworks/fastify.d.mts.map +1 -1
- package/dist/frameworks/fastify.mjs +3 -3
- package/dist/frameworks/fastify.mjs.map +1 -1
- package/dist/frameworks/hono.d.mts +1 -1
- package/dist/frameworks/hono.d.mts.map +1 -1
- package/dist/frameworks/hono.mjs +4 -4
- package/dist/frameworks/hono.mjs.map +1 -1
- package/dist/frameworks/next.d.mts +3 -2
- package/dist/frameworks/next.d.mts.map +1 -1
- package/dist/frameworks/next.mjs +4 -4
- package/dist/frameworks/next.mjs.map +1 -1
- package/dist/frameworks/react.d.mts +1 -1
- package/dist/frameworks/trpc.d.mts +1 -1
- package/dist/frameworks/trpc.d.mts.map +1 -1
- package/dist/frameworks/trpc.mjs +4 -4
- package/dist/frameworks/trpc.mjs.map +1 -1
- package/dist/gerbil-BHrJJIa4.mjs +1656 -0
- package/dist/gerbil-BHrJJIa4.mjs.map +1 -0
- package/dist/gerbil-BT9fCydo.d.mts +488 -0
- package/dist/gerbil-BT9fCydo.d.mts.map +1 -0
- package/dist/gerbil-DomNfIr1.mjs +4 -0
- package/dist/gpu/hooks.d.mts +520 -0
- package/dist/gpu/hooks.d.mts.map +1 -0
- package/dist/gpu/hooks.mjs +1188 -0
- package/dist/gpu/hooks.mjs.map +1 -0
- package/dist/gpu/index.d.mts +2 -0
- package/dist/gpu/index.mjs +6 -0
- package/dist/gpu-33qCAtHW.mjs +3615 -0
- package/dist/gpu-33qCAtHW.mjs.map +1 -0
- package/dist/index-Dgmb2kE3.d.mts +245 -0
- package/dist/index-Dgmb2kE3.d.mts.map +1 -0
- package/dist/index-jEAL2s-A.d.mts +2022 -0
- package/dist/index-jEAL2s-A.d.mts.map +1 -0
- package/dist/index.d.mts +22 -487
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +13 -8
- package/dist/index.mjs.map +1 -1
- package/dist/indexeddb-store-BWIMtxxH.mjs +103 -0
- package/dist/indexeddb-store-BWIMtxxH.mjs.map +1 -0
- package/dist/indexeddb-store-ClH12Xnl.mjs +4 -0
- package/dist/integrations/ai-sdk.d.mts +75 -6
- package/dist/integrations/ai-sdk.d.mts.map +1 -1
- package/dist/integrations/ai-sdk.mjs +131 -15
- package/dist/integrations/ai-sdk.mjs.map +1 -1
- package/dist/integrations/langchain.d.mts +1 -1
- package/dist/integrations/langchain.d.mts.map +1 -1
- package/dist/integrations/langchain.mjs +5 -5
- package/dist/integrations/langchain.mjs.map +1 -1
- package/dist/integrations/llamaindex.d.mts +1 -1
- package/dist/integrations/llamaindex.d.mts.map +1 -1
- package/dist/integrations/llamaindex.mjs +5 -5
- package/dist/integrations/llamaindex.mjs.map +1 -1
- package/dist/integrations/mcp-client.mjs +3 -3
- package/dist/integrations/mcp-client.mjs.map +1 -1
- package/dist/integrations/mcp.d.mts +3 -2
- package/dist/integrations/mcp.d.mts.map +1 -1
- package/dist/integrations/mcp.mjs +5 -5
- package/dist/{mcp-BvbriaBy.mjs → mcp-1DaMsaBc.mjs} +4 -4
- package/dist/mcp-1DaMsaBc.mjs.map +1 -0
- package/dist/memory/index.d.mts +3 -0
- package/dist/memory/index.mjs +6 -0
- package/dist/memory-D1P7Tmda.mjs +4 -0
- package/dist/memory-DVN0MnIG.mjs +132 -0
- package/dist/memory-DVN0MnIG.mjs.map +1 -0
- package/dist/memory-Dj0J1v88.mjs +294 -0
- package/dist/memory-Dj0J1v88.mjs.map +1 -0
- package/dist/moonshine-stt-BLyVoRpB.mjs +4 -0
- package/dist/moonshine-stt-v_P_Ci_m.mjs +11936 -0
- package/dist/moonshine-stt-v_P_Ci_m.mjs.map +1 -0
- package/dist/{one-liner-s-lD8rCC.mjs → one-liner-DnQn7HJK.mjs} +14 -16
- package/dist/one-liner-DnQn7HJK.mjs.map +1 -0
- package/dist/repl-jV5gcJFA.mjs +9 -0
- package/dist/skills/index.d.mts +270 -320
- package/dist/skills/index.d.mts.map +1 -1
- package/dist/skills/index.mjs +5 -5
- package/dist/{skills-CD3Orlex.mjs → skills-DX8D59UH.mjs} +187 -32
- package/dist/skills-DX8D59UH.mjs.map +1 -0
- package/dist/{tools-Bi1P7Xoy.mjs → tools-DQ1mPUw5.mjs} +34 -22
- package/dist/tools-DQ1mPUw5.mjs.map +1 -0
- package/dist/{types-CiTc7ez3.d.mts → types-D6FiR_oh.d.mts} +106 -12
- package/dist/types-D6FiR_oh.d.mts.map +1 -0
- package/dist/types-DQBe2lFo.d.mts +165 -0
- package/dist/types-DQBe2lFo.d.mts.map +1 -0
- package/dist/{utils-CZBZ8dgR.mjs → utils-DKO55ZmZ.mjs} +1 -1
- package/dist/{utils-CZBZ8dgR.mjs.map → utils-DKO55ZmZ.mjs.map} +1 -1
- package/dist/vector-B0panuy6.mjs +95 -0
- package/dist/vector-B0panuy6.mjs.map +1 -0
- package/docs/PROJECT-STATE.md +321 -0
- package/docs/adding-a-model-family.md +280 -0
- package/docs/ai-sdk.md +70 -61
- package/docs/architecture/overview.md +17 -7
- package/docs/browser.md +203 -8
- package/docs/embeddings.md +156 -0
- package/docs/gerbil-site-native-migration.md +217 -0
- package/docs/gpu-engine/architectures.md +398 -0
- package/docs/gpu-engine/ir.md +372 -0
- package/docs/gpu-engine/kernels.md +718 -0
- package/docs/gpu-engine/paper.html +1759 -0
- package/docs/gpu-engine/paper.md +2109 -0
- package/docs/gpu-engine/safetensors.md +312 -0
- package/docs/gpu-engine/tokenizer.md +302 -0
- package/docs/memory-rag.md +91 -0
- package/docs/metal-safari-intel.md +190 -0
- package/docs/mobile-failure-diagnosis.md +124 -0
- package/docs/mobile.md +99 -0
- package/docs/observability.md +230 -0
- package/docs/onnx-removal-plan.md +339 -0
- package/docs/research/autoresearch-portable.md +904 -0
- package/docs/research/dispatch-reduction-hivemind.md +84 -0
- package/docs/research/ios-safari-model-caching.md +117 -0
- package/docs/research/mobile-webgpu-speed-fusion.md +135 -0
- package/docs/research/native-stt-model-selection.md +49 -0
- package/docs/research/native-tts-model-selection.md +90 -0
- package/docs/research/native-vs-chromium-decision.md +152 -0
- package/docs/research/nemotron-mamba2-inference.md +910 -0
- package/docs/research/qwen35-multimodal.md +293 -0
- package/docs/research/qwen36-gemma4-targets.md +337 -0
- package/docs/research/sota-embedding-models.md +179 -0
- package/docs/research/sota-mobile-models-2026.md +263 -0
- package/docs/research/sota-modality-models.md +202 -0
- package/docs/research/tps-baselines.md +71 -0
- package/docs/research/webgpu-m4-reference.md +104 -0
- package/docs/site-update-plan.md +155 -0
- package/docs/structured-output.md +123 -0
- package/docs/stt.md +63 -446
- package/docs/tts.md +77 -499
- package/docs/vision.md +100 -338
- package/package.json +22 -7
- package/dist/chrome-backend-CORwaIyC.mjs +0 -1212
- package/dist/chrome-backend-CORwaIyC.mjs.map +0 -1
- package/dist/chrome-backend-DIKYoWj-.mjs +0 -3
- package/dist/gerbil-CJ3ifloF.mjs +0 -4
- package/dist/gerbil-Dw4Qj77e.mjs +0 -1631
- package/dist/gerbil-Dw4Qj77e.mjs.map +0 -1
- package/dist/gerbil-qOTe1nl2.d.mts +0 -431
- package/dist/gerbil-qOTe1nl2.d.mts.map +0 -1
- package/dist/kokoro-BNTb6egA.mjs +0 -20210
- package/dist/kokoro-BNTb6egA.mjs.map +0 -1
- package/dist/kokoro-CMOGDSgT.js +0 -20212
- package/dist/kokoro-CMOGDSgT.js.map +0 -1
- package/dist/mcp-BvbriaBy.mjs.map +0 -1
- package/dist/one-liner-s-lD8rCC.mjs.map +0 -1
- package/dist/repl-DveXw36T.mjs +0 -9
- package/dist/skills-CD3Orlex.mjs.map +0 -1
- package/dist/stt-Bu-E23Sc.js +0 -433
- package/dist/stt-Bu-E23Sc.js.map +0 -1
- package/dist/stt-CpLYbGFd.mjs +0 -433
- package/dist/stt-CpLYbGFd.mjs.map +0 -1
- package/dist/stt-DRPLEEHB.mjs +0 -3
- package/dist/tools-Bi1P7Xoy.mjs.map +0 -1
- package/dist/transformers.web-DiD1gTwk.js +0 -44695
- package/dist/transformers.web-DiD1gTwk.js.map +0 -1
- package/dist/transformers.web-u34VxRFM.js +0 -3
- package/dist/tts-CqroPaSK.js +0 -724
- package/dist/tts-CqroPaSK.js.map +0 -1
- package/dist/tts-DXgsKGCe.mjs +0 -3
- package/dist/tts-DeGANMNV.mjs +0 -730
- package/dist/tts-DeGANMNV.mjs.map +0 -1
- package/dist/types-CiTc7ez3.d.mts.map +0 -1
- /package/dist/{auto-update-S9s5-g0C.mjs → auto-update-BVaLXcDE.mjs} +0 -0
- /package/dist/{chunk-CkXuGtQK.mjs → chunk-B9cbKln6.mjs} +0 -0
- /package/dist/{microphone-DaMZFRuR.mjs → microphone-Bqmoz9_K.mjs} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["BUILTIN_MODELS: Record<string, ModelConfig>","currentResolve: ((text: string) => void) | null","currentReject: ((error: Error) => void) | null","gerbilWorker: GerbilWorker","options","userMessage: Message","assistantMessage: Message","KOKORO_BROWSER_VOICES: BrowserVoiceInfo[]","SUPERTONIC_BROWSER_VOICES: BrowserVoiceInfo[]","TTS_MODELS: Record<\n TTSModelId,\n { repo: string; defaultVoice: string; sampleRate: number; voices: BrowserVoiceInfo[] }\n>","audioData: Float32Array","sampleRate: number","audioContext: AudioContext | null","progress: STTProgress","e: any"],"sources":["../../src/core/models.ts","../../src/browser/index.ts"],"sourcesContent":["/**\n * Model Registry\n *\n * Supports built-in models and any HuggingFace model via hf:org/model syntax\n */\n\nimport type { ModelConfig, ModelSource } from \"./types.js\";\n\n// ============================================\n// Built-in Models (curated & tested)\n// ============================================\n\nexport const BUILTIN_MODELS: Record<string, ModelConfig> = {\n \"qwen3-0.6b\": {\n id: \"qwen3-0.6b\",\n repo: \"onnx-community/Qwen3-0.6B-ONNX\",\n description: \"Qwen3 0.6B - Best balance of speed and quality, supports thinking\",\n size: \"~400MB\",\n contextLength: 32_768,\n supportsThinking: true,\n supportsJson: true,\n family: \"qwen\",\n },\n \"qwen2.5-0.5b\": {\n id: \"qwen2.5-0.5b\",\n repo: \"onnx-community/Qwen2.5-0.5B-Instruct\",\n description: \"Qwen2.5 0.5B - Fast and capable\",\n size: \"~350MB\",\n contextLength: 32_768,\n supportsThinking: false,\n supportsJson: true,\n family: \"qwen\",\n },\n \"qwen2.5-coder-0.5b\": {\n id: \"qwen2.5-coder-0.5b\",\n repo: \"onnx-community/Qwen2.5-Coder-0.5B-Instruct\",\n description: \"Qwen2.5 Coder 0.5B - Optimized for code\",\n size: \"~400MB\",\n contextLength: 32_768,\n supportsThinking: false,\n supportsJson: true,\n family: \"qwen\",\n },\n \"smollm2-360m\": {\n id: \"smollm2-360m\",\n repo: \"HuggingFaceTB/SmolLM2-360M-Instruct\",\n description: \"SmolLM2 360M - Fast, good for simple tasks\",\n size: \"~250MB\",\n contextLength: 8192,\n supportsThinking: false,\n supportsJson: false,\n family: \"smollm\",\n },\n \"smollm2-135m\": {\n id: \"smollm2-135m\",\n repo: \"HuggingFaceTB/SmolLM2-135M-Instruct\",\n description: \"SmolLM2 135M - Fastest, basic generation\",\n size: \"~100MB\",\n contextLength: 8192,\n supportsThinking: false,\n supportsJson: false,\n family: \"smollm\",\n },\n \"phi-3-mini\": {\n id: \"phi-3-mini\",\n repo: \"microsoft/Phi-3-mini-4k-instruct-onnx\",\n description: \"Phi-3 Mini - High quality, larger model\",\n size: \"~2.1GB\",\n contextLength: 4096,\n supportsThinking: false,\n supportsJson: true,\n family: \"phi\",\n },\n \"ministral-3b\": {\n id: \"ministral-3b\",\n repo: \"mistralai/Ministral-3-3B-Instruct-2512-ONNX\",\n description: \"Ministral 3 3B - Vision + Reasoning, 256k context\",\n size: \"~2.5GB\",\n contextLength: 262_144,\n supportsThinking: true,\n supportsJson: true,\n supportsVision: true,\n visionEncoderSize: \"0.4B\",\n family: \"mistral\",\n },\n};\n\n// ============================================\n// Model Resolution\n// ============================================\n\n/**\n * Parse model identifier and resolve to source\n *\n * Supported formats:\n * - \"qwen3-0.6b\" (built-in)\n * - \"hf:org/model\" (HuggingFace shorthand)\n * - \"https://huggingface.co/org/model\" (full URL)\n * - \"file:./path/to/model\" (local path)\n */\nexport function resolveModel(modelId: string): ModelSource {\n // Built-in model\n if (BUILTIN_MODELS[modelId]) {\n return {\n type: \"builtin\",\n path: BUILTIN_MODELS[modelId].repo,\n };\n }\n\n // HuggingFace shorthand: hf:org/model\n if (modelId.startsWith(\"hf:\")) {\n const repo = modelId.slice(3);\n return {\n type: \"huggingface\",\n path: repo,\n };\n }\n\n // HuggingFace URL\n if (modelId.startsWith(\"https://huggingface.co/\")) {\n const repo = modelId.replace(\"https://huggingface.co/\", \"\");\n return {\n type: \"huggingface\",\n path: repo,\n };\n }\n\n // Local file\n if (modelId.startsWith(\"file:\")) {\n const path = modelId.slice(5);\n return {\n type: \"local\",\n path,\n };\n }\n\n // Assume it's a HuggingFace repo if it contains a slash\n if (modelId.includes(\"/\")) {\n return {\n type: \"huggingface\",\n path: modelId,\n };\n }\n\n // Unknown - treat as HuggingFace\n return {\n type: \"huggingface\",\n path: modelId,\n };\n}\n\n/**\n * Get model config (built-in only)\n */\nexport function getModelConfig(modelId: string): ModelConfig | null {\n return BUILTIN_MODELS[modelId] || null;\n}\n\n// Default context lengths by model family (when config.json is unavailable)\nconst FAMILY_CONTEXT_DEFAULTS: Record<string, number> = {\n qwen: 32_768,\n mistral: 262_144, // Ministral models support up to 256K\n llama: 8192,\n phi: 4096,\n smollm: 8192,\n other: 4096,\n};\n\n/**\n * Create model config for external model\n */\nexport function createExternalModelConfig(\n modelId: string,\n repo: string,\n contextLength?: number,\n): ModelConfig {\n // Try to infer family from repo name\n let family: ModelConfig[\"family\"] = \"other\";\n const repoLower = repo.toLowerCase();\n\n if (repoLower.includes(\"qwen\")) {\n family = \"qwen\";\n } else if (repoLower.includes(\"smollm\")) {\n family = \"smollm\";\n } else if (repoLower.includes(\"phi\")) {\n family = \"phi\";\n } else if (repoLower.includes(\"mistral\") || repoLower.includes(\"ministral\")) {\n family = \"mistral\";\n } else if (repoLower.includes(\"llama\")) {\n family = \"llama\";\n }\n\n // Detect vision models from common patterns\n const supportsVision =\n repoLower.includes(\"vision\") ||\n repoLower.includes(\"vlm\") ||\n repoLower.includes(\"image-text\") ||\n repoLower.includes(\"ministral\");\n\n return {\n id: modelId,\n repo,\n description: `External model: ${repo}`,\n size: \"Unknown\",\n contextLength: contextLength || FAMILY_CONTEXT_DEFAULTS[family] || 4096,\n supportsThinking: family === \"qwen\" || family === \"mistral\",\n supportsJson: family === \"qwen\" || family === \"phi\" || family === \"mistral\",\n supportsVision,\n family,\n };\n}\n\n/**\n * Fetch context length from HuggingFace model config\n */\nexport async function fetchModelContextLength(repo: string): Promise<number | null> {\n try {\n const res = await fetch(`https://huggingface.co/${repo}/raw/main/config.json`);\n if (!res.ok) {\n return null;\n }\n\n const config = await res.json();\n\n // Different models use different field names\n return (\n config.max_position_embeddings ||\n config.n_positions ||\n config.max_seq_len ||\n config.sliding_window || // Some models use this\n config.context_length ||\n null\n );\n } catch {\n return null;\n }\n}\n\n/**\n * List all built-in models\n */\nexport function listBuiltinModels(): ModelConfig[] {\n return Object.values(BUILTIN_MODELS);\n}\n\n/**\n * Search HuggingFace models (placeholder - would need HF API)\n */\nexport async function searchModels(query: string): Promise<ModelConfig[]> {\n // TODO: Implement HuggingFace API search\n // For now, filter built-in models\n const q = query.toLowerCase();\n return listBuiltinModels().filter(\n (m) =>\n m.id.toLowerCase().includes(q) ||\n m.description.toLowerCase().includes(q) ||\n m.family.toLowerCase().includes(q),\n );\n}\n","/**\n * Gerbil Browser Support\n *\n * Run LLMs directly in the browser with WebGPU acceleration.\n *\n * @example useChat (React)\n * ```tsx\n * import { useChat } from \"@tryhamster/gerbil/browser\";\n *\n * function Chat() {\n * const { messages, input, setInput, handleSubmit, isLoading } = useChat();\n *\n * if (isLoading) return <div>Loading model...</div>;\n *\n * return (\n * <form onSubmit={handleSubmit}>\n * {messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}\n * <input value={input} onChange={e => setInput(e.target.value)} />\n * </form>\n * );\n * }\n * ```\n *\n * @example useCompletion (React)\n * ```tsx\n * import { useCompletion } from \"@tryhamster/gerbil/browser\";\n *\n * function App() {\n * const { complete, completion, isLoading } = useCompletion();\n * if (isLoading) return <div>Loading...</div>;\n * return <button onClick={() => complete(\"Write a haiku\")}>{completion}</button>;\n * }\n * ```\n *\n * @example Low-level API\n * ```ts\n * import { createGerbilWorker } from \"@tryhamster/gerbil/browser\";\n *\n * const gerbil = await createGerbilWorker({\n * modelId: \"qwen3-0.6b\",\n * onToken: (token) => console.log(token.text),\n * });\n * await gerbil.generate(\"Hello!\");\n * gerbil.terminate();\n * ```\n */\n\nimport { resolveModel } from \"../core/models.js\";\n\n// Re-export models and types (browser-safe, no Node.js dependencies)\nexport { BUILTIN_MODELS } from \"../core/models.js\";\nexport type * from \"../core/types.js\";\n\n// NOTE: We intentionally do NOT export Gerbil from core here.\n// The core Gerbil class has Node.js code paths (chrome-backend/puppeteer)\n// that break browser bundlers. Use createGerbilWorker() instead for browser.\n\n// ============================================\n// Types\n// ============================================\n\nexport type WorkerProgress = {\n status: \"loading\" | \"downloading\" | \"ready\" | \"error\";\n message?: string;\n file?: string;\n progress?: number;\n /** Number of files being downloaded (0 = loading from cache) */\n downloadCount?: number;\n /** Total files to process */\n totalFiles?: number;\n error?: string;\n};\n\nexport type WorkerToken = {\n status: \"token\";\n text: string;\n state: \"thinking\" | \"answering\";\n numTokens: number;\n tps: number;\n};\n\nexport type WorkerComplete = {\n status: \"complete\";\n text: string;\n numTokens: number;\n totalTime: number;\n tps: number;\n};\n\nexport type GerbilWorkerOptions = {\n /** Model ID to load (default: \"qwen3-0.6b\") */\n modelId?: string;\n /** Called during model loading with progress updates */\n onProgress?: (progress: WorkerProgress) => void;\n /** Called for each token during streaming generation */\n onToken?: (token: WorkerToken) => void;\n /** Called when generation is complete */\n onComplete?: (result: WorkerComplete) => void;\n /** Called on errors */\n onError?: (error: string) => void;\n /** Worker script URL (auto-detected if not provided) */\n workerUrl?: string;\n};\n\nexport type GenerateStreamOptions = {\n /** Maximum tokens to generate */\n maxTokens?: number;\n /** Temperature for sampling (0 = deterministic) */\n temperature?: number;\n /** Top-p nucleus sampling */\n topP?: number;\n /** Top-k sampling */\n topK?: number;\n /** Enable thinking mode (Qwen3) */\n thinking?: boolean;\n /** System prompt */\n system?: string;\n /** Image URLs or data URIs (for vision models) */\n images?: string[];\n /** Conversation history for multi-turn (includes all previous messages) */\n history?: Array<{ role: \"user\" | \"assistant\" | \"system\"; content: string }>;\n};\n\nexport type GerbilWorker = {\n /** Generate text with streaming */\n generate: (prompt: string, options?: GenerateStreamOptions) => Promise<string>;\n /** Interrupt current generation */\n interrupt: () => void;\n /** Reset conversation cache */\n reset: () => void;\n /** Terminate the worker */\n terminate: () => void;\n /** Check if model is loaded */\n isReady: () => boolean;\n};\n\n// ============================================\n// Web Worker Factory\n// ============================================\n\n/**\n * Create a Gerbil worker for streaming WebGPU inference\n *\n * Uses a Web Worker to keep the UI responsive during model loading\n * and text generation, with real-time token streaming.\n */\nexport async function createGerbilWorker(options: GerbilWorkerOptions = {}): Promise<GerbilWorker> {\n const { modelId = \"qwen3-0.6b\", onProgress, onToken, onComplete, onError } = options;\n\n // Resolve model to HuggingFace path\n const source = resolveModel(modelId);\n\n return new Promise((resolve, reject) => {\n // Create inline worker from the worker code\n const workerCode = `\n import {\n AutoTokenizer,\n AutoModelForCausalLM,\n AutoProcessor,\n AutoModelForImageTextToText,\n RawImage,\n TextStreamer,\n InterruptableStoppingCriteria,\n env,\n } from \"https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1\";\n\n // Enable IndexedDB caching for browser (prevents re-downloading models)\n env.useBrowserCache = true;\n env.allowLocalModels = false;\n\n class ModelPipeline {\n static tokenizer = null;\n static model = null;\n static processor = null;\n static visionModel = null;\n static modelId = \"\";\n static isVision = false;\n\n static async getInstance(modelId, options = {}, progressCallback) {\n if (this.modelId !== modelId) {\n this.tokenizer = null;\n this.model = null;\n this.processor = null;\n this.visionModel = null;\n }\n this.modelId = modelId;\n \n // Detect vision models\n this.isVision = options.vision || \n modelId.toLowerCase().includes(\"ministral\") ||\n modelId.toLowerCase().includes(\"vision\") ||\n modelId.toLowerCase().includes(\"vlm\");\n\n const dtype = options.dtype || \"q4f16\";\n const device = options.device || \"webgpu\";\n\n if (this.isVision) {\n // Load vision model components\n // Note: Don't specify dtype for vision models - let transformers.js pick defaults\n if (!this.processor) {\n this.processor = await AutoProcessor.from_pretrained(modelId, {\n progress_callback: progressCallback,\n });\n }\n if (!this.visionModel) {\n this.visionModel = await AutoModelForImageTextToText.from_pretrained(modelId, {\n device,\n progress_callback: progressCallback,\n });\n }\n return { \n processor: this.processor, \n model: this.visionModel, \n tokenizer: this.processor.tokenizer,\n isVision: true \n };\n } else {\n // Load text-only model components\n if (!this.tokenizer) {\n this.tokenizer = await AutoTokenizer.from_pretrained(modelId, {\n progress_callback: progressCallback,\n });\n }\n if (!this.model) {\n this.model = await AutoModelForCausalLM.from_pretrained(modelId, {\n dtype,\n device,\n progress_callback: progressCallback,\n });\n }\n return { \n tokenizer: this.tokenizer, \n model: this.model, \n isVision: false \n };\n }\n }\n }\n\n const stoppingCriteria = new InterruptableStoppingCriteria();\n let pastKeyValuesCache = null;\n\n async function load(data) {\n const { modelId, options = {} } = data;\n self.postMessage({ status: \"loading\", message: \"Loading model...\" });\n\n const downloadState = {\n downloading: new Set(),\n completed: new Set(),\n isDownloading: false,\n };\n\n try {\n const result = await ModelPipeline.getInstance(\n modelId,\n options,\n (progress) => {\n if (progress.status === \"progress\" && progress.file) {\n const pct = Math.round(progress.progress || 0);\n if (pct < 100) {\n downloadState.downloading.add(progress.file);\n downloadState.isDownloading = true;\n } else if (pct === 100) {\n downloadState.downloading.delete(progress.file);\n downloadState.completed.add(progress.file);\n }\n if (downloadState.isDownloading) {\n self.postMessage({\n status: \"downloading\",\n file: progress.file,\n progress: pct,\n downloadCount: downloadState.downloading.size,\n totalFiles: downloadState.completed.size + downloadState.downloading.size,\n });\n }\n }\n }\n );\n\n self.postMessage({ status: \"loading\", message: \"Compiling shaders...\" });\n \n // Warmup differs for vision vs text models\n if (result.isVision) {\n // Vision models need both text and vision warmup\n // Text warmup first\n const textWarmupInputs = result.tokenizer(\"hello\");\n await result.model.generate({ ...textWarmupInputs, max_new_tokens: 1 });\n \n // Vision warmup with synthetic image\n self.postMessage({ status: \"loading\", message: \"Warming up vision encoder...\" });\n try {\n // Create a tiny 8x8 test image using OffscreenCanvas\n const canvas = new OffscreenCanvas(8, 8);\n const ctx = canvas.getContext(\"2d\");\n ctx.fillStyle = \"red\";\n ctx.fillRect(0, 0, 8, 8);\n const blob = await canvas.convertToBlob({ type: \"image/png\" });\n const warmupImage = await RawImage.fromBlob(blob);\n \n // Process with vision pipeline\n const warmupContent = [{ type: \"image\" }, { type: \"text\", text: \"hi\" }];\n const warmupMessages = [{ role: \"user\", content: warmupContent }];\n const warmupPrompt = result.processor.apply_chat_template(warmupMessages, { add_generation_prompt: true });\n const warmupInputs = await result.processor(warmupImage, warmupPrompt, { add_special_tokens: false });\n \n // Run vision warmup generation\n await result.model.generate({\n ...warmupInputs,\n max_new_tokens: 1,\n });\n } catch (warmupErr) {\n console.warn(\"Vision warmup failed (non-fatal):\", warmupErr);\n }\n } else {\n const warmupInputs = result.tokenizer(\"a\");\n await result.model.generate({ ...warmupInputs, max_new_tokens: 1 });\n }\n\n self.postMessage({ status: \"ready\", isVision: result.isVision });\n } catch (error) {\n self.postMessage({ status: \"error\", error: error.message || String(error) });\n }\n }\n\n async function generate(data) {\n const { messages, images = [], options = {} } = data;\n const { maxTokens = 256, temperature = 0.7, topP = 0.9, topK = 20, thinking = false } = options;\n\n try {\n const result = await ModelPipeline.getInstance(ModelPipeline.modelId, {});\n \n // Route to vision or text generation\n if (result.isVision && images.length > 0) {\n await generateVision(result, messages, images, options);\n } else {\n await generateText(result, messages, options);\n }\n } catch (error) {\n self.postMessage({ status: \"error\", error: error.message || String(error) });\n }\n }\n\n async function generateText(result, messages, options) {\n const { maxTokens = 256, temperature = 0.7, topP = 0.9, topK = 20, thinking = false } = options;\n const { tokenizer, model } = result;\n\n const inputs = tokenizer.apply_chat_template(messages, {\n add_generation_prompt: true,\n return_dict: true,\n enable_thinking: thinking,\n });\n\n let state = \"answering\";\n const [START_THINKING_TOKEN_ID, END_THINKING_TOKEN_ID] = tokenizer.encode(\n \"<think></think>\",\n { add_special_tokens: false }\n );\n\n let startTime = null;\n let numTokens = 0;\n\n const tokenCallback = (tokens) => {\n startTime ??= performance.now();\n numTokens += 1;\n const tokenId = Number(tokens[0]);\n if (tokenId === START_THINKING_TOKEN_ID) state = \"thinking\";\n else if (tokenId === END_THINKING_TOKEN_ID) state = \"answering\";\n };\n\n const streamCallback = (text) => {\n const tps = startTime ? (numTokens / (performance.now() - startTime)) * 1000 : 0;\n self.postMessage({ status: \"token\", text, state, numTokens, tps });\n };\n\n const streamer = new TextStreamer(tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: streamCallback,\n token_callback_function: tokenCallback,\n });\n\n self.postMessage({ status: \"start\" });\n\n const { past_key_values, sequences } = await model.generate({\n ...inputs,\n past_key_values: pastKeyValuesCache,\n do_sample: temperature > 0,\n temperature: temperature > 0 ? temperature : undefined,\n top_p: topP,\n top_k: topK,\n max_new_tokens: maxTokens,\n streamer,\n stopping_criteria: stoppingCriteria,\n return_dict_in_generate: true,\n });\n\n pastKeyValuesCache = past_key_values;\n\n const endTime = performance.now();\n const totalTime = startTime ? endTime - startTime : 0;\n const decoded = tokenizer.batch_decode(sequences, { skip_special_tokens: true });\n\n self.postMessage({\n status: \"complete\",\n text: decoded[0] || \"\",\n numTokens,\n totalTime,\n tps: totalTime > 0 ? (numTokens / totalTime) * 1000 : 0,\n });\n }\n\n async function generateVision(result, messages, images, options) {\n const { maxTokens = 2048, temperature = 0.7, topP = 0.9, topK = 20 } = options;\n const { processor, model, tokenizer } = result;\n\n self.postMessage({ status: \"progress\", message: \"Preparing vision request...\" });\n\n // Build message content with image placeholders and text\n const lastMessage = messages[messages.length - 1];\n const content = [];\n for (const _ of images) {\n content.push({ type: \"image\" });\n }\n content.push({ type: \"text\", text: lastMessage.content });\n\n // For vision models, include a brief system instruction for concise responses\n // Note: Vision processors handle system differently than text models\n const visionMessages = [\n { role: \"system\", content: \"You are a helpful assistant. Be concise and direct in your responses.\" },\n { role: \"user\", content }\n ];\n\n // Apply chat template with generation prompt\n const chatPrompt = processor.apply_chat_template(visionMessages, {\n add_generation_prompt: true\n });\n\n // Load images (handle both string URLs and { source: string } objects)\n self.postMessage({ status: \"progress\", message: \"Loading images...\" });\n const loadedImages = await Promise.all(\n images.map(img => {\n const url = typeof img === \"string\" ? img : img.source;\n return RawImage.fromURL(url);\n })\n );\n self.postMessage({ status: \"progress\", message: \"Processing inputs...\" });\n\n // Process inputs\n const inputs = await processor(\n loadedImages.length === 1 ? loadedImages[0] : loadedImages,\n chatPrompt,\n { add_special_tokens: false }\n );\n self.postMessage({ status: \"progress\", message: \"Generating response...\" });\n\n let startTime = null;\n let numTokens = 0;\n\n const streamCallback = (text) => {\n startTime ??= performance.now();\n numTokens += 1;\n const tps = (numTokens / (performance.now() - startTime)) * 1000;\n self.postMessage({ status: \"token\", text, state: \"answering\", numTokens, tps });\n };\n\n const streamer = new TextStreamer(tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: streamCallback,\n });\n\n self.postMessage({ status: \"start\" });\n\n const outputs = await model.generate({\n ...inputs,\n max_new_tokens: maxTokens,\n do_sample: temperature > 0,\n temperature: temperature > 0 ? temperature : undefined,\n top_p: topP,\n top_k: topK,\n streamer,\n stopping_criteria: stoppingCriteria,\n });\n\n // Decode output (skip prompt)\n const inputLength = inputs.input_ids.dims?.at(-1) || 0;\n const decoded = processor.batch_decode(\n outputs.slice(null, [inputLength, null]),\n { skip_special_tokens: true }\n );\n\n const endTime = performance.now();\n const totalTime = startTime ? endTime - startTime : 0;\n\n self.postMessage({\n status: \"complete\",\n text: decoded[0] || \"\",\n numTokens,\n totalTime,\n tps: totalTime > 0 ? (numTokens / totalTime) * 1000 : 0,\n });\n }\n\n self.addEventListener(\"message\", async (e) => {\n const { type, ...data } = e.data;\n switch (type) {\n case \"load\": await load(data); break;\n case \"generate\": stoppingCriteria.reset(); await generate(data); break;\n case \"interrupt\": stoppingCriteria.interrupt(); break;\n case \"reset\": pastKeyValuesCache = null; stoppingCriteria.reset(); break;\n }\n });\n\n self.postMessage({ status: \"init\" });\n `;\n\n const blob = new Blob([workerCode], { type: \"application/javascript\" });\n const workerUrl = URL.createObjectURL(blob);\n const worker = new Worker(workerUrl, { type: \"module\" });\n\n let isReady = false;\n let currentResolve: ((text: string) => void) | null = null;\n let currentReject: ((error: Error) => void) | null = null;\n let _generatedText = \"\";\n\n worker.onmessage = (e) => {\n const msg = e.data;\n\n switch (msg.status) {\n case \"init\":\n // Worker initialized, load the model\n worker.postMessage({ type: \"load\", modelId: source.path });\n break;\n\n case \"loading\":\n case \"downloading\":\n onProgress?.(msg as WorkerProgress);\n break;\n\n case \"ready\":\n isReady = true;\n onProgress?.(msg as WorkerProgress);\n resolve(gerbilWorker);\n break;\n\n case \"start\":\n _generatedText = \"\";\n break;\n\n case \"token\":\n _generatedText += msg.text;\n onToken?.(msg as WorkerToken);\n break;\n\n case \"complete\":\n onComplete?.(msg as WorkerComplete);\n currentResolve?.(msg.text);\n currentResolve = null;\n currentReject = null;\n break;\n\n case \"error\":\n onError?.(msg.error);\n onProgress?.({ status: \"error\", error: msg.error });\n if (currentReject) {\n currentReject(new Error(msg.error));\n currentResolve = null;\n currentReject = null;\n } else {\n reject(new Error(msg.error));\n }\n break;\n }\n };\n\n worker.onerror = (e) => {\n const error = e.message || \"Worker error\";\n onError?.(error);\n reject(new Error(error));\n };\n\n const gerbilWorker: GerbilWorker = {\n generate: (prompt: string, options: GenerateStreamOptions = {}) =>\n new Promise((res, rej) => {\n currentResolve = res;\n currentReject = rej;\n\n const system = options.system || \"You are a helpful assistant.\";\n\n // Use history if provided (for multi-turn conversations)\n // Otherwise, just use system + current prompt\n const messages = options.history\n ? [{ role: \"system\", content: system }, ...options.history]\n : [\n { role: \"system\", content: system },\n { role: \"user\", content: prompt },\n ];\n\n // When using history, reset KV cache first to avoid position mismatches\n // (full history is provided, so we don't need cached context)\n if (options.history) {\n worker.postMessage({ type: \"reset\" });\n }\n\n worker.postMessage({\n type: \"generate\",\n messages,\n images: options.images || [],\n options: {\n maxTokens: options.maxTokens ?? (options.images?.length ? 2048 : 256),\n temperature: options.temperature ?? 0.7,\n topP: options.topP ?? 0.9,\n topK: options.topK ?? 20,\n thinking: options.thinking ?? false,\n },\n });\n }),\n\n interrupt: () => {\n worker.postMessage({ type: \"interrupt\" });\n },\n\n reset: () => {\n worker.postMessage({ type: \"reset\" });\n },\n\n terminate: () => {\n worker.terminate();\n URL.revokeObjectURL(workerUrl);\n },\n\n isReady: () => isReady,\n };\n });\n}\n\n// ============================================\n// React Hooks\n// ============================================\n\n/** Message in a chat conversation */\nexport type Message = {\n id: string;\n role: \"user\" | \"assistant\";\n content: string;\n thinking?: string;\n /** Attached images (URLs or data URIs) - for vision models */\n images?: string[];\n};\n\n/** Loading progress state */\nexport type LoadingProgress = {\n status: \"loading\" | \"downloading\" | \"ready\" | \"error\";\n message?: string;\n file?: string;\n progress?: number;\n /** Number of files being downloaded (0 = loading from cache) */\n downloadCount?: number;\n /** Total files to process */\n totalFiles?: number;\n};\n\n/** Options for useChat hook */\nexport type UseChatOptions = {\n /** Model ID (default: \"qwen3-0.6b\") */\n model?: string;\n /** System prompt */\n system?: string;\n /** Enable thinking mode (Qwen3) */\n thinking?: boolean;\n /** Max tokens per response */\n maxTokens?: number;\n /** Temperature (0-2) */\n temperature?: number;\n /** Initial messages */\n initialMessages?: Message[];\n /** Auto-load model on mount (default: false - loads on first generate or load()) */\n autoLoad?: boolean;\n /** Called when model is ready */\n onReady?: () => void;\n /** Called on error */\n onError?: (error: string) => void;\n};\n\n/** Return type for useChat hook */\nexport type UseChatReturn = {\n /** Chat messages */\n messages: Message[];\n /** Current input value */\n input: string;\n /** Set input value */\n setInput: (value: string) => void;\n /** Submit current input */\n handleSubmit: (e?: { preventDefault?: () => void }) => void;\n /** Whether model is loading */\n isLoading: boolean;\n /** Loading progress */\n loadingProgress: LoadingProgress | null;\n /** Whether generating a response */\n isGenerating: boolean;\n /** Current thinking content (streaming) */\n thinking: string;\n /** Stop generation */\n stop: () => void;\n /** Clear all messages */\n clear: () => void;\n /** Current tokens per second */\n tps: number;\n /** Whether model is ready */\n isReady: boolean;\n /** Error message if any */\n error: string | null;\n /** Load the model (only needed if lazy: true) */\n load: () => void;\n /** Currently attached images (for next message) */\n attachedImages: string[];\n /** Attach an image to the next message */\n attachImage: (imageUrl: string) => void;\n /** Remove an attached image */\n removeImage: (index: number) => void;\n /** Clear all attached images */\n clearImages: () => void;\n /** Send message with specific images (convenience method) */\n sendWithImages: (text: string, images: string[]) => void;\n};\n\n/**\n * React hook for chat with local LLM\n *\n * @example\n * ```tsx\n * import { useChat } from \"@tryhamster/gerbil/browser\";\n *\n * function Chat() {\n * const { messages, input, setInput, handleSubmit, isLoading, isGenerating } = useChat();\n *\n * if (isLoading) return <div>Loading model...</div>;\n *\n * return (\n * <div>\n * {messages.map(m => (\n * <div key={m.id}>{m.role}: {m.content}</div>\n * ))}\n * <form onSubmit={handleSubmit}>\n * <input value={input} onChange={e => setInput(e.target.value)} />\n * <button disabled={isGenerating}>Send</button>\n * </form>\n * </div>\n * );\n * }\n * ```\n */\nexport function useChat(options: UseChatOptions = {}): UseChatReturn {\n // Lazy import React to avoid SSR issues\n const React = (globalThis as any).React;\n if (!React) {\n throw new Error(\"useChat requires React. Import React before using this hook.\");\n }\n\n const { useState, useEffect, useRef, useCallback } = React as {\n useState: <T>(initial: T) => [T, (v: T | ((prev: T) => T)) => void];\n useEffect: (effect: () => void | (() => void), deps?: unknown[]) => void;\n useRef: <T>(initial: T) => { current: T };\n useCallback: <T>(fn: T, deps: unknown[]) => T;\n };\n\n const {\n model = \"qwen3-0.6b\",\n system = \"You are a helpful assistant.\",\n thinking: enableThinking = false,\n maxTokens = 512,\n temperature = 0.7,\n initialMessages = [],\n autoLoad = false,\n onReady,\n onError,\n } = options;\n\n const [messages, setMessages] = useState<Message[]>(initialMessages);\n const [input, setInput] = useState<string>(\"\");\n const [isLoading, setIsLoading] = useState<boolean>(autoLoad);\n const [loadingProgress, setLoadingProgress] = useState<LoadingProgress | null>(null);\n const [isGenerating, setIsGenerating] = useState<boolean>(false);\n const [thinking, setThinking] = useState<string>(\"\");\n const [currentResponse, setCurrentResponse] = useState<string>(\"\");\n const [tps, setTps] = useState<number>(0);\n const [error, setError] = useState<string | null>(null);\n const [isReady, setIsReady] = useState<boolean>(false);\n const [shouldLoad, setShouldLoad] = useState<boolean>(autoLoad);\n const [attachedImages, setAttachedImages] = useState<string[]>([]);\n\n const workerRef = useRef<GerbilWorker | null>(null);\n const messageIdRef = useRef<number>(0);\n const mountedRef = useRef<boolean>(true);\n\n // Load function - can be called manually or auto-triggered on generate\n const load = useCallback(() => {\n if (workerRef.current || isLoading) {\n return;\n }\n setIsLoading(true);\n setShouldLoad(true);\n }, [isLoading]);\n\n // Initialize worker\n useEffect(() => {\n if (!shouldLoad) {\n return;\n }\n\n if (!isWebGPUSupported()) {\n setError(\"WebGPU not supported. Use Chrome/Edge 113+.\");\n setIsLoading(false);\n onError?.(\"WebGPU not supported\");\n return;\n }\n\n mountedRef.current = true;\n\n createGerbilWorker({\n modelId: model,\n onProgress: (p) => {\n if (!mountedRef.current) {\n return;\n }\n setLoadingProgress(p);\n if (p.status === \"ready\") {\n setIsLoading(false);\n setIsReady(true);\n onReady?.();\n }\n },\n onToken: (token) => {\n if (!mountedRef.current) {\n return;\n }\n setTps(token.tps);\n if (token.state === \"thinking\") {\n setThinking((t: string) => t + token.text);\n } else {\n setCurrentResponse((r: string) => r + token.text);\n }\n },\n onComplete: () => {\n if (!mountedRef.current) {\n return;\n }\n setIsGenerating(false);\n },\n onError: (err) => {\n if (!mountedRef.current) {\n return;\n }\n setError(err);\n setIsGenerating(false);\n onError?.(err);\n },\n })\n .then((worker) => {\n if (mountedRef.current) {\n workerRef.current = worker;\n } else {\n worker.terminate();\n }\n })\n .catch((err) => {\n if (mountedRef.current) {\n setError(err.message);\n setIsLoading(false);\n onError?.(err.message);\n }\n });\n\n return () => {\n mountedRef.current = false;\n workerRef.current?.terminate();\n };\n }, [model, shouldLoad]);\n\n // Commit response to messages when generation completes\n useEffect(() => {\n if (!isGenerating && currentResponse) {\n setMessages((msgs: Message[]) => {\n const lastMsg = msgs.at(-1);\n if (lastMsg?.role === \"assistant\") {\n return msgs.map((m: Message, i: number) =>\n i === msgs.length - 1\n ? { ...m, content: currentResponse, thinking: thinking || undefined }\n : m,\n );\n }\n return msgs;\n });\n setCurrentResponse(\"\");\n setThinking(\"\");\n }\n }, [isGenerating, currentResponse, thinking]);\n\n // Store pending message for auto-load scenario\n const pendingMessageRef = useRef<string | null>(null);\n const pendingImagesRef = useRef<string[]>([]);\n\n // Image management functions\n const attachImage = useCallback((imageUrl: string) => {\n setAttachedImages((imgs: string[]) => [...imgs, imageUrl]);\n }, []);\n\n const removeImage = useCallback((index: number) => {\n setAttachedImages((imgs: string[]) => imgs.filter((_: string, i: number) => i !== index));\n }, []);\n\n const clearImages = useCallback(() => {\n setAttachedImages([]);\n }, []);\n\n // Internal function to send a message with specific images\n const sendMessageWithImages = useCallback(\n (text: string, images: string[]) => {\n if (!text.trim() || isGenerating) {\n return;\n }\n\n messageIdRef.current += 1;\n const userMessage: Message = {\n id: `msg-${messageIdRef.current}`,\n role: \"user\",\n content: text.trim(),\n images: images.length > 0 ? images : undefined,\n };\n\n messageIdRef.current += 1;\n const assistantMessage: Message = {\n id: `msg-${messageIdRef.current}`,\n role: \"assistant\",\n content: \"\",\n };\n\n setMessages((msgs: Message[]) => [...msgs, userMessage, assistantMessage]);\n setCurrentResponse(\"\");\n setThinking(\"\");\n\n // If worker not loaded, trigger load and queue the message\n if (!workerRef.current) {\n pendingMessageRef.current = text.trim();\n pendingImagesRef.current = images;\n load();\n return;\n }\n\n setIsGenerating(true);\n workerRef.current.generate(text.trim(), {\n system,\n thinking: enableThinking,\n maxTokens: images.length > 0 ? Math.max(maxTokens, 2048) : maxTokens,\n temperature,\n images: images.length > 0 ? images : undefined,\n });\n },\n [isGenerating, system, enableThinking, maxTokens, temperature, load],\n );\n\n const handleSubmit = useCallback(\n (e?: { preventDefault?: () => void }) => {\n e?.preventDefault?.();\n\n if (!input.trim() || isGenerating) {\n return;\n }\n\n // Send with any attached images\n sendMessageWithImages(input, attachedImages);\n setInput(\"\");\n setAttachedImages([]);\n },\n [input, isGenerating, attachedImages, sendMessageWithImages],\n );\n\n // Convenience method to send with specific images\n const sendWithImages = useCallback(\n (text: string, images: string[]) => {\n sendMessageWithImages(text, images);\n },\n [sendMessageWithImages],\n );\n\n // Process pending message when worker becomes ready\n useEffect(() => {\n if (isReady && pendingMessageRef.current && workerRef.current) {\n const pendingContent = pendingMessageRef.current;\n const pendingImages = pendingImagesRef.current;\n pendingMessageRef.current = null;\n pendingImagesRef.current = [];\n setIsGenerating(true);\n workerRef.current.generate(pendingContent, {\n system,\n thinking: enableThinking,\n maxTokens: pendingImages.length > 0 ? Math.max(maxTokens, 2048) : maxTokens,\n temperature,\n images: pendingImages.length > 0 ? pendingImages : undefined,\n });\n }\n }, [isReady, system, enableThinking, maxTokens, temperature]);\n\n const stop = useCallback(() => {\n workerRef.current?.interrupt();\n setIsGenerating(false);\n }, []);\n\n const clear = useCallback(() => {\n workerRef.current?.reset();\n setMessages([]);\n setCurrentResponse(\"\");\n setThinking(\"\");\n setAttachedImages([]);\n }, []);\n\n // Update last message with streaming content\n const displayMessages = messages.map((m: Message, i: number) => {\n if (i === messages.length - 1 && m.role === \"assistant\" && isGenerating) {\n return { ...m, content: currentResponse, thinking: thinking || undefined };\n }\n return m;\n });\n\n return {\n messages: displayMessages,\n input,\n setInput,\n handleSubmit,\n isLoading,\n loadingProgress,\n isGenerating,\n thinking,\n stop,\n clear,\n tps,\n isReady,\n error,\n load,\n attachedImages,\n attachImage,\n removeImage,\n clearImages,\n sendWithImages,\n };\n}\n\n/** Options for useCompletion hook */\nexport type UseCompletionOptions = {\n /** Model ID (default: \"qwen3-0.6b\") */\n model?: string;\n /** System prompt */\n system?: string;\n /** Enable thinking mode (Qwen3) */\n thinking?: boolean;\n /** Max tokens */\n maxTokens?: number;\n /** Temperature (0-2) */\n temperature?: number;\n /** Auto-load model on mount (default: false - loads on first complete() or load()) */\n autoLoad?: boolean;\n /** Called when model is ready */\n onReady?: () => void;\n /** Called on error */\n onError?: (error: string) => void;\n};\n\n/** Options for single completion call */\nexport type CompleteOptions = {\n /** Image URLs or data URIs to analyze (for vision models) */\n images?: string[];\n};\n\n/** Return type for useCompletion hook */\nexport type UseCompletionReturn = {\n /** Generated completion */\n completion: string;\n /** Thinking content (if enabled) */\n thinking: string;\n /** Generate completion (optionally with images for vision models) */\n complete: (prompt: string, options?: CompleteOptions) => Promise<string>;\n /** Whether model is loading */\n isLoading: boolean;\n /** Loading progress */\n loadingProgress: LoadingProgress | null;\n /** Whether generating */\n isGenerating: boolean;\n /** Stop generation */\n stop: () => void;\n /** Current tokens per second */\n tps: number;\n /** Whether model is ready */\n isReady: boolean;\n /** Error message if any */\n error: string | null;\n /** Load the model (only needed if lazy: true) */\n load: () => void;\n};\n\n/**\n * React hook for text completion with local LLM\n *\n * @example\n * ```tsx\n * import { useCompletion } from \"@tryhamster/gerbil/browser\";\n *\n * function App() {\n * const { complete, completion, isLoading, isGenerating } = useCompletion();\n *\n * if (isLoading) return <div>Loading...</div>;\n *\n * return (\n * <div>\n * <button onClick={() => complete(\"Write a haiku\")}>Generate</button>\n * <p>{completion}</p>\n * </div>\n * );\n * }\n * ```\n */\nexport function useCompletion(options: UseCompletionOptions = {}): UseCompletionReturn {\n const React = (globalThis as any).React;\n if (!React) {\n throw new Error(\"useCompletion requires React. Import React before using this hook.\");\n }\n\n const { useState, useEffect, useRef, useCallback } = React as {\n useState: <T>(initial: T) => [T, (v: T | ((prev: T) => T)) => void];\n useEffect: (effect: () => void | (() => void), deps?: unknown[]) => void;\n useRef: <T>(initial: T) => { current: T };\n useCallback: <T>(fn: T, deps: unknown[]) => T;\n };\n\n const {\n model = \"qwen3-0.6b\",\n system = \"You are a helpful assistant.\",\n thinking: enableThinking = false,\n maxTokens = 512,\n temperature = 0.7,\n autoLoad = false,\n onReady,\n onError,\n } = options;\n\n const [completion, setCompletion] = useState<string>(\"\");\n const [thinking, setThinking] = useState<string>(\"\");\n const [isLoading, setIsLoading] = useState<boolean>(autoLoad);\n const [loadingProgress, setLoadingProgress] = useState<LoadingProgress | null>(null);\n const [isGenerating, setIsGenerating] = useState<boolean>(false);\n const [tps, setTps] = useState<number>(0);\n const [error, setError] = useState<string | null>(null);\n const [isReady, setIsReady] = useState<boolean>(false);\n const [shouldLoad, setShouldLoad] = useState<boolean>(autoLoad);\n\n const workerRef = useRef<GerbilWorker | null>(null);\n const resolveRef = useRef<((text: string) => void) | null>(null);\n const rejectRef = useRef<((err: Error) => void) | null>(null);\n const pendingPromptRef = useRef<string | null>(null);\n const pendingImagesRef = useRef<string[] | undefined>(undefined);\n const mountedRef = useRef<boolean>(true);\n\n // Load function - can be called manually or auto-triggered on complete()\n const load = useCallback(() => {\n if (workerRef.current || isLoading) {\n return;\n }\n setIsLoading(true);\n setShouldLoad(true);\n }, [isLoading]);\n\n useEffect(() => {\n if (!shouldLoad) {\n return;\n }\n\n if (!isWebGPUSupported()) {\n setError(\"WebGPU not supported. Use Chrome/Edge 113+.\");\n setIsLoading(false);\n onError?.(\"WebGPU not supported\");\n return;\n }\n\n mountedRef.current = true;\n\n createGerbilWorker({\n modelId: model,\n onProgress: (p) => {\n if (!mountedRef.current) {\n return;\n }\n setLoadingProgress(p);\n if (p.status === \"ready\") {\n setIsLoading(false);\n setIsReady(true);\n onReady?.();\n }\n },\n onToken: (token) => {\n if (!mountedRef.current) {\n return;\n }\n setTps(token.tps);\n if (token.state === \"thinking\") {\n setThinking((t: string) => t + token.text);\n } else {\n setCompletion((c: string) => c + token.text);\n }\n },\n onComplete: (result) => {\n if (!mountedRef.current) {\n return;\n }\n setIsGenerating(false);\n resolveRef.current?.(result.text);\n resolveRef.current = null;\n },\n onError: (err) => {\n if (!mountedRef.current) {\n return;\n }\n setError(err);\n setIsGenerating(false);\n onError?.(err);\n },\n })\n .then((worker) => {\n if (mountedRef.current) {\n workerRef.current = worker;\n } else {\n worker.terminate();\n }\n })\n .catch((err) => {\n if (mountedRef.current) {\n setError(err.message);\n setIsLoading(false);\n onError?.(err.message);\n }\n });\n\n return () => {\n mountedRef.current = false;\n workerRef.current?.terminate();\n };\n }, [model, shouldLoad]);\n\n const complete = useCallback(\n (prompt: string, completeOptions?: CompleteOptions): Promise<string> => {\n return new Promise((resolve, reject) => {\n setCompletion(\"\");\n setThinking(\"\");\n resolveRef.current = resolve;\n rejectRef.current = reject;\n\n // If worker not loaded, trigger load and queue the prompt\n if (!workerRef.current) {\n pendingPromptRef.current = prompt;\n pendingImagesRef.current = completeOptions?.images;\n load();\n return;\n }\n\n setIsGenerating(true);\n workerRef.current.generate(prompt, {\n system,\n thinking: enableThinking,\n maxTokens,\n temperature,\n images: completeOptions?.images,\n });\n });\n },\n [system, enableThinking, maxTokens, temperature, load],\n );\n\n // Process pending prompt when worker becomes ready\n useEffect(() => {\n if (isReady && pendingPromptRef.current && workerRef.current) {\n const pendingPrompt = pendingPromptRef.current;\n const pendingImages = pendingImagesRef.current;\n pendingPromptRef.current = null;\n pendingImagesRef.current = undefined;\n setIsGenerating(true);\n workerRef.current.generate(pendingPrompt, {\n system,\n thinking: enableThinking,\n maxTokens,\n temperature,\n images: pendingImages,\n });\n }\n }, [isReady, system, enableThinking, maxTokens, temperature]);\n\n const stop = useCallback(() => {\n workerRef.current?.interrupt();\n setIsGenerating(false);\n }, []);\n\n return {\n completion,\n thinking,\n complete,\n isLoading,\n loadingProgress,\n isGenerating,\n stop,\n tps,\n isReady,\n error,\n load,\n };\n}\n\n// ============================================\n// Text-to-Speech (useSpeech hook)\n// ============================================\n\n/** TTS loading progress */\nexport type TTSProgress = {\n status: \"idle\" | \"loading\" | \"downloading\" | \"ready\" | \"error\";\n message?: string;\n file?: string;\n progress?: number;\n error?: string;\n};\n\n/** Available TTS models */\nexport type TTSModelId = \"kokoro-82m\" | \"supertonic-66m\";\n\n/** Voice info for TTS models */\nexport type BrowserVoiceInfo = {\n id: string;\n name: string;\n gender: \"male\" | \"female\";\n language: string;\n description: string;\n};\n\n/** Kokoro voice definitions (24kHz, high quality) */\nconst KOKORO_BROWSER_VOICES: BrowserVoiceInfo[] = [\n {\n id: \"af_heart\",\n name: \"Heart\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female, highest quality (Grade A)\",\n },\n {\n id: \"af_bella\",\n name: \"Bella\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female, warm and friendly (Grade A-)\",\n },\n {\n id: \"af_nicole\",\n name: \"Nicole\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female, soft and gentle\",\n },\n {\n id: \"af_sarah\",\n name: \"Sarah\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female, clear and professional\",\n },\n {\n id: \"af_sky\",\n name: \"Sky\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female, young and energetic\",\n },\n {\n id: \"af_alloy\",\n name: \"Alloy\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female\",\n },\n {\n id: \"af_aoede\",\n name: \"Aoede\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female, mythical\",\n },\n {\n id: \"af_jessica\",\n name: \"Jessica\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female\",\n },\n {\n id: \"af_kore\",\n name: \"Kore\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female\",\n },\n {\n id: \"af_nova\",\n name: \"Nova\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female\",\n },\n {\n id: \"af_river\",\n name: \"River\",\n gender: \"female\",\n language: \"en-us\",\n description: \"American female\",\n },\n {\n id: \"am_fenrir\",\n name: \"Fenrir\",\n gender: \"male\",\n language: \"en-us\",\n description: \"American male, best quality\",\n },\n {\n id: \"am_michael\",\n name: \"Michael\",\n gender: \"male\",\n language: \"en-us\",\n description: \"American male, warm and friendly\",\n },\n { id: \"am_adam\", name: \"Adam\", gender: \"male\", language: \"en-us\", description: \"American male\" },\n { id: \"am_echo\", name: \"Echo\", gender: \"male\", language: \"en-us\", description: \"American male\" },\n { id: \"am_eric\", name: \"Eric\", gender: \"male\", language: \"en-us\", description: \"American male\" },\n { id: \"am_liam\", name: \"Liam\", gender: \"male\", language: \"en-us\", description: \"American male\" },\n { id: \"am_onyx\", name: \"Onyx\", gender: \"male\", language: \"en-us\", description: \"American male\" },\n { id: \"am_puck\", name: \"Puck\", gender: \"male\", language: \"en-us\", description: \"American male\" },\n {\n id: \"am_santa\",\n name: \"Santa\",\n gender: \"male\",\n language: \"en-us\",\n description: \"American male, festive\",\n },\n {\n id: \"bf_emma\",\n name: \"Emma\",\n gender: \"female\",\n language: \"en-gb\",\n description: \"British female, elegant and clear\",\n },\n {\n id: \"bf_isabella\",\n name: \"Isabella\",\n gender: \"female\",\n language: \"en-gb\",\n description: \"British female, sophisticated\",\n },\n {\n id: \"bf_alice\",\n name: \"Alice\",\n gender: \"female\",\n language: \"en-gb\",\n description: \"British female\",\n },\n {\n id: \"bf_lily\",\n name: \"Lily\",\n gender: \"female\",\n language: \"en-gb\",\n description: \"British female\",\n },\n {\n id: \"bm_george\",\n name: \"George\",\n gender: \"male\",\n language: \"en-gb\",\n description: \"British male, distinguished\",\n },\n {\n id: \"bm_lewis\",\n name: \"Lewis\",\n gender: \"male\",\n language: \"en-gb\",\n description: \"British male, friendly\",\n },\n {\n id: \"bm_daniel\",\n name: \"Daniel\",\n gender: \"male\",\n language: \"en-gb\",\n description: \"British male\",\n },\n { id: \"bm_fable\", name: \"Fable\", gender: \"male\", language: \"en-gb\", description: \"British male\" },\n];\n\n/** Supertonic voice definitions (44.1kHz, faster) */\nconst SUPERTONIC_BROWSER_VOICES: BrowserVoiceInfo[] = [\n {\n id: \"F1\",\n name: \"Female 1\",\n gender: \"female\",\n language: \"en\",\n description: \"Female voice 1 - Clear and natural\",\n },\n {\n id: \"F2\",\n name: \"Female 2\",\n gender: \"female\",\n language: \"en\",\n description: \"Female voice 2 - Warm and expressive\",\n },\n {\n id: \"M1\",\n name: \"Male 1\",\n gender: \"male\",\n language: \"en\",\n description: \"Male voice 1 - Deep and confident\",\n },\n {\n id: \"M2\",\n name: \"Male 2\",\n gender: \"male\",\n language: \"en\",\n description: \"Male voice 2 - Friendly and casual\",\n },\n];\n\n/** TTS model configuration */\nconst TTS_MODELS: Record<\n TTSModelId,\n { repo: string; defaultVoice: string; sampleRate: number; voices: BrowserVoiceInfo[] }\n> = {\n \"kokoro-82m\": {\n repo: \"onnx-community/Kokoro-82M-v1.0-ONNX\",\n defaultVoice: \"af_heart\",\n sampleRate: 24000,\n voices: KOKORO_BROWSER_VOICES,\n },\n \"supertonic-66m\": {\n repo: \"onnx-community/Supertonic-TTS-ONNX\",\n defaultVoice: \"F1\",\n sampleRate: 44100,\n voices: SUPERTONIC_BROWSER_VOICES,\n },\n};\n\n/** Options for useSpeech hook */\nexport type UseSpeechOptions = {\n /** TTS model to use (default: \"kokoro-82m\") */\n model?: TTSModelId;\n /** Default voice ID (default: model's default voice) */\n voice?: string;\n /** Speech speed multiplier (default: 1.0) */\n speed?: number;\n /** Auto-load TTS model on mount (default: false) */\n autoLoad?: boolean;\n /** Called when model is ready */\n onReady?: () => void;\n /** Called on error */\n onError?: (error: string) => void;\n /** Called when speech starts */\n onStart?: () => void;\n /** Called when speech ends */\n onEnd?: () => void;\n};\n\n/** Return type for useSpeech hook */\nexport type UseSpeechReturn = {\n /** Speak text aloud */\n speak: (text: string, options?: { voice?: string; speed?: number }) => Promise<void>;\n /** Stop current speech */\n stop: () => void;\n /** Whether TTS model is loading */\n isLoading: boolean;\n /** Loading progress */\n loadingProgress: TTSProgress | null;\n /** Whether currently speaking */\n isSpeaking: boolean;\n /** Whether TTS model is ready */\n isReady: boolean;\n /** Load the TTS model */\n load: () => void;\n /** Error message if any */\n error: string | null;\n /** List available voices for current model */\n listVoices: () => BrowserVoiceInfo[];\n /** Current voice ID */\n currentVoice: string;\n /** Set current voice */\n setVoice: (voiceId: string) => void;\n /** Current speed */\n currentSpeed: number;\n /** Set speed */\n setSpeed: (speed: number) => void;\n /** Current TTS model ID */\n currentModel: TTSModelId;\n /** Sample rate for current model (24000 for Kokoro, 44100 for Supertonic) */\n sampleRate: number;\n};\n\n/**\n * React hook for text-to-speech with Web Audio API playback\n *\n * Supports both Kokoro (24kHz, high quality) and Supertonic (44.1kHz, faster).\n *\n * @example\n * ```tsx\n * import { useSpeech } from \"@tryhamster/gerbil/browser\";\n *\n * function App() {\n * // Default: Kokoro TTS\n * const { speak, stop, isLoading, isSpeaking, listVoices, setVoice } = useSpeech();\n *\n * // Or use Supertonic (44.1kHz, faster)\n * // const { speak, listVoices } = useSpeech({ model: \"supertonic-66m\" });\n *\n * if (isLoading) return <div>Loading TTS...</div>;\n *\n * return (\n * <div>\n * <select onChange={e => setVoice(e.target.value)}>\n * {listVoices().map(v => (\n * <option key={v.id} value={v.id}>{v.name}</option>\n * ))}\n * </select>\n * <button onClick={() => speak(\"Hello world!\")}>\n * {isSpeaking ? \"Speaking...\" : \"Speak\"}\n * </button>\n * {isSpeaking && <button onClick={stop}>Stop</button>}\n * </div>\n * );\n * }\n * ```\n */\nexport function useSpeech(options: UseSpeechOptions = {}): UseSpeechReturn {\n const React = (globalThis as any).React;\n if (!React) {\n throw new Error(\"useSpeech requires React. Import React before using this hook.\");\n }\n\n const { useState, useEffect, useRef, useCallback } = React as {\n useState: <T>(initial: T) => [T, (v: T | ((prev: T) => T)) => void];\n useEffect: (effect: () => void | (() => void), deps?: unknown[]) => void;\n useRef: <T>(initial: T) => { current: T };\n useCallback: <T>(fn: T, deps: unknown[]) => T;\n };\n\n const {\n model: modelId = \"kokoro-82m\",\n speed: defaultSpeed = 1.0,\n autoLoad = false,\n onReady,\n onError,\n onStart,\n onEnd,\n } = options;\n\n // Get model config\n const modelConfig = TTS_MODELS[modelId];\n const defaultVoice = options.voice || modelConfig.defaultVoice;\n\n const [isLoading, setIsLoading] = useState<boolean>(autoLoad);\n const [loadingProgress, setLoadingProgress] = useState<TTSProgress | null>(null);\n const [isSpeaking, setIsSpeaking] = useState<boolean>(false);\n const [isReady, setIsReady] = useState<boolean>(false);\n const [error, setError] = useState<string | null>(null);\n const [shouldLoad, setShouldLoad] = useState<boolean>(autoLoad);\n const [currentVoice, setCurrentVoice] = useState<string>(defaultVoice);\n const [currentSpeed, setCurrentSpeed] = useState<number>(defaultSpeed);\n\n const ttsRef = useRef<any>(null);\n const voiceEmbeddingsRef = useRef<Map<string, Float32Array>>(new Map());\n const audioContextRef = useRef<AudioContext | null>(null);\n const sourceNodeRef = useRef<AudioBufferSourceNode | null>(null);\n const mountedRef = useRef<boolean>(true);\n const modelIdRef = useRef<TTSModelId>(modelId);\n\n // Voice list based on selected model\n const listVoices = useCallback((): BrowserVoiceInfo[] => {\n return modelConfig.voices;\n }, [modelConfig.voices]);\n\n // Load function\n const load = useCallback(() => {\n if (ttsRef.current || isLoading) return;\n setIsLoading(true);\n setShouldLoad(true);\n }, [isLoading]);\n\n // Initialize TTS based on model\n useEffect(() => {\n if (!shouldLoad) return;\n\n mountedRef.current = true;\n modelIdRef.current = modelId;\n\n const initTTS = async () => {\n try {\n const isSupertonic = modelId === \"supertonic-66m\";\n const config = TTS_MODELS[modelId];\n\n setLoadingProgress({\n status: \"loading\",\n message: `Loading ${isSupertonic ? \"Supertonic\" : \"Kokoro\"} TTS...`,\n });\n\n if (isSupertonic) {\n // Load Supertonic using transformers.js pipeline (bundled into browser build)\n const { pipeline } = await import(\"@huggingface/transformers\");\n\n const tts = await pipeline(\"text-to-speech\", config.repo, {\n device: \"webgpu\",\n progress_callback: (progress: any) => {\n if (!mountedRef.current) return;\n if (progress.status === \"progress\" && progress.file) {\n setLoadingProgress({\n status: \"downloading\",\n file: progress.file,\n progress: Math.round(progress.progress || 0),\n });\n }\n },\n });\n\n if (!mountedRef.current) return;\n\n // Load speaker embeddings from the voices folder\n const voicesUrl = `https://huggingface.co/${config.repo}/resolve/main/voices/`;\n const embeddingsMap = new Map<string, Float32Array>();\n\n // Load all voice embeddings\n await Promise.all(\n config.voices.map(async (voice) => {\n try {\n const response = await fetch(`${voicesUrl}${voice.id}.bin`);\n if (response.ok) {\n const buffer = await response.arrayBuffer();\n embeddingsMap.set(voice.id, new Float32Array(buffer));\n }\n } catch (e) {\n console.warn(`Failed to load voice embedding for ${voice.id}:`, e);\n }\n }),\n );\n\n if (!mountedRef.current) return;\n\n // Warmup the model with a dummy embedding\n try {\n await tts(\"Hello\", {\n speaker_embeddings: new Float32Array(1 * 101 * 128),\n num_inference_steps: 1,\n speed: 1.0,\n });\n } catch (e) {\n console.warn(\"Supertonic warmup failed:\", e);\n }\n\n voiceEmbeddingsRef.current = embeddingsMap;\n ttsRef.current = { type: \"supertonic\", pipeline: tts, config };\n } else {\n // Load Kokoro using kokoro-js (bundled into browser build)\n const kokoroModule = await import(\"kokoro-js\");\n const { KokoroTTS } = kokoroModule;\n\n const tts = await KokoroTTS.from_pretrained(config.repo, {\n dtype: \"fp32\",\n progress_callback: (progress: any) => {\n if (!mountedRef.current) return;\n if (progress.status === \"progress\" && progress.file) {\n setLoadingProgress({\n status: \"downloading\",\n file: progress.file,\n progress: Math.round(progress.progress || 0),\n });\n }\n },\n });\n\n if (!mountedRef.current) return;\n\n ttsRef.current = { type: \"kokoro\", instance: tts, config };\n }\n\n setIsLoading(false);\n setIsReady(true);\n setLoadingProgress({ status: \"ready\" });\n onReady?.();\n } catch (err) {\n if (!mountedRef.current) return;\n const errorMsg = err instanceof Error ? err.message : String(err);\n setError(errorMsg);\n setIsLoading(false);\n setLoadingProgress({ status: \"error\", error: errorMsg });\n onError?.(errorMsg);\n }\n };\n\n initTTS();\n\n return () => {\n mountedRef.current = false;\n };\n }, [shouldLoad, modelId, onReady, onError]);\n\n // Cleanup AudioContext only on unmount (not on re-renders)\n useEffect(() => {\n return () => {\n try {\n sourceNodeRef.current?.stop();\n } catch {\n // Ignore if already stopped\n }\n try {\n if (audioContextRef.current && audioContextRef.current.state !== \"closed\") {\n audioContextRef.current.close();\n }\n } catch {\n // Ignore if already closed\n }\n };\n }, []);\n\n // Speak function with Web Audio API playback\n const speak = useCallback(\n async (text: string, opts?: { voice?: string; speed?: number }) => {\n const voice = opts?.voice || currentVoice;\n const speed = opts?.speed || currentSpeed;\n\n // Auto-load if not loaded\n if (!ttsRef.current) {\n load();\n // Queue speak for after load\n return;\n }\n\n try {\n setIsSpeaking(true);\n onStart?.();\n\n let audioData: Float32Array;\n let sampleRate: number;\n\n const ttsBackend = ttsRef.current;\n\n if (ttsBackend.type === \"supertonic\") {\n // Supertonic: use transformers.js pipeline with speaker embeddings\n const config = ttsBackend.config;\n\n // Validate voice\n const voiceInfo = config.voices.find((v: BrowserVoiceInfo) => v.id === voice);\n if (!voiceInfo) {\n const validVoices = config.voices.map((v: BrowserVoiceInfo) => v.id).join(\", \");\n throw new Error(`Voice \"${voice}\" not found. Should be one of: ${validVoices}.`);\n }\n\n // Get or load voice embedding (101x128 = 12,928 floats)\n let speakerEmbedding = voiceEmbeddingsRef.current.get(voice);\n if (!speakerEmbedding) {\n try {\n const voiceUrl = `https://huggingface.co/${config.repo}/resolve/main/voices/${voice}.bin`;\n const response = await fetch(voiceUrl);\n if (response.ok) {\n const buffer = await response.arrayBuffer();\n speakerEmbedding = new Float32Array(buffer);\n voiceEmbeddingsRef.current.set(voice, speakerEmbedding);\n } else {\n throw new Error(`Failed to load voice: ${response.status}`);\n }\n } catch {\n // Fallback: create neutral embedding\n speakerEmbedding = new Float32Array(101 * 128).fill(0.1);\n voiceEmbeddingsRef.current.set(voice, speakerEmbedding);\n }\n }\n\n // Generate audio\n const result = await ttsBackend.pipeline(text, {\n speaker_embeddings: speakerEmbedding,\n speed: speed,\n });\n audioData = result.audio as Float32Array;\n sampleRate = result.sampling_rate as number;\n } else {\n // Kokoro: use kokoro-js generate\n const config = ttsBackend.config;\n\n // Validate voice\n const voiceInfo = config.voices.find((v: BrowserVoiceInfo) => v.id === voice);\n if (!voiceInfo) {\n const validVoices = config.voices.map((v: BrowserVoiceInfo) => v.id).join(\", \");\n throw new Error(`Voice \"${voice}\" not found. Should be one of: ${validVoices}.`);\n }\n\n const result = await ttsBackend.instance.generate(text, { voice, speed });\n audioData = result.audio as Float32Array;\n sampleRate = result.sampling_rate as number;\n }\n\n if (!mountedRef.current) return;\n\n // Create or recreate AudioContext if needed\n if (!audioContextRef.current || audioContextRef.current.state === \"closed\") {\n audioContextRef.current = new AudioContext();\n }\n\n const audioContext = audioContextRef.current;\n\n // Resume context if suspended (browser autoplay policy)\n if (audioContext.state === \"suspended\") {\n await audioContext.resume();\n }\n\n // Create audio buffer (ensure we have a proper ArrayBuffer-backed Float32Array)\n const audioBuffer = audioContext.createBuffer(1, audioData.length, sampleRate);\n const channelData = new Float32Array(audioData);\n audioBuffer.copyToChannel(channelData, 0);\n\n // Stop any current playback\n if (sourceNodeRef.current) {\n sourceNodeRef.current.stop();\n sourceNodeRef.current.disconnect();\n }\n\n // Create and play source node\n const sourceNode = audioContext.createBufferSource();\n sourceNode.buffer = audioBuffer;\n sourceNode.connect(audioContext.destination);\n\n sourceNode.onended = () => {\n if (mountedRef.current) {\n setIsSpeaking(false);\n onEnd?.();\n }\n };\n\n sourceNodeRef.current = sourceNode;\n sourceNode.start();\n } catch (err) {\n if (!mountedRef.current) return;\n const errorMsg = err instanceof Error ? err.message : String(err);\n setError(errorMsg);\n setIsSpeaking(false);\n onError?.(errorMsg);\n }\n },\n [currentVoice, currentSpeed, load, onStart, onEnd, onError],\n );\n\n // Stop function\n const stop = useCallback(() => {\n if (sourceNodeRef.current) {\n sourceNodeRef.current.stop();\n sourceNodeRef.current.disconnect();\n sourceNodeRef.current = null;\n }\n setIsSpeaking(false);\n }, []);\n\n // Voice setter with validation\n const setVoice = useCallback(\n (voiceId: string) => {\n const voiceInfo = modelConfig.voices.find((v) => v.id === voiceId);\n if (voiceInfo) {\n setCurrentVoice(voiceId);\n } else {\n console.warn(\n `Voice \"${voiceId}\" not valid for ${modelId}. Available: ${modelConfig.voices.map((v) => v.id).join(\", \")}`,\n );\n }\n },\n [modelConfig.voices, modelId],\n );\n\n // Speed setter\n const setSpeed = useCallback((speed: number) => {\n setCurrentSpeed(Math.max(0.5, Math.min(2.0, speed)));\n }, []);\n\n return {\n speak,\n stop,\n isLoading,\n loadingProgress,\n isSpeaking,\n isReady,\n load,\n error,\n listVoices,\n currentVoice,\n setVoice,\n currentSpeed,\n setSpeed,\n currentModel: modelId,\n sampleRate: modelConfig.sampleRate,\n };\n}\n\n// ============================================\n// Audio Playback Utilities\n// ============================================\n\n/**\n * Play audio from Float32Array using Web Audio API\n *\n * @example\n * ```ts\n * import { playAudio } from \"@tryhamster/gerbil/browser\";\n *\n * const audio = new Float32Array([...]); // TTS output\n * const controller = await playAudio(audio, 24000);\n *\n * // Stop playback\n * controller.stop();\n * ```\n */\nexport async function playAudio(\n audio: Float32Array,\n sampleRate: number = 24000,\n): Promise<{ stop: () => void; onEnded: Promise<void> }> {\n const audioContext = new AudioContext();\n\n // Resume if suspended\n if (audioContext.state === \"suspended\") {\n await audioContext.resume();\n }\n\n const audioBuffer = audioContext.createBuffer(1, audio.length, sampleRate);\n const channelData = new Float32Array(audio);\n audioBuffer.copyToChannel(channelData, 0);\n\n const sourceNode = audioContext.createBufferSource();\n sourceNode.buffer = audioBuffer;\n sourceNode.connect(audioContext.destination);\n\n const onEnded = new Promise<void>((resolve) => {\n sourceNode.onended = () => {\n audioContext.close();\n resolve();\n };\n });\n\n sourceNode.start();\n\n return {\n stop: () => {\n sourceNode.stop();\n audioContext.close();\n },\n onEnded,\n };\n}\n\n/**\n * Create a reusable audio player for streaming TTS\n *\n * @example\n * ```ts\n * import { createAudioPlayer } from \"@tryhamster/gerbil/browser\";\n *\n * const player = createAudioPlayer(24000);\n *\n * // Queue audio chunks as they arrive\n * player.queue(chunk1);\n * player.queue(chunk2);\n *\n * // Stop and clear\n * player.stop();\n * ```\n */\nexport function createAudioPlayer(sampleRate: number = 24000): {\n queue: (audio: Float32Array) => void;\n stop: () => void;\n isPlaying: () => boolean;\n} {\n let audioContext: AudioContext | null = null;\n let nextStartTime = 0;\n let isActive = false;\n\n const ensureContext = async () => {\n if (!audioContext) {\n audioContext = new AudioContext();\n }\n if (audioContext.state === \"suspended\") {\n await audioContext.resume();\n }\n return audioContext;\n };\n\n return {\n queue: async (audio: Float32Array) => {\n const ctx = await ensureContext();\n isActive = true;\n\n const buffer = ctx.createBuffer(1, audio.length, sampleRate);\n const channelData = new Float32Array(audio);\n buffer.copyToChannel(channelData, 0);\n\n const source = ctx.createBufferSource();\n source.buffer = buffer;\n source.connect(ctx.destination);\n\n // Schedule seamlessly after previous chunk\n const startTime = Math.max(ctx.currentTime, nextStartTime);\n source.start(startTime);\n nextStartTime = startTime + buffer.duration;\n\n source.onended = () => {\n if (ctx.currentTime >= nextStartTime - 0.1) {\n isActive = false;\n }\n };\n },\n\n stop: () => {\n isActive = false;\n nextStartTime = 0;\n if (audioContext) {\n audioContext.close();\n audioContext = null;\n }\n },\n\n isPlaying: () => isActive,\n };\n}\n\n// ============================================\n// Voice Input Hook (STT)\n// ============================================\n\n/**\n * Progress info for STT loading\n */\nexport type STTProgress = {\n status: \"downloading\" | \"loading\" | \"ready\" | \"error\";\n message?: string;\n progress?: number;\n file?: string;\n};\n\n/**\n * Options for useVoiceInput hook\n */\nexport type UseVoiceInputOptions = {\n /** STT model ID (default: whisper-tiny.en) */\n model?: string;\n /** Auto-load model on mount (default: false) */\n autoLoad?: boolean;\n /** Callback when model is ready */\n onReady?: () => void;\n /** Callback when transcription completes (or for each chunk in streaming mode) */\n onTranscript?: (text: string) => void;\n /** Callback on error */\n onError?: (error: string) => void;\n /** Callback during loading */\n onProgress?: (progress: STTProgress) => void;\n /** Enable streaming transcription - transcribes audio in chunks as you speak */\n streaming?: boolean;\n /** Chunk duration in ms for streaming mode (default: 3000 = 3 seconds) */\n chunkDuration?: number;\n /** Callback for each streaming chunk with partial transcript */\n onChunk?: (text: string, chunkIndex: number) => void;\n};\n\n/**\n * Return type for useVoiceInput hook\n */\nexport type UseVoiceInputReturn = {\n /** Start recording audio */\n startRecording: () => Promise<void>;\n /** Stop recording and transcribe */\n stopRecording: () => Promise<string>;\n /** Cancel recording without transcribing */\n cancelRecording: () => void;\n /** Transcribe raw audio data (Float32Array at 16kHz) */\n transcribe: (audio: Float32Array) => Promise<string>;\n /** Whether currently recording */\n isRecording: boolean;\n /** Whether transcribing */\n isTranscribing: boolean;\n /** Whether model is loading */\n isLoading: boolean;\n /** Whether model is ready */\n isReady: boolean;\n /** Latest transcription result (full transcript in streaming mode) */\n transcript: string;\n /** Current streaming chunk being transcribed (streaming mode only) */\n streamingChunk: string;\n /** Number of chunks transcribed so far (streaming mode only) */\n chunkCount: number;\n /** Loading progress */\n loadingProgress: STTProgress | null;\n /** Error message */\n error: string | null;\n /** Manually load the model */\n load: () => void;\n};\n\n/**\n * React hook for voice input with browser microphone\n *\n * Uses MediaRecorder to capture audio and Whisper for transcription.\n * Supports both one-shot and streaming transcription modes.\n *\n * @example Basic usage (one-shot)\n * ```tsx\n * function VoiceInput() {\n * const { startRecording, stopRecording, isRecording, transcript } = useVoiceInput({\n * onTranscript: (text) => console.log(\"User said:\", text),\n * });\n *\n * return (\n * <button onClick={isRecording ? stopRecording : startRecording}>\n * {isRecording ? \"Stop\" : \"Record\"}\n * </button>\n * );\n * }\n * ```\n *\n * @example Streaming transcription (real-time)\n * ```tsx\n * function LiveTranscription() {\n * const { startRecording, stopRecording, isRecording, transcript, streamingChunk } = useVoiceInput({\n * streaming: true, // Enable streaming mode\n * chunkDuration: 1500, // Transcribe every 1.5 seconds (default)\n * onChunk: (text, idx) => console.log(`Chunk ${idx}: ${text}`),\n * });\n *\n * return (\n * <div>\n * <button onClick={isRecording ? stopRecording : startRecording}>\n * {isRecording ? \"Stop\" : \"Start Live Transcription\"}\n * </button>\n * <p>Current chunk: {streamingChunk}</p>\n * <p>Full transcript: {transcript}</p>\n * </div>\n * );\n * }\n * ```\n */\nexport function useVoiceInput(options: UseVoiceInputOptions = {}): UseVoiceInputReturn {\n const React = (globalThis as any).React;\n if (!React) {\n throw new Error(\"useVoiceInput requires React. Import React before using this hook.\");\n }\n\n const { useState, useEffect, useRef, useCallback } = React as {\n useState: <T>(initial: T) => [T, (v: T | ((prev: T) => T)) => void];\n useEffect: (effect: () => void | (() => void), deps?: unknown[]) => void;\n useRef: <T>(initial: T) => { current: T };\n useCallback: <T>(fn: T, deps: unknown[]) => T;\n };\n\n const {\n model = \"whisper-tiny.en\",\n autoLoad = false,\n onReady,\n onTranscript,\n onError,\n onProgress,\n streaming = false,\n chunkDuration = 1500, // Transcribe every 1.5 seconds for near real-time\n onChunk,\n } = options;\n\n const [isLoading, setIsLoading] = useState<boolean>(autoLoad);\n const [loadingProgress, setLoadingProgress] = useState<STTProgress | null>(null);\n const [isReady, setIsReady] = useState<boolean>(false);\n const [isRecording, setIsRecording] = useState<boolean>(false);\n const [isTranscribing, setIsTranscribing] = useState<boolean>(false);\n const [transcript, setTranscript] = useState<string>(\"\");\n const [streamingChunk, setStreamingChunk] = useState<string>(\"\");\n const [chunkCount, setChunkCount] = useState<number>(0);\n const [error, setError] = useState<string | null>(null);\n const [shouldLoad, setShouldLoad] = useState<boolean>(autoLoad);\n\n const sttRef = useRef<any>(null);\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const audioChunksRef = useRef<Blob[]>([]);\n const streamRef = useRef<MediaStream | null>(null);\n const mountedRef = useRef<boolean>(true);\n const streamingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n const pendingChunksRef = useRef<Blob[]>([]);\n const fullTranscriptRef = useRef<string>(\"\");\n\n // Load the STT model\n useEffect(() => {\n if (!shouldLoad || isReady) return;\n\n let cancelled = false;\n\n const loadModel = async () => {\n try {\n setIsLoading(true);\n setLoadingProgress({ status: \"loading\", message: \"Loading STT model...\" });\n onProgress?.({ status: \"loading\", message: \"Loading STT model...\" });\n\n // Dynamic import to avoid bundling when not used\n const { WhisperSTT } = await import(\"../core/stt.js\");\n\n if (cancelled || !mountedRef.current) return;\n\n const stt = new WhisperSTT(model);\n await stt.load({\n onProgress: (p: any) => {\n if (!mountedRef.current) return;\n const progress: STTProgress = {\n status: p.progress !== undefined ? \"downloading\" : \"loading\",\n message: p.status,\n progress: p.progress,\n file: p.file,\n };\n setLoadingProgress(progress);\n onProgress?.(progress);\n },\n });\n\n if (cancelled || !mountedRef.current) {\n stt.dispose();\n return;\n }\n\n sttRef.current = stt;\n setIsReady(true);\n setIsLoading(false);\n setLoadingProgress({ status: \"ready\" });\n onProgress?.({ status: \"ready\" });\n onReady?.();\n } catch (e: any) {\n if (!mountedRef.current) return;\n const errMsg = e.message || \"Failed to load STT model\";\n setError(errMsg);\n setIsLoading(false);\n setLoadingProgress({ status: \"error\", message: errMsg });\n onProgress?.({ status: \"error\", message: errMsg });\n onError?.(errMsg);\n }\n };\n\n loadModel();\n\n return () => {\n cancelled = true;\n };\n }, [shouldLoad, isReady, model, onReady, onError, onProgress]);\n\n // Cleanup on unmount\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n if (sttRef.current) {\n sttRef.current.dispose();\n }\n if (streamRef.current) {\n for (const track of streamRef.current.getTracks()) {\n track.stop();\n }\n }\n };\n }, []);\n\n // Manual load trigger\n const load = useCallback(() => {\n if (!shouldLoad && !isReady && !isLoading) {\n setShouldLoad(true);\n }\n }, [shouldLoad, isReady, isLoading]);\n\n // Convert audio blob to Float32Array at 16kHz\n const blobToFloat32 = useCallback(async (blob: Blob): Promise<Float32Array> => {\n const audioContext = new AudioContext({ sampleRate: 16000 });\n const arrayBuffer = await blob.arrayBuffer();\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n\n // Get mono channel\n const channelData = audioBuffer.getChannelData(0);\n\n // Resample if needed\n if (audioBuffer.sampleRate !== 16000) {\n const ratio = 16000 / audioBuffer.sampleRate;\n const newLength = Math.round(channelData.length * ratio);\n const resampled = new Float32Array(newLength);\n for (let i = 0; i < newLength; i++) {\n const srcIndex = i / ratio;\n const floor = Math.floor(srcIndex);\n const ceil = Math.min(floor + 1, channelData.length - 1);\n const t = srcIndex - floor;\n resampled[i] = channelData[floor] * (1 - t) + channelData[ceil] * t;\n }\n audioContext.close();\n return resampled;\n }\n\n audioContext.close();\n return new Float32Array(channelData);\n }, []);\n\n // Transcribe audio\n const transcribe = useCallback(\n async (audio: Float32Array): Promise<string> => {\n if (!sttRef.current) {\n if (!shouldLoad) {\n setShouldLoad(true);\n throw new Error(\"STT model not loaded. Loading now, please try again.\");\n }\n throw new Error(\"STT model not loaded\");\n }\n\n setIsTranscribing(true);\n try {\n const result = await sttRef.current.transcribe(audio);\n let text = result.text.trim();\n // Filter out Whisper artifacts\n if (text === \"[BLANK_AUDIO]\" || text === \"(blank audio)\" || text === \"[BLANK AUDIO]\") {\n text = \"\";\n }\n setTranscript(text);\n onTranscript?.(text);\n return text;\n } finally {\n if (mountedRef.current) {\n setIsTranscribing(false);\n }\n }\n },\n [shouldLoad, onTranscript],\n );\n\n // Track how many samples we've processed for streaming\n const processedSamplesRef = useRef<number>(0);\n\n // Transcribe a chunk of audio (for streaming mode)\n // Uses audioChunksRef (all chunks) to ensure valid WebM container\n const transcribeChunk = useCallback(\n async (chunkIdx: number): Promise<string> => {\n if (!sttRef.current || audioChunksRef.current.length === 0) return \"\";\n\n try {\n // Create blob from ALL chunks (needed for valid WebM header)\n const audioBlob = new Blob(audioChunksRef.current, { type: \"audio/webm\" });\n const audioData = await blobToFloat32(audioBlob);\n\n // Calculate new samples since last transcription\n const newSamplesStart = processedSamplesRef.current;\n const totalSamples = audioData.length;\n\n // Skip if no new audio (< 0.5 seconds = 8000 samples at 16kHz)\n if (totalSamples - newSamplesStart < 8000) return \"\";\n\n // Extract only the new portion of audio\n const newAudio = audioData.slice(newSamplesStart);\n\n // Update processed count\n processedSamplesRef.current = totalSamples;\n\n const result = await sttRef.current.transcribe(newAudio);\n let text = result.text.trim();\n\n // Filter out Whisper artifacts\n if (text === \"[BLANK_AUDIO]\" || text === \"(blank audio)\" || text === \"[BLANK AUDIO]\") {\n text = \"\";\n }\n\n if (text && mountedRef.current) {\n setStreamingChunk(text);\n onChunk?.(text, chunkIdx);\n }\n\n return text;\n } catch {\n return \"\";\n }\n },\n [blobToFloat32, onChunk],\n );\n\n // Start recording\n const startRecording = useCallback(async () => {\n if (isRecording) return;\n\n try {\n // For streaming mode, ensure STT model is loaded first\n if (streaming && !sttRef.current) {\n if (!shouldLoad) {\n setShouldLoad(true);\n }\n // Wait for model to load\n setIsLoading(true);\n const { WhisperSTT } = await import(\"../core/stt.js\");\n const stt = new WhisperSTT(model);\n await stt.load({\n onProgress: (p: any) => {\n if (mountedRef.current) {\n const progress: STTProgress = {\n status:\n p.status === \"downloading\"\n ? \"downloading\"\n : p.status === \"ready\"\n ? \"ready\"\n : \"loading\",\n message: p.status,\n progress: p.progress,\n file: p.file,\n };\n setLoadingProgress(progress);\n onProgress?.(progress);\n }\n },\n });\n if (!mountedRef.current) {\n stt.dispose();\n return;\n }\n sttRef.current = stt;\n setIsReady(true);\n setIsLoading(false);\n setLoadingProgress({ status: \"ready\" });\n onProgress?.({ status: \"ready\" });\n onReady?.();\n }\n\n // Request microphone permission\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n sampleRate: 16000,\n channelCount: 1,\n echoCancellation: true,\n noiseSuppression: true,\n },\n });\n\n streamRef.current = stream;\n audioChunksRef.current = [];\n pendingChunksRef.current = [];\n fullTranscriptRef.current = \"\";\n processedSamplesRef.current = 0;\n setTranscript(\"\");\n setStreamingChunk(\"\");\n setChunkCount(0);\n\n const mediaRecorder = new MediaRecorder(stream);\n mediaRecorderRef.current = mediaRecorder;\n\n mediaRecorder.ondataavailable = (event) => {\n if (event.data.size > 0) {\n audioChunksRef.current.push(event.data);\n if (streaming) {\n pendingChunksRef.current.push(event.data);\n }\n }\n };\n\n mediaRecorder.start(100); // Collect data every 100ms\n setIsRecording(true);\n setError(null);\n\n // If streaming mode, set up recursive transcription loop\n if (streaming && sttRef.current) {\n let chunkIdx = 0;\n let shouldContinue = true;\n\n // Use recursive setTimeout instead of setInterval to avoid timing issues\n // with heavy WebGPU/WASM operations\n const processNextChunk = async () => {\n if (!shouldContinue || !mountedRef.current) {\n return;\n }\n\n const numPending = pendingChunksRef.current.length;\n\n // Check if we have new audio to process\n if (numPending > 0) {\n // Clear pending counter (we'll process via audioChunksRef which has all data)\n pendingChunksRef.current = [];\n\n try {\n setIsTranscribing(true);\n const chunkText = await transcribeChunk(chunkIdx);\n\n if (chunkText && mountedRef.current) {\n chunkIdx++;\n setChunkCount(chunkIdx);\n\n // Append to full transcript using functional update\n setTranscript((prev) => {\n const newTranscript = prev + (prev ? \" \" : \"\") + chunkText;\n fullTranscriptRef.current = newTranscript;\n onTranscript?.(newTranscript);\n return newTranscript;\n });\n }\n } catch (e) {\n console.error(\"[useVoiceInput] Chunk transcription error:\", e);\n } finally {\n if (mountedRef.current) {\n setIsTranscribing(false);\n }\n }\n }\n\n // Schedule next check if still running\n if (shouldContinue && mountedRef.current) {\n streamingIntervalRef.current = setTimeout(processNextChunk, chunkDuration) as any;\n }\n };\n\n // Start the loop\n streamingIntervalRef.current = setTimeout(processNextChunk, chunkDuration) as any;\n\n // Store a way to stop the loop\n (streamingIntervalRef as any)._stop = () => {\n shouldContinue = false;\n };\n }\n } catch (e: any) {\n const errMsg = e.message || \"Failed to start recording\";\n setError(errMsg);\n onError?.(errMsg);\n }\n }, [\n isRecording,\n streaming,\n shouldLoad,\n model,\n chunkDuration,\n transcribeChunk,\n onTranscript,\n onError,\n onProgress,\n onReady,\n ]);\n\n // Stop recording and transcribe\n const stopRecording = useCallback(async (): Promise<string> => {\n // Stop streaming loop\n if ((streamingIntervalRef as any)._stop) {\n (streamingIntervalRef as any)._stop();\n }\n if (streamingIntervalRef.current) {\n clearTimeout(streamingIntervalRef.current);\n streamingIntervalRef.current = null;\n }\n\n return new Promise((resolve, reject) => {\n if (!mediaRecorderRef.current || !isRecording) {\n reject(new Error(\"Not recording\"));\n return;\n }\n\n const mediaRecorder = mediaRecorderRef.current;\n\n mediaRecorder.onstop = async () => {\n // Stop all tracks\n if (streamRef.current) {\n for (const track of streamRef.current.getTracks()) {\n track.stop();\n }\n streamRef.current = null;\n }\n\n setIsRecording(false);\n\n // In streaming mode, process any remaining chunks and return full transcript\n if (streaming) {\n // Process any remaining audio\n if (audioChunksRef.current.length > 0 && processedSamplesRef.current > 0) {\n setIsTranscribing(true);\n pendingChunksRef.current = [];\n\n try {\n const finalChunkText = await transcribeChunk(chunkCount);\n if (finalChunkText && mountedRef.current) {\n setTranscript((prev) => {\n const newTranscript = prev + (prev ? \" \" : \"\") + finalChunkText;\n fullTranscriptRef.current = newTranscript;\n return newTranscript;\n });\n }\n } finally {\n if (mountedRef.current) {\n setIsTranscribing(false);\n }\n }\n }\n\n const finalText = fullTranscriptRef.current;\n onTranscript?.(finalText);\n resolve(finalText);\n return;\n }\n\n // Non-streaming mode: transcribe entire recording\n const audioBlob = new Blob(audioChunksRef.current, { type: \"audio/webm\" });\n\n try {\n // Ensure model is loaded\n if (!sttRef.current) {\n if (!shouldLoad) {\n setShouldLoad(true);\n }\n // Wait for model to load\n await new Promise<void>((res, rej) => {\n const checkReady = setInterval(() => {\n if (sttRef.current) {\n clearInterval(checkReady);\n res();\n }\n }, 100);\n setTimeout(() => {\n clearInterval(checkReady);\n rej(new Error(\"Timeout waiting for STT model\"));\n }, 30000);\n });\n }\n\n // Convert blob to Float32Array\n const audioData = await blobToFloat32(audioBlob);\n\n // Transcribe\n const text = await transcribe(audioData);\n resolve(text);\n } catch (e: any) {\n const errMsg = e.message || \"Transcription failed\";\n setError(errMsg);\n onError?.(errMsg);\n reject(e);\n }\n };\n\n mediaRecorder.stop();\n });\n }, [\n isRecording,\n streaming,\n chunkCount,\n shouldLoad,\n blobToFloat32,\n transcribe,\n transcribeChunk,\n onTranscript,\n onError,\n ]);\n\n // Cancel recording\n const cancelRecording = useCallback(() => {\n // Stop streaming loop\n if ((streamingIntervalRef as any)._stop) {\n (streamingIntervalRef as any)._stop();\n }\n if (streamingIntervalRef.current) {\n clearTimeout(streamingIntervalRef.current);\n streamingIntervalRef.current = null;\n }\n\n if (mediaRecorderRef.current && isRecording) {\n mediaRecorderRef.current.stop();\n }\n if (streamRef.current) {\n for (const track of streamRef.current.getTracks()) {\n track.stop();\n }\n streamRef.current = null;\n }\n audioChunksRef.current = [];\n pendingChunksRef.current = [];\n processedSamplesRef.current = 0;\n setIsRecording(false);\n }, [isRecording]);\n\n return {\n startRecording,\n stopRecording,\n cancelRecording,\n transcribe,\n isRecording,\n isTranscribing,\n isLoading,\n isReady,\n transcript,\n streamingChunk,\n chunkCount,\n loadingProgress,\n error,\n load,\n };\n}\n\n// ============================================\n// Voice Chat Hook (STT + LLM + TTS)\n// ============================================\n\n/**\n * Options for useVoiceChat hook\n */\nexport type UseVoiceChatOptions = {\n /** LLM model ID (default: qwen3-0.6b) */\n llmModel?: string;\n /** STT model ID (default: whisper-tiny.en) */\n sttModel?: string;\n /** TTS model ID (default: kokoro-82m, also supports supertonic-66m) */\n ttsModel?: TTSModelId;\n /** System prompt for LLM */\n system?: string;\n /** Enable thinking mode (default: false) */\n thinking?: boolean;\n /** TTS voice ID (default: model's default voice) */\n voice?: string;\n /** TTS speech speed (default: 1.0) */\n speed?: number;\n /** Auto-load all models on mount (default: false) */\n autoLoad?: boolean;\n /** Callback when user speaks */\n onUserSpeak?: (text: string) => void;\n /** Callback when assistant responds */\n onAssistantSpeak?: (text: string) => void;\n /** Callback on error */\n onError?: (error: string) => void;\n};\n\n/**\n * Message in voice chat\n */\nexport type VoiceChatMessage = {\n id: string;\n role: \"user\" | \"assistant\";\n content: string;\n thinking?: string;\n audioUrl?: string;\n};\n\n/**\n * Return type for useVoiceChat hook\n */\nexport type UseVoiceChatReturn = {\n /** Messages in the conversation */\n messages: VoiceChatMessage[];\n /** Start recording user speech */\n startListening: () => Promise<void>;\n /** Stop recording and process (STT → LLM → TTS) */\n stopListening: () => Promise<void>;\n /** Cancel current operation */\n cancel: () => void;\n /** Clear conversation history */\n clear: () => void;\n /** Whether recording user speech */\n isListening: boolean;\n /** Whether processing (STT/LLM/TTS) */\n isProcessing: boolean;\n /** Whether assistant is speaking */\n isSpeaking: boolean;\n /** Current stage: idle, listening, transcribing, thinking, speaking */\n stage: \"idle\" | \"listening\" | \"transcribing\" | \"thinking\" | \"speaking\";\n /** Whether all models are loaded */\n isReady: boolean;\n /** Whether loading models */\n isLoading: boolean;\n /** Loading progress message */\n loadingMessage: string;\n /** Error message */\n error: string | null;\n /** Manually load all models */\n load: () => void;\n};\n\n/**\n * React hook for voice conversation with STT + LLM + TTS\n *\n * Complete voice-to-voice conversation loop:\n * 1. User presses button to speak\n * 2. Speech is transcribed (Whisper)\n * 3. LLM generates response\n * 4. Response is spoken aloud (Kokoro or Supertonic TTS)\n *\n * @example\n * ```tsx\n * function VoiceChat() {\n * const {\n * messages,\n * startListening,\n * stopListening,\n * isListening,\n * isSpeaking,\n * stage,\n * } = useVoiceChat({\n * system: \"You are a helpful voice assistant.\",\n * voice: \"af_bella\",\n * // Or use Supertonic for faster synthesis:\n * // ttsModel: \"supertonic-66m\",\n * // voice: \"F1\",\n * });\n *\n * return (\n * <div>\n * {messages.map(m => (\n * <div key={m.id}>{m.role}: {m.content}</div>\n * ))}\n * <button\n * onMouseDown={startListening}\n * onMouseUp={stopListening}\n * >\n * {stage === \"idle\" ? \"🎤 Hold to Speak\" : stage}\n * </button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useVoiceChat(options: UseVoiceChatOptions = {}): UseVoiceChatReturn {\n const React = (globalThis as any).React;\n if (!React) {\n throw new Error(\"useVoiceChat requires React. Import React before using this hook.\");\n }\n\n const { useState, useEffect, useRef, useCallback } = React as {\n useState: <T>(initial: T) => [T, (v: T | ((prev: T) => T)) => void];\n useEffect: (effect: () => void | (() => void), deps?: unknown[]) => void;\n useRef: <T>(initial: T) => { current: T };\n useCallback: <T>(fn: T, deps: unknown[]) => T;\n };\n\n // Get TTS model config for default voice\n const ttsModelId = options.ttsModel || \"kokoro-82m\";\n const ttsConfig = TTS_MODELS[ttsModelId];\n\n const {\n llmModel = \"qwen3-0.6b\",\n sttModel = \"whisper-tiny.en\",\n system = \"You are a helpful voice assistant. Keep responses brief and conversational.\",\n thinking = false,\n voice = ttsConfig.defaultVoice,\n speed = 1.0,\n autoLoad = false,\n onUserSpeak,\n onAssistantSpeak,\n onError,\n } = options;\n\n const [messages, setMessages] = useState<VoiceChatMessage[]>([]);\n const [stage, setStage] = useState<\n \"idle\" | \"listening\" | \"transcribing\" | \"thinking\" | \"speaking\"\n >(\"idle\");\n const [isLoading, setIsLoading] = useState<boolean>(autoLoad);\n const [loadingMessage, setLoadingMessage] = useState<string>(\"\");\n const [isReady, setIsReady] = useState<boolean>(false);\n const [error, setError] = useState<string | null>(null);\n const [shouldLoad, setShouldLoad] = useState<boolean>(autoLoad);\n\n // Refs for models and audio\n const llmWorkerRef = useRef<any>(null);\n const sttRef = useRef<any>(null);\n const ttsRef = useRef<any>(null);\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const audioChunksRef = useRef<Blob[]>([]);\n const streamRef = useRef<MediaStream | null>(null);\n const audioContextRef = useRef<AudioContext | null>(null);\n const sourceNodeRef = useRef<AudioBufferSourceNode | null>(null);\n const mountedRef = useRef<boolean>(true);\n const cancelledRef = useRef<boolean>(false);\n\n // Computed states\n const isListening = stage === \"listening\";\n const isProcessing = stage === \"transcribing\" || stage === \"thinking\";\n const isSpeaking = stage === \"speaking\";\n\n // Load all models\n useEffect(() => {\n if (!shouldLoad || isReady) return;\n\n let cancelled = false;\n\n const loadModels = async () => {\n try {\n setIsLoading(true);\n setError(null);\n\n // Load STT\n setLoadingMessage(\"Loading speech recognition (Whisper)...\");\n const { WhisperSTT } = await import(\"../core/stt.js\");\n if (cancelled || !mountedRef.current) return;\n\n const stt = new WhisperSTT(sttModel);\n await stt.load({\n onProgress: (p: any) => {\n if (!mountedRef.current) return;\n setLoadingMessage(p.status || \"Loading STT...\");\n },\n });\n if (cancelled || !mountedRef.current) {\n stt.dispose();\n return;\n }\n sttRef.current = stt;\n\n // Load LLM worker\n setLoadingMessage(\"Loading language model...\");\n const worker = await createGerbilWorker({\n modelId: llmModel,\n onProgress: (p) => {\n if (!mountedRef.current) return;\n setLoadingMessage(p.message || \"Loading LLM...\");\n },\n });\n if (cancelled || !mountedRef.current) {\n worker.terminate();\n return;\n }\n llmWorkerRef.current = worker;\n\n // Load TTS (Kokoro or Supertonic based on ttsModel option)\n const isSupertonic = ttsModelId === \"supertonic-66m\";\n setLoadingMessage(`Loading text-to-speech (${isSupertonic ? \"Supertonic\" : \"Kokoro\"})...`);\n\n const { createTTS } = await import(\"../core/tts.js\");\n if (cancelled || !mountedRef.current) return;\n\n const tts = createTTS(ttsModelId);\n await tts.load({\n onProgress: (p: any) => {\n if (!mountedRef.current) return;\n setLoadingMessage(p.status || \"Loading TTS...\");\n },\n });\n if (cancelled || !mountedRef.current) {\n await tts.dispose();\n return;\n }\n ttsRef.current = tts;\n\n setIsReady(true);\n setIsLoading(false);\n setLoadingMessage(\"Ready!\");\n } catch (e: any) {\n if (!mountedRef.current) return;\n const errMsg = e.message || \"Failed to load models\";\n setError(errMsg);\n setIsLoading(false);\n onError?.(errMsg);\n }\n };\n\n loadModels();\n\n return () => {\n cancelled = true;\n };\n }, [shouldLoad, isReady, llmModel, sttModel, ttsModelId, onError]);\n\n // Cleanup on unmount\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n llmWorkerRef.current?.terminate();\n sttRef.current?.dispose();\n ttsRef.current?.dispose();\n if (streamRef.current) {\n for (const track of streamRef.current.getTracks()) {\n track.stop();\n }\n }\n audioContextRef.current?.close();\n };\n }, []);\n\n // Load trigger\n const load = useCallback(() => {\n if (!shouldLoad && !isReady && !isLoading) {\n setShouldLoad(true);\n }\n }, [shouldLoad, isReady, isLoading]);\n\n // Convert blob to Float32 at 16kHz\n const blobToFloat32 = useCallback(async (blob: Blob): Promise<Float32Array> => {\n const audioContext = new AudioContext({ sampleRate: 16000 });\n const arrayBuffer = await blob.arrayBuffer();\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n const channelData = audioBuffer.getChannelData(0);\n\n if (audioBuffer.sampleRate !== 16000) {\n const ratio = 16000 / audioBuffer.sampleRate;\n const newLength = Math.round(channelData.length * ratio);\n const resampled = new Float32Array(newLength);\n for (let i = 0; i < newLength; i++) {\n const srcIndex = i / ratio;\n const floor = Math.floor(srcIndex);\n const ceil = Math.min(floor + 1, channelData.length - 1);\n const t = srcIndex - floor;\n resampled[i] = channelData[floor] * (1 - t) + channelData[ceil] * t;\n }\n audioContext.close();\n return resampled;\n }\n\n audioContext.close();\n return new Float32Array(channelData);\n }, []);\n\n // Play audio through Web Audio API\n const playAudioBuffer = useCallback(\n async (audio: Float32Array, sampleRate: number): Promise<void> => {\n return new Promise((resolve) => {\n if (!audioContextRef.current) {\n audioContextRef.current = new AudioContext();\n }\n const ctx = audioContextRef.current;\n\n const buffer = ctx.createBuffer(1, audio.length, sampleRate);\n const channelData = new Float32Array(audio);\n buffer.copyToChannel(channelData, 0);\n\n const source = ctx.createBufferSource();\n source.buffer = buffer;\n source.connect(ctx.destination);\n source.onended = () => {\n if (mountedRef.current) {\n resolve();\n }\n };\n source.start();\n sourceNodeRef.current = source;\n });\n },\n [],\n );\n\n // Start listening\n const startListening = useCallback(async () => {\n if (stage !== \"idle\") return;\n\n // Trigger load if not ready\n if (!isReady && !isLoading) {\n setShouldLoad(true);\n return;\n }\n\n cancelledRef.current = false;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true },\n });\n\n streamRef.current = stream;\n audioChunksRef.current = [];\n\n const mediaRecorder = new MediaRecorder(stream);\n mediaRecorderRef.current = mediaRecorder;\n\n mediaRecorder.ondataavailable = (event) => {\n if (event.data.size > 0) {\n audioChunksRef.current.push(event.data);\n }\n };\n\n mediaRecorder.start(100);\n setStage(\"listening\");\n setError(null);\n } catch (e: any) {\n const errMsg = e.message || \"Failed to access microphone\";\n setError(errMsg);\n onError?.(errMsg);\n }\n }, [stage, isReady, isLoading, onError]);\n\n // Stop listening and process\n const stopListening = useCallback(async () => {\n if (stage !== \"listening\") return;\n\n const mediaRecorder = mediaRecorderRef.current;\n if (!mediaRecorder) return;\n\n return new Promise<void>((resolve) => {\n mediaRecorder.onstop = async () => {\n // Stop mic\n if (streamRef.current) {\n for (const track of streamRef.current.getTracks()) {\n track.stop();\n }\n streamRef.current = null;\n }\n\n if (cancelledRef.current) {\n setStage(\"idle\");\n resolve();\n return;\n }\n\n const audioBlob = new Blob(audioChunksRef.current, { type: \"audio/webm\" });\n\n try {\n // STT\n setStage(\"transcribing\");\n const audioData = await blobToFloat32(audioBlob);\n const sttResult = await sttRef.current.transcribe(audioData);\n let userText = sttResult.text.trim();\n\n // Filter out Whisper artifacts\n if (\n userText === \"[BLANK_AUDIO]\" ||\n userText === \"(blank audio)\" ||\n userText === \"[BLANK AUDIO]\"\n ) {\n userText = \"\";\n }\n\n if (cancelledRef.current || !userText) {\n setStage(\"idle\");\n resolve();\n return;\n }\n\n // Add user message\n const userMsgId = `user-${Date.now()}`;\n setMessages((m) => [...m, { id: userMsgId, role: \"user\", content: userText }]);\n onUserSpeak?.(userText);\n\n // LLM\n setStage(\"thinking\");\n\n // Build conversation history\n const history = messages.map((m) => ({\n role: m.role as \"user\" | \"assistant\",\n content: m.content,\n }));\n history.push({ role: \"user\", content: userText });\n\n let responseText = \"\";\n let thinkingText = \"\";\n\n await llmWorkerRef.current.generate(userText, {\n system,\n thinking,\n history,\n onToken: (token: WorkerToken) => {\n if (cancelledRef.current) return;\n if (token.state === \"thinking\") {\n thinkingText += token.text;\n } else {\n responseText += token.text;\n }\n },\n });\n\n if (cancelledRef.current) {\n setStage(\"idle\");\n resolve();\n return;\n }\n\n // Add assistant message\n const assistantMsgId = `assistant-${Date.now()}`;\n setMessages((m) => [\n ...m,\n {\n id: assistantMsgId,\n role: \"assistant\",\n content: responseText,\n thinking: thinkingText || undefined,\n },\n ]);\n onAssistantSpeak?.(responseText);\n\n // TTS\n if (responseText.trim()) {\n setStage(\"speaking\");\n const ttsResult = await ttsRef.current.speak(responseText, { voice, speed });\n\n if (!cancelledRef.current) {\n await playAudioBuffer(ttsResult.audio, ttsResult.sampleRate);\n }\n }\n\n setStage(\"idle\");\n resolve();\n } catch (e: any) {\n if (!mountedRef.current) return;\n const errMsg = e.message || \"Processing failed\";\n setError(errMsg);\n setStage(\"idle\");\n onError?.(errMsg);\n resolve();\n }\n };\n\n mediaRecorder.stop();\n });\n }, [\n stage,\n messages,\n system,\n thinking,\n voice,\n speed,\n blobToFloat32,\n playAudioBuffer,\n onUserSpeak,\n onAssistantSpeak,\n onError,\n ]);\n\n // Cancel\n const cancel = useCallback(() => {\n cancelledRef.current = true;\n\n if (mediaRecorderRef.current && stage === \"listening\") {\n mediaRecorderRef.current.stop();\n }\n\n if (streamRef.current) {\n for (const track of streamRef.current.getTracks()) {\n track.stop();\n }\n streamRef.current = null;\n }\n\n if (sourceNodeRef.current) {\n try {\n sourceNodeRef.current.stop();\n } catch {}\n }\n\n audioChunksRef.current = [];\n setStage(\"idle\");\n }, [stage]);\n\n // Clear messages\n const clear = useCallback(() => {\n setMessages([]);\n }, []);\n\n return {\n messages,\n startListening,\n stopListening,\n cancel,\n clear,\n isListening,\n isProcessing,\n isSpeaking,\n stage,\n isReady,\n isLoading,\n loadingMessage,\n error,\n load,\n };\n}\n\n// ============================================\n// Utilities\n// ============================================\n\n/**\n * Check if WebGPU is supported\n */\nexport function isWebGPUSupported(): boolean {\n if (typeof navigator === \"undefined\") {\n return false;\n }\n return \"gpu\" in navigator;\n}\n\n/**\n * Get WebGPU adapter info\n */\nexport async function getWebGPUInfo(): Promise<{\n supported: boolean;\n adapter?: string;\n device?: string;\n} | null> {\n if (!isWebGPUSupported()) {\n return { supported: false };\n }\n\n try {\n const adapter = await (navigator as any).gpu.requestAdapter();\n if (!adapter) {\n return { supported: false };\n }\n\n const info = await adapter.requestAdapterInfo();\n return {\n supported: true,\n adapter: info.vendor,\n device: info.device,\n };\n } catch {\n return { supported: false };\n }\n}\n\nexport default {\n isWebGPUSupported,\n getWebGPUInfo,\n createGerbilWorker,\n playAudio,\n createAudioPlayer,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["BUILTIN_MODELS: Record<string, ModelConfig>","audioContext: AudioContext | null","MODEL_SIZES: Record<string, number>","chat: string","reason: string","state: SessionState","bytesDownloaded","e: any"],"sources":["../../src/core/models.ts","../../src/browser/pwa.ts","../../src/browser/audio.ts","../../src/browser/device-guards.ts","../../src/browser/download.ts"],"sourcesContent":["/**\n * Model Registry\n *\n * Supports built-in models and any HuggingFace model via hf:org/model syntax\n */\n\nimport type { ModelConfig, ModelSource } from \"./types.js\";\n\n// ============================================\n// Canonical default model\n// ============================================\n\n/**\n * The default model used everywhere a model id is not explicitly provided\n * (CLI flags, REPL, framework adapters, integrations, one-liner). This is the\n * e2e-validated model; reference this constant instead of hard-coding the id.\n */\nexport const DEFAULT_MODEL = \"qwen3.5-0.8b\";\n\n// ============================================\n// Built-in Models (curated & tested)\n// ============================================\n\n// Every entry is a standard HuggingFace safetensors repo whose architecture the\n// native WebGPU engine supports (Qwen2/Qwen3/Qwen3.5, LFM2 — see\n// src/gpu/architectures/index.ts). The engine quantizes weights to INT4 on load;\n// `size` is the bf16/fp16 download size. Only add a repo here whose architecture\n// has a graph generator in the registry.\nexport const BUILTIN_MODELS: Record<string, ModelConfig> = {\n \"qwen3.5-0.8b\": {\n id: \"qwen3.5-0.8b\",\n repo: \"Qwen/Qwen3.5-0.8B\",\n description:\n \"Qwen3.5 0.8B - Fast, multimodal (vision), 262k context, supports thinking (default)\",\n size: \"~1.6GB\",\n contextLength: 262_144,\n supportsThinking: true,\n supportsJson: true,\n supportsVision: true,\n family: \"qwen\",\n },\n \"qwen3.5-2b\": {\n id: \"qwen3.5-2b\",\n repo: \"Qwen/Qwen3.5-2B\",\n description:\n \"Qwen3.5 2B - Higher quality, multimodal (vision), 262k context, supports thinking\",\n size: \"~4GB\",\n contextLength: 262_144,\n supportsThinking: true,\n supportsJson: true,\n supportsVision: true,\n family: \"qwen\",\n },\n \"lfm2.5-1.2b-thinking\": {\n id: \"lfm2.5-1.2b-thinking\",\n repo: \"LiquidAI/LFM2.5-1.2B-Thinking\",\n description: \"LFM2.5 1.2B Thinking - Efficient reasoning model, 128k context\",\n size: \"~2.4GB\",\n contextLength: 128_000,\n supportsThinking: true,\n supportsJson: false,\n family: \"other\",\n },\n};\n\n// ============================================\n// Model Resolution\n// ============================================\n\n/**\n * Parse model identifier and resolve to source\n *\n * Supported formats:\n * - \"qwen3.5-0.8b\" (built-in)\n * - \"hf:org/model\" (HuggingFace shorthand)\n * - \"https://huggingface.co/org/model\" (full URL)\n * - \"file:./path/to/model\" (local path)\n */\nexport function resolveModel(modelId: string): ModelSource {\n // Built-in model\n if (BUILTIN_MODELS[modelId]) {\n return {\n type: \"builtin\",\n path: BUILTIN_MODELS[modelId].repo,\n };\n }\n\n // HuggingFace shorthand: hf:org/model\n if (modelId.startsWith(\"hf:\")) {\n const repo = modelId.slice(3);\n return {\n type: \"huggingface\",\n path: repo,\n };\n }\n\n // HuggingFace URL\n if (modelId.startsWith(\"https://huggingface.co/\")) {\n const repo = modelId.replace(\"https://huggingface.co/\", \"\");\n return {\n type: \"huggingface\",\n path: repo,\n };\n }\n\n // Local file\n if (modelId.startsWith(\"file:\")) {\n const path = modelId.slice(5);\n return {\n type: \"local\",\n path,\n };\n }\n\n // Assume it's a HuggingFace repo if it contains a slash\n if (modelId.includes(\"/\")) {\n return {\n type: \"huggingface\",\n path: modelId,\n };\n }\n\n // Unknown - treat as HuggingFace\n return {\n type: \"huggingface\",\n path: modelId,\n };\n}\n\n/**\n * Get model config (built-in only)\n */\nexport function getModelConfig(modelId: string): ModelConfig | null {\n return BUILTIN_MODELS[modelId] || null;\n}\n\n// Default context lengths for the families the native engine actually supports\n// (a graph generator exists in src/gpu/architectures). Other families fall back\n// to a conservative default.\nconst FAMILY_CONTEXT_DEFAULTS: Record<string, number> = {\n qwen: 32_768,\n other: 32_768, // LFM2 supports 128k but config.json is the real source of truth\n};\n\n/**\n * Create model config for an external HuggingFace model.\n *\n * Inference is restricted to families the engine can actually run — Qwen\n * (Qwen2/Qwen3/Qwen3.5) and LFM2 (Liquid). Everything else is left as \"other\"\n * with conservative capability flags so the REPL doesn't advertise features the\n * engine can't deliver.\n */\nexport function createExternalModelConfig(\n modelId: string,\n repo: string,\n contextLength?: number,\n): ModelConfig {\n const repoLower = repo.toLowerCase();\n\n // Only infer families that have a graph generator in the registry.\n let family: ModelConfig[\"family\"] = \"other\";\n if (repoLower.includes(\"qwen\")) {\n family = \"qwen\";\n }\n\n const isLiquid = repoLower.includes(\"lfm\") || repoLower.includes(\"liquid\");\n const isQwen = family === \"qwen\";\n\n return {\n id: modelId,\n repo,\n description: `External model: ${repo}`,\n size: \"Unknown\",\n contextLength: contextLength || FAMILY_CONTEXT_DEFAULTS[family] || 32_768,\n // Qwen3/Qwen3.5 and LFM2.5-Thinking expose thinking; nothing here is vision.\n supportsThinking: isQwen || isLiquid,\n supportsJson: isQwen,\n family,\n };\n}\n\n/**\n * Fetch context length from HuggingFace model config\n */\nexport async function fetchModelContextLength(repo: string): Promise<number | null> {\n try {\n const res = await fetch(`https://huggingface.co/${repo}/raw/main/config.json`);\n if (!res.ok) {\n return null;\n }\n\n const config = await res.json();\n\n // Different models use different field names\n return (\n config.max_position_embeddings ||\n config.n_positions ||\n config.max_seq_len ||\n config.sliding_window || // Some models use this\n config.context_length ||\n null\n );\n } catch {\n return null;\n }\n}\n\n/**\n * List all built-in models\n */\nexport function listBuiltinModels(): ModelConfig[] {\n return Object.values(BUILTIN_MODELS);\n}\n\n/**\n * Search HuggingFace models (placeholder - would need HF API)\n */\nexport async function searchModels(query: string): Promise<ModelConfig[]> {\n // TODO: Implement HuggingFace API search\n // For now, filter built-in models\n const q = query.toLowerCase();\n return listBuiltinModels().filter(\n (m) =>\n m.id.toLowerCase().includes(q) ||\n m.description.toLowerCase().includes(q) ||\n m.family.toLowerCase().includes(q),\n );\n}\n","/**\n * Mobile / PWA storage helpers.\n *\n * On-device models are large (a 4-bit 0.8B is ~400 MB; vision/larger models are\n * GBs). Mobile browsers — iOS Safari especially — wall a web origin off from the\n * real disk with TWO independent ceilings:\n *\n * 1. **Storage quota** (disk for the model cache). An *uninstalled* Safari tab\n * gets only ~1 GB, best-effort and evictable, regardless of how much free\n * disk the device has. Exceed it and every cache write fails → the model\n * re-downloads on every visit.\n * 2. **Tab memory** (RAM during load/inference) — a separate, smaller ceiling.\n *\n * The unlock for the storage ceiling is **persistent storage**, which iOS Safari\n * grants when the site is **installed to the Home Screen** (a PWA). Installed, the\n * quota jumps to a large fraction of actual disk and is never evicted — so models\n * cache once and stay. These helpers let an app surface that to its users and\n * request it, so on-device AI is actually practical on mobile.\n *\n * All functions are SSR/Node-safe (guarded; return conservative defaults).\n */\n\n/** True when the page is running as an installed/standalone PWA (Home Screen). */\nexport function isStandalone(): boolean {\n if (typeof window === \"undefined\") return false;\n // iOS Safari exposes navigator.standalone; everyone else uses display-mode.\n const iosStandalone = (navigator as { standalone?: boolean }).standalone === true;\n const displayStandalone =\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(display-mode: standalone)\").matches;\n return iosStandalone || displayStandalone;\n}\n\n/** True when running on iOS/iPadOS (where install is the quota unlock and the\n * install flow is manual: Share → Add to Home Screen). iPadOS masquerades as\n * macOS, so we also treat touch-capable WebKit-on-Mac as iOS. */\nexport function isIOS(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const ua = navigator.userAgent || \"\";\n if (/iPhone|iPad|iPod/.test(ua)) return true;\n // iPadOS 13+ reports a Mac UA — detect via touch points + WebKit.\n const isMacWebKit = /Macintosh/.test(ua) && /AppleWebKit/.test(ua) && !/Chrome/.test(ua);\n return isMacWebKit && ((navigator as { maxTouchPoints?: number }).maxTouchPoints ?? 0) > 1;\n}\n\nexport type StorageStatus = {\n /** Total quota granted to this origin, in MB (best-effort estimate). */\n quotaMB: number;\n /** Bytes currently used by this origin, in MB. */\n usageMB: number;\n /** quota − usage, in MB. */\n availableMB: number;\n /** Storage is persistent (exempt from eviction). On iOS this is effectively\n * only true once the site is installed to the Home Screen. */\n persisted: boolean;\n /** Running as an installed/standalone PWA. */\n installed: boolean;\n /** Platform is iOS/iPadOS (install is the quota unlock here). */\n ios: boolean;\n};\n\n/** Snapshot of the origin's storage situation — quota, usage, persistence, and\n * whether the app is installed. Use it to decide whether to recommend install\n * before downloading a large model. */\nexport async function getStorageStatus(): Promise<StorageStatus> {\n const installed = isStandalone();\n const ios = isIOS();\n let quotaMB = 0;\n let usageMB = 0;\n let persisted = false;\n try {\n const est = await navigator.storage?.estimate?.();\n quotaMB = Math.round((est?.quota || 0) / 1e6);\n usageMB = Math.round((est?.usage || 0) / 1e6);\n } catch {\n /* estimate unsupported */\n }\n try {\n persisted = (await navigator.storage?.persisted?.()) ?? false;\n } catch {\n /* persisted unsupported */\n }\n return {\n quotaMB,\n usageMB,\n availableMB: Math.max(0, quotaMB - usageMB),\n persisted,\n installed,\n ios,\n };\n}\n\n/**\n * Request persistent storage (exempt from eviction). Returns whether the origin\n * is persistent afterwards. Browsers grant this based on engagement/installation;\n * on iOS Safari it is effectively granted only to an installed (Home Screen) PWA,\n * so call this AND guide users to install when it returns false on iOS.\n */\nexport async function requestPersistentStorage(): Promise<boolean> {\n try {\n if (await navigator.storage?.persisted?.()) return true;\n return (await navigator.storage?.persist?.()) ?? false;\n } catch {\n return false;\n }\n}\n\nexport type ModelFit = {\n /** The model likely fits in the currently-available quota. */\n fits: boolean;\n availableMB: number;\n /** Caching durably would benefit from installing to the Home Screen — true when\n * not installed on iOS, or when the model doesn't fit the current quota. */\n recommendInstall: boolean;\n};\n\n/**\n * Estimate whether a model of `sizeMB` will cache in the current quota, and\n * whether you should recommend installing to the Home Screen first. Pair with a\n * one-time \"Install for offline use\" prompt before a large download on mobile.\n */\nexport async function canCacheModel(sizeMB: number): Promise<ModelFit> {\n const s = await getStorageStatus();\n // Headroom: leave ~10% slack so we don't recommend a download that just barely\n // fits and then fails mid-write.\n const fits = s.availableMB >= sizeMB * 1.1;\n const recommendInstall = (!fits || (s.ios && !s.installed)) && !s.persisted;\n return { fits, availableMB: s.availableMB, recommendInstall };\n}\n\n/**\n * Platform-appropriate install guidance. iOS Safari has NO programmatic install\n * prompt — installation is manual (Share → Add to Home Screen), so apps should\n * show these instructions. Other platforms (Android/Chrome) fire\n * `beforeinstallprompt`, which apps can capture for a one-tap button.\n */\nexport function getInstallGuidance(): { installed: boolean; manual: boolean; steps: string } {\n const installed = isStandalone();\n if (installed) return { installed: true, manual: false, steps: \"Already installed.\" };\n if (isIOS()) {\n return {\n installed: false,\n manual: true,\n steps:\n \"Tap the Share button, then 'Add to Home Screen'. Installing unlocks durable storage so models download once instead of every visit.\",\n };\n }\n return {\n installed: false,\n manual: false,\n steps:\n \"Use your browser's Install option (or the install icon in the address bar) to add this app for offline use and durable model storage.\",\n };\n}\n","// ============================================\n// Audio Playback Utilities\n// ============================================\n\n/**\n * Play audio from Float32Array using Web Audio API\n *\n * @example\n * ```ts\n * import { playAudio } from \"@tryhamster/gerbil/browser\";\n *\n * const audio = new Float32Array([...]); // TTS output\n * const controller = await playAudio(audio, 24000);\n *\n * // Stop playback\n * controller.stop();\n * ```\n */\nexport async function playAudio(\n audio: Float32Array,\n sampleRate: number = 24000,\n): Promise<{ stop: () => void; onEnded: Promise<void> }> {\n const audioContext = new AudioContext();\n\n // Resume if suspended\n if (audioContext.state === \"suspended\") {\n await audioContext.resume();\n }\n\n const audioBuffer = audioContext.createBuffer(1, audio.length, sampleRate);\n const channelData = new Float32Array(audio);\n audioBuffer.copyToChannel(channelData, 0);\n\n const sourceNode = audioContext.createBufferSource();\n sourceNode.buffer = audioBuffer;\n sourceNode.connect(audioContext.destination);\n\n const onEnded = new Promise<void>((resolve) => {\n sourceNode.onended = () => {\n audioContext.close();\n resolve();\n };\n });\n\n sourceNode.start();\n\n return {\n stop: () => {\n sourceNode.stop();\n audioContext.close();\n },\n onEnded,\n };\n}\n\n/**\n * Create a reusable audio player for streaming TTS\n *\n * @example\n * ```ts\n * import { createAudioPlayer } from \"@tryhamster/gerbil/browser\";\n *\n * const player = createAudioPlayer(24000);\n *\n * // Queue audio chunks as they arrive\n * player.queue(chunk1);\n * player.queue(chunk2);\n *\n * // Stop and clear\n * player.stop();\n * ```\n */\nexport function createAudioPlayer(sampleRate: number = 24000): {\n queue: (audio: Float32Array) => void;\n stop: () => void;\n isPlaying: () => boolean;\n} {\n let audioContext: AudioContext | null = null;\n let nextStartTime = 0;\n let isActive = false;\n\n const ensureContext = async () => {\n if (!audioContext) {\n audioContext = new AudioContext();\n }\n if (audioContext.state === \"suspended\") {\n await audioContext.resume();\n }\n return audioContext;\n };\n\n return {\n queue: async (audio: Float32Array) => {\n const ctx = await ensureContext();\n isActive = true;\n\n const buffer = ctx.createBuffer(1, audio.length, sampleRate);\n const channelData = new Float32Array(audio);\n buffer.copyToChannel(channelData, 0);\n\n const source = ctx.createBufferSource();\n source.buffer = buffer;\n source.connect(ctx.destination);\n\n // Schedule seamlessly after previous chunk\n const startTime = Math.max(ctx.currentTime, nextStartTime);\n source.start(startTime);\n nextStartTime = startTime + buffer.duration;\n\n source.onended = () => {\n if (ctx.currentTime >= nextStartTime - 0.1) {\n isActive = false;\n }\n };\n },\n\n stop: () => {\n isActive = false;\n nextStartTime = 0;\n if (audioContext) {\n audioContext.close();\n audioContext = null;\n }\n },\n\n isPlaying: () => isActive,\n };\n}\n","// ============================================\n// iOS Model Guards & Device Capability Detection\n// ============================================\n\n// ============================================\n// Real native-engine model ids\n// ============================================\n//\n// The website loads models by their actual repo id (MLX 4-bit on-device builds,\n// or the upstream Qwen/Liquid repos on desktop). The guard matches on those ids\n// — NOT the old ONNX-era shorthands — so it must stay in sync with the real\n// checkpoints the engine can run. Matching is substring-based on a normalized\n// id so both `mlx-community/Qwen3.5-0.8B-4bit` and `Qwen/Qwen3.5-0.8B` resolve.\n\n/** Recommended safe model ids per modality (used as fallbacks on mobile). */\nconst SAFE_MOBILE_CHAT = \"mlx-community/Qwen3.5-0.8B-4bit\";\n\n/**\n * Approximate on-device (INT4) memory footprint in MB for the models the native\n * engine actually ships. Used for memory-aware selection and messaging.\n */\nexport const MODEL_SIZES: Record<string, number> = {\n // Chat models (INT4, on-device)\n \"qwen3.5-0.8b\": 650, // ~0.65GB\n \"qwen3.5-2b\": 1700, // ~1.7GB\n \"gemma-4-e2b\": 3600, // ~3.6GB\n \"lfm2.5-350m\": 300, // ~0.3GB\n // TTS models\n \"kokoro-82m\": 350,\n \"supertonic-66m\": 300,\n // STT models\n \"whisper-tiny\": 150,\n \"whisper-tiny.en\": 150,\n \"whisper-small\": 500,\n // Embedding models\n \"all-minilm-l6-v2\": 100,\n};\n\n/**\n * Normalize a repo/model id to a lowercase token stream for substring matching\n * (strips org prefixes' punctuation while preserving the model name tokens).\n */\nfunction normalizeId(modelId: string): string {\n return modelId.toLowerCase().replace(/[^a-z0-9]/g, \"-\");\n}\n\n/**\n * iOS (WKWebView) model classification keyed off the REAL native-engine ids.\n *\n * - blocked: too large for the WKWebView memory ceiling on iPhone — will crash.\n * gemma-4-e2b (~3.6GB) plus any vision checkpoint (the vision encoder pushes\n * the working set well past what an iPhone can hold).\n * - risky: borderline on iPhone — Qwen3.5-2B (~1.7GB) fits on newer devices but\n * warns and can OOM on older ones.\n * - everything else (Qwen3.5-0.8B ~0.65GB, LFM2.5-350M) is allowed everywhere.\n */\nconst IOS_MODEL_LIMITS = {\n /** Substrings (normalized) of ids that HARD-BLOCK on iPhone. */\n blocked: [\"gemma-4-e2b\", \"gemma-4-e4b\"],\n /** Substrings that mark a vision checkpoint (blocked on iPhone). */\n visionMarkers: [\"vision\", \"-vl-\", \"-vl\", \"vlm\", \"image-text\", \"-it-vision\"],\n /** Substrings of ids that WARN on iPhone (Qwen3.5-2B class). */\n risky: [\"qwen3.5-2b\", \"qwen3-5-2b\"],\n /** Maximum total memory budget in MB for iOS WKWebView. */\n maxBudgetMB: 1800,\n} as const;\n\n/**\n * Check if a model is safe to load on the current device.\n * Returns guidance specific to iOS memory constraints. Matches on the real\n * native-engine repo ids (MLX 4-bit / upstream Qwen / Liquid).\n */\nexport function isModelSafeForDevice(modelId: string): {\n safe: boolean;\n reason: string;\n recommendation?: string;\n maxSafeModel?: string;\n} {\n const ua = typeof navigator !== \"undefined\" ? navigator.userAgent : \"\";\n const isIPhone = /iPhone|iPod/.test(ua);\n const isIPad = /iPad/.test(ua);\n const isIOS = isIPhone || isIPad;\n const isIOSChrome = isIOS && /CriOS/.test(ua);\n const normalizedId = normalizeId(modelId);\n\n const isVision = IOS_MODEL_LIMITS.visionMarkers.some((m) =>\n normalizedId.includes(normalizeId(m)),\n );\n const isBlocked =\n IOS_MODEL_LIMITS.blocked.some((m) => normalizedId.includes(normalizeId(m))) || isVision;\n const isRisky = IOS_MODEL_LIMITS.risky.some((m) => normalizedId.includes(normalizeId(m)));\n\n // iPhone is the hard constraint (smallest WKWebView budget). iPad has more\n // headroom, so only the genuinely huge / vision checkpoints are blocked there.\n if (isIPhone) {\n if (isBlocked) {\n const browserNote = isIOSChrome ? \" (iOS Chrome uses WKWebView, same limits as Safari)\" : \"\";\n const why = isVision\n ? \"Vision checkpoints need a separate image encoder in memory\"\n : \"It is too large (~3.6GB)\";\n return {\n safe: false,\n reason: `Model ${modelId} will crash on iPhone${browserNote}. ${why}, which exceeds the WKWebView memory ceiling.`,\n recommendation: `Use ${SAFE_MOBILE_CHAT} (Qwen3.5-0.8B) on iPhone, or run larger models on desktop.`,\n maxSafeModel: SAFE_MOBILE_CHAT,\n };\n }\n if (isRisky) {\n return {\n safe: true,\n reason: `Model ${modelId} (~1.7GB) is borderline on iPhone and may run out of memory on older devices.`,\n recommendation: `If it crashes, fall back to ${SAFE_MOBILE_CHAT} (Qwen3.5-0.8B).`,\n maxSafeModel: SAFE_MOBILE_CHAT,\n };\n }\n return { safe: true, reason: \"Model is within iPhone memory limits.\" };\n }\n\n if (isIPad) {\n // iPad tolerates the 2B class; only block the huge / vision checkpoints.\n if (isBlocked) {\n const why = isVision\n ? \"Vision checkpoints need a separate image encoder in memory\"\n : \"It is too large (~3.6GB)\";\n return {\n safe: false,\n reason: `Model ${modelId} may crash on iPad. ${why}, which can exceed the WKWebView memory ceiling.`,\n recommendation: `Use ${SAFE_MOBILE_CHAT} (Qwen3.5-0.8B) or Qwen3.5-2B on iPad.`,\n maxSafeModel: \"mlx-community/Qwen3.5-2B-4bit\",\n };\n }\n return { safe: true, reason: \"Model is within iPad memory limits.\" };\n }\n\n // Android - block the huge / vision checkpoints, allow the rest.\n const isAndroid = /Android/.test(ua);\n if (isAndroid && isBlocked) {\n return {\n safe: false,\n reason: `Model ${modelId} is very large and may crash on Android devices.`,\n recommendation: `Use ${SAFE_MOBILE_CHAT} (Qwen3.5-0.8B) or Qwen3.5-2B on Android.`,\n maxSafeModel: \"mlx-community/Qwen3.5-2B-4bit\",\n };\n }\n\n // Desktop - all models are safe.\n return { safe: true, reason: \"Desktop browser has sufficient memory.\" };\n}\n\n/**\n * Get recommended models based on device memory and capabilities.\n * Helps prevent OOM crashes on low-memory mobile devices.\n */\nexport function getRecommendedModels(): {\n chat: string;\n tts: string;\n stt: string;\n embedding: string;\n reason: string;\n deviceMemory: number | null;\n isMobile: boolean;\n} {\n const ua = typeof navigator !== \"undefined\" ? navigator.userAgent : \"\";\n const deviceMemory = typeof navigator !== \"undefined\" ? (navigator as any).deviceMemory : null;\n const isMobile = /iPhone|iPad|iPod|Android|Mobile/.test(ua);\n\n // Estimate available memory (deviceMemory reports total GB, not available)\n // Mobile devices typically have less free memory due to OS overhead\n const effectiveMemory = deviceMemory ? (isMobile ? deviceMemory * 0.4 : deviceMemory * 0.6) : 4;\n const availableMB = effectiveMemory * 1024;\n\n let chat: string;\n let reason: string;\n\n if (availableMB < 600) {\n chat = \"LiquidAI/LFM2.5-350M\";\n reason = \"Very low memory device - using smallest model (LFM2.5-350M)\";\n } else if (isMobile && availableMB < 2200) {\n chat = SAFE_MOBILE_CHAT;\n reason = \"Mobile device - using Qwen3.5-0.8B to stay within the WKWebView memory limit\";\n } else if (availableMB < 2200) {\n chat = SAFE_MOBILE_CHAT;\n reason = \"Standard model for moderate memory (Qwen3.5-0.8B)\";\n } else {\n chat = \"mlx-community/Qwen3.5-2B-4bit\";\n reason = \"High memory available - using Qwen3.5-2B for better quality\";\n }\n\n return {\n chat,\n tts: \"kokoro-82m\",\n stt: \"whisper-tiny.en\",\n embedding: \"all-MiniLM-L6-v2\",\n reason,\n deviceMemory,\n isMobile,\n };\n}\n\n// ============================================\n// Session Phase Tracking (Reload Detection)\n// ============================================\n\ntype DownloadPhase = \"idle\" | \"downloading\" | \"caching\" | \"initializing\" | \"ready\" | \"error\";\n\nexport const SESSION_STORAGE_KEY = \"gerbil_session_phase\";\n\nexport type SessionState = {\n phase: DownloadPhase;\n modelId: string | null;\n sessionId: string;\n timestamp: number;\n bytesDownloaded?: number;\n totalBytes?: number;\n};\n\n/**\n * Generate a unique session ID for tracking across reloads.\n */\nfunction generateSessionId(): string {\n return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n}\n\n/**\n * Get or create the current session ID.\n */\nfunction getSessionId(): string {\n if (typeof localStorage === \"undefined\") return generateSessionId();\n\n let sessionId = sessionStorage.getItem(\"gerbil_session_id\");\n if (!sessionId) {\n sessionId = generateSessionId();\n sessionStorage.setItem(\"gerbil_session_id\", sessionId);\n }\n return sessionId;\n}\n\n/**\n * Set the current download/initialization phase.\n * Used to detect if a reload happened during a critical operation.\n */\nexport function setDownloadPhase(\n phase: DownloadPhase,\n modelId?: string,\n progress?: { bytesDownloaded: number; totalBytes: number },\n): void {\n if (typeof localStorage === \"undefined\") return;\n\n const state: SessionState = {\n phase,\n modelId: modelId || null,\n sessionId: getSessionId(),\n timestamp: Date.now(),\n bytesDownloaded: progress?.bytesDownloaded,\n totalBytes: progress?.totalBytes,\n };\n\n localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(state));\n}\n\n/**\n * Get the last known download phase from storage.\n */\nexport function getDownloadPhase(): SessionState | null {\n if (typeof localStorage === \"undefined\") return null;\n\n try {\n const raw = localStorage.getItem(SESSION_STORAGE_KEY);\n if (!raw) return null;\n return JSON.parse(raw) as SessionState;\n } catch {\n return null;\n }\n}\n\n/**\n * Detect if the page reloaded during a model download/initialization.\n * This typically indicates an iOS memory crash.\n *\n * @returns Detection result with recommended action\n */\nexport function detectMemoryCrash(): {\n crashed: boolean;\n phase?: DownloadPhase;\n modelId?: string;\n timeSinceCrash?: number;\n recommendation?: string;\n} {\n const lastState = getDownloadPhase();\n const currentSessionId = getSessionId();\n\n if (!lastState) {\n return { crashed: false };\n }\n\n // If session ID changed and we were in a critical phase, it's likely a crash/reload\n const criticalPhases: DownloadPhase[] = [\"downloading\", \"caching\", \"initializing\"];\n const wasInCriticalPhase = criticalPhases.includes(lastState.phase);\n const sessionChanged = lastState.sessionId !== currentSessionId;\n const timeSinceCrash = Date.now() - lastState.timestamp;\n\n // Only count as crash if it happened recently (within 5 minutes) and session changed\n const recentCrash = timeSinceCrash < 5 * 60 * 1000;\n\n if (wasInCriticalPhase && sessionChanged && recentCrash) {\n // Clear the state to prevent repeated detection\n localStorage.removeItem(SESSION_STORAGE_KEY);\n\n return {\n crashed: true,\n phase: lastState.phase,\n modelId: lastState.modelId || undefined,\n timeSinceCrash,\n recommendation:\n lastState.modelId && /2b|gemma|vision/i.test(lastState.modelId)\n ? \"The model was too large for your device. Try Qwen3.5-0.8B instead.\"\n : \"Your device ran out of memory. Try a smaller model or use a desktop browser.\",\n };\n }\n\n return { crashed: false };\n}\n\n/**\n * Clear session phase (call when model loads successfully).\n */\nexport function clearDownloadPhase(): void {\n if (typeof localStorage === \"undefined\") return;\n localStorage.removeItem(SESSION_STORAGE_KEY);\n}\n\n// ============================================\n// WebKit Submit-Granularity (group size) Probe\n// ============================================\n//\n// On WebKit/iOS WebGPU, decode speed is bound by GPU round-trips: the executor\n// groups `webkitGroupSize` dispatches per command buffer, then submits + drains\n// (queue.onSubmittedWorkDone) per group. Larger groups amortize the drains and\n// can be ~5x faster on iPad. BUT the safe ceiling is device-dependent in three\n// classes:\n// 1. Newer WebKit (iPad 26.5): high group is correct + fast.\n// 2. Older WebKit: high group yields zero/garbage logits (wrong output).\n// 3. iPhone (iOS 18.7): high group HARD-CRASHES the GPU process (page dies).\n//\n// The crash class is the hard one: a bad group kills the page before any code\n// can record the result. So we use a localStorage BREADCRUMB that OUTLIVES the\n// page kill: we persist the candidate we're about to try BEFORE running any\n// GPU-heavy work. If the page survives, a later promotion clears `trying` and\n// records it as known-good. If the page crashed, `trying` is still set on the\n// next load, so we KNOW that rung crashed and cap below it.\n//\n// The probe escalates UP from the safe floor (never starts optimistic — a\n// first-visit crash is a terrible UX) and persists per-device in localStorage.\n\nexport const WEBKIT_GROUP_PROBE_KEY = \"gerbil-webkit-group-v1\";\n\n/**\n * Candidate group-size ladder. We escalate UP one rung per page load. 128 is\n * treated as effectively \"batch-all\" for this model — kept simple on purpose.\n */\nexport const WEBKIT_GROUP_LADDER = [1, 8, 32, 64, 128] as const;\n\nexport type WebkitGroupProbe = {\n /** Largest group size proven crash-free AND correct on this device. Starts at 1. */\n knownGood: number;\n /** Candidate being attempted this page-load (null when not probing). */\n trying: number | null;\n /** True once we hit a crash/incorrect ceiling and should stop escalating. */\n capped: boolean;\n};\n\nconst DEFAULT_PROBE: WebkitGroupProbe = { knownGood: 1, trying: null, capped: false };\n\n/** Read the persisted WebKit group probe record (guarded; safe on node). */\nexport function readGroupProbe(): WebkitGroupProbe {\n if (typeof localStorage === \"undefined\") return { ...DEFAULT_PROBE };\n try {\n const raw = localStorage.getItem(WEBKIT_GROUP_PROBE_KEY);\n if (!raw) return { ...DEFAULT_PROBE };\n const parsed = JSON.parse(raw) as Partial<WebkitGroupProbe>;\n return {\n knownGood:\n typeof parsed.knownGood === \"number\" && parsed.knownGood >= 1 ? parsed.knownGood : 1,\n trying: typeof parsed.trying === \"number\" ? parsed.trying : null,\n capped: parsed.capped === true,\n };\n } catch {\n return { ...DEFAULT_PROBE };\n }\n}\n\n/** Persist the WebKit group probe record (guarded; no-op on node). */\nexport function writeGroupProbe(rec: WebkitGroupProbe): void {\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.setItem(WEBKIT_GROUP_PROBE_KEY, JSON.stringify(rec));\n } catch {\n // Best-effort; private mode / quota can throw.\n }\n}\n\n/**\n * The validated non-phone sweet spot. iPad swept 1→7.9, 8→19, 32→24.8, 64→26.6,\n * 128→26.9 (peak), 256→26.2 tok/s — a plateau from ~64 up, so 128 is the best\n * stable target (more batching just costs memory). Non-phone WebKit jumps here\n * directly; the crash breadcrumb caps it down if a device can't sustain it.\n */\nconst NONPHONE_TARGET_GROUP = 128;\n\n/**\n * Resolve the WebKit group size to use this session, recording `trying` as a\n * side effect so a crash this load is detectable on the next load.\n *\n * Algorithm (only meaningful on WebKit; inert otherwise):\n * 1. Read the record (default {knownGood:1, trying:null, capped:false}).\n * 2. If `trying !== null` on entry → the previous load set it but never cleared\n * it → that load CRASHED at `trying`. Cap there, keep `knownGood`, clear\n * `trying`. Use `knownGood` this session.\n * 3. Else if !capped and there is a rung above `knownGood` → set `trying = next`,\n * persist BEFORE any GPU work, and use it (we're escalating).\n * 4. Else → use `knownGood`.\n *\n * @returns the group size to use this session.\n */\nexport function resolveWebkitGroupSize(args: {\n override?: number;\n isWebKit: boolean;\n /**\n * Memory-constrained devices (phones) are the \"crash class\": batching crashes\n * the GPU process, and discovering that costs one user-visible crash before the\n * breadcrumb caps it. When `conservative` is set we never auto-escalate — the\n * device stays at its proven floor (group=1 on first run) and never risks that\n * calibration crash. `?group=N` still lets such a device opt in explicitly.\n */\n conservative?: boolean;\n}): number {\n // Explicit ?group=N override wins and skips the probe entirely.\n if (args.override && args.override > 0) return args.override;\n // Non-WebKit (desktop / Dawn / node) never groups — floor of 1, no bookkeeping.\n if (!args.isWebKit) return 1;\n\n const rec = readGroupProbe();\n\n // Step 2: a stale `trying` means last load crashed at that rung.\n if (rec.trying !== null) {\n const crashedAt = rec.trying;\n const next: WebkitGroupProbe = {\n knownGood: rec.knownGood,\n trying: null,\n capped: true,\n };\n writeGroupProbe(next);\n console.log(\n `[engine] webkit group probe: previous load crashed at group=${crashedAt} → capping at knownGood=${next.knownGood}`,\n );\n return next.knownGood;\n }\n\n // Step 3: non-phone WebKit (iPad / Mac Safari) jumps straight to the validated\n // sweet spot (group=128) instead of climbing rung-by-rung — these devices\n // tolerate batching (iPad ran every rung to 256 coherently), so there's no\n // reason to crawl. The breadcrumb (Step 2) caps it down if a particular device\n // can't sustain it. Phones (conservative) NEVER escalate — they stay at the\n // proven floor (group=1) to avoid the one calibration crash batching costs on\n // the crash class.\n if (!rec.capped && !args.conservative && rec.knownGood < NONPHONE_TARGET_GROUP) {\n const next: WebkitGroupProbe = {\n knownGood: rec.knownGood,\n trying: NONPHONE_TARGET_GROUP,\n capped: false,\n };\n writeGroupProbe(next); // PERSIST BEFORE any GPU-heavy work runs\n console.log(\n `[engine] webkit group probe: trying target group=${NONPHONE_TARGET_GROUP} (knownGood=${rec.knownGood})`,\n );\n return NONPHONE_TARGET_GROUP;\n }\n\n // Step 4: nothing to escalate (capped or at top rung) — use the proven floor.\n console.log(\n `[engine] webkit group probe: knownGood=${rec.knownGood} trying=null capped=${rec.capped} → using ${rec.knownGood}`,\n );\n return rec.knownGood;\n}\n\n/**\n * Promote (or cap) the WebKit group probe after the first successful forward.\n *\n * Call this once per page-load, after the model has loaded AND a first forward\n * completed without the page dying. The breadcrumb already handles the crash\n * class (the page death leaves `trying` set for the next load); this handles the\n * wrong-output class and records success.\n *\n * @param correct true if the first forward produced non-corrupt output.\n * - correct → promote: knownGood = trying, trying = null.\n * - incorrect → cap: keep knownGood at the prior rung, trying = null, capped.\n */\nexport function promoteGroupProbe(correct: boolean): void {\n if (typeof localStorage === \"undefined\") return;\n const rec = readGroupProbe();\n // Nothing was being attempted this load — nothing to promote.\n if (rec.trying === null) return;\n\n if (correct) {\n writeGroupProbe({ knownGood: rec.trying, trying: null, capped: rec.capped });\n console.log(`[engine] webkit group probe: PROMOTED group=${rec.trying} to known-good`);\n } else {\n // Wrong-output class: this rung is incorrect (but did not crash). Keep the\n // prior knownGood and stop escalating.\n writeGroupProbe({ knownGood: rec.knownGood, trying: null, capped: true });\n console.log(\n `[engine] webkit group probe: group=${rec.trying} produced INCORRECT output → capping at knownGood=${rec.knownGood}`,\n );\n }\n}\n","import { setDownloadPhase } from \"./device-guards.js\";\n\n// ============================================\n// Chunked Resumable Downloader\n// ============================================\n\n/** Chunk size for downloads: 1.5MB (safe for iOS IndexedDB transactions) */\nexport const CHUNK_SIZE_BYTES = 1.5 * 1024 * 1024;\n\n/** IndexedDB database name for chunked downloads */\nexport const DOWNLOAD_DB_NAME = \"gerbil-model-chunks\";\nconst DOWNLOAD_DB_VERSION = 1;\n\n/**\n * Manifest stored in IndexedDB to track download progress.\n */\ntype DownloadManifest = {\n modelId: string;\n url: string;\n etag: string | null;\n totalBytes: number;\n chunkSize: number;\n completedChunks: number[];\n createdAt: number;\n updatedAt: number;\n};\n\n/**\n * Open (or create) the IndexedDB for chunked downloads.\n */\nasync function openDownloadDB(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(DOWNLOAD_DB_NAME, DOWNLOAD_DB_VERSION);\n\n request.onerror = () =>\n reject(new Error(`Failed to open download DB: ${request.error?.message}`));\n\n request.onsuccess = () => resolve(request.result);\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n\n // Store for download manifests\n if (!db.objectStoreNames.contains(\"manifests\")) {\n db.createObjectStore(\"manifests\", { keyPath: \"modelId\" });\n }\n\n // Store for actual chunks (key: modelId-chunkIndex)\n if (!db.objectStoreNames.contains(\"chunks\")) {\n db.createObjectStore(\"chunks\");\n }\n };\n });\n}\n\n/**\n * Get download manifest for a model.\n */\nasync function getManifest(db: IDBDatabase, modelId: string): Promise<DownloadManifest | null> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(\"manifests\", \"readonly\");\n const store = tx.objectStore(\"manifests\");\n const request = store.get(modelId);\n\n request.onerror = () => reject(new Error(`Failed to get manifest: ${request.error?.message}`));\n request.onsuccess = () => resolve(request.result || null);\n });\n}\n\n/**\n * Save download manifest.\n */\nasync function saveManifest(db: IDBDatabase, manifest: DownloadManifest): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(\"manifests\", \"readwrite\");\n const store = tx.objectStore(\"manifests\");\n const request = store.put(manifest);\n\n request.onerror = () => reject(new Error(`Failed to save manifest: ${request.error?.message}`));\n request.onsuccess = () => resolve();\n });\n}\n\n/**\n * Save a single chunk.\n */\nasync function saveChunk(\n db: IDBDatabase,\n modelId: string,\n chunkIndex: number,\n data: ArrayBuffer,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(\"chunks\", \"readwrite\");\n const store = tx.objectStore(\"chunks\");\n const key = `${modelId}-${chunkIndex}`;\n const request = store.put(data, key);\n\n request.onerror = () =>\n reject(new Error(`Failed to save chunk ${chunkIndex}: ${request.error?.message}`));\n request.onsuccess = () => resolve();\n });\n}\n\n/**\n * Get a single chunk.\n */\nasync function getChunk(\n db: IDBDatabase,\n modelId: string,\n chunkIndex: number,\n): Promise<ArrayBuffer | null> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(\"chunks\", \"readonly\");\n const store = tx.objectStore(\"chunks\");\n const key = `${modelId}-${chunkIndex}`;\n const request = store.get(key);\n\n request.onerror = () =>\n reject(new Error(`Failed to get chunk ${chunkIndex}: ${request.error?.message}`));\n request.onsuccess = () => resolve(request.result || null);\n });\n}\n\n/**\n * Delete all chunks and manifest for a model.\n */\nasync function clearModelData(db: IDBDatabase, modelId: string): Promise<void> {\n // Get manifest to know how many chunks to delete\n const manifest = await getManifest(db, modelId);\n\n return new Promise((resolve, reject) => {\n const tx = db.transaction([\"manifests\", \"chunks\"], \"readwrite\");\n\n // Delete manifest\n tx.objectStore(\"manifests\").delete(modelId);\n\n // Delete all chunks\n if (manifest) {\n const totalChunks = Math.ceil(manifest.totalBytes / manifest.chunkSize);\n const chunkStore = tx.objectStore(\"chunks\");\n for (let i = 0; i < totalChunks; i++) {\n chunkStore.delete(`${modelId}-${i}`);\n }\n }\n\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(new Error(`Failed to clear model data: ${tx.error?.message}`));\n });\n}\n\n/**\n * Chunked resumable downloader for large model files.\n * Downloads in 1.5MB chunks to avoid iOS memory pressure.\n */\nexport async function downloadModelChunked(\n url: string,\n modelId: string,\n options: {\n onProgress?: (info: {\n phase: string;\n bytesDownloaded: number;\n totalBytes: number;\n percent: number;\n }) => void;\n signal?: AbortSignal;\n } = {},\n): Promise<ArrayBuffer> {\n const { onProgress, signal } = options;\n\n // Update session phase\n setDownloadPhase(\"downloading\", modelId);\n\n const db = await openDownloadDB();\n\n try {\n // Check for existing manifest\n let manifest = await getManifest(db, modelId);\n\n // Fetch headers to get content-length and etag\n const headResponse = await fetch(url, { method: \"HEAD\", signal });\n if (!headResponse.ok) {\n throw new Error(`HEAD request failed: ${headResponse.status} ${headResponse.statusText}`);\n }\n\n const contentLength = parseInt(headResponse.headers.get(\"content-length\") || \"0\", 10);\n const etag = headResponse.headers.get(\"etag\");\n const acceptRanges = headResponse.headers.get(\"accept-ranges\");\n\n if (!contentLength) {\n throw new Error(\"Server did not provide content-length\");\n }\n\n // Check if we need to restart (etag mismatch means model updated)\n if (manifest && manifest.etag !== etag) {\n console.warn(`Model ${modelId} has been updated (etag mismatch). Clearing cached chunks.`);\n await clearModelData(db, modelId);\n manifest = null;\n }\n\n // Check if server supports range requests\n const supportsRange = acceptRanges === \"bytes\";\n\n if (!supportsRange) {\n // Fall back to regular download\n console.warn(`Server doesn't support range requests for ${modelId}. Using regular download.`);\n db.close();\n\n const response = await fetch(url, { signal });\n if (!response.ok) throw new Error(`Download failed: ${response.status}`);\n\n setDownloadPhase(\"caching\", modelId);\n const buffer = await response.arrayBuffer();\n setDownloadPhase(\"ready\", modelId);\n return buffer;\n }\n\n // Create or update manifest\n const totalChunks = Math.ceil(contentLength / CHUNK_SIZE_BYTES);\n\n if (!manifest) {\n manifest = {\n modelId,\n url,\n etag,\n totalBytes: contentLength,\n chunkSize: CHUNK_SIZE_BYTES,\n completedChunks: [],\n createdAt: Date.now(),\n updatedAt: Date.now(),\n };\n await saveManifest(db, manifest);\n }\n\n // Download missing chunks\n for (let i = 0; i < totalChunks; i++) {\n if (signal?.aborted) {\n throw new Error(\"Download aborted\");\n }\n\n // Skip already downloaded chunks\n if (manifest.completedChunks.includes(i)) {\n const bytesDownloaded = (manifest.completedChunks.length / totalChunks) * contentLength;\n onProgress?.({\n phase: \"resuming\",\n bytesDownloaded,\n totalBytes: contentLength,\n percent: Math.round((bytesDownloaded / contentLength) * 100),\n });\n continue;\n }\n\n const start = i * CHUNK_SIZE_BYTES;\n const end = Math.min(start + CHUNK_SIZE_BYTES - 1, contentLength - 1);\n\n // Download chunk with Range header\n const response = await fetch(url, {\n headers: { Range: `bytes=${start}-${end}` },\n signal,\n });\n\n if (response.status !== 206) {\n throw new Error(`Range request failed: ${response.status} (expected 206)`);\n }\n\n const chunkData = await response.arrayBuffer();\n\n // Save chunk to IndexedDB\n await saveChunk(db, modelId, i, chunkData);\n\n // Update manifest\n manifest.completedChunks.push(i);\n manifest.updatedAt = Date.now();\n await saveManifest(db, manifest);\n\n // Update session phase with progress\n const bytesDownloaded = manifest.completedChunks.length * CHUNK_SIZE_BYTES;\n setDownloadPhase(\"downloading\", modelId, { bytesDownloaded, totalBytes: contentLength });\n\n onProgress?.({\n phase: \"downloading\",\n bytesDownloaded: Math.min(bytesDownloaded, contentLength),\n totalBytes: contentLength,\n percent: Math.round((manifest.completedChunks.length / totalChunks) * 100),\n });\n\n // Null out reference to allow GC before next chunk\n // @ts-expect-error - intentional null for GC\n response.body = null;\n }\n\n // All chunks downloaded - reassemble\n setDownloadPhase(\"caching\", modelId);\n onProgress?.({\n phase: \"assembling\",\n bytesDownloaded: contentLength,\n totalBytes: contentLength,\n percent: 100,\n });\n\n // Assemble chunks into final buffer\n // We do this incrementally to avoid holding all chunks in memory at once\n const finalBuffer = new ArrayBuffer(contentLength);\n const finalView = new Uint8Array(finalBuffer);\n\n for (let i = 0; i < totalChunks; i++) {\n const chunk = await getChunk(db, modelId, i);\n if (!chunk) {\n throw new Error(`Missing chunk ${i} during assembly`);\n }\n\n const offset = i * CHUNK_SIZE_BYTES;\n finalView.set(new Uint8Array(chunk), offset);\n\n // Null out chunk reference immediately after copying\n // This helps GC reclaim memory on iOS\n }\n\n // Clean up - delete chunks now that we have the full file\n await clearModelData(db, modelId);\n db.close();\n\n setDownloadPhase(\"ready\", modelId);\n return finalBuffer;\n } catch (error) {\n setDownloadPhase(\"error\", modelId);\n db.close();\n throw error;\n }\n}\n\n/**\n * Check if a model has an incomplete download.\n */\nexport async function hasIncompleteDownload(modelId: string): Promise<{\n incomplete: boolean;\n bytesDownloaded?: number;\n totalBytes?: number;\n percent?: number;\n}> {\n try {\n const db = await openDownloadDB();\n const manifest = await getManifest(db, modelId);\n db.close();\n\n if (!manifest) {\n return { incomplete: false };\n }\n\n const totalChunks = Math.ceil(manifest.totalBytes / manifest.chunkSize);\n const completedChunks = manifest.completedChunks.length;\n\n if (completedChunks < totalChunks) {\n return {\n incomplete: true,\n bytesDownloaded: completedChunks * manifest.chunkSize,\n totalBytes: manifest.totalBytes,\n percent: Math.round((completedChunks / totalChunks) * 100),\n };\n }\n\n return { incomplete: false };\n } catch {\n return { incomplete: false };\n }\n}\n\n/**\n * Clear incomplete download data for a model.\n */\nexport async function clearIncompleteDownload(modelId: string): Promise<void> {\n try {\n const db = await openDownloadDB();\n await clearModelData(db, modelId);\n db.close();\n } catch {\n // Ignore errors\n }\n}\n\n/**\n * Check if there's enough storage quota for a model download.\n * Returns estimated available space and whether download should proceed.\n */\nexport async function checkStorageQuota(requiredMB: number = 500): Promise<{\n ok: boolean;\n availableMB: number;\n usedMB: number;\n quotaMB: number;\n message?: string;\n}> {\n if (typeof navigator === \"undefined\" || !navigator.storage?.estimate) {\n return {\n ok: true,\n availableMB: -1,\n usedMB: -1,\n quotaMB: -1,\n message: \"Storage API not available\",\n };\n }\n\n try {\n const { quota, usage } = await navigator.storage.estimate();\n const quotaMB = Math.round((quota || 0) / 1_000_000);\n const usedMB = Math.round((usage || 0) / 1_000_000);\n const availableMB = quotaMB - usedMB;\n\n if (availableMB < requiredMB) {\n return {\n ok: false,\n availableMB,\n usedMB,\n quotaMB,\n message: `Need ${requiredMB}MB but only ${availableMB}MB available. Clear browser data or free up space.`,\n };\n }\n\n return { ok: true, availableMB, usedMB, quotaMB };\n } catch (e: any) {\n return {\n ok: true, // Proceed optimistically if we can't check\n availableMB: -1,\n usedMB: -1,\n quotaMB: -1,\n message: `Storage check failed: ${e.message}`,\n };\n }\n}\n"],"mappings":"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"}
|