dexterm-cli 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +394 -0
- package/dist/packages/agents/src/executor.js +202 -0
- package/dist/packages/agents/src/gate.js +179 -0
- package/dist/packages/agents/src/identity.js +218 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +119 -0
- package/dist/packages/agents/src/tools.js +72 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +75 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +74 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +336 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
- package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
- package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
- package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
- package/dist/packages/cli/src/alerts/creds.js +83 -0
- package/dist/packages/cli/src/alerts/format.js +41 -0
- package/dist/packages/cli/src/alerts/index.js +10 -0
- package/dist/packages/cli/src/alerts/notifier.js +53 -0
- package/dist/packages/cli/src/alerts/route.js +19 -0
- package/dist/packages/cli/src/alerts/types.js +1 -0
- package/dist/packages/cli/src/alerts-cmd.js +212 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +208 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +375 -0
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +511 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5785 -0
- package/dist/packages/cli/src/mcp-cmd.js +326 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +358 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/update.js +294 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/voice-cmd.js +119 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +538 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +211 -0
- package/dist/packages/data/src/intel/helius-history.js +70 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/link-baseline.js +196 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +184 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +252 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +936 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +20 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +18 -0
- package/dist/packages/tui/src/keymap.js +357 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/raster-font.js +195 -0
- package/dist/packages/tui/src/raster.js +213 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +407 -0
- package/dist/packages/voice/src/doctor.js +131 -0
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +84 -0
- package/dist/packages/voice/src/index.js +20 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +112 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/espeak.js +131 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/piper.js +206 -0
- package/dist/packages/voice/src/tts/say.js +132 -0
- package/dist/packages/voice/src/tts/voices.js +206 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +38 -4
- package/README.md +0 -3
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// VoiceController (task-1282): the push-to-talk state machine that ties speech-in, the safe router,
|
|
2
|
+
// and speech-out together, measures latency, and stays entirely UI-agnostic so it runs headless in
|
|
3
|
+
// tests (fake engines) and in the app (real macOS engines). It emits a decision the app performs; it
|
|
4
|
+
// NEVER performs a trade itself. The mic is open only in the 'listening' mode — the single place
|
|
5
|
+
// listen() is called — so the on-screen indicator and the real mic state can never disagree.
|
|
6
|
+
import { route, isSafe } from "./router.js";
|
|
7
|
+
import { spokenSummary } from "./summary.js";
|
|
8
|
+
import { isBannedVoice } from "./tts/voices.js";
|
|
9
|
+
export class VoiceController {
|
|
10
|
+
stt;
|
|
11
|
+
tts;
|
|
12
|
+
opts;
|
|
13
|
+
now;
|
|
14
|
+
enabled = false;
|
|
15
|
+
mode = 'off';
|
|
16
|
+
muted;
|
|
17
|
+
voice;
|
|
18
|
+
rate;
|
|
19
|
+
pitch;
|
|
20
|
+
voices = [];
|
|
21
|
+
lastTranscript = null;
|
|
22
|
+
lastSay = null;
|
|
23
|
+
message = null;
|
|
24
|
+
session = null;
|
|
25
|
+
speaking = null;
|
|
26
|
+
/** Key-release timestamp awaiting its first-spoken-word, and the path it will be attributed to. */
|
|
27
|
+
pendingLatency = null;
|
|
28
|
+
maxListenMs;
|
|
29
|
+
setTimer;
|
|
30
|
+
clearTimer;
|
|
31
|
+
/** The live mic's auto-stop handle; non-null only while listening. */
|
|
32
|
+
listenTimer = null;
|
|
33
|
+
/**
|
|
34
|
+
* True from the talk key until stt.listen() has resolved (task-1339, re-verify N3). A second talk key
|
|
35
|
+
* in that window is ignored instead of opening a second, orphaned mic session; a cancel in that
|
|
36
|
+
* window closes the session the moment it arrives.
|
|
37
|
+
*/
|
|
38
|
+
starting = false;
|
|
39
|
+
startCancelled = false;
|
|
40
|
+
samples = [];
|
|
41
|
+
constructor(opts) {
|
|
42
|
+
this.opts = opts;
|
|
43
|
+
this.stt = opts.stt;
|
|
44
|
+
this.tts = opts.tts;
|
|
45
|
+
this.muted = opts.muted ?? false;
|
|
46
|
+
this.voice = opts.voice ?? null;
|
|
47
|
+
this.rate = opts.rate ?? 190;
|
|
48
|
+
this.pitch = opts.pitch;
|
|
49
|
+
this.now = opts.now ?? (() => Date.now());
|
|
50
|
+
this.maxListenMs = opts.maxListenMs ?? 15_000;
|
|
51
|
+
this.setTimer = opts.setTimer ?? ((fn, ms) => { const h = setTimeout(fn, ms); h.unref?.(); return h; });
|
|
52
|
+
this.clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
53
|
+
}
|
|
54
|
+
status() {
|
|
55
|
+
return {
|
|
56
|
+
enabled: this.enabled,
|
|
57
|
+
mode: this.mode,
|
|
58
|
+
starting: this.starting,
|
|
59
|
+
muted: this.muted,
|
|
60
|
+
voice: this.voice,
|
|
61
|
+
sttLabel: this.stt?.label ?? null,
|
|
62
|
+
ttsLabel: this.tts?.label ?? null,
|
|
63
|
+
lastTranscript: this.lastTranscript,
|
|
64
|
+
lastSay: this.lastSay,
|
|
65
|
+
message: this.message,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
notify() { try {
|
|
69
|
+
this.opts.onState?.(this.status());
|
|
70
|
+
}
|
|
71
|
+
catch { /* UI handler must not break us */ } }
|
|
72
|
+
setMode(m) { this.mode = m; this.notify(); }
|
|
73
|
+
get isEnabled() { return this.enabled; }
|
|
74
|
+
get isListening() { return this.mode === 'listening'; }
|
|
75
|
+
/** The mic is live OR about to be (listen() in flight): the states a key must cancel. */
|
|
76
|
+
get micEngaged() { return this.mode === 'listening' || this.starting; }
|
|
77
|
+
/**
|
|
78
|
+
* Load the voice list lazily (used by cycleVoice and the status line). Novelty voices on the ban-list
|
|
79
|
+
* are filtered OUT so the user can never cycle into "Zarvox" / "Bad News" and so a null starting voice
|
|
80
|
+
* is seeded to a safe one, never the machine's (possibly novelty) system voice (task-1393).
|
|
81
|
+
*/
|
|
82
|
+
async loadVoices() {
|
|
83
|
+
if (!this.tts)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
this.voices = (await this.tts.listVoices()).filter((n) => !isBannedVoice(n));
|
|
87
|
+
if (!this.voice && this.voices.length)
|
|
88
|
+
this.voice = this.voices[0];
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
this.voices = [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Turn voice mode on/off. Enabling with no STT engine still works for spoken replies to typed Qs. */
|
|
95
|
+
enable() {
|
|
96
|
+
if (this.enabled)
|
|
97
|
+
return;
|
|
98
|
+
this.enabled = true;
|
|
99
|
+
this.message = this.stt ? null : 'voice-in unavailable on this machine — type questions; replies are still spoken';
|
|
100
|
+
this.setMode('idle');
|
|
101
|
+
void this.loadVoices();
|
|
102
|
+
}
|
|
103
|
+
disable() {
|
|
104
|
+
if (!this.enabled)
|
|
105
|
+
return;
|
|
106
|
+
this.cancelListen();
|
|
107
|
+
this.stopSpeaking();
|
|
108
|
+
this.enabled = false;
|
|
109
|
+
this.pendingLatency = null;
|
|
110
|
+
this.setMode('off');
|
|
111
|
+
}
|
|
112
|
+
toggle() { this.enabled ? this.disable() : this.enable(); }
|
|
113
|
+
/**
|
|
114
|
+
* The push-to-talk key. Terminals deliver no key-up, so this toggles: first press opens the mic
|
|
115
|
+
* (mode 'listening' — the only place the mic is live), second press closes it, transcribes, and
|
|
116
|
+
* routes. Starting to talk also interrupts any reply in progress. Returns when the decision (if any)
|
|
117
|
+
* has been emitted, so tests can await it.
|
|
118
|
+
*/
|
|
119
|
+
async toggleTalk() {
|
|
120
|
+
if (!this.enabled)
|
|
121
|
+
this.enable();
|
|
122
|
+
if (this.mode === 'listening')
|
|
123
|
+
return this.stopTalkAndRoute();
|
|
124
|
+
if (this.mode === 'transcribing' || this.mode === 'thinking')
|
|
125
|
+
return; // busy; ignore re-press
|
|
126
|
+
if (this.starting)
|
|
127
|
+
return; // a second press while the mic is opening: never a second session (N3)
|
|
128
|
+
return this.startTalk();
|
|
129
|
+
}
|
|
130
|
+
async startTalk() {
|
|
131
|
+
if (!this.stt) {
|
|
132
|
+
this.message = 'voice-in unavailable — type your question instead';
|
|
133
|
+
this.notify();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.stopSpeaking(); // starting to talk cuts off any reply
|
|
137
|
+
this.starting = true;
|
|
138
|
+
this.startCancelled = false;
|
|
139
|
+
this.notify();
|
|
140
|
+
let session = null;
|
|
141
|
+
try {
|
|
142
|
+
session = await this.stt.listen();
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
this.starting = false;
|
|
146
|
+
this.session = null;
|
|
147
|
+
this.message = `mic/STT could not start: ${describe(e)}`;
|
|
148
|
+
this.setMode('idle');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.starting = false;
|
|
152
|
+
// The user moved on (a key, esc, disable) while the mic was opening: close it at once (N3/N8).
|
|
153
|
+
if (this.startCancelled || !this.enabled) {
|
|
154
|
+
try {
|
|
155
|
+
session.cancel();
|
|
156
|
+
}
|
|
157
|
+
catch { /* ignore */ }
|
|
158
|
+
this.session = null;
|
|
159
|
+
this.setMode(this.enabled ? 'idle' : 'off');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.session = session;
|
|
163
|
+
this.setMode('listening');
|
|
164
|
+
this.armListenTimeout(); // the mic can never stay live indefinitely (task-1282 rework D2)
|
|
165
|
+
}
|
|
166
|
+
/** Start the hard cap that auto-stops a live mic. Cleared whenever we leave 'listening'. */
|
|
167
|
+
armListenTimeout() {
|
|
168
|
+
this.clearListenTimeout();
|
|
169
|
+
if (this.maxListenMs <= 0)
|
|
170
|
+
return;
|
|
171
|
+
this.listenTimer = this.setTimer(() => {
|
|
172
|
+
this.listenTimer = null;
|
|
173
|
+
if (this.mode === 'listening') {
|
|
174
|
+
this.cancelListen();
|
|
175
|
+
this.message = 'mic timed out — press v to talk again';
|
|
176
|
+
this.notify();
|
|
177
|
+
}
|
|
178
|
+
}, this.maxListenMs);
|
|
179
|
+
}
|
|
180
|
+
clearListenTimeout() {
|
|
181
|
+
if (this.listenTimer !== null) {
|
|
182
|
+
try {
|
|
183
|
+
this.clearTimer(this.listenTimer);
|
|
184
|
+
}
|
|
185
|
+
catch { /* ignore */ }
|
|
186
|
+
this.listenTimer = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async stopTalkAndRoute() {
|
|
190
|
+
this.clearListenTimeout(); // leaving 'listening': the mic is no longer live
|
|
191
|
+
const session = this.session;
|
|
192
|
+
this.session = null;
|
|
193
|
+
this.setMode('transcribing');
|
|
194
|
+
// "Key release": the clock for key-release → first-spoken-word starts now.
|
|
195
|
+
const releaseAt = this.now();
|
|
196
|
+
let result;
|
|
197
|
+
try {
|
|
198
|
+
result = session ? await session.stop() : { text: '', confidence: 0 };
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
// Always release the capture on a transcription error so the mic can never be left open.
|
|
202
|
+
try {
|
|
203
|
+
session?.cancel();
|
|
204
|
+
}
|
|
205
|
+
catch { /* ignore */ }
|
|
206
|
+
this.message = `transcription failed: ${describe(e)}`;
|
|
207
|
+
this.setMode('idle');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.lastTranscript = result.text || null;
|
|
211
|
+
// An empty capture used to route to a silent 'ignore': the user saw the badge vanish and nothing else.
|
|
212
|
+
if (!result.text.trim()) {
|
|
213
|
+
this.message = "didn't hear anything — press v, wait for the red LISTENING badge, speak, then press v";
|
|
214
|
+
this.setMode('idle');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const ss = this.opts.getScreenState?.() ?? {};
|
|
218
|
+
const prov = {
|
|
219
|
+
pttActive: true, // this transcript came from a real PTT session, by construction
|
|
220
|
+
speakerCount: result.speakerCount ?? 1,
|
|
221
|
+
echoFlagged: result.echoFlagged ?? false,
|
|
222
|
+
sttConfidence: result.confidence,
|
|
223
|
+
lowConfTokens: result.lowConfTokens ?? [],
|
|
224
|
+
};
|
|
225
|
+
const decision = route(result.text, prov, ss);
|
|
226
|
+
// Hard safety gate: never act on a decision that is not in the safe set (defence in depth).
|
|
227
|
+
if (!isSafe(decision)) {
|
|
228
|
+
this.message = 'blocked an unsafe voice decision';
|
|
229
|
+
this.setMode('idle');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.dispatch(decision, releaseAt);
|
|
233
|
+
}
|
|
234
|
+
/** Emit the decision for the app, speak any immediate confirmation, and arm latency timing. */
|
|
235
|
+
dispatch(d, releaseAt) {
|
|
236
|
+
if (d.kind === 'analyst') {
|
|
237
|
+
// The spoken output is the analyst's answer; the app calls speakAnswer() when it lands. Time the
|
|
238
|
+
// key-release → first word of that answer.
|
|
239
|
+
this.pendingLatency = { at: releaseAt, path: 'analyst', transcript: this.lastTranscript ?? '' };
|
|
240
|
+
this.setMode('thinking');
|
|
241
|
+
try {
|
|
242
|
+
this.opts.onDecision?.(d);
|
|
243
|
+
}
|
|
244
|
+
catch { /* app handler */ }
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// Instant paths (navigate/open/filter/watchlist/open-ticket/popup/clarify) speak their own one-liner —
|
|
248
|
+
// unless the app answered itself (returned false): then its reply stands and `say` is NOT read over
|
|
249
|
+
// it, so a clarify such as "I see 2 tokens called X" is what the user actually hears (N1).
|
|
250
|
+
this.pendingLatency = { at: releaseAt, path: 'instant', transcript: this.lastTranscript ?? '' };
|
|
251
|
+
let appAnswered = false;
|
|
252
|
+
try {
|
|
253
|
+
appAnswered = this.opts.onDecision?.(d) === false;
|
|
254
|
+
}
|
|
255
|
+
catch { /* app handler */ }
|
|
256
|
+
if (appAnswered) {
|
|
257
|
+
if (this.mode !== 'speaking')
|
|
258
|
+
this.setMode('idle');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const line = d.kind === 'clarify' ? d.question : ('say' in d ? d.say : null);
|
|
262
|
+
if (line)
|
|
263
|
+
this.speak(line);
|
|
264
|
+
else {
|
|
265
|
+
this.setMode('idle');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Speak the analyst's answer (first ~2 sentences). Called by the app when a turn settles. Honours
|
|
270
|
+
* mute, is interruptible, and records the pending analyst-path latency on the first audio sample.
|
|
271
|
+
*/
|
|
272
|
+
speakAnswer(src) {
|
|
273
|
+
const line = spokenSummary(src);
|
|
274
|
+
if (!line) {
|
|
275
|
+
this.setMode('idle');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
this.speak(line);
|
|
279
|
+
}
|
|
280
|
+
/** Low-level speak: stops any current reply, then reads `text`. Muted → skip audio but update state. */
|
|
281
|
+
speak(text) {
|
|
282
|
+
this.lastSay = text;
|
|
283
|
+
if (!this.tts || this.muted) {
|
|
284
|
+
// Still resolve the latency sample (the "spoken word" is just suppressed) so the table is complete.
|
|
285
|
+
this.recordFirstAudio();
|
|
286
|
+
this.setMode('idle');
|
|
287
|
+
this.notify();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
this.stopSpeaking();
|
|
291
|
+
this.setMode('speaking');
|
|
292
|
+
const handle = this.tts.speak(text, {
|
|
293
|
+
voice: this.voice ?? undefined,
|
|
294
|
+
rate: this.rate,
|
|
295
|
+
pitch: this.pitch,
|
|
296
|
+
onFirstAudio: () => this.recordFirstAudio(),
|
|
297
|
+
});
|
|
298
|
+
this.speaking = handle;
|
|
299
|
+
handle.done.then(() => {
|
|
300
|
+
if (this.speaking === handle) {
|
|
301
|
+
this.speaking = null;
|
|
302
|
+
if (this.mode === 'speaking')
|
|
303
|
+
this.setMode('idle');
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
recordFirstAudio() {
|
|
308
|
+
const p = this.pendingLatency;
|
|
309
|
+
if (!p)
|
|
310
|
+
return;
|
|
311
|
+
this.pendingLatency = null;
|
|
312
|
+
this.samples.push({ path: p.path, ms: this.now() - p.at, transcript: p.transcript });
|
|
313
|
+
}
|
|
314
|
+
/** Interrupt a spoken reply (a key, or the user starting to talk). */
|
|
315
|
+
stopSpeaking() {
|
|
316
|
+
if (this.speaking) {
|
|
317
|
+
try {
|
|
318
|
+
this.speaking.stop();
|
|
319
|
+
}
|
|
320
|
+
catch { /* ignore */ }
|
|
321
|
+
this.speaking = null;
|
|
322
|
+
}
|
|
323
|
+
if (this.mode === 'speaking')
|
|
324
|
+
this.setMode('idle');
|
|
325
|
+
}
|
|
326
|
+
/** Abandon a live capture without transcribing. Always closes the mic and the auto-stop timer. */
|
|
327
|
+
cancelListen() {
|
|
328
|
+
this.clearListenTimeout();
|
|
329
|
+
if (this.starting)
|
|
330
|
+
this.startCancelled = true; // the session is closed the moment listen() resolves
|
|
331
|
+
if (this.session) {
|
|
332
|
+
try {
|
|
333
|
+
this.session.cancel();
|
|
334
|
+
}
|
|
335
|
+
catch { /* ignore */ }
|
|
336
|
+
this.session = null;
|
|
337
|
+
}
|
|
338
|
+
if (this.mode === 'listening')
|
|
339
|
+
this.setMode('idle');
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Interrupt whatever voice is doing and return to idle — a mic capture, a spoken reply, OR a pending
|
|
343
|
+
* analyst turn ('thinking') / in-flight 'transcribing'. Resetting 'thinking' is the task-1282 rework
|
|
344
|
+
* D3 fix: an analyst error used to leave the mode stuck in 'thinking', where toggleTalk ignores every
|
|
345
|
+
* press, so voice was dead until restart. Now any key (esc) or an error cleanly returns to idle.
|
|
346
|
+
*/
|
|
347
|
+
interrupt() {
|
|
348
|
+
this.stopSpeaking();
|
|
349
|
+
this.cancelListen();
|
|
350
|
+
this.pendingLatency = null;
|
|
351
|
+
if (this.enabled && this.mode !== 'idle' && this.mode !== 'off')
|
|
352
|
+
this.setMode('idle');
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Abort the current turn because the analyst run failed/was cancelled, and surface a clear error both
|
|
356
|
+
* on screen (the HUD message) and aloud. Always leaves voice in idle so the next `v` works
|
|
357
|
+
* (task-1282 rework D3).
|
|
358
|
+
*/
|
|
359
|
+
abortTurn(message = "Sorry — I couldn't get an answer. Try again.") {
|
|
360
|
+
this.interrupt();
|
|
361
|
+
if (!this.enabled) {
|
|
362
|
+
this.setMode('off');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.message = message;
|
|
366
|
+
this.mode = 'idle'; // speak()'s done handler returns to idle from 'speaking'
|
|
367
|
+
this.speak(message);
|
|
368
|
+
}
|
|
369
|
+
setMuted(m) { this.muted = m; if (m)
|
|
370
|
+
this.stopSpeaking(); this.notify(); }
|
|
371
|
+
toggleMute() { this.setMuted(!this.muted); }
|
|
372
|
+
get isMuted() { return this.muted; }
|
|
373
|
+
/** Cycle to the next installed (safe) voice and persist the choice. No-op when there are none. */
|
|
374
|
+
cycleVoice() {
|
|
375
|
+
if (!this.voices.length) {
|
|
376
|
+
this.notify();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const i = this.voice ? this.voices.indexOf(this.voice) : -1;
|
|
380
|
+
this.voice = this.voices[(i + 1) % this.voices.length];
|
|
381
|
+
try {
|
|
382
|
+
this.opts.onVoiceChange?.(this.voice);
|
|
383
|
+
}
|
|
384
|
+
catch { /* persistence is best-effort; never break the UI */ }
|
|
385
|
+
this.notify();
|
|
386
|
+
}
|
|
387
|
+
currentVoice() { return this.voice; }
|
|
388
|
+
availableVoices() { return this.voices.slice(); }
|
|
389
|
+
/** A compact latency report for the handoff / `--latency` demo. */
|
|
390
|
+
report() {
|
|
391
|
+
const paths = ['instant', 'analyst'];
|
|
392
|
+
return paths.map((path) => {
|
|
393
|
+
const xs = this.samples.filter((s) => s.path === path).map((s) => s.ms).sort((a, b) => a - b);
|
|
394
|
+
return { path, n: xs.length, p50: pct(xs, 0.5), min: xs[0] ?? null, max: xs[xs.length - 1] ?? null };
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function pct(sorted, p) {
|
|
399
|
+
if (!sorted.length)
|
|
400
|
+
return null;
|
|
401
|
+
const i = Math.min(sorted.length - 1, Math.floor(p * sorted.length));
|
|
402
|
+
return sorted[i];
|
|
403
|
+
}
|
|
404
|
+
function describe(e) {
|
|
405
|
+
const m = e?.message;
|
|
406
|
+
return typeof m === 'string' ? m : String(e);
|
|
407
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Voice-mode doctor checks (task-1282 scope item 4). Returns the same check shape the CLI doctor uses
|
|
2
|
+
// so `dexterm doctor` can render them inline. Everything the checks depend on (platform, which engines
|
|
3
|
+
// resolved, whether swiftc / a whisper model / a mic tool are present) is injected, so the checks are
|
|
4
|
+
// deterministic in tests and never spawn or hang. The guiding rule: if a component is missing, say
|
|
5
|
+
// exactly what to install, and make clear voice DEGRADES TO TEXT rather than breaking the app.
|
|
6
|
+
/** Produce the voice checks. Never throws. */
|
|
7
|
+
export function voiceDoctor(deps) {
|
|
8
|
+
const checks = [];
|
|
9
|
+
if (deps.platform !== 'darwin') {
|
|
10
|
+
const win = deps.platform === 'win32';
|
|
11
|
+
// Speech OUT off macOS: Piper (neural, preferred) → espeak-ng (baseline) → warn (task-1408).
|
|
12
|
+
// NOTE: `apt install piper` on Debian is an UNRELATED gaming-mouse tool — never suggest it; the
|
|
13
|
+
// Piper TTS binary comes from the rhasspy/piper prebuilt release, the voice from `dexterm voice download`.
|
|
14
|
+
const piperRelease = 'github.com/rhasspy/piper/releases';
|
|
15
|
+
const whisperRelease = 'github.com/ggml-org/whisper.cpp/releases';
|
|
16
|
+
const piperReady = !!deps.piperPresent && !!deps.piperVoicePresent && !!deps.playerName;
|
|
17
|
+
// What Piper is still missing (for an upgrade hint / the no-voice fix).
|
|
18
|
+
const piperBits = [];
|
|
19
|
+
if (!deps.piperPresent)
|
|
20
|
+
piperBits.push(`the Piper binary (prebuilt from ${piperRelease} into \`~/.dexterm/piper/\`, NOT \`apt install piper\`)`);
|
|
21
|
+
if (!deps.piperVoicePresent)
|
|
22
|
+
piperBits.push('the voice model (`dexterm voice download piper-voice` — asks first, checksum-verified)');
|
|
23
|
+
if (!deps.playerName)
|
|
24
|
+
piperBits.push(win ? 'an audio player (ffplay from ffmpeg)' : 'an audio player (`sudo apt install alsa-utils` for aplay, or pulseaudio-utils for paplay)');
|
|
25
|
+
if (piperReady) {
|
|
26
|
+
checks.push({ name: 'Voice out (Piper)', status: 'pass', detail: `Piper neural voice ready — en_GB-alan-medium, plays via ${deps.playerName} (local, no key)` });
|
|
27
|
+
}
|
|
28
|
+
else if (deps.espeakPresent) {
|
|
29
|
+
checks.push({
|
|
30
|
+
name: 'Voice out (espeak-ng)', status: 'pass',
|
|
31
|
+
detail: `espeak-ng ready — British male default (local, no key). For a higher-quality neural voice, add Piper: ${piperBits.join('; ')}`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
checks.push({
|
|
36
|
+
name: 'Voice out', status: 'warn',
|
|
37
|
+
detail: `spoken replies are off on ${deps.platform} until a speech engine is installed; the analyst still answers in text`,
|
|
38
|
+
fix: win
|
|
39
|
+
? 'Install espeak-ng (`choco install espeak` or the espeak-ng release). For a neural voice, add Piper (prebuilt from '
|
|
40
|
+
+ `${piperRelease}) + \`dexterm voice download piper-voice\` + ffplay.`
|
|
41
|
+
: 'Fastest: `sudo apt install espeak-ng`. Better (neural): install the Piper prebuilt binary from '
|
|
42
|
+
+ `${piperRelease} into \`~/.dexterm/piper/\`, run \`dexterm voice download piper-voice\`, and \`sudo apt install alsa-utils\` for a player. `
|
|
43
|
+
+ 'Do NOT `apt install piper` — on Debian that is an unrelated mouse tool.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Speech IN — whisper.cpp (the default off macOS). Not in Debian: use the prebuilt release.
|
|
47
|
+
if (deps.resolvedStt === 'whisper' || (deps.whisperBinPresent && deps.whisperModelPresent && deps.micToolPresent)) {
|
|
48
|
+
checks.push({ name: 'Voice in (whisper)', status: 'pass', detail: 'whisper.cpp base.en + a mic tool (local, no key)' });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const bits = [];
|
|
52
|
+
if (!deps.whisperBinPresent)
|
|
53
|
+
bits.push('no whisper-cli');
|
|
54
|
+
else if (!deps.whisperModelPresent)
|
|
55
|
+
bits.push('no base.en model');
|
|
56
|
+
if (!deps.micToolPresent)
|
|
57
|
+
bits.push('no mic tool (arecord/sox/ffmpeg)');
|
|
58
|
+
checks.push({
|
|
59
|
+
name: 'Voice in', status: 'warn',
|
|
60
|
+
detail: `speech-to-text is off (${bits.join(', ') || 'whisper not set up'}); you can still type questions — voice degrades to text`,
|
|
61
|
+
fix: `Install whisper.cpp (not in Debian — use the prebuilt release from ${whisperRelease}) + a mic tool `
|
|
62
|
+
+ '(`sudo apt install alsa-utils` for arecord, or sox), then `dexterm voice download whisper` (asks first, checksum-verified).',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (win) {
|
|
66
|
+
checks.push({
|
|
67
|
+
name: 'Windows voice', status: 'warn',
|
|
68
|
+
detail: 'espeak-ng (or Piper) is the supported Windows speech-out path; native SAPI is not wired in this build',
|
|
69
|
+
fix: 'Use espeak-ng or Piper on Windows for now. (SAPI support is a later enhancement.)',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return checks;
|
|
73
|
+
}
|
|
74
|
+
// Speech OUT — macOS say.
|
|
75
|
+
if (deps.sayAvailable) {
|
|
76
|
+
checks.push({ name: 'Voice out (say)', status: 'pass', detail: `macOS \`say\` ready · ${deps.voiceCount} voice${deps.voiceCount === 1 ? '' : 's'} (press the voice key, then cycle the voice)` });
|
|
77
|
+
// Which voice speaks, and why — so a machine can never silently use a novelty/odd system voice (task-1393).
|
|
78
|
+
if (deps.resolvedVoice) {
|
|
79
|
+
checks.push({ name: 'Default voice', status: 'pass', detail: `${deps.resolvedVoiceReason ?? deps.resolvedVoice} · , changes it` });
|
|
80
|
+
}
|
|
81
|
+
else if (deps.resolvedVoice === null) {
|
|
82
|
+
checks.push({
|
|
83
|
+
name: 'Default voice', status: 'warn',
|
|
84
|
+
detail: deps.resolvedVoiceReason ?? 'no safe voice installed — spoken replies are off (text still works)',
|
|
85
|
+
fix: 'Install a British male voice in System Settings → Accessibility → Spoken Content → System Voice → Manage Voices (Daniel, English (UK)); the Enhanced/Premium download sounds best.',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
checks.push({
|
|
91
|
+
name: 'Voice out (say)', status: 'warn',
|
|
92
|
+
detail: 'macOS `say` not found; spoken replies are off, the analyst still answers in text',
|
|
93
|
+
fix: '`say` ships with macOS — if it is missing, reinstall the Command Line Tools (`xcode-select --install`).',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// Speech IN — Apple Speech (preferred) or whisper (fallback).
|
|
97
|
+
if (deps.resolvedStt === 'apple' || deps.swiftcPresent) {
|
|
98
|
+
checks.push({ name: 'Voice in (Apple Speech)', status: 'pass', detail: 'on-device Apple Speech via swiftc (no key, streaming) — the first run asks for Microphone + Speech permission at the Mac' });
|
|
99
|
+
}
|
|
100
|
+
else if (deps.resolvedStt === 'whisper' || (deps.whisperBinPresent && deps.whisperModelPresent && deps.micToolPresent)) {
|
|
101
|
+
checks.push({ name: 'Voice in (whisper)', status: 'pass', detail: 'whisper.cpp base.en + a mic tool (local, no key)' });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Nothing for voice-in: steer to the fastest free path and make clear text still works.
|
|
105
|
+
const bits = [];
|
|
106
|
+
if (!deps.swiftcPresent)
|
|
107
|
+
bits.push('no swiftc (Apple Speech)');
|
|
108
|
+
if (!deps.whisperBinPresent)
|
|
109
|
+
bits.push('no whisper-cli');
|
|
110
|
+
else if (!deps.whisperModelPresent)
|
|
111
|
+
bits.push('no base.en model');
|
|
112
|
+
if (!deps.micToolPresent)
|
|
113
|
+
bits.push('no mic tool (sox/ffmpeg)');
|
|
114
|
+
checks.push({
|
|
115
|
+
name: 'Voice in', status: 'warn',
|
|
116
|
+
detail: `speech-to-text is off (${bits.join(', ')}); you can still type questions — voice degrades to text`,
|
|
117
|
+
fix: 'Fastest free path: install Xcode command line tools for Apple Speech (`xcode-select --install`). '
|
|
118
|
+
+ 'Or whisper: `brew install whisper-cpp sox`, then `dexterm voice download whisper` (asks first, checksum-verified, writes '
|
|
119
|
+
+ '`~/.dexterm/models/ggml-base.en.bin`). Never use large-v3-turbo on a CPU-only Mac — it is unusably slow.',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Mic-capture tool note (only relevant to the whisper path; Apple Speech captures its own audio).
|
|
123
|
+
if (!deps.micToolPresent && deps.resolvedStt !== 'apple') {
|
|
124
|
+
checks.push({
|
|
125
|
+
name: 'Mic capture', status: 'warn',
|
|
126
|
+
detail: 'no sox/ffmpeg found; the whisper voice-in path needs one (Apple Speech does not)',
|
|
127
|
+
fix: '`brew install sox` (smallest) or `brew install ffmpeg`.',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return checks;
|
|
131
|
+
}
|