@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.
- package/CHANGELOG.md +347 -7
- package/dist/cli.js +1615 -1231
- package/dist/types/async/job-manager.d.ts +15 -0
- package/dist/types/autolearn/controller.d.ts +25 -0
- package/dist/types/autolearn/managed-skills.d.ts +45 -0
- package/dist/types/autoresearch/state.d.ts +1 -1
- package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
- package/dist/types/autoresearch/types.d.ts +1 -1
- package/dist/types/cli/args.d.ts +19 -2
- package/dist/types/cli/models-cli.d.ts +49 -0
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/cli/setup-model-picker.d.ts +14 -0
- package/dist/types/collab/protocol.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +0 -3
- package/dist/types/commands/models.d.ts +33 -0
- package/dist/types/commands/say.d.ts +24 -0
- package/dist/types/commands/token.d.ts +25 -0
- package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
- package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
- package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
- package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +3 -3
- package/dist/types/config/model-registry.d.ts +17 -0
- package/dist/types/config/models-config-schema.d.ts +13 -1
- package/dist/types/config/models-config.d.ts +8 -2
- package/dist/types/config/settings-schema.d.ts +281 -58
- package/dist/types/edit/hashline/params.d.ts +1 -1
- package/dist/types/edit/modes/apply-patch.d.ts +1 -1
- package/dist/types/edit/modes/patch.d.ts +1 -1
- package/dist/types/edit/modes/replace.d.ts +1 -1
- package/dist/types/export/html/index.d.ts +2 -1
- package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +49 -3
- package/dist/types/extensibility/hooks/index.d.ts +2 -1
- package/dist/types/extensibility/hooks/types.d.ts +2 -2
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
- package/dist/types/extensibility/plugins/loader.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +10 -0
- package/dist/types/goals/guided-setup.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +1 -1
- package/dist/types/hindsight/transcript.d.ts +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/internal-urls/local-protocol.d.ts +4 -2
- package/dist/types/lsp/types.d.ts +1 -1
- package/dist/types/main.d.ts +4 -3
- package/dist/types/mcp/manager.d.ts +8 -0
- package/dist/types/mcp/startup-events.d.ts +11 -0
- package/dist/types/memories/index.d.ts +7 -0
- package/dist/types/memory-backend/local-backend.d.ts +4 -3
- package/dist/types/mnemopi/config.d.ts +28 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -2
- package/dist/types/modes/components/agent-hub.d.ts +6 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
- package/dist/types/modes/components/custom-editor.d.ts +39 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +9 -5
- package/dist/types/modes/components/status-line/types.d.ts +2 -1
- package/dist/types/modes/components/tool-execution.d.ts +26 -16
- package/dist/types/modes/components/transcript-container.d.ts +23 -2
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/usage-row.d.ts +3 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/event-controller.d.ts +0 -17
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/gradient-highlight.d.ts +9 -4
- package/dist/types/modes/image-references.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +27 -6
- package/dist/types/modes/magic-keywords.d.ts +13 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
- package/dist/types/modes/runtime-init.d.ts +4 -0
- package/dist/types/modes/theme/theme.d.ts +13 -2
- package/dist/types/modes/types.d.ts +8 -7
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +17 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +28 -35
- package/dist/types/session/agent-storage.d.ts +2 -1
- package/dist/types/session/indexed-session-storage.d.ts +3 -3
- package/dist/types/session/messages.d.ts +8 -10
- package/dist/types/session/session-context.d.ts +39 -0
- package/dist/types/session/session-entries.d.ts +159 -0
- package/dist/types/session/session-listing.d.ts +69 -0
- package/dist/types/session/session-loader.d.ts +16 -0
- package/dist/types/session/session-manager.d.ts +85 -462
- package/dist/types/session/session-migrations.d.ts +12 -0
- package/dist/types/session/session-paths.d.ts +25 -0
- package/dist/types/session/session-persistence.d.ts +8 -0
- package/dist/types/session/session-storage.d.ts +11 -7
- package/dist/types/session/snapcompact-inline.d.ts +12 -1
- package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
- package/dist/types/session/tool-choice-queue.d.ts +6 -6
- package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
- package/dist/types/stt/asr-client.d.ts +90 -0
- package/dist/types/stt/asr-protocol.d.ts +97 -0
- package/dist/types/stt/asr-worker.d.ts +2 -0
- package/dist/types/stt/downloader.d.ts +38 -0
- package/dist/types/stt/endpointer.d.ts +59 -0
- package/dist/types/stt/index.d.ts +5 -1
- package/dist/types/stt/models.d.ts +120 -0
- package/dist/types/stt/recorder.d.ts +17 -0
- package/dist/types/stt/stt-controller.d.ts +6 -0
- package/dist/types/stt/transcriber.d.ts +5 -7
- package/dist/types/stt/wav.d.ts +29 -0
- package/dist/types/system-prompt.d.ts +4 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/index.d.ts +9 -1
- package/dist/types/task/types.d.ts +37 -1
- package/dist/types/tools/ask.d.ts +1 -1
- package/dist/types/tools/ast-edit.d.ts +1 -1
- package/dist/types/tools/ast-grep.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +3 -3
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
- package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
- package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
- package/dist/types/tools/browser/registry.d.ts +16 -3
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/checkpoint.d.ts +1 -1
- package/dist/types/tools/debug.d.ts +1 -1
- package/dist/types/tools/eval-render.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/gh.d.ts +1 -1
- package/dist/types/tools/image-gen.d.ts +1 -1
- package/dist/types/tools/index.d.ts +14 -2
- package/dist/types/tools/inspect-image.d.ts +1 -1
- package/dist/types/tools/irc.d.ts +2 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/learn.d.ts +51 -0
- package/dist/types/tools/manage-skill.d.ts +40 -0
- package/dist/types/tools/memory-edit.d.ts +1 -1
- package/dist/types/tools/memory-recall.d.ts +1 -1
- package/dist/types/tools/memory-reflect.d.ts +1 -1
- package/dist/types/tools/memory-retain.d.ts +1 -1
- package/dist/types/tools/plan-mode-guard.d.ts +10 -0
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/render-mermaid.d.ts +1 -1
- package/dist/types/tools/renderers.d.ts +7 -11
- package/dist/types/tools/resolve.d.ts +1 -1
- package/dist/types/tools/review.d.ts +1 -1
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/tools/ssh.d.ts +2 -2
- package/dist/types/tools/todo.d.ts +2 -2
- package/dist/types/tools/tts.d.ts +26 -1
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/tts/downloader.d.ts +20 -0
- package/dist/types/tts/index.d.ts +8 -0
- package/dist/types/tts/models.d.ts +82 -0
- package/dist/types/tts/player.d.ts +32 -0
- package/dist/types/tts/runtime.d.ts +6 -0
- package/dist/types/tts/streaming-player.d.ts +41 -0
- package/dist/types/tts/tts-client.d.ts +93 -0
- package/dist/types/tts/tts-protocol.d.ts +95 -0
- package/dist/types/tts/tts-worker.d.ts +2 -0
- package/dist/types/tts/vocalizer.d.ts +41 -0
- package/dist/types/tts/wav.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +4 -3
- package/dist/types/utils/image-loading.d.ts +18 -1
- package/dist/types/utils/thinking-display.d.ts +17 -0
- package/dist/types/utils/tool-choice.d.ts +8 -0
- package/dist/types/utils/tools-manager.d.ts +2 -1
- package/dist/types/utils/tools-manager.test.d.ts +1 -0
- package/dist/types/web/scrapers/github.d.ts +1 -1
- package/dist/types/web/search/index.d.ts +1 -1
- package/package.json +17 -16
- package/src/async/job-manager.ts +49 -0
- package/src/autolearn/controller.ts +139 -0
- package/src/autolearn/managed-skills.ts +257 -0
- package/src/autoresearch/state.ts +1 -1
- package/src/autoresearch/storage.ts +2 -1
- package/src/autoresearch/tools/init-experiment.ts +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/autoresearch/tools/run-experiment.ts +1 -1
- package/src/autoresearch/tools/update-notes.ts +1 -1
- package/src/autoresearch/types.ts +1 -1
- package/src/cli/args.ts +56 -10
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/models-cli.ts +427 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +148 -47
- package/src/cli/setup-model-picker.ts +43 -0
- package/src/cli-commands.ts +3 -0
- package/src/cli.ts +45 -13
- package/src/collab/host.ts +10 -13
- package/src/collab/protocol.ts +1 -1
- package/src/commands/launch.ts +0 -3
- package/src/commands/models.ts +61 -0
- package/src/commands/say.ts +102 -0
- package/src/commands/setup.ts +1 -1
- package/src/commands/token.ts +89 -0
- package/src/commit/agentic/tools/analyze-file.ts +4 -1
- package/src/commit/agentic/tools/git-file-diff.ts +1 -1
- package/src/commit/agentic/tools/git-hunk.ts +1 -1
- package/src/commit/agentic/tools/git-overview.ts +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +1 -1
- package/src/commit/agentic/tools/propose-commit.ts +1 -1
- package/src/commit/agentic/tools/recent-commits.ts +1 -1
- package/src/commit/agentic/tools/schemas.ts +1 -1
- package/src/commit/agentic/tools/split-commit.ts +1 -1
- package/src/commit/analysis/summary.ts +1 -1
- package/src/commit/changelog/generate.ts +1 -1
- package/src/commit/shared-llm.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-discovery.ts +11 -5
- package/src/config/model-registry.ts +79 -21
- package/src/config/model-resolver.ts +2 -2
- package/src/config/models-config-schema.ts +5 -2
- package/src/config/models-config.ts +2 -1
- package/src/config/settings-schema.ts +266 -32
- package/src/config/settings.ts +10 -0
- package/src/discovery/builtin.ts +23 -1
- package/src/discovery/claude-plugins.ts +44 -5
- package/src/discovery/helpers.ts +41 -1
- package/src/edit/hashline/params.ts +1 -1
- package/src/edit/modes/apply-patch.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/edit/modes/replace.ts +1 -1
- package/src/eval/__tests__/budget-bridge.test.ts +1 -1
- package/src/eval/agent-bridge.ts +1 -1
- package/src/eval/completion-bridge.ts +1 -1
- package/src/eval/js/shared/prelude.txt +69 -17
- package/src/export/html/index.ts +3 -6
- package/src/export/html/template.js +24 -2
- package/src/export/html/tool-views.generated.js +2 -2
- package/src/extensibility/custom-commands/loader.ts +1 -1
- package/src/extensibility/custom-commands/types.ts +2 -2
- package/src/extensibility/custom-tools/loader.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +2 -2
- package/src/extensibility/extensions/model-api.ts +41 -0
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +54 -3
- package/src/extensibility/extensions/wrapper.ts +41 -5
- package/src/extensibility/hooks/index.ts +2 -1
- package/src/extensibility/hooks/loader.ts +1 -1
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
- package/src/extensibility/plugins/loader.ts +30 -19
- package/src/extensibility/plugins/manager.ts +221 -90
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +101 -5
- package/src/goals/guided-setup.ts +133 -0
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/hindsight/transcript.ts +1 -1
- package/src/index.ts +5 -0
- package/src/internal-urls/docs-index.generated.ts +13 -10
- package/src/internal-urls/history-protocol.ts +1 -1
- package/src/internal-urls/local-protocol.ts +29 -7
- package/src/lsp/types.ts +1 -1
- package/src/main.ts +27 -32
- package/src/mcp/config-writer.ts +7 -3
- package/src/mcp/manager.ts +11 -0
- package/src/mcp/startup-events.ts +21 -0
- package/src/mcp/transports/stdio.ts +2 -1
- package/src/memories/index.ts +149 -12
- package/src/memories/storage.ts +2 -1
- package/src/memory-backend/local-backend.ts +11 -5
- package/src/mnemopi/backend.ts +1 -0
- package/src/mnemopi/config.ts +112 -12
- package/src/modes/acp/acp-agent.ts +8 -53
- package/src/modes/acp/acp-event-mapper.ts +5 -1
- package/src/modes/components/agent-hub.ts +51 -5
- package/src/modes/components/assistant-message.ts +12 -44
- package/src/modes/components/compaction-summary-message.ts +125 -26
- package/src/modes/components/custom-editor.test.ts +96 -0
- package/src/modes/components/custom-editor.ts +164 -8
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/logout-account-selector.ts +130 -0
- package/src/modes/components/mcp-add-wizard.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/status-line/component.ts +54 -157
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +2 -1
- package/src/modes/components/tool-execution.ts +82 -43
- package/src/modes/components/transcript-container.ts +70 -1
- package/src/modes/components/tree-selector.ts +1 -1
- package/src/modes/components/usage-row.ts +18 -0
- package/src/modes/components/user-message.ts +4 -2
- package/src/modes/controllers/command-controller.ts +14 -16
- package/src/modes/controllers/event-controller.ts +101 -73
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +311 -57
- package/src/modes/controllers/mcp-command-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +68 -12
- package/src/modes/controllers/streaming-reveal.ts +4 -3
- package/src/modes/gradient-highlight.ts +21 -9
- package/src/modes/image-references.ts +20 -0
- package/src/modes/interactive-mode.ts +288 -48
- package/src/modes/magic-keywords.ts +27 -5
- package/src/modes/rpc/rpc-mode.ts +146 -14
- package/src/modes/rpc/rpc-subagents.ts +2 -2
- package/src/modes/rpc/rpc-types.ts +8 -2
- package/src/modes/runtime-init.ts +28 -3
- package/src/modes/theme/theme.ts +99 -51
- package/src/modes/types.ts +6 -7
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +36 -7
- package/src/priority.json +5 -1
- package/src/prompts/agents/task.md +1 -0
- package/src/prompts/goals/guided-goal-interview.md +8 -0
- package/src/prompts/goals/guided-goal-system.md +12 -0
- package/src/prompts/memories/read-path.md +6 -0
- package/src/prompts/system/autolearn-guidance-learn.md +1 -0
- package/src/prompts/system/autolearn-guidance.md +7 -0
- package/src/prompts/system/autolearn-nudge.md +3 -0
- package/src/prompts/system/eager-task.md +7 -0
- package/src/prompts/system/eager-todo.md +11 -6
- package/src/prompts/system/empty-stop-retry.md +4 -6
- package/src/prompts/system/subagent-system-prompt.md +4 -0
- package/src/prompts/system/system-prompt.md +10 -5
- package/src/prompts/system/title-marker-instruction.md +1 -0
- package/src/prompts/system/title-system-marker.md +16 -0
- package/src/prompts/tools/job.md +1 -0
- package/src/prompts/tools/learn.md +7 -0
- package/src/prompts/tools/manage-skill.md +9 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/registry/agent-registry.ts +30 -0
- package/src/sdk.ts +103 -43
- package/src/secrets/obfuscator.ts +1 -1
- package/src/session/agent-session.ts +331 -318
- package/src/session/agent-storage.ts +18 -9
- package/src/session/history-storage.ts +3 -2
- package/src/session/indexed-session-storage.ts +7 -10
- package/src/session/messages.ts +9 -11
- package/src/session/session-context.ts +352 -0
- package/src/session/session-dump-format.ts +4 -2
- package/src/session/session-entries.ts +194 -0
- package/src/session/session-listing.ts +588 -0
- package/src/session/session-loader.ts +106 -0
- package/src/session/session-manager.ts +968 -3064
- package/src/session/session-migrations.ts +78 -0
- package/src/session/session-paths.ts +193 -0
- package/src/session/session-persistence.ts +131 -0
- package/src/session/session-storage.ts +91 -30
- package/src/session/snapcompact-inline.ts +21 -1
- package/src/session/snapcompact-savings-journal.ts +113 -0
- package/src/session/tool-choice-queue.ts +23 -11
- package/src/slash-commands/builtin-registry.ts +40 -4
- package/src/slash-commands/helpers/logout.ts +88 -0
- package/src/stt/asr-client.ts +520 -0
- package/src/stt/asr-protocol.ts +65 -0
- package/src/stt/asr-worker.ts +790 -0
- package/src/stt/downloader.ts +107 -47
- package/src/stt/endpointer.ts +259 -0
- package/src/stt/index.ts +5 -1
- package/src/stt/models.ts +150 -0
- package/src/stt/recorder.ts +247 -60
- package/src/stt/stt-controller.ts +201 -22
- package/src/stt/transcriber.ts +37 -68
- package/src/stt/wav.ts +173 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/agents.ts +1 -2
- package/src/task/executor.ts +49 -15
- package/src/task/index.ts +60 -6
- package/src/task/render.ts +83 -8
- package/src/task/types.ts +54 -1
- package/src/tools/ask.ts +9 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +1 -1
- package/src/tools/bash.ts +5 -4
- package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
- package/src/tools/browser/cmux/rpc.ts +156 -0
- package/src/tools/browser/cmux/socket-client.ts +309 -0
- package/src/tools/browser/registry.ts +37 -3
- package/src/tools/browser/render.ts +6 -1
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +189 -18
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/browser.ts +16 -1
- package/src/tools/checkpoint.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval-render.ts +4 -3
- package/src/tools/eval.ts +11 -6
- package/src/tools/fetch.ts +13 -2
- package/src/tools/find.ts +1 -1
- package/src/tools/gh.ts +1 -1
- package/src/tools/github-cache.ts +2 -1
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +43 -5
- package/src/tools/inspect-image.ts +3 -1
- package/src/tools/irc.ts +11 -3
- package/src/tools/job.ts +15 -3
- package/src/tools/learn.ts +144 -0
- package/src/tools/manage-skill.ts +104 -0
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +1 -1
- package/src/tools/memory-retain.ts +1 -1
- package/src/tools/plan-mode-guard.ts +53 -19
- package/src/tools/read.ts +8 -2
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/renderers.ts +7 -11
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +1 -1
- package/src/tools/search-tool-bm25.ts +1 -1
- package/src/tools/search.ts +1 -1
- package/src/tools/ssh.ts +5 -4
- package/src/tools/todo.ts +2 -2
- package/src/tools/tts.ts +204 -93
- package/src/tools/write.ts +19 -3
- package/src/tts/downloader.ts +64 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/models.ts +137 -0
- package/src/tts/player.ts +137 -0
- package/src/tts/runtime.ts +21 -0
- package/src/tts/streaming-player.ts +266 -0
- package/src/tts/tts-client.ts +647 -0
- package/src/tts/tts-protocol.ts +60 -0
- package/src/tts/tts-worker.ts +497 -0
- package/src/tts/vocalizer.ts +162 -0
- package/src/tts/wav.ts +58 -0
- package/src/utils/clipboard.ts +35 -18
- package/src/utils/image-loading.ts +35 -4
- package/src/utils/thinking-display.ts +37 -0
- package/src/utils/title-generator.ts +48 -5
- package/src/utils/tool-choice.ts +16 -0
- package/src/utils/tools-manager.test.ts +25 -0
- package/src/utils/tools-manager.ts +19 -1
- package/src/web/scrapers/github.ts +96 -0
- package/src/web/search/index.ts +14 -1
- package/src/web/search/providers/searxng.ts +13 -1
- package/dist/types/cli/list-models.d.ts +0 -30
- package/dist/types/stt/setup.d.ts +0 -18
- package/src/cli/list-models.ts +0 -194
- package/src/stt/setup.ts +0 -52
- package/src/stt/transcribe.py +0 -70
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { Subprocess } from "bun";
|
|
4
|
+
import { settings } from "../config/settings";
|
|
5
|
+
import { tinyWorkerEnvOverlay } from "../tiny/title-client";
|
|
6
|
+
import type { SttProgressEvent, SttWorkerInbound, SttWorkerOutbound } from "./asr-protocol";
|
|
7
|
+
import type { SttModelKey } from "./models";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Abstraction over the speech-recognition subprocess. Modelled as a worker
|
|
11
|
+
* interface so the parent composes lifecycle, ping/pong, and request/response
|
|
12
|
+
* correlation uniformly; the runtime implementation is a Bun child process so
|
|
13
|
+
* `onnxruntime-node`'s NAPI finalizer never runs inside the main agent address
|
|
14
|
+
* space — that destructor segfaults Bun on shutdown (issue #1606).
|
|
15
|
+
*/
|
|
16
|
+
interface WorkerHandle {
|
|
17
|
+
send(message: SttWorkerInbound): void;
|
|
18
|
+
onMessage(handler: (message: SttWorkerOutbound) => void): () => void;
|
|
19
|
+
onError(handler: (error: Error) => void): () => void;
|
|
20
|
+
terminate(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type PendingRequest =
|
|
24
|
+
| { kind: "transcribe"; modelKey: SttModelKey; resolve: (text: string) => void; reject: (error: Error) => void }
|
|
25
|
+
| { kind: "download"; modelKey: SttModelKey; resolve: (ok: boolean) => void };
|
|
26
|
+
|
|
27
|
+
export interface SttTranscribeOptions {
|
|
28
|
+
language?: string;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SttDownloadOptions {
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
onProgress?: (event: SttProgressEvent) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Live streaming session handle returned by {@link SttClient.startStream}. */
|
|
38
|
+
export interface SttStreamHandle {
|
|
39
|
+
/** Feed 16 kHz mono float samples as the recorder produces them. */
|
|
40
|
+
pushAudio(audio: Float32Array): void;
|
|
41
|
+
/** Flush the trailing segment and resolve with the full joined transcript. */
|
|
42
|
+
stop(): Promise<string>;
|
|
43
|
+
/** Tear the session down without a final flush (resolves `stop()` with ""). */
|
|
44
|
+
cancel(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SttStreamOptions {
|
|
48
|
+
language?: string;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
/** Volatile transcript of the in-progress segment, refreshed as audio arrives. */
|
|
51
|
+
onPartial?: (text: string) => void;
|
|
52
|
+
/** A finalized segment, emitted once when the endpointer commits it. */
|
|
53
|
+
onSegment?: (text: string, index: number) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface StreamState {
|
|
57
|
+
modelKey: SttModelKey;
|
|
58
|
+
onPartial: ((text: string) => void) | undefined;
|
|
59
|
+
onSegment: ((text: string, index: number) => void) | undefined;
|
|
60
|
+
resolve: (text: string) => void;
|
|
61
|
+
reject: (error: Error) => void;
|
|
62
|
+
/** Run `apply` (resolve/reject) once, then unregister the stream. */
|
|
63
|
+
finish: (apply: () => void) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Cold-starting the worker subprocess from a compiled binary (decompress +
|
|
67
|
+
// module graph load) is slow on contended CI runners; the probe only needs to
|
|
68
|
+
// prove the worker spawns and ponges, so a generous bound removes the flake.
|
|
69
|
+
const SMOKE_TEST_TIMEOUT_MS = 30_000;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Hidden subcommand on the main CLI that boots the speech-recognition worker in
|
|
73
|
+
* the spawned subprocess. Kept in sync with the dispatch in `cli.ts`.
|
|
74
|
+
*/
|
|
75
|
+
export const STT_WORKER_ARG = "__omp_stt_worker";
|
|
76
|
+
|
|
77
|
+
function readTinyModelSetting(key: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
|
|
78
|
+
try {
|
|
79
|
+
const value = settings.get(key);
|
|
80
|
+
return typeof value === "string" ? value : undefined;
|
|
81
|
+
} catch {
|
|
82
|
+
// Settings may be uninitialized (e.g. `omp --smoke-test`); fall back to env/default.
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Env handed to the speech subprocess. The `PI_TINY_DEVICE` / `PI_TINY_DTYPE`
|
|
89
|
+
* env vars win; otherwise the persisted `providers.tinyModelDevice` /
|
|
90
|
+
* `providers.tinyModelDtype` settings are mapped onto those vars so the
|
|
91
|
+
* subprocess's env-based resolution picks them up (shared with tiny models).
|
|
92
|
+
*/
|
|
93
|
+
function sttWorkerEnv(): Record<string, string> {
|
|
94
|
+
const overlay = tinyWorkerEnvOverlay(
|
|
95
|
+
$env,
|
|
96
|
+
readTinyModelSetting("providers.tinyModelDevice"),
|
|
97
|
+
readTinyModelSetting("providers.tinyModelDtype"),
|
|
98
|
+
);
|
|
99
|
+
const base = $env as Record<string, string | undefined>;
|
|
100
|
+
const merged: Record<string, string> = {};
|
|
101
|
+
for (const key in base) {
|
|
102
|
+
const value = base[key];
|
|
103
|
+
if (typeof value === "string") merged[key] = value;
|
|
104
|
+
}
|
|
105
|
+
for (const key in overlay) merged[key] = overlay[key];
|
|
106
|
+
return merged;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface SttWorkerSpawnCommand {
|
|
110
|
+
cmd: string[];
|
|
111
|
+
cwd?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the command used to relaunch the agent CLI into stt-worker mode. In a
|
|
116
|
+
* compiled binary the entry point is the binary itself; otherwise re-enter the
|
|
117
|
+
* declared worker-host entry with a cwd-relative script path (Bun's subprocess
|
|
118
|
+
* IPC is more reliable that way under `bun test`), falling back to this
|
|
119
|
+
* package's own `src/cli.ts` when no host entry is declared.
|
|
120
|
+
*/
|
|
121
|
+
function sttWorkerSpawnCmd(): SttWorkerSpawnCommand {
|
|
122
|
+
if (isCompiledBinary()) return { cmd: [process.execPath, STT_WORKER_ARG] };
|
|
123
|
+
const hostEntry = workerHostEntry();
|
|
124
|
+
if (hostEntry) {
|
|
125
|
+
return { cmd: [process.execPath, path.basename(hostEntry), STT_WORKER_ARG], cwd: path.dirname(hostEntry) };
|
|
126
|
+
}
|
|
127
|
+
const packageRoot = path.resolve(import.meta.dir, "..", "..");
|
|
128
|
+
return { cmd: [process.execPath, "src/cli.ts", STT_WORKER_ARG], cwd: packageRoot };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface SpawnedSubprocess {
|
|
132
|
+
proc: Subprocess<"ignore", "ignore", "ignore">;
|
|
133
|
+
inbound: Set<(message: SttWorkerOutbound) => void>;
|
|
134
|
+
errors: Set<(error: Error) => void>;
|
|
135
|
+
/**
|
|
136
|
+
* Flipped to `true` right before the parent SIGKILLs the child so `onExit`
|
|
137
|
+
* can distinguish the expected hard-kill from a crash/OOM/external signal.
|
|
138
|
+
*/
|
|
139
|
+
intentionalExit: { value: boolean };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Spawn the speech worker as a subprocess. Exported for tests and the smoke
|
|
144
|
+
* probe; production callers go through {@link spawnSttWorker}.
|
|
145
|
+
*/
|
|
146
|
+
export function createSttSubprocess(): SpawnedSubprocess {
|
|
147
|
+
const inbound = new Set<(message: SttWorkerOutbound) => void>();
|
|
148
|
+
const errors = new Set<(error: Error) => void>();
|
|
149
|
+
const intentionalExit = { value: false };
|
|
150
|
+
const spawnCommand = sttWorkerSpawnCmd();
|
|
151
|
+
const proc = Bun.spawn({
|
|
152
|
+
cmd: spawnCommand.cmd,
|
|
153
|
+
cwd: spawnCommand.cwd,
|
|
154
|
+
env: sttWorkerEnv(),
|
|
155
|
+
stdin: "ignore",
|
|
156
|
+
stdout: "ignore",
|
|
157
|
+
stderr: "ignore",
|
|
158
|
+
serialization: "advanced",
|
|
159
|
+
windowsHide: true,
|
|
160
|
+
ipc(message) {
|
|
161
|
+
for (const handler of inbound) handler(message as SttWorkerOutbound);
|
|
162
|
+
},
|
|
163
|
+
onExit(_proc, exitCode, signalCode) {
|
|
164
|
+
if (exitCode === 0) return;
|
|
165
|
+
// Swallow only the expected SIGKILL from `terminate()`; every other
|
|
166
|
+
// signal exit is a real worker death that must fault in-flight
|
|
167
|
+
// requests so callers don't await forever.
|
|
168
|
+
if (exitCode === null && intentionalExit.value) return;
|
|
169
|
+
const reason = exitCode !== null ? `code ${exitCode}` : `signal ${signalCode ?? "unknown"}`;
|
|
170
|
+
const err = new Error(`stt subprocess exited with ${reason}`);
|
|
171
|
+
for (const handler of errors) handler(err);
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
// Don't keep the parent event loop alive on an idle worker; dispose calls
|
|
175
|
+
// `terminate()` explicitly. Bun's test runner can starve IPC delivery for
|
|
176
|
+
// unref'd subprocesses, so keep it referenced under tests.
|
|
177
|
+
if (!isBunTestRuntime()) proc.unref();
|
|
178
|
+
return { proc, inbound, errors, intentionalExit };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
|
|
182
|
+
return {
|
|
183
|
+
send(message) {
|
|
184
|
+
try {
|
|
185
|
+
proc.send(message);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.debug("stt: send to subprocess failed", {
|
|
188
|
+
error: error instanceof Error ? error.message : String(error),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
onMessage(handler) {
|
|
193
|
+
inbound.add(handler);
|
|
194
|
+
return () => inbound.delete(handler);
|
|
195
|
+
},
|
|
196
|
+
onError(handler) {
|
|
197
|
+
errors.add(handler);
|
|
198
|
+
return () => errors.delete(handler);
|
|
199
|
+
},
|
|
200
|
+
async terminate() {
|
|
201
|
+
// SIGKILL: the whole point of subprocess isolation is that the parent
|
|
202
|
+
// never runs `onnxruntime-node`'s NAPI finalizer. Hard-kill instead —
|
|
203
|
+
// the model lives in process memory and the OS reclaims everything.
|
|
204
|
+
intentionalExit.value = true;
|
|
205
|
+
try {
|
|
206
|
+
proc.kill("SIGKILL");
|
|
207
|
+
} catch {
|
|
208
|
+
// Already gone.
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function spawnInlineUnavailableWorker(error: unknown): WorkerHandle {
|
|
215
|
+
const listeners = new Set<(message: SttWorkerOutbound) => void>();
|
|
216
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
217
|
+
const emit = (message: SttWorkerOutbound): void => {
|
|
218
|
+
for (const listener of listeners) listener(message);
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
send(message) {
|
|
222
|
+
queueMicrotask(() => {
|
|
223
|
+
if (message.type === "ping") {
|
|
224
|
+
emit({ type: "pong", id: message.id });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
emit({ type: "error", id: message.id, error: errorMessage });
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
onMessage(handler) {
|
|
231
|
+
listeners.add(handler);
|
|
232
|
+
return () => listeners.delete(handler);
|
|
233
|
+
},
|
|
234
|
+
onError() {
|
|
235
|
+
return () => {};
|
|
236
|
+
},
|
|
237
|
+
async terminate() {
|
|
238
|
+
listeners.clear();
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function spawnSttWorker(): WorkerHandle {
|
|
244
|
+
try {
|
|
245
|
+
return wrapSubprocess(createSttSubprocess());
|
|
246
|
+
} catch (error) {
|
|
247
|
+
logger.warn("stt worker spawn failed; speech-to-text disabled", {
|
|
248
|
+
error: error instanceof Error ? error.message : String(error),
|
|
249
|
+
});
|
|
250
|
+
return spawnInlineUnavailableWorker(error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function logWorkerMessage(message: Extract<SttWorkerOutbound, { type: "log" }>): void {
|
|
255
|
+
if (message.level === "debug") logger.debug(message.msg, message.meta);
|
|
256
|
+
else if (message.level === "warn") logger.warn(message.msg, message.meta);
|
|
257
|
+
else logger.error(message.msg, message.meta);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export class SttClient {
|
|
261
|
+
#worker: WorkerHandle | null = null;
|
|
262
|
+
#unsubscribeMessage: (() => void) | null = null;
|
|
263
|
+
#unsubscribeError: (() => void) | null = null;
|
|
264
|
+
#pending = new Map<string, PendingRequest>();
|
|
265
|
+
#streams = new Map<string, StreamState>();
|
|
266
|
+
#progressListeners = new Set<(event: SttProgressEvent) => void>();
|
|
267
|
+
#nextRequestId = 0;
|
|
268
|
+
#spawnWorker: () => WorkerHandle;
|
|
269
|
+
|
|
270
|
+
constructor(spawnWorker: () => WorkerHandle = spawnSttWorker) {
|
|
271
|
+
this.#spawnWorker = spawnWorker;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
onProgress(listener: (event: SttProgressEvent) => void): () => void {
|
|
275
|
+
this.#progressListeners.add(listener);
|
|
276
|
+
return () => this.#progressListeners.delete(listener);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Transcribe 16 kHz mono audio on the warm worker. Rejects with the worker
|
|
281
|
+
* error on failure and with an `AbortError` when the signal fires (the warm
|
|
282
|
+
* worker keeps the model loaded across calls — the model is never reloaded).
|
|
283
|
+
*/
|
|
284
|
+
async transcribe(modelKey: SttModelKey, audio: Float32Array, options: SttTranscribeOptions = {}): Promise<string> {
|
|
285
|
+
options.signal?.throwIfAborted();
|
|
286
|
+
const worker = this.#ensureWorker();
|
|
287
|
+
const id = String(++this.#nextRequestId);
|
|
288
|
+
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
289
|
+
this.#pending.set(id, { kind: "transcribe", modelKey, resolve, reject });
|
|
290
|
+
const abort = (): void => {
|
|
291
|
+
const pending = this.#pending.get(id);
|
|
292
|
+
if (pending?.kind !== "transcribe") return;
|
|
293
|
+
this.#pending.delete(id);
|
|
294
|
+
pending.reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
295
|
+
};
|
|
296
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
297
|
+
try {
|
|
298
|
+
worker.send({ type: "transcribe", id, modelKey, audio, language: options.language });
|
|
299
|
+
return await promise;
|
|
300
|
+
} finally {
|
|
301
|
+
options.signal?.removeEventListener("abort", abort);
|
|
302
|
+
this.#pending.delete(id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Open a live streaming session on the warm worker. Audio fed through the
|
|
308
|
+
* returned handle is segmented by the worker's endpointer: `onSegment` fires
|
|
309
|
+
* once per committed segment and `onPartial` for the volatile in-progress
|
|
310
|
+
* preview. `stop()` resolves with the full joined transcript; `cancel()` (or
|
|
311
|
+
* an aborted signal) tears the session down and resolves `stop()` with "".
|
|
312
|
+
*/
|
|
313
|
+
startStream(modelKey: SttModelKey, options: SttStreamOptions = {}): SttStreamHandle {
|
|
314
|
+
const worker = this.#ensureWorker();
|
|
315
|
+
const id = String(++this.#nextRequestId);
|
|
316
|
+
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
317
|
+
const signal = options.signal;
|
|
318
|
+
let settled = false;
|
|
319
|
+
const onAbort = (): void => handle.cancel();
|
|
320
|
+
const finish = (apply: () => void): void => {
|
|
321
|
+
if (settled) return;
|
|
322
|
+
settled = true;
|
|
323
|
+
this.#streams.delete(id);
|
|
324
|
+
signal?.removeEventListener("abort", onAbort);
|
|
325
|
+
apply();
|
|
326
|
+
};
|
|
327
|
+
this.#streams.set(id, {
|
|
328
|
+
modelKey,
|
|
329
|
+
onPartial: options.onPartial,
|
|
330
|
+
onSegment: options.onSegment,
|
|
331
|
+
resolve,
|
|
332
|
+
reject,
|
|
333
|
+
finish,
|
|
334
|
+
});
|
|
335
|
+
worker.send({ type: "stream_start", id, modelKey, language: options.language });
|
|
336
|
+
const handle: SttStreamHandle = {
|
|
337
|
+
pushAudio: audio => {
|
|
338
|
+
if (!settled) worker.send({ type: "stream_audio", id, audio });
|
|
339
|
+
},
|
|
340
|
+
stop: () => {
|
|
341
|
+
if (!settled) worker.send({ type: "stream_stop", id });
|
|
342
|
+
return promise;
|
|
343
|
+
},
|
|
344
|
+
cancel: () => {
|
|
345
|
+
if (settled) return;
|
|
346
|
+
worker.send({ type: "stream_cancel", id });
|
|
347
|
+
finish(() => resolve(""));
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
if (signal?.aborted) handle.cancel();
|
|
351
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
352
|
+
return handle;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async downloadModel(modelKey: SttModelKey, options: SttDownloadOptions = {}): Promise<boolean> {
|
|
356
|
+
if (options.signal?.aborted) return false;
|
|
357
|
+
const unsubscribe = options.onProgress ? this.onProgress(options.onProgress) : undefined;
|
|
358
|
+
try {
|
|
359
|
+
const worker = this.#ensureWorker();
|
|
360
|
+
const id = String(++this.#nextRequestId);
|
|
361
|
+
const { promise, resolve } = Promise.withResolvers<boolean>();
|
|
362
|
+
this.#pending.set(id, { kind: "download", modelKey, resolve });
|
|
363
|
+
const abort = (): void => {
|
|
364
|
+
const pending = this.#pending.get(id);
|
|
365
|
+
if (pending?.kind !== "download") return;
|
|
366
|
+
this.#pending.delete(id);
|
|
367
|
+
pending.resolve(false);
|
|
368
|
+
};
|
|
369
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
370
|
+
try {
|
|
371
|
+
worker.send({ type: "download", id, modelKey });
|
|
372
|
+
return await promise;
|
|
373
|
+
} finally {
|
|
374
|
+
options.signal?.removeEventListener("abort", abort);
|
|
375
|
+
this.#pending.delete(id);
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
logger.debug("stt: local model download failed", {
|
|
379
|
+
modelKey,
|
|
380
|
+
error: error instanceof Error ? error.message : String(error),
|
|
381
|
+
});
|
|
382
|
+
return false;
|
|
383
|
+
} finally {
|
|
384
|
+
unsubscribe?.();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async terminate(): Promise<void> {
|
|
389
|
+
const worker = this.#worker;
|
|
390
|
+
this.#worker = null;
|
|
391
|
+
this.#unsubscribeMessage?.();
|
|
392
|
+
this.#unsubscribeMessage = null;
|
|
393
|
+
this.#unsubscribeError?.();
|
|
394
|
+
this.#unsubscribeError = null;
|
|
395
|
+
for (const pending of this.#pending.values()) {
|
|
396
|
+
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
397
|
+
if (pending.kind === "transcribe") pending.reject(new Error("stt worker terminated"));
|
|
398
|
+
else pending.resolve(false);
|
|
399
|
+
}
|
|
400
|
+
this.#pending.clear();
|
|
401
|
+
this.#failStreams(new Error("stt worker terminated"));
|
|
402
|
+
try {
|
|
403
|
+
await worker?.terminate();
|
|
404
|
+
} catch {
|
|
405
|
+
// Already gone.
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#ensureWorker(): WorkerHandle {
|
|
410
|
+
if (this.#worker) return this.#worker;
|
|
411
|
+
const worker = this.#spawnWorker();
|
|
412
|
+
this.#worker = worker;
|
|
413
|
+
this.#unsubscribeMessage = worker.onMessage(message => this.#handleMessage(message));
|
|
414
|
+
this.#unsubscribeError = worker.onError(error => this.#handleWorkerError(error));
|
|
415
|
+
return worker;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
#handleMessage(message: SttWorkerOutbound): void {
|
|
419
|
+
if (message.type === "log") {
|
|
420
|
+
logWorkerMessage(message);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (message.type === "progress") {
|
|
424
|
+
this.#emitProgress(message.event);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (message.type === "pong") return;
|
|
428
|
+
|
|
429
|
+
if (message.type === "partial" || message.type === "segment" || message.type === "stream_done") {
|
|
430
|
+
const stream = this.#streams.get(message.id);
|
|
431
|
+
if (!stream) return;
|
|
432
|
+
if (message.type === "partial") stream.onPartial?.(message.text);
|
|
433
|
+
else if (message.type === "segment") stream.onSegment?.(message.text, message.index);
|
|
434
|
+
else stream.finish(() => stream.resolve(message.text));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const pending = this.#pending.get(message.id);
|
|
439
|
+
if (!pending) {
|
|
440
|
+
if (message.type === "error") {
|
|
441
|
+
const stream = this.#streams.get(message.id);
|
|
442
|
+
if (stream) {
|
|
443
|
+
this.#emitProgress({ modelKey: stream.modelKey, status: "error" });
|
|
444
|
+
stream.finish(() => stream.reject(new Error(message.error)));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
this.#pending.delete(message.id);
|
|
450
|
+
if (message.type === "transcription") {
|
|
451
|
+
if (pending.kind === "transcribe") pending.resolve(message.text);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (message.type === "downloaded") {
|
|
455
|
+
if (pending.kind === "download") pending.resolve(true);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
// message.type === "error"
|
|
459
|
+
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
460
|
+
if (pending.kind === "transcribe") pending.reject(new Error(message.error));
|
|
461
|
+
else pending.resolve(false);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
#emitProgress(event: SttProgressEvent): void {
|
|
465
|
+
for (const listener of this.#progressListeners) listener(event);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#failStreams(error: Error): void {
|
|
469
|
+
for (const stream of [...this.#streams.values()]) {
|
|
470
|
+
this.#emitProgress({ modelKey: stream.modelKey, status: "error" });
|
|
471
|
+
stream.finish(() => stream.reject(error));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
#handleWorkerError(error: Error): void {
|
|
476
|
+
logger.warn("stt: worker error", { error: error.message });
|
|
477
|
+
for (const pending of this.#pending.values()) {
|
|
478
|
+
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
479
|
+
if (pending.kind === "transcribe") pending.reject(error);
|
|
480
|
+
else pending.resolve(false);
|
|
481
|
+
}
|
|
482
|
+
this.#pending.clear();
|
|
483
|
+
this.#failStreams(error);
|
|
484
|
+
void this.terminate();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export const sttClient = new SttClient();
|
|
489
|
+
|
|
490
|
+
export async function shutdownSttClient(): Promise<void> {
|
|
491
|
+
await sttClient.terminate();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export async function smokeTestSttWorker({
|
|
495
|
+
timeoutMs = SMOKE_TEST_TIMEOUT_MS,
|
|
496
|
+
}: {
|
|
497
|
+
timeoutMs?: number;
|
|
498
|
+
} = {}): Promise<void> {
|
|
499
|
+
const handle = wrapSubprocess(createSttSubprocess());
|
|
500
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
501
|
+
const timer = setTimeout(() => reject(new Error(`stt worker did not pong within ${timeoutMs}ms`)), timeoutMs);
|
|
502
|
+
const unsubscribeMessage = handle.onMessage(message => {
|
|
503
|
+
if (message.type === "pong") {
|
|
504
|
+
resolve();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (message.type === "log") return;
|
|
508
|
+
reject(new Error(`stt worker: expected pong, got ${JSON.stringify(message)}`));
|
|
509
|
+
});
|
|
510
|
+
const unsubscribeError = handle.onError(reject);
|
|
511
|
+
try {
|
|
512
|
+
handle.send({ type: "ping", id: "smoke" } satisfies SttWorkerInbound);
|
|
513
|
+
await promise;
|
|
514
|
+
} finally {
|
|
515
|
+
clearTimeout(timer);
|
|
516
|
+
unsubscribeMessage();
|
|
517
|
+
unsubscribeError();
|
|
518
|
+
await handle.terminate();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SttModelKey } from "./models";
|
|
2
|
+
|
|
3
|
+
export type SttProgressStatus = "initiate" | "download" | "progress" | "progress_total" | "done" | "ready" | "error";
|
|
4
|
+
|
|
5
|
+
export interface SttProgressFileState {
|
|
6
|
+
loaded: number;
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SttProgressEvent {
|
|
11
|
+
modelKey: SttModelKey;
|
|
12
|
+
status: SttProgressStatus;
|
|
13
|
+
name?: string;
|
|
14
|
+
file?: string;
|
|
15
|
+
progress?: number;
|
|
16
|
+
loaded?: number;
|
|
17
|
+
total?: number;
|
|
18
|
+
files?: Record<string, SttProgressFileState>;
|
|
19
|
+
task?: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SttWorkerInbound =
|
|
24
|
+
| { type: "ping"; id: string }
|
|
25
|
+
| { type: "transcribe"; id: string; modelKey: SttModelKey; audio: Float32Array; language?: string }
|
|
26
|
+
| { type: "download"; id: string; modelKey: SttModelKey }
|
|
27
|
+
// ── Live streaming session ──
|
|
28
|
+
// `stream_start` warms the model and opens a session; `stream_audio` feeds
|
|
29
|
+
// 16 kHz mono float frames as they arrive from the recorder; `stream_stop`
|
|
30
|
+
// flushes the trailing speech segment and ends the session; `stream_cancel`
|
|
31
|
+
// tears it down without a final flush. All carry the same `id`.
|
|
32
|
+
| { type: "stream_start"; id: string; modelKey: SttModelKey; language?: string }
|
|
33
|
+
| { type: "stream_audio"; id: string; audio: Float32Array }
|
|
34
|
+
| { type: "stream_stop"; id: string }
|
|
35
|
+
| { type: "stream_cancel"; id: string };
|
|
36
|
+
|
|
37
|
+
export type SttWorkerOutbound =
|
|
38
|
+
| { type: "pong"; id: string }
|
|
39
|
+
| { type: "transcription"; id: string; text: string }
|
|
40
|
+
| { type: "downloaded"; id: string }
|
|
41
|
+
| { type: "error"; id: string; error: string }
|
|
42
|
+
| { type: "progress"; id: string; event: SttProgressEvent }
|
|
43
|
+
| { type: "log"; level: "debug" | "warn" | "error"; msg: string; meta?: Record<string, unknown> }
|
|
44
|
+
// ── Live streaming session ──
|
|
45
|
+
// `partial` is the volatile transcript of the in-progress speech segment
|
|
46
|
+
// (refreshed as more audio arrives, never appended verbatim); `segment` is a
|
|
47
|
+
// finalized segment committed once at an endpoint; `stream_done` carries the
|
|
48
|
+
// full transcript (all committed segments joined) when the session ends.
|
|
49
|
+
| { type: "partial"; id: string; text: string }
|
|
50
|
+
| { type: "segment"; id: string; index: number; text: string }
|
|
51
|
+
| { type: "stream_done"; id: string; text: string };
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wire transport between the parent (`SttClient`) and the speech-recognition
|
|
55
|
+
* subprocess. The parent owns the subprocess lifecycle (graceful work, hard
|
|
56
|
+
* SIGKILL on shutdown); the protocol therefore carries no explicit close
|
|
57
|
+
* handshake — once the parent decides to terminate, it signals the OS to reap
|
|
58
|
+
* the child so `onnxruntime-node`'s NAPI finalizer never runs in any shared
|
|
59
|
+
* address space (the destructor segfaults Bun on shutdown; issue #1606). See
|
|
60
|
+
* `asr-client.ts` for the spawn/kill glue.
|
|
61
|
+
*/
|
|
62
|
+
export interface SttTransport {
|
|
63
|
+
send(message: SttWorkerOutbound): void;
|
|
64
|
+
onMessage(handler: (message: SttWorkerInbound) => void): () => void;
|
|
65
|
+
}
|