@oh-my-pi/pi-coding-agent 15.12.3 → 15.13.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.
Files changed (457) hide show
  1. package/CHANGELOG.md +347 -7
  2. package/dist/cli.js +1615 -1231
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  8. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  9. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  10. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  11. package/dist/types/autoresearch/types.d.ts +1 -1
  12. package/dist/types/cli/args.d.ts +19 -2
  13. package/dist/types/cli/models-cli.d.ts +49 -0
  14. package/dist/types/cli/session-picker.d.ts +1 -1
  15. package/dist/types/cli/setup-cli.d.ts +1 -1
  16. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  17. package/dist/types/collab/protocol.d.ts +1 -1
  18. package/dist/types/commands/launch.d.ts +0 -3
  19. package/dist/types/commands/models.d.ts +33 -0
  20. package/dist/types/commands/say.d.ts +24 -0
  21. package/dist/types/commands/token.d.ts +25 -0
  22. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  23. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  24. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  25. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  26. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  27. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  28. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  29. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  30. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  31. package/dist/types/commit/changelog/generate.d.ts +1 -1
  32. package/dist/types/commit/shared-llm.d.ts +1 -1
  33. package/dist/types/config/keybindings.d.ts +3 -3
  34. package/dist/types/config/model-registry.d.ts +17 -0
  35. package/dist/types/config/models-config-schema.d.ts +13 -1
  36. package/dist/types/config/models-config.d.ts +8 -2
  37. package/dist/types/config/settings-schema.d.ts +281 -58
  38. package/dist/types/edit/hashline/params.d.ts +1 -1
  39. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  40. package/dist/types/edit/modes/patch.d.ts +1 -1
  41. package/dist/types/edit/modes/replace.d.ts +1 -1
  42. package/dist/types/export/html/index.d.ts +2 -1
  43. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  44. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  46. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  47. package/dist/types/extensibility/extensions/types.d.ts +49 -3
  48. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  49. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  50. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  51. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  52. package/dist/types/extensibility/shared-events.d.ts +1 -1
  53. package/dist/types/extensibility/skills.d.ts +10 -0
  54. package/dist/types/goals/guided-setup.d.ts +18 -0
  55. package/dist/types/goals/state.d.ts +1 -1
  56. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  57. package/dist/types/hindsight/transcript.d.ts +1 -1
  58. package/dist/types/index.d.ts +5 -0
  59. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  60. package/dist/types/lsp/types.d.ts +1 -1
  61. package/dist/types/main.d.ts +4 -3
  62. package/dist/types/mcp/manager.d.ts +8 -0
  63. package/dist/types/mcp/startup-events.d.ts +11 -0
  64. package/dist/types/memories/index.d.ts +7 -0
  65. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  66. package/dist/types/mnemopi/config.d.ts +28 -0
  67. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  68. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  69. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  70. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  71. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  72. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +1 -0
  74. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  75. package/dist/types/modes/components/session-selector.d.ts +1 -1
  76. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  77. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  78. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  79. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  80. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  81. package/dist/types/modes/components/usage-row.d.ts +3 -0
  82. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  83. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  84. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  85. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  86. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  87. package/dist/types/modes/image-references.d.ts +6 -0
  88. package/dist/types/modes/interactive-mode.d.ts +27 -6
  89. package/dist/types/modes/magic-keywords.d.ts +13 -1
  90. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  91. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  92. package/dist/types/modes/runtime-init.d.ts +4 -0
  93. package/dist/types/modes/theme/theme.d.ts +13 -2
  94. package/dist/types/modes/types.d.ts +8 -7
  95. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  96. package/dist/types/registry/agent-registry.d.ts +17 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +28 -35
  99. package/dist/types/session/agent-storage.d.ts +2 -1
  100. package/dist/types/session/indexed-session-storage.d.ts +3 -3
  101. package/dist/types/session/messages.d.ts +8 -10
  102. package/dist/types/session/session-context.d.ts +39 -0
  103. package/dist/types/session/session-entries.d.ts +159 -0
  104. package/dist/types/session/session-listing.d.ts +69 -0
  105. package/dist/types/session/session-loader.d.ts +16 -0
  106. package/dist/types/session/session-manager.d.ts +85 -462
  107. package/dist/types/session/session-migrations.d.ts +12 -0
  108. package/dist/types/session/session-paths.d.ts +25 -0
  109. package/dist/types/session/session-persistence.d.ts +8 -0
  110. package/dist/types/session/session-storage.d.ts +11 -7
  111. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  112. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  113. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  114. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  115. package/dist/types/stt/asr-client.d.ts +90 -0
  116. package/dist/types/stt/asr-protocol.d.ts +97 -0
  117. package/dist/types/stt/asr-worker.d.ts +2 -0
  118. package/dist/types/stt/downloader.d.ts +38 -0
  119. package/dist/types/stt/endpointer.d.ts +59 -0
  120. package/dist/types/stt/index.d.ts +5 -1
  121. package/dist/types/stt/models.d.ts +120 -0
  122. package/dist/types/stt/recorder.d.ts +17 -0
  123. package/dist/types/stt/stt-controller.d.ts +6 -0
  124. package/dist/types/stt/transcriber.d.ts +5 -7
  125. package/dist/types/stt/wav.d.ts +29 -0
  126. package/dist/types/system-prompt.d.ts +4 -0
  127. package/dist/types/task/executor.d.ts +2 -0
  128. package/dist/types/task/index.d.ts +9 -1
  129. package/dist/types/task/types.d.ts +37 -1
  130. package/dist/types/tools/ask.d.ts +1 -1
  131. package/dist/types/tools/ast-edit.d.ts +1 -1
  132. package/dist/types/tools/ast-grep.d.ts +1 -1
  133. package/dist/types/tools/bash.d.ts +3 -3
  134. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  135. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  136. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  137. package/dist/types/tools/browser/registry.d.ts +16 -3
  138. package/dist/types/tools/browser/render.d.ts +2 -0
  139. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  140. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  141. package/dist/types/tools/browser.d.ts +3 -1
  142. package/dist/types/tools/checkpoint.d.ts +1 -1
  143. package/dist/types/tools/debug.d.ts +1 -1
  144. package/dist/types/tools/eval-render.d.ts +1 -1
  145. package/dist/types/tools/eval.d.ts +1 -1
  146. package/dist/types/tools/find.d.ts +1 -1
  147. package/dist/types/tools/gh.d.ts +1 -1
  148. package/dist/types/tools/image-gen.d.ts +1 -1
  149. package/dist/types/tools/index.d.ts +14 -2
  150. package/dist/types/tools/inspect-image.d.ts +1 -1
  151. package/dist/types/tools/irc.d.ts +2 -1
  152. package/dist/types/tools/job.d.ts +1 -1
  153. package/dist/types/tools/learn.d.ts +51 -0
  154. package/dist/types/tools/manage-skill.d.ts +40 -0
  155. package/dist/types/tools/memory-edit.d.ts +1 -1
  156. package/dist/types/tools/memory-recall.d.ts +1 -1
  157. package/dist/types/tools/memory-reflect.d.ts +1 -1
  158. package/dist/types/tools/memory-retain.d.ts +1 -1
  159. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  160. package/dist/types/tools/read.d.ts +1 -1
  161. package/dist/types/tools/render-mermaid.d.ts +1 -1
  162. package/dist/types/tools/renderers.d.ts +7 -11
  163. package/dist/types/tools/resolve.d.ts +1 -1
  164. package/dist/types/tools/review.d.ts +1 -1
  165. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  166. package/dist/types/tools/search.d.ts +1 -1
  167. package/dist/types/tools/ssh.d.ts +2 -2
  168. package/dist/types/tools/todo.d.ts +2 -2
  169. package/dist/types/tools/tts.d.ts +26 -1
  170. package/dist/types/tools/write.d.ts +2 -2
  171. package/dist/types/tts/downloader.d.ts +20 -0
  172. package/dist/types/tts/index.d.ts +8 -0
  173. package/dist/types/tts/models.d.ts +82 -0
  174. package/dist/types/tts/player.d.ts +32 -0
  175. package/dist/types/tts/runtime.d.ts +6 -0
  176. package/dist/types/tts/streaming-player.d.ts +41 -0
  177. package/dist/types/tts/tts-client.d.ts +93 -0
  178. package/dist/types/tts/tts-protocol.d.ts +95 -0
  179. package/dist/types/tts/tts-worker.d.ts +2 -0
  180. package/dist/types/tts/vocalizer.d.ts +41 -0
  181. package/dist/types/tts/wav.d.ts +8 -0
  182. package/dist/types/utils/clipboard.d.ts +4 -3
  183. package/dist/types/utils/image-loading.d.ts +18 -1
  184. package/dist/types/utils/thinking-display.d.ts +17 -0
  185. package/dist/types/utils/tool-choice.d.ts +8 -0
  186. package/dist/types/utils/tools-manager.d.ts +2 -1
  187. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  188. package/dist/types/web/scrapers/github.d.ts +1 -1
  189. package/dist/types/web/search/index.d.ts +1 -1
  190. package/package.json +17 -16
  191. package/src/async/job-manager.ts +49 -0
  192. package/src/autolearn/controller.ts +139 -0
  193. package/src/autolearn/managed-skills.ts +257 -0
  194. package/src/autoresearch/state.ts +1 -1
  195. package/src/autoresearch/storage.ts +2 -1
  196. package/src/autoresearch/tools/init-experiment.ts +1 -1
  197. package/src/autoresearch/tools/log-experiment.ts +1 -1
  198. package/src/autoresearch/tools/run-experiment.ts +1 -1
  199. package/src/autoresearch/tools/update-notes.ts +1 -1
  200. package/src/autoresearch/types.ts +1 -1
  201. package/src/cli/args.ts +56 -10
  202. package/src/cli/auth-gateway-cli.ts +1 -1
  203. package/src/cli/bench-cli.ts +1 -1
  204. package/src/cli/dry-balance-cli.ts +1 -1
  205. package/src/cli/models-cli.ts +427 -0
  206. package/src/cli/session-picker.ts +2 -1
  207. package/src/cli/setup-cli.ts +148 -47
  208. package/src/cli/setup-model-picker.ts +43 -0
  209. package/src/cli-commands.ts +3 -0
  210. package/src/cli.ts +45 -13
  211. package/src/collab/host.ts +10 -13
  212. package/src/collab/protocol.ts +1 -1
  213. package/src/commands/launch.ts +0 -3
  214. package/src/commands/models.ts +61 -0
  215. package/src/commands/say.ts +102 -0
  216. package/src/commands/setup.ts +1 -1
  217. package/src/commands/token.ts +89 -0
  218. package/src/commit/agentic/tools/analyze-file.ts +4 -1
  219. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  220. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  221. package/src/commit/agentic/tools/git-overview.ts +1 -1
  222. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  223. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  224. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  225. package/src/commit/agentic/tools/schemas.ts +1 -1
  226. package/src/commit/agentic/tools/split-commit.ts +1 -1
  227. package/src/commit/analysis/summary.ts +1 -1
  228. package/src/commit/changelog/generate.ts +1 -1
  229. package/src/commit/shared-llm.ts +1 -1
  230. package/src/config/keybindings.ts +2 -2
  231. package/src/config/model-discovery.ts +11 -5
  232. package/src/config/model-registry.ts +79 -21
  233. package/src/config/model-resolver.ts +2 -2
  234. package/src/config/models-config-schema.ts +5 -2
  235. package/src/config/models-config.ts +2 -1
  236. package/src/config/settings-schema.ts +266 -32
  237. package/src/config/settings.ts +10 -0
  238. package/src/discovery/builtin.ts +23 -1
  239. package/src/discovery/claude-plugins.ts +44 -5
  240. package/src/discovery/helpers.ts +41 -1
  241. package/src/edit/hashline/params.ts +1 -1
  242. package/src/edit/modes/apply-patch.ts +1 -1
  243. package/src/edit/modes/patch.ts +1 -1
  244. package/src/edit/modes/replace.ts +1 -1
  245. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  246. package/src/eval/agent-bridge.ts +1 -1
  247. package/src/eval/completion-bridge.ts +1 -1
  248. package/src/eval/js/shared/prelude.txt +69 -17
  249. package/src/export/html/index.ts +3 -6
  250. package/src/export/html/template.js +24 -2
  251. package/src/export/html/tool-views.generated.js +2 -2
  252. package/src/extensibility/custom-commands/loader.ts +1 -1
  253. package/src/extensibility/custom-commands/types.ts +2 -2
  254. package/src/extensibility/custom-tools/loader.ts +1 -1
  255. package/src/extensibility/custom-tools/types.ts +2 -2
  256. package/src/extensibility/extensions/loader.ts +2 -2
  257. package/src/extensibility/extensions/model-api.ts +41 -0
  258. package/src/extensibility/extensions/runner.ts +4 -0
  259. package/src/extensibility/extensions/types.ts +54 -3
  260. package/src/extensibility/extensions/wrapper.ts +41 -5
  261. package/src/extensibility/hooks/index.ts +2 -1
  262. package/src/extensibility/hooks/loader.ts +1 -1
  263. package/src/extensibility/hooks/types.ts +2 -2
  264. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  265. package/src/extensibility/plugins/loader.ts +30 -19
  266. package/src/extensibility/plugins/manager.ts +221 -90
  267. package/src/extensibility/shared-events.ts +1 -1
  268. package/src/extensibility/skills.ts +101 -5
  269. package/src/goals/guided-setup.ts +133 -0
  270. package/src/goals/state.ts +1 -1
  271. package/src/goals/tools/goal-tool.ts +1 -1
  272. package/src/hindsight/transcript.ts +1 -1
  273. package/src/index.ts +5 -0
  274. package/src/internal-urls/docs-index.generated.ts +13 -10
  275. package/src/internal-urls/history-protocol.ts +1 -1
  276. package/src/internal-urls/local-protocol.ts +29 -7
  277. package/src/lsp/types.ts +1 -1
  278. package/src/main.ts +27 -32
  279. package/src/mcp/config-writer.ts +7 -3
  280. package/src/mcp/manager.ts +11 -0
  281. package/src/mcp/startup-events.ts +21 -0
  282. package/src/mcp/transports/stdio.ts +2 -1
  283. package/src/memories/index.ts +149 -12
  284. package/src/memories/storage.ts +2 -1
  285. package/src/memory-backend/local-backend.ts +11 -5
  286. package/src/mnemopi/backend.ts +1 -0
  287. package/src/mnemopi/config.ts +112 -12
  288. package/src/modes/acp/acp-agent.ts +8 -53
  289. package/src/modes/acp/acp-event-mapper.ts +5 -1
  290. package/src/modes/components/agent-hub.ts +51 -5
  291. package/src/modes/components/assistant-message.ts +12 -44
  292. package/src/modes/components/compaction-summary-message.ts +125 -26
  293. package/src/modes/components/custom-editor.test.ts +96 -0
  294. package/src/modes/components/custom-editor.ts +164 -8
  295. package/src/modes/components/index.ts +1 -0
  296. package/src/modes/components/logout-account-selector.ts +130 -0
  297. package/src/modes/components/mcp-add-wizard.ts +1 -1
  298. package/src/modes/components/model-selector.ts +2 -2
  299. package/src/modes/components/session-selector.ts +1 -1
  300. package/src/modes/components/settings-defs.ts +7 -0
  301. package/src/modes/components/status-line/component.ts +54 -157
  302. package/src/modes/components/status-line/segments.ts +1 -1
  303. package/src/modes/components/status-line/types.ts +2 -1
  304. package/src/modes/components/tool-execution.ts +82 -43
  305. package/src/modes/components/transcript-container.ts +70 -1
  306. package/src/modes/components/tree-selector.ts +1 -1
  307. package/src/modes/components/usage-row.ts +18 -0
  308. package/src/modes/components/user-message.ts +4 -2
  309. package/src/modes/controllers/command-controller.ts +14 -16
  310. package/src/modes/controllers/event-controller.ts +101 -73
  311. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  312. package/src/modes/controllers/input-controller.ts +311 -57
  313. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  314. package/src/modes/controllers/selector-controller.ts +68 -12
  315. package/src/modes/controllers/streaming-reveal.ts +4 -3
  316. package/src/modes/gradient-highlight.ts +21 -9
  317. package/src/modes/image-references.ts +20 -0
  318. package/src/modes/interactive-mode.ts +288 -48
  319. package/src/modes/magic-keywords.ts +27 -5
  320. package/src/modes/rpc/rpc-mode.ts +146 -14
  321. package/src/modes/rpc/rpc-subagents.ts +2 -2
  322. package/src/modes/rpc/rpc-types.ts +8 -2
  323. package/src/modes/runtime-init.ts +28 -3
  324. package/src/modes/theme/theme.ts +99 -51
  325. package/src/modes/types.ts +6 -7
  326. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  327. package/src/modes/utils/ui-helpers.ts +36 -7
  328. package/src/priority.json +5 -1
  329. package/src/prompts/agents/task.md +1 -0
  330. package/src/prompts/goals/guided-goal-interview.md +8 -0
  331. package/src/prompts/goals/guided-goal-system.md +12 -0
  332. package/src/prompts/memories/read-path.md +6 -0
  333. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  334. package/src/prompts/system/autolearn-guidance.md +7 -0
  335. package/src/prompts/system/autolearn-nudge.md +3 -0
  336. package/src/prompts/system/eager-task.md +7 -0
  337. package/src/prompts/system/eager-todo.md +11 -6
  338. package/src/prompts/system/empty-stop-retry.md +4 -6
  339. package/src/prompts/system/subagent-system-prompt.md +4 -0
  340. package/src/prompts/system/system-prompt.md +10 -5
  341. package/src/prompts/system/title-marker-instruction.md +1 -0
  342. package/src/prompts/system/title-system-marker.md +16 -0
  343. package/src/prompts/tools/job.md +1 -0
  344. package/src/prompts/tools/learn.md +7 -0
  345. package/src/prompts/tools/manage-skill.md +9 -0
  346. package/src/prompts/tools/task.md +3 -0
  347. package/src/registry/agent-registry.ts +30 -0
  348. package/src/sdk.ts +103 -43
  349. package/src/secrets/obfuscator.ts +1 -1
  350. package/src/session/agent-session.ts +331 -318
  351. package/src/session/agent-storage.ts +18 -9
  352. package/src/session/history-storage.ts +3 -2
  353. package/src/session/indexed-session-storage.ts +7 -10
  354. package/src/session/messages.ts +9 -11
  355. package/src/session/session-context.ts +352 -0
  356. package/src/session/session-dump-format.ts +4 -2
  357. package/src/session/session-entries.ts +194 -0
  358. package/src/session/session-listing.ts +588 -0
  359. package/src/session/session-loader.ts +106 -0
  360. package/src/session/session-manager.ts +968 -3064
  361. package/src/session/session-migrations.ts +78 -0
  362. package/src/session/session-paths.ts +193 -0
  363. package/src/session/session-persistence.ts +131 -0
  364. package/src/session/session-storage.ts +91 -30
  365. package/src/session/snapcompact-inline.ts +21 -1
  366. package/src/session/snapcompact-savings-journal.ts +113 -0
  367. package/src/session/tool-choice-queue.ts +23 -11
  368. package/src/slash-commands/builtin-registry.ts +40 -4
  369. package/src/slash-commands/helpers/logout.ts +88 -0
  370. package/src/stt/asr-client.ts +520 -0
  371. package/src/stt/asr-protocol.ts +65 -0
  372. package/src/stt/asr-worker.ts +790 -0
  373. package/src/stt/downloader.ts +107 -47
  374. package/src/stt/endpointer.ts +259 -0
  375. package/src/stt/index.ts +5 -1
  376. package/src/stt/models.ts +150 -0
  377. package/src/stt/recorder.ts +247 -60
  378. package/src/stt/stt-controller.ts +201 -22
  379. package/src/stt/transcriber.ts +37 -68
  380. package/src/stt/wav.ts +173 -0
  381. package/src/system-prompt.ts +8 -0
  382. package/src/task/agents.ts +1 -2
  383. package/src/task/executor.ts +49 -15
  384. package/src/task/index.ts +60 -6
  385. package/src/task/render.ts +83 -8
  386. package/src/task/types.ts +54 -1
  387. package/src/tools/ask.ts +9 -1
  388. package/src/tools/ast-edit.ts +1 -1
  389. package/src/tools/ast-grep.ts +1 -1
  390. package/src/tools/bash.ts +5 -4
  391. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  392. package/src/tools/browser/cmux/rpc.ts +156 -0
  393. package/src/tools/browser/cmux/socket-client.ts +309 -0
  394. package/src/tools/browser/registry.ts +37 -3
  395. package/src/tools/browser/render.ts +6 -1
  396. package/src/tools/browser/tab-protocol.ts +2 -0
  397. package/src/tools/browser/tab-supervisor.ts +189 -18
  398. package/src/tools/browser/tab-worker.ts +1 -1
  399. package/src/tools/browser.ts +16 -1
  400. package/src/tools/checkpoint.ts +1 -1
  401. package/src/tools/debug.ts +1 -1
  402. package/src/tools/eval-render.ts +4 -3
  403. package/src/tools/eval.ts +11 -6
  404. package/src/tools/fetch.ts +13 -2
  405. package/src/tools/find.ts +1 -1
  406. package/src/tools/gh.ts +1 -1
  407. package/src/tools/github-cache.ts +2 -1
  408. package/src/tools/image-gen.ts +1 -1
  409. package/src/tools/index.ts +43 -5
  410. package/src/tools/inspect-image.ts +3 -1
  411. package/src/tools/irc.ts +11 -3
  412. package/src/tools/job.ts +15 -3
  413. package/src/tools/learn.ts +144 -0
  414. package/src/tools/manage-skill.ts +104 -0
  415. package/src/tools/memory-edit.ts +1 -1
  416. package/src/tools/memory-recall.ts +1 -1
  417. package/src/tools/memory-reflect.ts +1 -1
  418. package/src/tools/memory-retain.ts +1 -1
  419. package/src/tools/plan-mode-guard.ts +53 -19
  420. package/src/tools/read.ts +8 -2
  421. package/src/tools/render-mermaid.ts +1 -1
  422. package/src/tools/renderers.ts +7 -11
  423. package/src/tools/report-tool-issue.ts +3 -2
  424. package/src/tools/resolve.ts +1 -1
  425. package/src/tools/review.ts +1 -1
  426. package/src/tools/search-tool-bm25.ts +1 -1
  427. package/src/tools/search.ts +1 -1
  428. package/src/tools/ssh.ts +5 -4
  429. package/src/tools/todo.ts +2 -2
  430. package/src/tools/tts.ts +204 -93
  431. package/src/tools/write.ts +19 -3
  432. package/src/tts/downloader.ts +64 -0
  433. package/src/tts/index.ts +8 -0
  434. package/src/tts/models.ts +137 -0
  435. package/src/tts/player.ts +137 -0
  436. package/src/tts/runtime.ts +21 -0
  437. package/src/tts/streaming-player.ts +266 -0
  438. package/src/tts/tts-client.ts +647 -0
  439. package/src/tts/tts-protocol.ts +60 -0
  440. package/src/tts/tts-worker.ts +497 -0
  441. package/src/tts/vocalizer.ts +162 -0
  442. package/src/tts/wav.ts +58 -0
  443. package/src/utils/clipboard.ts +35 -18
  444. package/src/utils/image-loading.ts +35 -4
  445. package/src/utils/thinking-display.ts +37 -0
  446. package/src/utils/title-generator.ts +48 -5
  447. package/src/utils/tool-choice.ts +16 -0
  448. package/src/utils/tools-manager.test.ts +25 -0
  449. package/src/utils/tools-manager.ts +19 -1
  450. package/src/web/scrapers/github.ts +96 -0
  451. package/src/web/search/index.ts +14 -1
  452. package/src/web/search/providers/searxng.ts +13 -1
  453. package/dist/types/cli/list-models.d.ts +0 -30
  454. package/dist/types/stt/setup.d.ts +0 -18
  455. package/src/cli/list-models.ts +0 -194
  456. package/src/stt/setup.ts +0 -52
  457. package/src/stt/transcribe.py +0 -70
@@ -0,0 +1,790 @@
1
+ import * as fs from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import type {
6
+ AutomaticSpeechRecognitionOutput,
7
+ AutomaticSpeechRecognitionPipeline,
8
+ ProgressInfo,
9
+ } from "@huggingface/transformers";
10
+ import {
11
+ ensureRuntimeInstalled,
12
+ getTinyModelsCacheDir,
13
+ installRuntimeModuleResolver,
14
+ isCompiledBinary,
15
+ resolveRuntimeModule,
16
+ } from "@oh-my-pi/pi-utils";
17
+ import packageJson from "../../package.json" with { type: "json" };
18
+ import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "../tiny/device";
19
+ import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "../tiny/dtype";
20
+ import type { SttProgressEvent, SttTransport, SttWorkerInbound } from "./asr-protocol";
21
+ import { type EndpointerEvent, StreamEndpointer } from "./endpointer";
22
+ import {
23
+ getSttModelSpec,
24
+ type SherpaSttModelSpec,
25
+ type SttModel,
26
+ type SttModelKey,
27
+ type TransformersSttModelSpec,
28
+ } from "./models";
29
+
30
+ const ASR_TASK = "automatic-speech-recognition";
31
+ const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
32
+ const SHERPA_PACKAGE = "sherpa-onnx-node";
33
+ const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
34
+ // Whisper long-form decoding: split into 30s windows with 5s overlap so audio of
35
+ // any length transcribes without exceeding the 30s receptive field.
36
+ const CHUNK_LENGTH_S = 30;
37
+ const STRIDE_LENGTH_S = 5;
38
+ // The client always resamples to 16 kHz mono float32 before sending; sherpa-onnx
39
+ // is told the true input rate (it resamples internally to its feature config).
40
+ const ASR_SAMPLE_RATE = 16_000;
41
+ // Hub origin for raw sherpa-onnx model files (encoder/decoder/joiner/tokens).
42
+ const HF_RESOLVE_BASE = "https://huggingface.co";
43
+ // Coalesce download progress so streaming a multi-hundred-MB model file doesn't
44
+ // flood the IPC channel with one event per chunk.
45
+ const PROGRESS_EMIT_BYTES = 4_000_000;
46
+ const sourceRequire = createRequire(import.meta.url);
47
+
48
+ const sttModelDevicePreference = resolveTinyModelDevicePreference();
49
+ const sttModelDtypeOverride = resolveTinyModelDtypeOverride();
50
+
51
+ /**
52
+ * Subset of the transformers.js ASR call options we set. The index signature
53
+ * mirrors `GenerationFunctionParameters` so this is assignable to the pipeline's
54
+ * `Partial<AutomaticSpeechRecognitionConfig>` param (not re-exported from the
55
+ * package root, so we model only what we pass).
56
+ */
57
+ interface AsrCallOptions {
58
+ chunk_length_s: number;
59
+ stride_length_s: number;
60
+ return_timestamps: boolean;
61
+ task?: string;
62
+ language?: string;
63
+ [key: string]: unknown;
64
+ }
65
+
66
+ interface TransformersRuntime {
67
+ env: {
68
+ cacheDir?: string;
69
+ allowLocalModels?: boolean;
70
+ logLevel?: unknown;
71
+ };
72
+ LogLevel: {
73
+ ERROR: unknown;
74
+ };
75
+ pipeline: (
76
+ task: typeof ASR_TASK,
77
+ model: string,
78
+ options: {
79
+ device: TinyModelDevice;
80
+ dtype: TinyModelDtype;
81
+ progress_callback: (info: ProgressInfo) => void;
82
+ },
83
+ ) => Promise<AutomaticSpeechRecognitionPipeline>;
84
+ }
85
+
86
+ /** Recognition result returned by `sherpa-onnx-node`'s offline recognizer. */
87
+ interface SherpaOfflineResult {
88
+ text?: string;
89
+ }
90
+
91
+ /** A sherpa-onnx offline stream that accepts a single waveform before decoding. */
92
+ interface SherpaOfflineStream {
93
+ acceptWaveform(audio: { samples: Float32Array; sampleRate: number }): void;
94
+ }
95
+
96
+ interface SherpaOfflineRecognizer {
97
+ createStream(): SherpaOfflineStream;
98
+ decodeAsync(stream: SherpaOfflineStream): Promise<SherpaOfflineResult>;
99
+ }
100
+
101
+ /** Offline recognizer config passed to `sherpa-onnx-node` (transducer family). */
102
+ interface SherpaOfflineConfig {
103
+ modelConfig: {
104
+ transducer: { encoder: string; decoder: string; joiner: string };
105
+ tokens: string;
106
+ modelType: string;
107
+ numThreads: number;
108
+ provider: string;
109
+ debug: number;
110
+ };
111
+ decodingMethod: string;
112
+ }
113
+
114
+ /** Subset of the native `sherpa-onnx-node` module surface we use. */
115
+ interface SherpaRuntime {
116
+ OfflineRecognizer: {
117
+ createAsync(config: SherpaOfflineConfig): Promise<SherpaOfflineRecognizer>;
118
+ };
119
+ }
120
+
121
+ /** A warm model plus the engine that loaded it; cached per tier key. */
122
+ type LoadedModel =
123
+ | { engine: "transformers"; pipeline: AutomaticSpeechRecognitionPipeline }
124
+ | { engine: "sherpa"; recognizer: SherpaOfflineRecognizer };
125
+
126
+ const models = new Map<SttModelKey, Promise<LoadedModel>>();
127
+ // Serialize all model inference on a single chain: the recognizers are not
128
+ // guaranteed reentrant and there is one CPU-bound model per tier. Batch
129
+ // transcribes and live-stream segment/partial decodes share this lock.
130
+ let modelLock = Promise.resolve();
131
+ function runOnModel<T>(work: () => Promise<T>): Promise<T> {
132
+ const run = modelLock.then(work, work);
133
+ modelLock = run.then(
134
+ () => undefined,
135
+ () => undefined,
136
+ );
137
+ return run;
138
+ }
139
+ let transformersRuntime: Promise<TransformersRuntime> | null = null;
140
+ let sherpaRuntime: Promise<SherpaRuntime> | null = null;
141
+
142
+ let cachedTransformersVersionSpec: string | undefined;
143
+ function resolveTransformersVersionSpec(): string {
144
+ const manifest = packageJson as {
145
+ optionalDependencies?: Record<string, string>;
146
+ dependencies?: Record<string, string>;
147
+ };
148
+ const versionSpec =
149
+ manifest.optionalDependencies?.[TRANSFORMERS_PACKAGE] ?? manifest.dependencies?.[TRANSFORMERS_PACKAGE];
150
+ if (!versionSpec) throw new Error(`${TRANSFORMERS_PACKAGE} is missing from package.json optionalDependencies`);
151
+ if (!versionSpec.startsWith("catalog:")) return versionSpec;
152
+ if (COMPILED_TRANSFORMERS_VERSION) return COMPILED_TRANSFORMERS_VERSION;
153
+ const installed = sourceRequire(`${TRANSFORMERS_PACKAGE}/package.json`) as { version: string };
154
+ return installed.version;
155
+ }
156
+
157
+ /**
158
+ * Lazily resolve (and memoize) the transformers version spec. In the `catalog:`
159
+ * case this `require`s the installed package manifest, so defer it to the
160
+ * compiled-binary runtime-install path (only reached on a real transcribe /
161
+ * download) — loading this worker for a smoke ping never triggers the resolve.
162
+ */
163
+ function getTransformersVersionSpec(): string {
164
+ cachedTransformersVersionSpec ??= resolveTransformersVersionSpec();
165
+ return cachedTransformersVersionSpec;
166
+ }
167
+
168
+ let cachedSherpaVersionSpec: string | undefined;
169
+ function resolveSherpaVersionSpec(): string {
170
+ const manifest = packageJson as {
171
+ optionalDependencies?: Record<string, string>;
172
+ dependencies?: Record<string, string>;
173
+ };
174
+ const versionSpec = manifest.optionalDependencies?.[SHERPA_PACKAGE] ?? manifest.dependencies?.[SHERPA_PACKAGE];
175
+ if (!versionSpec) throw new Error(`${SHERPA_PACKAGE} is missing from package.json optionalDependencies`);
176
+ return versionSpec;
177
+ }
178
+
179
+ function getSherpaVersionSpec(): string {
180
+ cachedSherpaVersionSpec ??= resolveSherpaVersionSpec();
181
+ return cachedSherpaVersionSpec;
182
+ }
183
+
184
+ function errorText(error: unknown): string {
185
+ return error instanceof Error ? (error.stack ?? error.message) : String(error);
186
+ }
187
+
188
+ function errorMessage(error: unknown): string {
189
+ return error instanceof Error ? error.message : String(error);
190
+ }
191
+
192
+ function sendLog(
193
+ transport: SttTransport,
194
+ level: "debug" | "warn" | "error",
195
+ msg: string,
196
+ meta?: Record<string, unknown>,
197
+ ): void {
198
+ transport.send({ type: "log", level, msg, meta });
199
+ }
200
+
201
+ function getSttRuntimeDir(): string {
202
+ const key = getTransformersVersionSpec().replace(/[^A-Za-z0-9._-]/g, "_");
203
+ return path.join(path.dirname(getTinyModelsCacheDir()), "stt-runtime", `transformers-${key}`);
204
+ }
205
+
206
+ function getSherpaRuntimeDir(): string {
207
+ const key = getSherpaVersionSpec().replace(/[^A-Za-z0-9._-]/g, "_");
208
+ return path.join(path.dirname(getTinyModelsCacheDir()), "stt-runtime", `sherpa-${key}`);
209
+ }
210
+
211
+ function sendRuntimeInstallProgress(
212
+ transport: SttTransport,
213
+ requestId: string,
214
+ modelKey: SttModelKey,
215
+ status: "initiate" | "download" | "done",
216
+ name: string,
217
+ ): void {
218
+ transport.send({ type: "progress", id: requestId, event: { modelKey, status, name } });
219
+ }
220
+
221
+ /**
222
+ * Prepare the freshly-installed compiled runtime for loading: stub `sharp` (the
223
+ * speech pipeline is audio-only, so the native image codec is dead weight) and
224
+ * patch the module resolver so Transformers.js's bare requires resolve against
225
+ * the cache. Returns the absolute Transformers.js entrypoint to `require`.
226
+ */
227
+ async function prepareCompiledRuntime(runtimeDir: string): Promise<string> {
228
+ const nodeModules = path.join(runtimeDir, "node_modules");
229
+ const sharpStub = path.join(runtimeDir, "omp-sharp-stub.cjs");
230
+ await Bun.write(sharpStub, "module.exports = {};\n");
231
+ installRuntimeModuleResolver({ runtimeNodeModules: nodeModules, stubs: { sharp: sharpStub } });
232
+ const entry = resolveRuntimeModule(nodeModules, TRANSFORMERS_PACKAGE);
233
+ if (!entry) throw new Error(`Unable to resolve ${TRANSFORMERS_PACKAGE} in compiled runtime at ${nodeModules}`);
234
+ return entry;
235
+ }
236
+
237
+ function configureTransformers(transformers: TransformersRuntime): TransformersRuntime {
238
+ transformers.env.cacheDir = getTinyModelsCacheDir();
239
+ transformers.env.allowLocalModels = false;
240
+ transformers.env.logLevel = transformers.LogLevel.ERROR;
241
+ return transformers;
242
+ }
243
+
244
+ async function loadTransformers(
245
+ transport: SttTransport,
246
+ requestId: string,
247
+ modelKey: SttModelKey,
248
+ ): Promise<TransformersRuntime> {
249
+ if (transformersRuntime) return transformersRuntime;
250
+ transformersRuntime = (async () => {
251
+ if (!isCompiledBinary()) return configureTransformers(sourceRequire(TRANSFORMERS_PACKAGE) as TransformersRuntime);
252
+ const runtimeDir = await ensureRuntimeInstalled({
253
+ runtimeDir: getSttRuntimeDir(),
254
+ install: {
255
+ dependencies: { [TRANSFORMERS_PACKAGE]: getTransformersVersionSpec() },
256
+ trustedDependencies: ["onnxruntime-node"],
257
+ },
258
+ probePackage: TRANSFORMERS_PACKAGE,
259
+ onPhase: phase =>
260
+ sendRuntimeInstallProgress(
261
+ transport,
262
+ requestId,
263
+ modelKey,
264
+ phase,
265
+ `${TRANSFORMERS_PACKAGE}@${getTransformersVersionSpec()}`,
266
+ ),
267
+ });
268
+ const entry = await prepareCompiledRuntime(runtimeDir);
269
+ const require_ = createRequire(entry);
270
+ return configureTransformers(require_(entry) as TransformersRuntime);
271
+ })().catch(error => {
272
+ transformersRuntime = null;
273
+ throw error;
274
+ });
275
+ return transformersRuntime;
276
+ }
277
+
278
+ /**
279
+ * Resolve the native `sherpa-onnx-node` module. In a compiled binary the addon
280
+ * (plus its per-platform prebuilt `sherpa-onnx.node` + bundled onnxruntime
281
+ * dylibs) is installed into a side runtime dir; the addon resolves its native
282
+ * library relative to its own location, so a plain `createRequire` of the entry
283
+ * is enough — no module-resolver patch or bare-require stubbing is needed.
284
+ * Memoized so the runtime loads once per process.
285
+ */
286
+ async function loadSherpaRuntime(
287
+ transport: SttTransport,
288
+ requestId: string,
289
+ modelKey: SttModelKey,
290
+ ): Promise<SherpaRuntime> {
291
+ if (sherpaRuntime) return sherpaRuntime;
292
+ sherpaRuntime = (async () => {
293
+ if (!isCompiledBinary()) return sourceRequire(SHERPA_PACKAGE) as SherpaRuntime;
294
+ const runtimeDir = await ensureRuntimeInstalled({
295
+ runtimeDir: getSherpaRuntimeDir(),
296
+ install: { dependencies: { [SHERPA_PACKAGE]: getSherpaVersionSpec() } },
297
+ probePackage: SHERPA_PACKAGE,
298
+ onPhase: phase =>
299
+ sendRuntimeInstallProgress(
300
+ transport,
301
+ requestId,
302
+ modelKey,
303
+ phase,
304
+ `${SHERPA_PACKAGE}@${getSherpaVersionSpec()}`,
305
+ ),
306
+ });
307
+ const nodeModules = path.join(runtimeDir, "node_modules");
308
+ const entry = resolveRuntimeModule(nodeModules, SHERPA_PACKAGE);
309
+ if (!entry) throw new Error(`Unable to resolve ${SHERPA_PACKAGE} in compiled runtime at ${nodeModules}`);
310
+ return createRequire(entry)(entry) as SherpaRuntime;
311
+ })().catch(error => {
312
+ sherpaRuntime = null;
313
+ throw error;
314
+ });
315
+ return sherpaRuntime;
316
+ }
317
+
318
+ function toProgressEvent(modelKey: SttModelKey, info: ProgressInfo): SttProgressEvent {
319
+ if (info.status === "ready") {
320
+ return { modelKey, status: info.status, task: info.task, model: info.model };
321
+ }
322
+ if (info.status === "progress_total") {
323
+ return {
324
+ modelKey,
325
+ status: info.status,
326
+ name: info.name,
327
+ progress: info.progress,
328
+ loaded: info.loaded,
329
+ total: info.total,
330
+ files: info.files,
331
+ };
332
+ }
333
+ if (info.status === "progress") {
334
+ return {
335
+ modelKey,
336
+ status: info.status,
337
+ name: info.name,
338
+ file: info.file,
339
+ progress: info.progress,
340
+ loaded: info.loaded,
341
+ total: info.total,
342
+ };
343
+ }
344
+ return { modelKey, status: info.status, name: info.name, file: info.file };
345
+ }
346
+
347
+ function sendProgress(transport: SttTransport, id: string, modelKey: SttModelKey, info: ProgressInfo): void {
348
+ transport.send({ type: "progress", id, event: toProgressEvent(modelKey, info) });
349
+ }
350
+
351
+ async function loadPipelineOnDevice(
352
+ transformers: TransformersRuntime,
353
+ spec: TransformersSttModelSpec,
354
+ modelKey: SttModelKey,
355
+ transport: SttTransport,
356
+ requestId: string,
357
+ device: TinyModelDevice,
358
+ ): Promise<AutomaticSpeechRecognitionPipeline> {
359
+ return transformers.pipeline(ASR_TASK, spec.repo, {
360
+ device,
361
+ dtype: sttModelDtypeOverride ?? spec.dtype,
362
+ progress_callback: info => sendProgress(transport, requestId, modelKey, info),
363
+ });
364
+ }
365
+
366
+ async function loadPipelineWithDeviceFallback(
367
+ transformers: TransformersRuntime,
368
+ spec: TransformersSttModelSpec,
369
+ modelKey: SttModelKey,
370
+ transport: SttTransport,
371
+ requestId: string,
372
+ ): Promise<{ pipeline: AutomaticSpeechRecognitionPipeline; device: TinyModelDevice }> {
373
+ const devices = tinyModelDeviceLoadOrder(sttModelDevicePreference);
374
+ if (devices[0] !== sttModelDevicePreference.device) {
375
+ sendLog(transport, "warn", "stt: requested device is unsafe in the worker; using CPU", {
376
+ modelKey,
377
+ repo: spec.repo,
378
+ requestedDevice: sttModelDevicePreference.device,
379
+ device: devices[0],
380
+ });
381
+ }
382
+ for (let i = 0; i < devices.length; i += 1) {
383
+ const device = devices[i]!;
384
+ try {
385
+ return {
386
+ pipeline: await loadPipelineOnDevice(transformers, spec, modelKey, transport, requestId, device),
387
+ device,
388
+ };
389
+ } catch (error) {
390
+ if (i === devices.length - 1) throw error;
391
+ const fallbackDevice = devices[i + 1]!;
392
+ sendLog(transport, "warn", "stt: accelerated device failed; falling back", {
393
+ modelKey,
394
+ repo: spec.repo,
395
+ device,
396
+ fallbackDevice,
397
+ error: errorMessage(error),
398
+ });
399
+ }
400
+ }
401
+ throw new Error("No stt model devices configured");
402
+ }
403
+
404
+ async function loadTransformersModel(
405
+ spec: TransformersSttModelSpec,
406
+ modelKey: SttModelKey,
407
+ transport: SttTransport,
408
+ requestId: string,
409
+ ): Promise<LoadedModel> {
410
+ const transformers = await loadTransformers(transport, requestId, modelKey);
411
+ const startedAt = performance.now();
412
+ const { pipeline, device } = await loadPipelineWithDeviceFallback(
413
+ transformers,
414
+ spec,
415
+ modelKey,
416
+ transport,
417
+ requestId,
418
+ );
419
+ sendLog(transport, "debug", "stt: local model loaded", {
420
+ modelKey,
421
+ repo: spec.repo,
422
+ engine: "transformers",
423
+ device,
424
+ requestedDevice: sttModelDevicePreference.device,
425
+ dtype: sttModelDtypeOverride ?? spec.dtype,
426
+ elapsedMs: Math.round(performance.now() - startedAt),
427
+ });
428
+ return { engine: "transformers", pipeline };
429
+ }
430
+
431
+ /**
432
+ * Stream a single sherpa-onnx model file from the Hub into the cache, writing to
433
+ * a `.part` sidecar and renaming on completion so an interrupted fetch never
434
+ * reads as cached. Emits coalesced per-file progress for the aggregating client.
435
+ */
436
+ async function downloadSherpaFile(
437
+ repo: string,
438
+ filename: string,
439
+ dest: string,
440
+ modelKey: SttModelKey,
441
+ transport: SttTransport,
442
+ requestId: string,
443
+ ): Promise<void> {
444
+ const url = `${HF_RESOLVE_BASE}/${repo}/resolve/main/${filename}`;
445
+ const response = await fetch(url, { redirect: "follow" });
446
+ if (!response.ok || !response.body) {
447
+ throw new Error(`Failed to download ${filename} (${repo}): HTTP ${response.status}`);
448
+ }
449
+ const total = Number(response.headers.get("content-length") ?? 0);
450
+ transport.send({
451
+ type: "progress",
452
+ id: requestId,
453
+ event: { modelKey, status: "download", name: `${repo}/${filename}`, file: filename },
454
+ });
455
+ const part = `${dest}.part`;
456
+ const handle = await fs.open(part, "w");
457
+ let loaded = 0;
458
+ let lastEmitted = 0;
459
+ const reader = response.body.getReader();
460
+ try {
461
+ for (;;) {
462
+ const { done, value } = await reader.read();
463
+ if (done) break;
464
+ if (!value) continue;
465
+ await handle.write(value);
466
+ loaded += value.byteLength;
467
+ if (loaded - lastEmitted >= PROGRESS_EMIT_BYTES || (total > 0 && loaded >= total)) {
468
+ lastEmitted = loaded;
469
+ transport.send({
470
+ type: "progress",
471
+ id: requestId,
472
+ event: {
473
+ modelKey,
474
+ status: "progress",
475
+ name: `${repo}/${filename}`,
476
+ file: filename,
477
+ loaded,
478
+ total: total || loaded,
479
+ },
480
+ });
481
+ }
482
+ }
483
+ } finally {
484
+ await handle.close();
485
+ }
486
+ await fs.rename(part, dest);
487
+ }
488
+
489
+ /**
490
+ * Ensure all sherpa-onnx model files for a tier are present in the cache,
491
+ * downloading any that are missing, and return their absolute paths.
492
+ */
493
+ async function ensureSherpaModelFiles(
494
+ spec: SherpaSttModelSpec,
495
+ modelKey: SttModelKey,
496
+ transport: SttTransport,
497
+ requestId: string,
498
+ ): Promise<{ encoder: string; decoder: string; joiner: string; tokens: string }> {
499
+ const dir = path.join(getTinyModelsCacheDir(), spec.repo);
500
+ await fs.mkdir(dir, { recursive: true });
501
+ const resolved = {} as { encoder: string; decoder: string; joiner: string; tokens: string };
502
+ for (const role in spec.files) {
503
+ const key = role as keyof typeof spec.files;
504
+ const filename = spec.files[key];
505
+ const dest = path.join(dir, filename);
506
+ const present = await fs
507
+ .stat(dest)
508
+ .then(stats => stats.size > 0)
509
+ .catch(() => false);
510
+ if (!present) await downloadSherpaFile(spec.repo, filename, dest, modelKey, transport, requestId);
511
+ resolved[key] = dest;
512
+ }
513
+ return resolved;
514
+ }
515
+
516
+ async function loadSherpaModel(
517
+ spec: SherpaSttModelSpec,
518
+ modelKey: SttModelKey,
519
+ transport: SttTransport,
520
+ requestId: string,
521
+ ): Promise<LoadedModel> {
522
+ const runtime = await loadSherpaRuntime(transport, requestId, modelKey);
523
+ const files = await ensureSherpaModelFiles(spec, modelKey, transport, requestId);
524
+ const startedAt = performance.now();
525
+ const numThreads = Math.max(1, Math.min(4, os.availableParallelism()));
526
+ const recognizer = await runtime.OfflineRecognizer.createAsync({
527
+ modelConfig: {
528
+ transducer: { encoder: files.encoder, decoder: files.decoder, joiner: files.joiner },
529
+ tokens: files.tokens,
530
+ modelType: spec.modelType,
531
+ numThreads,
532
+ provider: "cpu",
533
+ debug: 0,
534
+ },
535
+ decodingMethod: "greedy_search",
536
+ });
537
+ sendLog(transport, "debug", "stt: local model loaded", {
538
+ modelKey,
539
+ repo: spec.repo,
540
+ engine: "sherpa",
541
+ provider: "cpu",
542
+ numThreads,
543
+ elapsedMs: Math.round(performance.now() - startedAt),
544
+ });
545
+ return { engine: "sherpa", recognizer };
546
+ }
547
+
548
+ async function loadModel(modelKey: SttModelKey, transport: SttTransport, requestId: string): Promise<LoadedModel> {
549
+ const spec = getSttModelSpec(modelKey);
550
+ if (!spec) throw new Error(`Unknown stt model: ${modelKey}`);
551
+ const cached = models.get(modelKey);
552
+ if (cached) {
553
+ void cached
554
+ .then(() => {
555
+ transport.send({
556
+ type: "progress",
557
+ id: requestId,
558
+ event: { modelKey, status: "ready", task: ASR_TASK, model: spec.repo },
559
+ });
560
+ })
561
+ .catch(() => undefined);
562
+ return cached;
563
+ }
564
+
565
+ const loading =
566
+ spec.engine === "sherpa"
567
+ ? loadSherpaModel(spec, modelKey, transport, requestId)
568
+ : loadTransformersModel(spec, modelKey, transport, requestId);
569
+ const loaded = loading.then(
570
+ model => {
571
+ transport.send({
572
+ type: "progress",
573
+ id: requestId,
574
+ event: { modelKey, status: "ready", task: ASR_TASK, model: spec.repo },
575
+ });
576
+ return model;
577
+ },
578
+ error => {
579
+ models.delete(modelKey);
580
+ throw error;
581
+ },
582
+ );
583
+ models.set(modelKey, loaded);
584
+ return loaded;
585
+ }
586
+
587
+ async function decodeSegment(
588
+ model: LoadedModel,
589
+ spec: SttModel,
590
+ audio: Float32Array,
591
+ language: string | undefined,
592
+ ): Promise<string> {
593
+ if (model.engine === "sherpa") {
594
+ const stream = model.recognizer.createStream();
595
+ stream.acceptWaveform({ samples: audio, sampleRate: ASR_SAMPLE_RATE });
596
+ const result = await model.recognizer.decodeAsync(stream);
597
+ return (result.text ?? "").trim();
598
+ }
599
+ const options: AsrCallOptions = {
600
+ chunk_length_s: CHUNK_LENGTH_S,
601
+ stride_length_s: STRIDE_LENGTH_S,
602
+ return_timestamps: false,
603
+ };
604
+ // English-only Whisper checkpoints reject `language`/`task`; multilingual ones
605
+ // take the configured source language (auto-detected when omitted).
606
+ if (!spec.englishOnly) {
607
+ options.task = "transcribe";
608
+ if (language) options.language = language;
609
+ }
610
+ const output = (await model.pipeline(audio, options)) as AutomaticSpeechRecognitionOutput;
611
+ return (output.text ?? "").trim();
612
+ }
613
+
614
+ async function transcribeAudio(
615
+ transport: SttTransport,
616
+ requestId: string,
617
+ modelKey: SttModelKey,
618
+ audio: Float32Array,
619
+ language: string | undefined,
620
+ ): Promise<string> {
621
+ const spec = getSttModelSpec(modelKey);
622
+ if (!spec) throw new Error(`Unknown stt model: ${modelKey}`);
623
+ const model = await loadModel(modelKey, transport, requestId);
624
+ return runOnModel(() => decodeSegment(model, spec, audio, language));
625
+ }
626
+
627
+ async function handleBatchRequest(
628
+ transport: SttTransport,
629
+ request: Extract<SttWorkerInbound, { type: "transcribe" | "download" }>,
630
+ ): Promise<void> {
631
+ try {
632
+ if (request.type === "download") {
633
+ await loadModel(request.modelKey, transport, request.id);
634
+ transport.send({ type: "downloaded", id: request.id });
635
+ return;
636
+ }
637
+ const text = await transcribeAudio(transport, request.id, request.modelKey, request.audio, request.language);
638
+ transport.send({ type: "transcription", id: request.id, text });
639
+ } catch (error) {
640
+ transport.send({ type: "error", id: request.id, error: errorText(error) });
641
+ }
642
+ }
643
+
644
+ // ── Live streaming sessions ─────────────────────────────────────────
645
+
646
+ /** State for one in-flight {@link StreamEndpointer}-driven streaming session. */
647
+ interface StreamingSession {
648
+ id: string;
649
+ spec: SttModel;
650
+ language: string | undefined;
651
+ model: Promise<LoadedModel>;
652
+ endpointer: StreamEndpointer;
653
+ /** Finalized segments awaiting decode, in order. */
654
+ segmentQueue: Float32Array[];
655
+ /** Latest in-progress segment audio awaiting a volatile partial decode (coalesced). */
656
+ pendingPartial: Float32Array | null;
657
+ /** Committed segment transcripts, joined for the final result. */
658
+ committed: string[];
659
+ segmentIndex: number;
660
+ pumping: boolean;
661
+ cancelled: boolean;
662
+ ended: boolean;
663
+ }
664
+
665
+ const sessions = new Map<string, StreamingSession>();
666
+
667
+ function startStreamingSession(
668
+ transport: SttTransport,
669
+ request: Extract<SttWorkerInbound, { type: "stream_start" }>,
670
+ ): void {
671
+ const spec = getSttModelSpec(request.modelKey);
672
+ if (!spec) {
673
+ transport.send({ type: "error", id: request.id, error: `Unknown stt model: ${request.modelKey}` });
674
+ return;
675
+ }
676
+ sessions.set(request.id, {
677
+ id: request.id,
678
+ spec,
679
+ language: request.language,
680
+ model: loadModel(request.modelKey, transport, request.id),
681
+ endpointer: new StreamEndpointer(),
682
+ segmentQueue: [],
683
+ pendingPartial: null,
684
+ committed: [],
685
+ segmentIndex: 0,
686
+ pumping: false,
687
+ cancelled: false,
688
+ ended: false,
689
+ });
690
+ }
691
+
692
+ function ingestStreamEvents(session: StreamingSession, events: EndpointerEvent[]): void {
693
+ for (const event of events) {
694
+ if (event.kind === "segment") session.segmentQueue.push(event.audio);
695
+ else session.pendingPartial = event.audio;
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Drain a session's pending work: finalized segments first (committed in order),
701
+ * then a single coalesced partial preview. Re-entrant-safe via `pumping`; new
702
+ * audio that arrives mid-decode is picked up when the current decode resolves.
703
+ */
704
+ async function pumpSession(session: StreamingSession, transport: SttTransport): Promise<void> {
705
+ if (session.pumping) return;
706
+ session.pumping = true;
707
+ try {
708
+ const model = await session.model;
709
+ while (!session.cancelled) {
710
+ if (session.segmentQueue.length > 0) {
711
+ const audio = session.segmentQueue.shift()!;
712
+ // A fresh segment supersedes any queued preview for the prior one.
713
+ session.pendingPartial = null;
714
+ const text = await runOnModel(() => decodeSegment(model, session.spec, audio, session.language));
715
+ if (session.cancelled) return;
716
+ if (text.length > 0) {
717
+ session.committed.push(text);
718
+ transport.send({ type: "segment", id: session.id, index: session.segmentIndex++, text });
719
+ }
720
+ continue;
721
+ }
722
+ if (session.pendingPartial) {
723
+ const audio = session.pendingPartial;
724
+ session.pendingPartial = null;
725
+ const text = await runOnModel(() => decodeSegment(model, session.spec, audio, session.language));
726
+ if (session.cancelled) return;
727
+ // Skip a now-stale preview if a segment finalized mid-decode.
728
+ if (text.length > 0 && session.segmentQueue.length === 0) {
729
+ transport.send({ type: "partial", id: session.id, text });
730
+ }
731
+ continue;
732
+ }
733
+ break;
734
+ }
735
+ if (session.ended && !session.cancelled && session.segmentQueue.length === 0 && !session.pendingPartial) {
736
+ transport.send({ type: "stream_done", id: session.id, text: session.committed.join(" ") });
737
+ sessions.delete(session.id);
738
+ }
739
+ } catch (error) {
740
+ if (!session.cancelled) transport.send({ type: "error", id: session.id, error: errorText(error) });
741
+ sessions.delete(session.id);
742
+ } finally {
743
+ session.pumping = false;
744
+ }
745
+ }
746
+
747
+ function handleStreamMessage(
748
+ transport: SttTransport,
749
+ message: Extract<SttWorkerInbound, { type: "stream_start" | "stream_audio" | "stream_stop" | "stream_cancel" }>,
750
+ ): void {
751
+ if (message.type === "stream_start") {
752
+ startStreamingSession(transport, message);
753
+ return;
754
+ }
755
+ const session = sessions.get(message.id);
756
+ if (!session || session.cancelled) return;
757
+ switch (message.type) {
758
+ case "stream_audio":
759
+ ingestStreamEvents(session, session.endpointer.push(message.audio));
760
+ void pumpSession(session, transport);
761
+ return;
762
+ case "stream_stop":
763
+ session.ended = true;
764
+ session.pendingPartial = null;
765
+ ingestStreamEvents(session, session.endpointer.flush());
766
+ void pumpSession(session, transport);
767
+ return;
768
+ case "stream_cancel":
769
+ session.cancelled = true;
770
+ sessions.delete(message.id);
771
+ return;
772
+ }
773
+ }
774
+
775
+ export function startSttWorker(transport: SttTransport): void {
776
+ transport.onMessage(message => {
777
+ switch (message.type) {
778
+ case "ping":
779
+ transport.send({ type: "pong", id: message.id });
780
+ return;
781
+ case "transcribe":
782
+ case "download":
783
+ void handleBatchRequest(transport, message);
784
+ return;
785
+ default:
786
+ handleStreamMessage(transport, message);
787
+ return;
788
+ }
789
+ });
790
+ }