@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,647 @@
|
|
|
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 { isTtsLocalModelKey, type TtsLocalModelKey } from "./models";
|
|
7
|
+
import type { TtsProgressEvent, TtsWorkerInbound, TtsWorkerOutbound } from "./tts-protocol";
|
|
8
|
+
|
|
9
|
+
/** Decoded PCM returned by a local synthesis request. */
|
|
10
|
+
export interface TtsAudio {
|
|
11
|
+
pcm: Float32Array;
|
|
12
|
+
sampleRate: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Abstraction over the TTS subprocess. The runtime implementation is a Bun child
|
|
17
|
+
* process so `onnxruntime-node`'s NAPI finalizer never runs inside the main agent
|
|
18
|
+
* address space — that destructor segfaults Bun during shutdown (issue #1606).
|
|
19
|
+
*/
|
|
20
|
+
interface WorkerHandle {
|
|
21
|
+
send(message: TtsWorkerInbound): void;
|
|
22
|
+
onMessage(handler: (message: TtsWorkerOutbound) => void): () => void;
|
|
23
|
+
onError(handler: (error: Error) => void): () => void;
|
|
24
|
+
/** Re-reference the subprocess so a pending request keeps the parent event loop alive. */
|
|
25
|
+
ref(): void;
|
|
26
|
+
/** Drop the reference once the worker is idle so it never blocks process exit. */
|
|
27
|
+
unref(): void;
|
|
28
|
+
terminate(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type PendingRequest =
|
|
32
|
+
| { kind: "synthesize"; modelKey: TtsLocalModelKey; resolve: (audio: TtsAudio | null) => void }
|
|
33
|
+
| { kind: "download"; modelKey: TtsLocalModelKey; resolve: (ok: boolean) => void }
|
|
34
|
+
| { kind: "stream"; modelKey: TtsLocalModelKey; channel: AudioChunkChannel };
|
|
35
|
+
|
|
36
|
+
export interface TtsSynthesizeOptions {
|
|
37
|
+
voice?: string;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TtsDownloadOptions {
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
onProgress?: (event: TtsProgressEvent) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TtsStreamOptions {
|
|
47
|
+
voice?: string;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One synthesized sentence of a streaming session, in emission order. */
|
|
52
|
+
export interface TtsAudioChunk {
|
|
53
|
+
index: number;
|
|
54
|
+
text: string;
|
|
55
|
+
pcm: Float32Array;
|
|
56
|
+
sampleRate: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A live streaming-synthesis session. Feed text incrementally with {@link push}
|
|
61
|
+
* and close the input with {@link end}; `chunks` yields each synthesized
|
|
62
|
+
* sentence's audio as soon as it is ready, then completes once the worker
|
|
63
|
+
* finishes draining the closed input.
|
|
64
|
+
*/
|
|
65
|
+
export interface TtsStreamHandle {
|
|
66
|
+
push(text: string): void;
|
|
67
|
+
end(): void;
|
|
68
|
+
chunks: AsyncIterableIterator<TtsAudioChunk>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Single-producer/single-consumer async queue bridging the worker's IPC
|
|
73
|
+
* `audio-chunk` messages to an async iterator. Chunks pushed while no consumer
|
|
74
|
+
* is awaiting are buffered in order; {@link close} ends the iterator and
|
|
75
|
+
* {@link fail} surfaces an error to the awaiting (or next) consumer.
|
|
76
|
+
*/
|
|
77
|
+
class AudioChunkChannel {
|
|
78
|
+
#queue: TtsAudioChunk[] = [];
|
|
79
|
+
#waiters: Array<{
|
|
80
|
+
resolve: (result: IteratorResult<TtsAudioChunk>) => void;
|
|
81
|
+
reject: (error: Error) => void;
|
|
82
|
+
}> = [];
|
|
83
|
+
#error: Error | null = null;
|
|
84
|
+
#settled = false;
|
|
85
|
+
#onSettle: (() => void) | undefined;
|
|
86
|
+
|
|
87
|
+
constructor(onSettle?: () => void) {
|
|
88
|
+
this.#onSettle = onSettle;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
push(chunk: TtsAudioChunk): void {
|
|
92
|
+
if (this.#settled) return;
|
|
93
|
+
const waiter = this.#waiters.shift();
|
|
94
|
+
if (waiter) waiter.resolve({ value: chunk, done: false });
|
|
95
|
+
else this.#queue.push(chunk);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
close(): void {
|
|
99
|
+
this.#settle(null);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fail(error: Error): void {
|
|
103
|
+
this.#settle(error);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#settle(error: Error | null): void {
|
|
107
|
+
if (this.#settled) return;
|
|
108
|
+
this.#settled = true;
|
|
109
|
+
this.#error = error;
|
|
110
|
+
for (const waiter of this.#waiters) {
|
|
111
|
+
if (error) waiter.reject(error);
|
|
112
|
+
else waiter.resolve({ value: undefined, done: true });
|
|
113
|
+
}
|
|
114
|
+
this.#waiters = [];
|
|
115
|
+
this.#onSettle?.();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async *iterator(): AsyncIterableIterator<TtsAudioChunk> {
|
|
119
|
+
while (true) {
|
|
120
|
+
const buffered = this.#queue.shift();
|
|
121
|
+
if (buffered) {
|
|
122
|
+
yield buffered;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (this.#error) throw this.#error;
|
|
126
|
+
if (this.#settled) return;
|
|
127
|
+
const { promise, resolve, reject } = Promise.withResolvers<IteratorResult<TtsAudioChunk>>();
|
|
128
|
+
this.#waiters.push({ resolve, reject });
|
|
129
|
+
const result = await promise;
|
|
130
|
+
if (result.done) return;
|
|
131
|
+
yield result.value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Cold-starting the worker from a compiled binary (decompress + module graph load)
|
|
137
|
+
// is slow on contended CI runners; the probe only proves the worker spawns and
|
|
138
|
+
// ponges, so a generous bound removes flakes without weakening the check.
|
|
139
|
+
const SMOKE_TEST_TIMEOUT_MS = 30_000;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Hidden subcommand on the main CLI that boots the TTS worker in the spawned
|
|
143
|
+
* subprocess. Kept in sync with the dispatch in `cli.ts` (Main-owned).
|
|
144
|
+
*/
|
|
145
|
+
export const TTS_WORKER_ARG = "__omp_tts_worker";
|
|
146
|
+
|
|
147
|
+
function readTinyModelSetting(path: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
|
|
148
|
+
try {
|
|
149
|
+
const value = settings.get(path);
|
|
150
|
+
return typeof value === "string" ? value : undefined;
|
|
151
|
+
} catch {
|
|
152
|
+
// Settings may be uninitialized (e.g. `omp --smoke-test`); fall back to env/default.
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Env handed to the TTS subprocess. The `PI_TINY_DEVICE` / `PI_TINY_DTYPE` env
|
|
159
|
+
* vars win; otherwise the persisted `providers.tinyModelDevice` /
|
|
160
|
+
* `providers.tinyModelDtype` settings are mapped onto those vars so the
|
|
161
|
+
* subprocess's env-based resolution governs speech the same way it governs the
|
|
162
|
+
* tiny LLM worker.
|
|
163
|
+
*/
|
|
164
|
+
function ttsWorkerEnv(): Record<string, string> {
|
|
165
|
+
const overlay = tinyWorkerEnvOverlay(
|
|
166
|
+
$env,
|
|
167
|
+
readTinyModelSetting("providers.tinyModelDevice"),
|
|
168
|
+
readTinyModelSetting("providers.tinyModelDtype"),
|
|
169
|
+
);
|
|
170
|
+
const base = $env as Record<string, string | undefined>;
|
|
171
|
+
const merged: Record<string, string> = {};
|
|
172
|
+
for (const key in base) {
|
|
173
|
+
const value = base[key];
|
|
174
|
+
if (typeof value === "string") merged[key] = value;
|
|
175
|
+
}
|
|
176
|
+
for (const key in overlay) merged[key] = overlay[key];
|
|
177
|
+
return merged;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface TtsWorkerSpawnCommand {
|
|
181
|
+
cmd: string[];
|
|
182
|
+
cwd?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve the command used to relaunch the agent CLI into TTS-worker mode. In a
|
|
187
|
+
* compiled binary the entry point is the binary itself; otherwise re-enter the
|
|
188
|
+
* declared worker-host entry (cwd-relative for reliable Bun IPC), falling back
|
|
189
|
+
* to this package's own `src/cli.ts` when no host entry is declared (bun test).
|
|
190
|
+
*/
|
|
191
|
+
function ttsWorkerSpawnCmd(): TtsWorkerSpawnCommand {
|
|
192
|
+
if (isCompiledBinary()) return { cmd: [process.execPath, TTS_WORKER_ARG] };
|
|
193
|
+
const hostEntry = workerHostEntry();
|
|
194
|
+
if (hostEntry) {
|
|
195
|
+
return { cmd: [process.execPath, path.basename(hostEntry), TTS_WORKER_ARG], cwd: path.dirname(hostEntry) };
|
|
196
|
+
}
|
|
197
|
+
const packageRoot = path.resolve(import.meta.dir, "..", "..");
|
|
198
|
+
return { cmd: [process.execPath, "src/cli.ts", TTS_WORKER_ARG], cwd: packageRoot };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface SpawnedSubprocess {
|
|
202
|
+
proc: Subprocess<"ignore", "ignore", "ignore">;
|
|
203
|
+
inbound: Set<(message: TtsWorkerOutbound) => void>;
|
|
204
|
+
errors: Set<(error: Error) => void>;
|
|
205
|
+
/** Flipped to `true` right before the deliberate SIGKILL so `onExit` can tell it apart from a crash. */
|
|
206
|
+
intentionalExit: { value: boolean };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Spawn the TTS worker as a subprocess. Exported for tests and the smoke probe;
|
|
211
|
+
* production callers go through {@link spawnTtsWorker}.
|
|
212
|
+
*/
|
|
213
|
+
export function createTtsSubprocess(): SpawnedSubprocess {
|
|
214
|
+
const inbound = new Set<(message: TtsWorkerOutbound) => void>();
|
|
215
|
+
const errors = new Set<(error: Error) => void>();
|
|
216
|
+
const intentionalExit = { value: false };
|
|
217
|
+
const spawnCommand = ttsWorkerSpawnCmd();
|
|
218
|
+
const proc = Bun.spawn({
|
|
219
|
+
cmd: spawnCommand.cmd,
|
|
220
|
+
cwd: spawnCommand.cwd,
|
|
221
|
+
env: ttsWorkerEnv(),
|
|
222
|
+
stdin: "ignore",
|
|
223
|
+
stdout: "ignore",
|
|
224
|
+
stderr: "ignore",
|
|
225
|
+
serialization: "advanced",
|
|
226
|
+
windowsHide: true,
|
|
227
|
+
ipc(message) {
|
|
228
|
+
for (const handler of inbound) handler(message as TtsWorkerOutbound);
|
|
229
|
+
},
|
|
230
|
+
onExit(_proc, exitCode, signalCode) {
|
|
231
|
+
if (exitCode === 0) return;
|
|
232
|
+
if (exitCode === null && intentionalExit.value) return;
|
|
233
|
+
const reason = exitCode !== null ? `code ${exitCode}` : `signal ${signalCode ?? "unknown"}`;
|
|
234
|
+
const err = new Error(`tts subprocess exited with ${reason}`);
|
|
235
|
+
for (const handler of errors) handler(err);
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
// Don't keep the parent event loop alive on an idle worker; the dispose path
|
|
239
|
+
// calls `terminate()` explicitly. Bun's test runner starves IPC for unref'd
|
|
240
|
+
// subprocesses, so keep it referenced only under tests.
|
|
241
|
+
if (!isBunTestRuntime()) proc.unref();
|
|
242
|
+
return { proc, inbound, errors, intentionalExit };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
|
|
246
|
+
return {
|
|
247
|
+
send(message) {
|
|
248
|
+
try {
|
|
249
|
+
proc.send(message);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
logger.debug("tts: send to subprocess failed", {
|
|
252
|
+
error: error instanceof Error ? error.message : String(error),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
onMessage(handler) {
|
|
257
|
+
inbound.add(handler);
|
|
258
|
+
return () => inbound.delete(handler);
|
|
259
|
+
},
|
|
260
|
+
onError(handler) {
|
|
261
|
+
errors.add(handler);
|
|
262
|
+
return () => errors.delete(handler);
|
|
263
|
+
},
|
|
264
|
+
ref() {
|
|
265
|
+
try {
|
|
266
|
+
proc.ref();
|
|
267
|
+
} catch {
|
|
268
|
+
// Already gone.
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
unref() {
|
|
272
|
+
try {
|
|
273
|
+
proc.unref();
|
|
274
|
+
} catch {
|
|
275
|
+
// Already gone.
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
async terminate() {
|
|
279
|
+
// SIGKILL: the point of subprocess isolation is that the parent never
|
|
280
|
+
// runs `onnxruntime-node`'s NAPI finalizer (it crashes Bun on Windows).
|
|
281
|
+
// Hard-kill instead; the OS reclaims the model memory.
|
|
282
|
+
intentionalExit.value = true;
|
|
283
|
+
try {
|
|
284
|
+
proc.kill("SIGKILL");
|
|
285
|
+
} catch {
|
|
286
|
+
// Already gone.
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function spawnInlineUnavailableWorker(error: unknown): WorkerHandle {
|
|
293
|
+
const listeners = new Set<(message: TtsWorkerOutbound) => void>();
|
|
294
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
295
|
+
const emit = (message: TtsWorkerOutbound): void => {
|
|
296
|
+
for (const listener of listeners) listener(message);
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
send(message) {
|
|
300
|
+
queueMicrotask(() => {
|
|
301
|
+
if (message.type === "ping") {
|
|
302
|
+
emit({ type: "pong", id: message.id });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
emit({ type: "error", id: message.id, error: errorMessage });
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
onMessage(handler) {
|
|
309
|
+
listeners.add(handler);
|
|
310
|
+
return () => listeners.delete(handler);
|
|
311
|
+
},
|
|
312
|
+
onError() {
|
|
313
|
+
return () => {};
|
|
314
|
+
},
|
|
315
|
+
ref() {},
|
|
316
|
+
unref() {},
|
|
317
|
+
async terminate() {
|
|
318
|
+
listeners.clear();
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function spawnTtsWorker(): WorkerHandle {
|
|
324
|
+
try {
|
|
325
|
+
return wrapSubprocess(createTtsSubprocess());
|
|
326
|
+
} catch (error) {
|
|
327
|
+
logger.warn("TTS worker spawn failed; local TTS disabled", {
|
|
328
|
+
error: error instanceof Error ? error.message : String(error),
|
|
329
|
+
});
|
|
330
|
+
return spawnInlineUnavailableWorker(error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function logWorkerMessage(message: Extract<TtsWorkerOutbound, { type: "log" }>): void {
|
|
335
|
+
if (message.level === "debug") logger.debug(message.msg, message.meta);
|
|
336
|
+
else if (message.level === "warn") logger.warn(message.msg, message.meta);
|
|
337
|
+
else logger.error(message.msg, message.meta);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export class TtsClient {
|
|
341
|
+
#worker: WorkerHandle | null = null;
|
|
342
|
+
#unsubscribeMessage: (() => void) | null = null;
|
|
343
|
+
#unsubscribeError: (() => void) | null = null;
|
|
344
|
+
#pending = new Map<string, PendingRequest>();
|
|
345
|
+
#progressListeners = new Set<(event: TtsProgressEvent) => void>();
|
|
346
|
+
#nextRequestId = 0;
|
|
347
|
+
#refed = false;
|
|
348
|
+
#spawnWorker: () => WorkerHandle;
|
|
349
|
+
|
|
350
|
+
constructor(spawnWorker: () => WorkerHandle = spawnTtsWorker) {
|
|
351
|
+
this.#spawnWorker = spawnWorker;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
onProgress(listener: (event: TtsProgressEvent) => void): () => void {
|
|
355
|
+
this.#progressListeners.add(listener);
|
|
356
|
+
return () => this.#progressListeners.delete(listener);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async synthesize(modelKey: string, text: string, options: TtsSynthesizeOptions = {}): Promise<TtsAudio | null> {
|
|
360
|
+
if (!isTtsLocalModelKey(modelKey)) return null;
|
|
361
|
+
if (options.signal?.aborted) return null;
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const worker = this.#ensureWorker();
|
|
365
|
+
const id = String(++this.#nextRequestId);
|
|
366
|
+
const { promise, resolve } = Promise.withResolvers<TtsAudio | null>();
|
|
367
|
+
this.#addPending(id, { kind: "synthesize", modelKey, resolve });
|
|
368
|
+
const abort = (): void => {
|
|
369
|
+
const pending = this.#pending.get(id);
|
|
370
|
+
if (pending?.kind !== "synthesize") return;
|
|
371
|
+
this.#deletePending(id);
|
|
372
|
+
pending.resolve(null);
|
|
373
|
+
};
|
|
374
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
375
|
+
try {
|
|
376
|
+
const request: TtsWorkerInbound = options.voice
|
|
377
|
+
? { type: "synthesize", id, modelKey, text, voice: options.voice }
|
|
378
|
+
: { type: "synthesize", id, modelKey, text };
|
|
379
|
+
worker.send(request);
|
|
380
|
+
return await promise;
|
|
381
|
+
} finally {
|
|
382
|
+
options.signal?.removeEventListener("abort", abort);
|
|
383
|
+
this.#deletePending(id);
|
|
384
|
+
}
|
|
385
|
+
} catch (error) {
|
|
386
|
+
logger.debug("tts: local synthesis failed", {
|
|
387
|
+
modelKey,
|
|
388
|
+
error: error instanceof Error ? error.message : String(error),
|
|
389
|
+
});
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Open a streaming-synthesis session. Text is fed incrementally through the
|
|
396
|
+
* returned handle's `push`/`end`; audio is emitted one synthesized sentence at
|
|
397
|
+
* a time via `chunks`, so playback can begin before the full text is known.
|
|
398
|
+
* Returns an inert handle (immediately-ended `chunks`) for unknown models or
|
|
399
|
+
* an already-aborted signal, and fails the iterator if the worker cannot spawn.
|
|
400
|
+
*/
|
|
401
|
+
synthesizeStream(modelKey: string, options: TtsStreamOptions = {}): TtsStreamHandle {
|
|
402
|
+
if (!isTtsLocalModelKey(modelKey) || options.signal?.aborted) {
|
|
403
|
+
const channel = new AudioChunkChannel();
|
|
404
|
+
channel.close();
|
|
405
|
+
return { push: () => {}, end: () => {}, chunks: channel.iterator() };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let worker: WorkerHandle;
|
|
409
|
+
try {
|
|
410
|
+
worker = this.#ensureWorker();
|
|
411
|
+
} catch (error) {
|
|
412
|
+
logger.debug("tts: stream synthesis failed to start", {
|
|
413
|
+
modelKey,
|
|
414
|
+
error: error instanceof Error ? error.message : String(error),
|
|
415
|
+
});
|
|
416
|
+
const channel = new AudioChunkChannel();
|
|
417
|
+
channel.fail(error instanceof Error ? error : new Error(String(error)));
|
|
418
|
+
return { push: () => {}, end: () => {}, chunks: channel.iterator() };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const id = String(++this.#nextRequestId);
|
|
422
|
+
const signal = options.signal;
|
|
423
|
+
let closed = false;
|
|
424
|
+
let ended = false;
|
|
425
|
+
const abort = (): void => {
|
|
426
|
+
if (closed) return;
|
|
427
|
+
closed = true;
|
|
428
|
+
ended = true;
|
|
429
|
+
if (!this.#pending.has(id)) return;
|
|
430
|
+
this.#deletePending(id);
|
|
431
|
+
worker.send({ type: "stream-cancel", id });
|
|
432
|
+
channel.close();
|
|
433
|
+
};
|
|
434
|
+
const channel = new AudioChunkChannel(() => signal?.removeEventListener("abort", abort));
|
|
435
|
+
this.#addPending(id, { kind: "stream", modelKey, channel });
|
|
436
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
437
|
+
|
|
438
|
+
const start: TtsWorkerInbound = options.voice
|
|
439
|
+
? { type: "stream-start", id, modelKey, voice: options.voice }
|
|
440
|
+
: { type: "stream-start", id, modelKey };
|
|
441
|
+
worker.send(start);
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
push: (text: string) => {
|
|
445
|
+
if (!closed && !ended) worker.send({ type: "stream-push", id, text });
|
|
446
|
+
},
|
|
447
|
+
end: () => {
|
|
448
|
+
if (closed || ended) return;
|
|
449
|
+
ended = true;
|
|
450
|
+
worker.send({ type: "stream-end", id });
|
|
451
|
+
},
|
|
452
|
+
chunks: channel.iterator(),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async downloadModel(modelKey: string, options: TtsDownloadOptions = {}): Promise<boolean> {
|
|
457
|
+
if (!isTtsLocalModelKey(modelKey)) return false;
|
|
458
|
+
if (options.signal?.aborted) return false;
|
|
459
|
+
|
|
460
|
+
const unsubscribe = options.onProgress ? this.onProgress(options.onProgress) : undefined;
|
|
461
|
+
try {
|
|
462
|
+
const worker = this.#ensureWorker();
|
|
463
|
+
const id = String(++this.#nextRequestId);
|
|
464
|
+
const { promise, resolve } = Promise.withResolvers<boolean>();
|
|
465
|
+
this.#addPending(id, { kind: "download", modelKey, resolve });
|
|
466
|
+
const abort = (): void => {
|
|
467
|
+
const pending = this.#pending.get(id);
|
|
468
|
+
if (pending?.kind !== "download") return;
|
|
469
|
+
this.#deletePending(id);
|
|
470
|
+
pending.resolve(false);
|
|
471
|
+
};
|
|
472
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
473
|
+
try {
|
|
474
|
+
worker.send({ type: "download", id, modelKey });
|
|
475
|
+
return await promise;
|
|
476
|
+
} finally {
|
|
477
|
+
options.signal?.removeEventListener("abort", abort);
|
|
478
|
+
this.#deletePending(id);
|
|
479
|
+
}
|
|
480
|
+
} catch (error) {
|
|
481
|
+
logger.debug("tts: local model download failed", {
|
|
482
|
+
modelKey,
|
|
483
|
+
error: error instanceof Error ? error.message : String(error),
|
|
484
|
+
});
|
|
485
|
+
return false;
|
|
486
|
+
} finally {
|
|
487
|
+
unsubscribe?.();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async terminate(): Promise<void> {
|
|
492
|
+
const worker = this.#worker;
|
|
493
|
+
this.#worker = null;
|
|
494
|
+
this.#unsubscribeMessage?.();
|
|
495
|
+
this.#unsubscribeMessage = null;
|
|
496
|
+
this.#unsubscribeError?.();
|
|
497
|
+
this.#unsubscribeError = null;
|
|
498
|
+
for (const pending of this.#pending.values()) {
|
|
499
|
+
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
500
|
+
if (pending.kind === "synthesize") pending.resolve(null);
|
|
501
|
+
else if (pending.kind === "download") pending.resolve(false);
|
|
502
|
+
else pending.channel.close();
|
|
503
|
+
}
|
|
504
|
+
this.#pending.clear();
|
|
505
|
+
this.#refed = false;
|
|
506
|
+
try {
|
|
507
|
+
await worker?.terminate();
|
|
508
|
+
} catch {
|
|
509
|
+
// Already gone.
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#ensureWorker(): WorkerHandle {
|
|
514
|
+
if (this.#worker) return this.#worker;
|
|
515
|
+
const worker = this.#spawnWorker();
|
|
516
|
+
this.#worker = worker;
|
|
517
|
+
this.#unsubscribeMessage = worker.onMessage(message => this.#handleMessage(message));
|
|
518
|
+
this.#unsubscribeError = worker.onError(error => this.#handleWorkerError(error));
|
|
519
|
+
return worker;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Register a pending request and keep the worker referenced while work is in flight. */
|
|
523
|
+
#addPending(id: string, request: PendingRequest): void {
|
|
524
|
+
this.#pending.set(id, request);
|
|
525
|
+
this.#syncWorkerRef();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Drop a pending request and unref the worker once nothing is in flight. */
|
|
529
|
+
#deletePending(id: string): void {
|
|
530
|
+
if (this.#pending.delete(id)) this.#syncWorkerRef();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* The TTS subprocess is spawned `unref`'d so an idle worker never blocks
|
|
535
|
+
* process exit. A short-lived CLI command (`omp say`) awaiting a request would
|
|
536
|
+
* otherwise let the event loop drain and exit before the audio arrives, so we
|
|
537
|
+
* `ref` the worker exactly while at least one request is pending.
|
|
538
|
+
*/
|
|
539
|
+
#syncWorkerRef(): void {
|
|
540
|
+
const worker = this.#worker;
|
|
541
|
+
if (!worker) return;
|
|
542
|
+
const shouldRef = this.#pending.size > 0;
|
|
543
|
+
if (shouldRef === this.#refed) return;
|
|
544
|
+
this.#refed = shouldRef;
|
|
545
|
+
if (shouldRef) worker.ref();
|
|
546
|
+
else worker.unref();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#handleMessage(message: TtsWorkerOutbound): void {
|
|
550
|
+
if (message.type === "log") {
|
|
551
|
+
logWorkerMessage(message);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (message.type === "progress") {
|
|
555
|
+
this.#emitProgress(message.event);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (message.type === "pong") return;
|
|
559
|
+
|
|
560
|
+
const pending = this.#pending.get(message.id);
|
|
561
|
+
if (!pending) return;
|
|
562
|
+
|
|
563
|
+
// Streaming chunks are non-terminal: keep the session registered until
|
|
564
|
+
// `stream-done` (or an error) so later chunks still route to its channel.
|
|
565
|
+
if (message.type === "audio-chunk") {
|
|
566
|
+
if (pending.kind === "stream") {
|
|
567
|
+
pending.channel.push({
|
|
568
|
+
index: message.index,
|
|
569
|
+
text: message.text,
|
|
570
|
+
pcm: message.pcm,
|
|
571
|
+
sampleRate: message.sampleRate,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
this.#deletePending(message.id);
|
|
578
|
+
if (message.type === "stream-done") {
|
|
579
|
+
if (pending.kind === "stream") pending.channel.close();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (message.type === "audio") {
|
|
583
|
+
if (pending.kind === "synthesize") pending.resolve({ pcm: message.pcm, sampleRate: message.sampleRate });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (message.type === "downloaded") {
|
|
587
|
+
if (pending.kind === "download") pending.resolve(true);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
logger.debug("tts: worker returned error", { error: message.error });
|
|
591
|
+
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
592
|
+
if (pending.kind === "synthesize") pending.resolve(null);
|
|
593
|
+
else if (pending.kind === "download") pending.resolve(false);
|
|
594
|
+
else pending.channel.fail(new Error(message.error));
|
|
595
|
+
void this.terminate();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
#emitProgress(event: TtsProgressEvent): void {
|
|
599
|
+
for (const listener of this.#progressListeners) listener(event);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#handleWorkerError(error: Error): void {
|
|
603
|
+
logger.warn("tts: worker error", { error: error.message });
|
|
604
|
+
for (const pending of this.#pending.values()) {
|
|
605
|
+
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
606
|
+
if (pending.kind === "synthesize") pending.resolve(null);
|
|
607
|
+
else if (pending.kind === "download") pending.resolve(false);
|
|
608
|
+
else pending.channel.fail(error);
|
|
609
|
+
}
|
|
610
|
+
this.#pending.clear();
|
|
611
|
+
void this.terminate();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export const ttsClient = new TtsClient();
|
|
616
|
+
|
|
617
|
+
export async function shutdownTtsClient(): Promise<void> {
|
|
618
|
+
await ttsClient.terminate();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export async function smokeTestTtsWorker({
|
|
622
|
+
timeoutMs = SMOKE_TEST_TIMEOUT_MS,
|
|
623
|
+
}: {
|
|
624
|
+
timeoutMs?: number;
|
|
625
|
+
} = {}): Promise<void> {
|
|
626
|
+
const handle = wrapSubprocess(createTtsSubprocess());
|
|
627
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
628
|
+
const timer = setTimeout(() => reject(new Error(`tts worker did not pong within ${timeoutMs}ms`)), timeoutMs);
|
|
629
|
+
const unsubscribeMessage = handle.onMessage(message => {
|
|
630
|
+
if (message.type === "pong") {
|
|
631
|
+
resolve();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (message.type === "log") return;
|
|
635
|
+
reject(new Error(`tts worker: expected pong, got ${JSON.stringify(message)}`));
|
|
636
|
+
});
|
|
637
|
+
const unsubscribeError = handle.onError(reject);
|
|
638
|
+
try {
|
|
639
|
+
handle.send({ type: "ping", id: "smoke" } satisfies TtsWorkerInbound);
|
|
640
|
+
await promise;
|
|
641
|
+
} finally {
|
|
642
|
+
clearTimeout(timer);
|
|
643
|
+
unsubscribeMessage();
|
|
644
|
+
unsubscribeError();
|
|
645
|
+
await handle.terminate();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { TtsLocalModelKey } from "./models";
|
|
2
|
+
|
|
3
|
+
export type TtsProgressStatus = "initiate" | "download" | "progress" | "progress_total" | "done" | "ready" | "error";
|
|
4
|
+
|
|
5
|
+
export interface TtsProgressFileState {
|
|
6
|
+
loaded: number;
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TtsProgressEvent {
|
|
11
|
+
modelKey: TtsLocalModelKey;
|
|
12
|
+
status: TtsProgressStatus;
|
|
13
|
+
name?: string;
|
|
14
|
+
file?: string;
|
|
15
|
+
progress?: number;
|
|
16
|
+
loaded?: number;
|
|
17
|
+
total?: number;
|
|
18
|
+
files?: Record<string, TtsProgressFileState>;
|
|
19
|
+
task?: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TtsWorkerInbound =
|
|
24
|
+
| { type: "ping"; id: string }
|
|
25
|
+
| { type: "synthesize"; id: string; modelKey: TtsLocalModelKey; text: string; voice?: string }
|
|
26
|
+
| { type: "download"; id: string; modelKey: TtsLocalModelKey }
|
|
27
|
+
// Streaming synthesis: a session is opened with `stream-start`, fed incrementally
|
|
28
|
+
// with `stream-push`, and closed with `stream-end`. `stream-cancel` interrupts
|
|
29
|
+
// without a final drain. The worker emits an `audio-chunk` per synthesized
|
|
30
|
+
// sentence and a final `stream-done` only for non-cancelled sessions.
|
|
31
|
+
| { type: "stream-start"; id: string; modelKey: TtsLocalModelKey; voice?: string }
|
|
32
|
+
| { type: "stream-push"; id: string; text: string }
|
|
33
|
+
| { type: "stream-end"; id: string }
|
|
34
|
+
| { type: "stream-cancel"; id: string };
|
|
35
|
+
|
|
36
|
+
export type TtsWorkerOutbound =
|
|
37
|
+
| { type: "pong"; id: string }
|
|
38
|
+
| { type: "audio"; id: string; pcm: Float32Array; sampleRate: number }
|
|
39
|
+
| { type: "downloaded"; id: string }
|
|
40
|
+
| { type: "error"; id: string; error: string }
|
|
41
|
+
| { type: "progress"; id: string; event: TtsProgressEvent }
|
|
42
|
+
| { type: "log"; level: "debug" | "warn" | "error"; msg: string; meta?: Record<string, unknown> }
|
|
43
|
+
// One synthesized sentence of a streaming session, in emission order, followed
|
|
44
|
+
// by a single `stream-done` once the input stream is closed and drained.
|
|
45
|
+
| { type: "audio-chunk"; id: string; index: number; text: string; pcm: Float32Array; sampleRate: number }
|
|
46
|
+
| { type: "stream-done"; id: string };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Wire transport between the parent (`TtsClient`) and the local TTS subprocess.
|
|
50
|
+
* The parent owns the subprocess lifecycle (graceful work, hard SIGKILL on
|
|
51
|
+
* shutdown); the protocol carries no explicit close handshake — once the parent
|
|
52
|
+
* decides to terminate, it signals the OS to reap the child so
|
|
53
|
+
* `onnxruntime-node`'s NAPI finalizer never runs in the main agent address
|
|
54
|
+
* space (it segfaults Bun on shutdown — issue #1606). See `tts-client.ts` for
|
|
55
|
+
* the spawn/kill glue.
|
|
56
|
+
*/
|
|
57
|
+
export interface TtsTransport {
|
|
58
|
+
send(message: TtsWorkerOutbound): void;
|
|
59
|
+
onMessage(handler: (message: TtsWorkerInbound) => void): () => void;
|
|
60
|
+
}
|