{"_id":"@achasoft/dsh-voice","_rev":"3-e76abfed7e7c2e384dd5e76075e37e7d","name":"@achasoft/dsh-voice","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@achasoft/dsh-voice","version":"0.1.0","keywords":["deepseek-harness","dsh","dsh-plugin","voice","speech-to-text","whisper"],"license":"MIT","_id":"@achasoft/dsh-voice@0.1.0","maintainers":[{"name":"navid.kianfar","email":"navid.kianfar@outlook.com"}],"homepage":"https://github.com/achasoft/dsh-voice#readme","bugs":{"url":"https://github.com/achasoft/dsh-voice/issues"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"inject":["@deepseek-ai/dsh-api-remotes","@deepseek-ai/dsh-client-locale","@deepseek-ai/dsh-client-ui-conversation","@deepseek-ai/dsh-client-ui-settings","@deepseek-ai/dsh-client-ui-settings-plugins"],"platform":"web"}},"dist":{"shasum":"bfa72ac6a7c88980b8cf28548de5e3b8b05df68e","tarball":"https://registry.npmjs.org/@achasoft/dsh-voice/-/dsh-voice-0.1.0.tgz","fileCount":32,"integrity":"sha512-eElUX4dG72yi/qFpv5PvU+dzdKGOOA3Ow2qbbUmwEjw7soHQnzDcTXlb0j1Skn4hBYrMRBb2pxoDq82dDxHF1w==","signatures":[{"sig":"MEUCIQCXBqTgEVtmxBxBj0J05ltlTa5MYTJ94BMLBuomheYUXAIgKK1Kmb+HkGzFe9M6JEiKBjIbEu/d/aiPcWUAQlbavmQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":673128},"main":"lib/index.js","type":"module","types":"./types/index.d.ts","engines":{"node":"^22.19 || >=24"},"exports":{".":{"types":"./types/index.d.ts","default":"./lib/index.js"},"./host":{"types":"./types/host/index.d.ts","default":"./lib/host.js"},"./client":{"default":"./lib/client.js"},"./remote":{"default":"./lib/remote.js"},"./typert":{"default":"./lib/typert.host.js"},"./whisper-cpp":{"default":"./lib/whisper-cpp.js"},"./package.json":"./package.json","./cordis.patch.yml":"./cordis.patch.yml","./openai-compatible":{"default":"./lib/openai-compatible.js"}},"gitHead":"789c88b16c5f393fb49ecaec611019c42d0559ed","scripts":{"test":"node scripts/check-typert.mjs && vitest run","build":"tsc -p tsconfig.build.json && tsdown","prepare":"tsc -p tsconfig.build.json && tsdown","typecheck":"tsc --noEmit","check:typert":"node scripts/check-typert.mjs","regen:typert":"node scripts/regen-typert.mjs"},"_npmUser":{"name":"navid.kianfar","email":"navid.kianfar@outlook.com"},"repository":{"url":"git+https://github.com/achasoft/dsh-voice.git","type":"git"},"_npmVersion":"11.19.0","description":"Voice input for the DeepSeek Harness Web Client: a microphone control in the composer, a transcription capability seam, and two selectable providers","directories":{},"_nodeVersion":"25.2.1","dependencies":{"zod":"^4.4.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^18.2.0","tsdown":"^0.15.6","vitest":"^3.2.7","typescript":"^5.9.2","@types/node":"^22.20.1","@types/react":"~18.3.1","lightningcss":"^1.30.1","@deepseek-ai/cordis":"link:../../deepseek-harness/vendor/cordis","@deepseek-ai/schemastery":"link:../../deepseek-harness/vendor/schemastery","@deepseek-ai/dsh-settings":"link:../../deepseek-harness/packages/settings/settings","@deepseek-ai/dsh-subprocess":"link:../../deepseek-harness/packages/subprocess/subprocess","@deepseek-ai/dsh-api-remotes":"link:../../deepseek-harness/packages/api/remotes","@deepseek-ai/dsh-credentials":"link:../../deepseek-harness/packages/credentials/credentials","@deepseek-ai/dsh-client-locale":"link:../../deepseek-harness/packages/client/locale","@deepseek-ai/dsh-client-runtime":"link:../../deepseek-harness/packages/client/runtime","@deepseek-ai/dsh-client-ui-slots":"link:../../deepseek-harness/packages/client/ui-slots","@deepseek-ai/dsh-typert-protocol":"link:../../deepseek-harness/packages/typert/protocol","@deepseek-ai/dsh-client-ui-settings":"link:../../deepseek-harness/packages/client/ui-settings","@deepseek-ai/dsh-client-ui-primitives":"link:../../deepseek-harness/packages/client/ui-primitives","@deepseek-ai/dsh-client-ui-conversation":"link:../../deepseek-harness/packages/client/ui-conversation","@deepseek-ai/dsh-client-ui-settings-plugins":"link:../../deepseek-harness/packages/client/ui-settings-plugins"},"peerDependencies":{"@deepseek-ai/cordis":"*","@deepseek-ai/schemastery":"*","@deepseek-ai/dsh-settings":"*","@deepseek-ai/dsh-subprocess":"*","@deepseek-ai/dsh-credentials":"*","@deepseek-ai/dsh-typert-protocol":"*"},"_npmOperationalInternal":{"tmp":"tmp/dsh-voice_0.1.0_1787701908000_0.712554487048279","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@achasoft/dsh-voice","version":"0.1.1","keywords":["deepseek-harness","dsh","dsh-plugin","voice-input","speech-to-text","transcription","whisper","whisper-cpp","stt","dictation","cordis"],"license":"MIT","_id":"@achasoft/dsh-voice@0.1.1","maintainers":[{"name":"navid.kianfar","email":"navid.kianfar@outlook.com"}],"homepage":"https://github.com/achasoft/dsh-voice#readme","bugs":{"url":"https://github.com/achasoft/dsh-voice/issues"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"inject":["@deepseek-ai/dsh-api-remotes","@deepseek-ai/dsh-client-locale","@deepseek-ai/dsh-client-ui-conversation","@deepseek-ai/dsh-client-ui-settings","@deepseek-ai/dsh-client-ui-settings-plugins"],"platform":"web"}},"dist":{"shasum":"b91d5a8538ed56ea1c62084ce3d7def59f2129bd","tarball":"https://registry.npmjs.org/@achasoft/dsh-voice/-/dsh-voice-0.1.1.tgz","fileCount":32,"integrity":"sha512-OJ3gEo8JCNyiIH1xPZ6hSgaN0KSoKtWav0pPVmbX423r8ujD0eI4Gtelum3n3R9Ah35vYTOyHWWwQfhoFiEBGw==","signatures":[{"sig":"MEYCIQC9gX+90tVFZctHmicVPJmNA6ImPIbaaqivwggy6Sv5DQIhAImCWCDR0nE7Tse/+Tach7aBkONhbLKRCMYNlHt1kf5Y","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":692031},"main":"lib/index.js","type":"module","types":"./types/index.d.ts","engines":{"node":"^22.19 || >=24"},"exports":{".":{"types":"./types/index.d.ts","default":"./lib/index.js"},"./host":{"types":"./types/host/index.d.ts","default":"./lib/host.js"},"./client":{"default":"./lib/client.js"},"./remote":{"default":"./lib/remote.js"},"./typert":{"default":"./lib/typert.host.js"},"./whisper-cpp":{"default":"./lib/whisper-cpp.js"},"./package.json":"./package.json","./cordis.patch.yml":"./cordis.patch.yml","./openai-compatible":{"default":"./lib/openai-compatible.js"}},"gitHead":"d347b6affd939048436343d4209d2560fb8ca0ac","scripts":{"test":"node scripts/check-typert.mjs && vitest run","build":"tsc -p tsconfig.build.json && tsdown","prepare":"tsc -p tsconfig.build.json && tsdown","typecheck":"tsc --noEmit","check:typert":"node scripts/check-typert.mjs","regen:typert":"node scripts/regen-typert.mjs"},"_npmUser":{"name":"navid.kianfar","email":"navid.kianfar@outlook.com"},"repository":{"url":"git+https://github.com/achasoft/dsh-voice.git","type":"git"},"_npmVersion":"11.19.0","description":"Voice input for the DeepSeek Harness Web Client: a microphone control in the composer, a transcription capability seam, and two selectable providers","directories":{},"_nodeVersion":"25.2.1","dependencies":{"zod":"^4.4.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^18.2.0","tsdown":"^0.15.6","vitest":"^3.2.7","typescript":"^5.9.2","@types/node":"^22.20.1","@types/react":"~18.3.1","lightningcss":"^1.30.1","@deepseek-ai/cordis":"link:../../deepseek-harness/vendor/cordis","@deepseek-ai/schemastery":"link:../../deepseek-harness/vendor/schemastery","@deepseek-ai/dsh-settings":"link:../../deepseek-harness/packages/settings/settings","@deepseek-ai/dsh-subprocess":"link:../../deepseek-harness/packages/subprocess/subprocess","@deepseek-ai/dsh-api-remotes":"link:../../deepseek-harness/packages/api/remotes","@deepseek-ai/dsh-credentials":"link:../../deepseek-harness/packages/credentials/credentials","@deepseek-ai/dsh-client-locale":"link:../../deepseek-harness/packages/client/locale","@deepseek-ai/dsh-client-runtime":"link:../../deepseek-harness/packages/client/runtime","@deepseek-ai/dsh-client-ui-slots":"link:../../deepseek-harness/packages/client/ui-slots","@deepseek-ai/dsh-typert-protocol":"link:../../deepseek-harness/packages/typert/protocol","@deepseek-ai/dsh-client-ui-settings":"link:../../deepseek-harness/packages/client/ui-settings","@deepseek-ai/dsh-client-ui-primitives":"link:../../deepseek-harness/packages/client/ui-primitives","@deepseek-ai/dsh-client-ui-conversation":"link:../../deepseek-harness/packages/client/ui-conversation","@deepseek-ai/dsh-client-ui-settings-plugins":"link:../../deepseek-harness/packages/client/ui-settings-plugins"},"peerDependencies":{"@deepseek-ai/cordis":"*","@deepseek-ai/schemastery":"*","@deepseek-ai/dsh-settings":"*","@deepseek-ai/dsh-subprocess":"*","@deepseek-ai/dsh-credentials":"*","@deepseek-ai/dsh-typert-protocol":"*"},"_npmOperationalInternal":{"tmp":"tmp/dsh-voice_0.1.1_1787704081874_0.8707302279462075","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@achasoft/dsh-voice","version":"0.2.0","description":"Voice input for the DeepSeek Harness Web Client: a microphone control in the composer, a transcription capability seam, and two selectable providers","license":"MIT","type":"module","main":"lib/index.js","exports":{".":{"types":"./types/index.d.ts","default":"./lib/index.js"},"./host":{"types":"./types/host/index.d.ts","default":"./lib/host.js"},"./openai-compatible":{"default":"./lib/openai-compatible.js"},"./whisper-cpp":{"default":"./lib/whisper-cpp.js"},"./client":{"default":"./lib/client.js"},"./remote":{"default":"./lib/remote.js"},"./typert":{"default":"./lib/typert.host.js"},"./cordis.patch.yml":"./cordis.patch.yml","./package.json":"./package.json"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"platform":"web","inject":["@deepseek-ai/dsh-api-remotes","@deepseek-ai/dsh-client-locale","@deepseek-ai/dsh-client-ui-conversation","@deepseek-ai/dsh-client-ui-settings","@deepseek-ai/dsh-client-ui-settings-plugins"]}},"scripts":{"build":"tsc -p tsconfig.build.json && tsdown","prepare":"tsc -p tsconfig.build.json && tsdown","typecheck":"tsc --noEmit","test":"node scripts/check-typert.mjs && vitest run","check:typert":"node scripts/check-typert.mjs","regen:typert":"node scripts/regen-typert.mjs"},"peerDependencies":{"@deepseek-ai/cordis":"*","@deepseek-ai/dsh-agent-default-model":"*","@deepseek-ai/dsh-credentials":"*","@deepseek-ai/dsh-llm":"*","@deepseek-ai/dsh-settings":"*","@deepseek-ai/dsh-subprocess":"*","@deepseek-ai/dsh-typert-protocol":"*","@deepseek-ai/schemastery":"*"},"dependencies":{"zod":"^4.4.3"},"devDependencies":{"@deepseek-ai/cordis":"link:../../deepseek-harness/vendor/cordis","@deepseek-ai/dsh-agent-default-model":"link:../../deepseek-harness/packages/core/agent-default-model","@deepseek-ai/dsh-api-remotes":"link:../../deepseek-harness/packages/api/remotes","@deepseek-ai/dsh-client-locale":"link:../../deepseek-harness/packages/client/locale","@deepseek-ai/dsh-client-runtime":"link:../../deepseek-harness/packages/client/runtime","@deepseek-ai/dsh-client-ui-conversation":"link:../../deepseek-harness/packages/client/ui-conversation","@deepseek-ai/dsh-client-ui-primitives":"link:../../deepseek-harness/packages/client/ui-primitives","@deepseek-ai/dsh-client-ui-settings":"link:../../deepseek-harness/packages/client/ui-settings","@deepseek-ai/dsh-client-ui-settings-plugins":"link:../../deepseek-harness/packages/client/ui-settings-plugins","@deepseek-ai/dsh-client-ui-slots":"link:../../deepseek-harness/packages/client/ui-slots","@deepseek-ai/dsh-credentials":"link:../../deepseek-harness/packages/credentials/credentials","@deepseek-ai/dsh-llm":"link:../../deepseek-harness/packages/llm/llm","@deepseek-ai/dsh-settings":"link:../../deepseek-harness/packages/settings/settings","@deepseek-ai/dsh-subprocess":"link:../../deepseek-harness/packages/subprocess/subprocess","@deepseek-ai/dsh-typert-protocol":"link:../../deepseek-harness/packages/typert/protocol","@deepseek-ai/schemastery":"link:../../deepseek-harness/vendor/schemastery","@types/node":"^22.20.1","@types/react":"~18.3.1","lightningcss":"^1.30.1","react":"^18.2.0","tsdown":"^0.15.6","typescript":"^5.9.2","vitest":"^3.2.7"},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/achasoft/dsh-voice.git"},"keywords":["deepseek-harness","dsh","dsh-plugin","voice-input","speech-to-text","transcription","whisper","whisper-cpp","stt","dictation","cordis"],"types":"./types/index.d.ts","engines":{"node":"^22.19 || >=24"},"gitHead":"8afde9bd6aec296f0c24b7174abe90141813fd39","_id":"@achasoft/dsh-voice@0.2.0","bugs":{"url":"https://github.com/achasoft/dsh-voice/issues"},"homepage":"https://github.com/achasoft/dsh-voice#readme","_nodeVersion":"25.2.1","_npmVersion":"11.19.0","dist":{"integrity":"sha512-CqxCXV8pxx2AYEDMgsiw+mvFOEH2eRT2A18WACL1cC4bTkzlRukiq3HeVwKL7vLM19SOZp00AV0puJV0vpqGoQ==","shasum":"4d3a874fc45b9beeab2f2d0cd0cac9bf88368a2b","tarball":"https://registry.npmjs.org/@achasoft/dsh-voice/-/dsh-voice-0.2.0.tgz","fileCount":33,"unpackedSize":746265,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIESdaBFVizP/8MLIrl1bWVZaHy/wMh7u7wo4Gzs45uzvAiATebxpnhopmIVrIgeTMiroSB3IXNKP/UrBrldnBtqL/Q=="}]},"_npmUser":{"name":"navid.kianfar","email":"navid.kianfar@outlook.com"},"directories":{},"maintainers":[{"name":"navid.kianfar","email":"navid.kianfar@outlook.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dsh-voice_0.2.0_1787705405190_0.16817432124933962"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-25T23:51:47.643Z","modified":"2026-08-26T00:50:05.541Z","0.1.0":"2026-08-25T23:51:48.137Z","0.1.1":"2026-08-26T00:28:02.066Z","0.2.0":"2026-08-26T00:50:05.330Z"},"bugs":{"url":"https://github.com/achasoft/dsh-voice/issues"},"license":"MIT","homepage":"https://github.com/achasoft/dsh-voice#readme","keywords":["deepseek-harness","dsh","dsh-plugin","voice-input","speech-to-text","transcription","whisper","whisper-cpp","stt","dictation","cordis"],"repository":{"type":"git","url":"git+https://github.com/achasoft/dsh-voice.git"},"description":"Voice input for the DeepSeek Harness Web Client: a microphone control in the composer, a transcription capability seam, and two selectable providers","maintainers":[{"name":"navid.kianfar","email":"navid.kianfar@outlook.com"}],"readme":"# @achasoft/dsh-voice\n\nVoice input for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Web Client. A microphone button in the composer records what you say, transcribes it, and drops the text into the draft — where you read and edit it before sending.\n\nTranscription is a swappable capability. Ship it against a hosted Whisper API, a Whisper server on your own machine, or a local `whisper.cpp` binary with no network at all.\n\n**What makes a dictation usable, not just possible:** the transcript is cleaned up by the model you already configured — fillers gone, punctuation restored, spoken enumerations turned into lists. Recording stops when you stop talking. A live level meter shows it is hearing you, and an optional provisional transcript appears while you speak. Text you type mid-dictation is never overwritten.\n\n## Requirements\n\n- A dsh installation with the Web Client (`@deepseek-ai/dsh-web-app`).\n- **A secure context.** Browsers only expose the microphone over HTTPS or on `localhost`; on any other plain-HTTP origin the button will not appear.\n- One transcription provider, configured below. Without one, the composer seat renders nothing and the settings card explains why — an unconfigured install shows no dead control.\n\n## Install\n\n`dsh plugin` forwards to pnpm, so any pnpm source works:\n\n```bash\ndsh plugin --profile default add @achasoft/dsh-voice\n```\n\n<details>\n<summary>Other install sources</summary>\n\n```bash\ndsh plugin --profile default add ./achasoft-dsh-voice-0.1.0.tgz   # from `pnpm pack`\ndsh plugin --profile default add ./dsh-voice                       # a local checkout\ndsh plugin --profile default add github:achasoft/dsh-voice#<sha>   # from git\n```\n\nA git install fetches sources, not build output. This package ships a `prepare` script that builds them, but pnpm ≥10 will not run it until you allow it — add the key pnpm names to your profile's `pnpm-workspace.yaml`:\n\n```yaml\nallowBuilds:\n  '@achasoft/dsh-voice': true\n```\n\nThat is permission to execute this package's code at install time. Prefer the npm or tarball forms, which need no such allowance.\n</details>\n\nThe bundle appends itself to your profile automatically. Verify with `dsh --profile default --dump-config`, which should show a `# == @achasoft/dsh-voice` layer.\n\n## Pick a provider\n\nBoth providers claim `ctx.transcription`. **Enable exactly one** — a composition that mounts both fails loudly at load rather than silently preferring one.\n\nEnable your choice from your profile's own `cordis.patch.yml` (`$DSH_HOME/profiles/<name>/cordis.patch.yml`). A patch replaces a row's entire `config`, so restate every key.\n\n### Hosted or self-hosted HTTP — `openai-compatible`\n\nSpeaks OpenAI's `/v1/audio/transcriptions`. One request shape reaches the hosted API, Groq, `faster-whisper-server`, `whisper.cpp`'s own server, and LM Studio.\n\n```yaml\n- id: voice-openai-compatible\n  disabled: false\n  config:\n    baseUrl: https://api.openai.com/v1\n    model: whisper-1\n    apiKeyEnv: OPENAI_API_KEY\n    timeoutMs: 120000\n```\n\n| Field | Meaning |\n|---|---|\n| `baseUrl` | Endpoint prefix without `/audio/transcriptions`. A local server might be `http://127.0.0.1:8000/v1`. |\n| `model` | Transcription model, e.g. `whisper-1` or `Systran/faster-whisper-small`. |\n| `apiKeyEnv` | **Name of an environment variable**, never the key. Omit entirely for a local server needing no authorization. |\n| `timeoutMs` | Deadline for one request. |\n\n**The key is addressed, never stored.** `apiKeyEnv` is a credential *reference*: the value is resolved from the harness credential seam at the start of every call and never cached, so rotating it reaches the next request with no restart. Your settings document stays safe to sync and to render in a UI.\n\n### Fully local, no network — `whisper-cpp`\n\nRuns a [whisper.cpp](https://github.com/ggerganov/whisper.cpp) binary through the harness subprocess seam. No server, no credential; the audio never leaves the machine.\n\n```yaml\n- id: voice-whisper-cpp\n  disabled: false\n  config:\n    binaryPath: /opt/whisper.cpp/build/bin/whisper-cli\n    modelPath: /opt/whisper.cpp/models/ggml-base.en.bin\n    timeoutMs: 300000\n    maxOutputBytes: 262144\n    graceMs: 5000\n    # threads: 4\n```\n\nThe binary reads **16 kHz mono WAV and nothing else**, so this provider advertises only `audio/wav`. The browser re-encodes to that format using its own audio pipeline — no transcoder is installed anywhere.\n\n## Settings\n\nThe **Voice input** card on the plugin settings tab edits these live; the values below are the composition defaults.\n\n| Field | Default | Meaning |\n|---|---|---|\n| `interactionMode` | `toggle` | `toggle` = click to start, click to stop. `hold` = hold the button to talk. |\n| `insertMode` | `append` | Whether a transcript appends to the draft or replaces it. |\n| `maxClipSeconds` | `120` | Longest recording the browser will make. |\n| `maxClipBytes` | `26214400` | Largest clip the Host accepts (25 MiB, the hosted API's ceiling). |\n| `language` | *(unset)* | BCP-47 hint. Blank asks the provider to detect the language. |\n| `polish` | `true` | Clean the transcript with the session's own model — no second credential. |\n| `polishPrompt` | *(unset)* | Custom cleanup instruction. Blank uses the built-in one, which is deliberately conservative: it rewrites nothing it was not asked to. |\n| `silenceStopMs` | `2500` | End the recording after this much continuous silence. Blank disables it. |\n| `liveIntervalMs` | *(unset)* | Show a provisional transcript this often while recording. **Each pass re-transcribes from the beginning** — cheap against a local binary, billed per pass against a hosted endpoint, which is why it is opt-in. |\n\nThe **microphone** is chosen in the card too, but stored in the browser rather than the settings document: which input device to use is a fact about the machine, not the account.\n\n### Polish uses your model, not another key\n\nCleanup runs through `ctx.llm` with the model your deployment already selected, so it needs no extra provider and no extra credential. A failed cleanup is never a failed dictation — the raw transcript is always what lands if the model request fails.\n\n`maxClipSeconds` is the browser's gate and `maxClipBytes` is the Host's — the Host cannot measure a duration without decoding the audio, so the two limits exist for different reasons.\n\n## How it works\n\n```\ncomposer mic seat ─ getUserMedia → MediaRecorder → (re-encode to WAV if needed)\n        │                                            └─ negotiated against the provider\n        ▼  base64 over one unary RPC\nHost  VoiceService.transcribe() ─ byte cap ─→ ctx.transcription ─┬─ openai-compatible\n        │                                                        └─ whisper-cpp\n        ▼  { text } | { code, message }\ncomposer draft ← inputActions.setDraft()\n```\n\nThree decisions worth knowing:\n\n- **Nothing is persisted and nothing is model-facing.** The transcript reaches the model only if you send it, as ordinary user-message text. The plugin registers no prompt, no tool, and no session event; the audio is decoded, transcribed, and discarded inside one call.\n- **Failures cross the wire as values, not exceptions.** The RPC gateway erases a thrown error's classification, and the composer's next move depends on which class it was — \"configure a provider\" is not \"try again\".\n- **The recorder negotiates format.** `describe()` reports what the mounted provider accepts, so a WAV-only local binary and a container-flexible hosted API are the same code path.\n\n## Writing another provider\n\nImport the package root for the Service Definition and register your own implementation as `ctx.transcription`:\n\n```ts\nimport { TranscriptionEngine, TranscriptionError } from '@achasoft/dsh-voice'\n\nexport default class MyTranscription extends TranscriptionEngine {\n  async transcribe(clip, signal) { /* … */ }\n  async describe() { /* … */ }\n}\n```\n\n`transcribe` rejects only with `TranscriptionError`; its `code` is the closed union callers switch on (`not-configured` is the one a UI must treat differently). Silence returns empty `text` rather than failing — \"nothing was said\" is a successful outcome.\n\n## Development\n\nDevelopment links against a **sibling deepseek-harness checkout**, because npm's published dsh\npackages lag the versions this plugin is built against. Clone both side by side:\n\n```\nyour-workspace/\n├── deepseek-harness/\n└── dsh-voice/          ← this repo\n```\n\n```bash\npnpm install\npnpm run build       # tsc emit → tsdown bundle (~1s)\npnpm test            # Typert drift check + unit tests\npnpm run typecheck\n```\n\n`generated/` holds the Typert RPC contract. It is a build output of the harness's generator, which only runs inside a deepseek-harness checkout, so it is committed here — and `pnpm test` fails if the Host surface changed without it:\n\n```bash\npnpm run regen:typert /path/to/deepseek-harness\n```\n\nThat script stages the Host sources in the harness, builds, copies the artifacts back, and restores the checkout. It refuses to run against a dirty working tree.\n\nOn a fresh clone pnpm may refuse esbuild's postinstall (`ERR_PNPM_IGNORED_BUILDS`), which then blocks every `pnpm run`. Run `pnpm approve-builds` once and pick esbuild. It is a dev-only transitive of vitest; consumers of the published package never install it.\n\n## Known limitations\n\n- **No streaming or partial transcripts.** The RPC gateway dispatches unary methods only, so a clip is transcribed after you stop recording. Live partials need a different transport.\n- **Provisional transcripts re-transcribe from the start.** A compressed stream's later chunks are not independently decodable, so each live pass covers the whole clip. That is why `liveIntervalMs` is opt-in rather than a default.\n- **Web Client only.** The terminal CLI has no capture path; adding one means a host-side recorder and an external binary.\n- **One provider at a time.** No runtime selection among several, and no fallback from a remote provider to a local one.\n- **`describe()` checks the whisper.cpp binary, not the model.** A missing or corrupt `modelPath` surfaces on the first real call, because verifying it means loading it.\n\n## License\n\nMIT\n\n---\n\n<details>\n<summary>Note on the publint CJS warning</summary>\n\n`publint` flags `exports[\"./client\"]` as CJS inside a `\"type\": \"module\"` package. That is the required shape, not a defect: the Web Client fetches the browser half over HTTP and evaluates it as an opaque `window.__ModuleLoader__.load({ id, factory })` closure, so Node's ESM resolver never sees it. The harness's own UI plugin packages are built exactly the same way.\n</details>\n","readmeFilename":"README.md"}