# Changelog All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.3.0] - 2026-09-09 **The install no longer fails, and the plugin no longer arrives switched off.** Measured on a clean machine against today's harness: `dsh plugin --profile web add dsh-kitt-voice` ended in an error, and because it ended in an error the harness never reached the step that activates the plugin. It was installed and inert. Somebody following the first line of the README saw a failure and then nothing at all. ### Fixed - **The cause was one dependency.** `msedge-tts` ships a `preinstall` of `npx only-allow pnpm` — a latch to force *its own* contributors onto pnpm. A harness profile installs with pnpm, and pnpm refuses to run anybody's install scripts without permission, so the command fails with `ERR_PNPM_IGNORED_BUILDS`. The read-aloud protocol is now implemented here instead: a hundred lines, no install hooks, and the package went from six dependencies to two. - **The false compatibility warning is gone.** The harness provides Cordis, the web app and React at runtime; they are never installed as packages. Declaring them as required peers made every install print that the plugin was missing them, which was not true. ### Also fixed on the way - **Text is now escaped before it goes into the SSML document.** An `&` or a `<` in a reply broke the request outright, and an agent's reply is exactly where those appear. The previous library documented this and left it to the caller. - **The reading speed now actually reaches the neural voices.** It was being sent as a bare number; it goes as the documented percentage. Measured: at double speed the audio is half the size. ### The trade, stated plainly The protocol is ours now, so its drift is ours too. Microsoft rotates the client version it expects, and the day it does, neural voices stop answering. It degrades gracefully — the reply is read by Piper or the system voice and the status line says so — and the version string to update is named in `lib/neural.js`. ## [1.2.0] - 2026-09-09 **A Piper voice no longer says "jotason" where the text says JSON.** Piper does not read text, it reads phonemes, and those are produced by espeak-ng with the voice's own language fixed — so a Spanish voice reads every English word as if it were Spanish. Measured against the phonemiser itself: `JSON` becomes /xˈotasˈon/, `GitHub` /xˈit ˈub/, `streaming` /stɾeˈamiŋ/. ### Added - **A pronunciation list, applied only to Piper.** English terms are rewritten the way somebody would spell them in Spanish before the text reaches the synthesiser: `JSON=yeisón`, `GitHub=guitjab`, `software=sófguer`. The result is not perfect English; it is a Spanish speaker saying an English term, which is what a person does. Forty-five entries out of the box, covering development and sim-racing vocabulary, editable as `piperPronunciation` in Settings. - **It applies to Piper and to nothing else.** The neural voices and the system voice say those words correctly on their own, so rewriting for them would break what already worked. The rewrite happens in the one place where it is certain that Piper will speak — the Piper branch of the synthesis route — and a test pins it there. - Whole words only, and with accents in mind: `cache` must not fire inside `caché`, which the usual word boundary would not have caught, because it does not consider an accented letter a letter at all. - 14 tests over the rewrite, and an end-to-end measurement: eight repetitions of `npm` produce audio the same length as eight of `ene pe eme`, and twice the length of a short control word. ### Known limit This covers what recurs, not what appears once. An unusual proper name, or something the agent invents on the spot, will fall outside the list. Training a voice cannot fix this either: the fault happens before the model. ## [1.1.0] - 2026-09-09 **It now tells you when the microphone is the problem.** The commonest complaint about any voice system is not that it hears you wrong: it is that it hears nothing and says «could not transcribe», which nobody can act on. The cause is almost always the same three things — the wrong device selected, a muted microphone, or a level so high that the waveform is clipped. ### Added - **A verdict on the recording level, said before the transcription comes back.** While a push-to-talk turn records, the peak and the voiced time are measured from the analyser that already draws the level meter — no second microphone, no extra loop. If the level explains a poor result, the status line says which of the three it was and what to do about it. The recording is always sent regardless: nothing anybody said is ever thrown away, and if the reply comes back understood, the notice disappears on its own. - The thresholds are the ones measured in-house against raw recordings: below 10 % of full scale there is nothing to recognise, and from 99 % the waveform is against the ceiling. Automatic gain control is requested here, which raises quiet input, so the «too quiet» notice is deliberately conservative. - **It refuses to judge what it could not measure.** The level is read in the drawing loop, and a tab behind a fullscreen game does not draw — which is this plugin's normal case. With too few frames there is no measurement and therefore no verdict. A false «I cannot hear you» while somebody is driving is worse than saying nothing. - 10 tests over that decision, including the no-measurement case. ## [1.0.1] - 2026-09-08 - The K turns green while the agent thinks as well as while it listens; blue now means only «KITT mode is on and nothing is happening». - Removed `lib/chunk.js`, a server-side reply splitter nothing had called since the client got its own. ## [1.0.0] - 2026-09-05 **First stable release. The conversation no longer gets stuck, talks to itself, or changes voice mid-reply — and the keys go to one page only.** Every one of these was found by using it; none raised an error. ### Fixed - **A reply that never ended.** Chrome's built-in voice sometimes stops mid-utterance without firing `end` or `error`. The read-aloud loop awaited that promise forever: the bar stayed red, «be quiet» did nothing, and the conversation could not be switched off. Every utterance now has a watchdog sized to its text, non-local voices get a keep-alive, and no voice is ever handed more than 220 characters at once. - **It transcribed its own voice.** The detector keeps hearing the reply's echo until its own 1.4 s of silence has passed; the ear was reopened 500 ms after the audio ended, so the echo's «speech end» arrived on an open ear and was sent to the agent as if the person had said it. The detector is now paused — which empties it — before the ear reopens. - **It cut itself off.** The echo floor was measured from the moment the first sentence was *requested*, not from when it started to *sound*. With the neural voice that is a second of silence, so the floor came out near zero and the reply's own echo cleared it. Calibration now starts when audio actually plays. - **«Be quiet» did not.** In a conversation, F11 silenced the current sentence and the loop carried on with the next; a piece already being fetched played anyway. It now ends the reading and the wait, and a stop requested mid-fetch is honoured. - **Ending the conversation waited for the paragraph to finish.** The blue button closed the detector but let the current audio play out. It stops the voice first. - **Two conversations from a double press.** The «already running» check came after two awaits. It is now a synchronous flag. - **A conversation nobody could stop.** Its handle lived in a React ref of the microphone button; when the harness re-rendered the tool row the handle was lost and the detector kept running. It now lives outside the component. - **The voice changed mid-reply.** Three causes, all fixed: the engine was re-decided per sentence, so one transient failure switched that one sentence to the system voice; Chrome's system voice list is empty until `voiceschanged` fires, so the first sentence got the default — often English — voice; and the server truncated any request over 2000 characters without saying so, while Piper timed out on long ones and fell back. Engine, voice and rate are now decided once per reply, a failed engine stays failed for the rest of that reply (and the status line says so), voices are collected ahead of time, and nothing longer than 220 characters is ever requested. - **Closing the companion window did not close it.** With `overlayAuto` on, the window was re-launched on *every* active state, so during a conversation it came back half a second after the × was pressed, sometimes racing its own single-instance lock. It now opens only when the voice goes from idle to in use; closed by hand, it stays closed until the next time the voice is started, or until the gear asks for it. - **Deaf after an interruption.** The detector's `pause()` and `start()` are asynchronous and were called back to back when a voice cut in: the pause's "stop the processor" landed after the start's "resume it", leaving the microphone open and the processor stopped — the bar blue, nothing heard, and no later start able to fix it. They are now serialised and awaited, and a start that fails is reported. The conversation also opens the microphone chosen in the menu (and re-opens the same one after every pause) instead of the system default. - **Two pages, one set of keys.** With the harness open in the installed app and in a tab, both received every order — two microphones, two sends — and the companion window flickered between their states. Each page now identifies itself; the harness sends each key to the page that is using the voice (or, when none is, the one in front), and only accepts state from it. - Smaller: a noise while the agent was thinking cleared «Thinking» from the screen; an empty transcription left «Listening» stuck on; the dictation key pressed during a conversation opened a second microphone (it now says the conversation is on instead); the turn detector's download had no time limit; a notice such as «read with the built-in voice» was stored but never shown; the companion window's menu swallowed every key, so its lists could not be moved with the keyboard; a dictation cut short by the tool row re-rendering left the microphone open. ### Changed - **The K is the KITT-mode control.** The companion bar had a red microphone and a blue speech bubble side by side — two ways of talking that were easy to confuse — and the brand mark opened the website, which was intrusive. Now the brand's K, on the left, switches the hands-free conversation on and off; while the mode is on it takes the colour of the state — blue waiting, green listening, red speaking — and its halo pulses gently while it thinks or speaks; the bubble button is gone; the website opens only from the «kittcat.com» text beside it, and only at rest. The bar reads, left to right: K, state, then red microphone, speaker, mute, gear, close. The harness tool row mirrors it, as it always has. ### Added - `lib/paginas.js`: which page the keys belong to when more than one is open, with 10 tests. - 9 tests over the splitter that feeds the voices. - Eight new entries in the traps document, one per fault above. ## [0.2.1] - 2026-09-02 **Fixes a 0.2.0 that does not load at all.** If you installed 0.2.0, the harness stops with `Failed to load plugins` and the whole plugin is gone — no microphone, no window, no toolbar. Upgrade. - `lib/client.js` is served to the browser as a single standalone file, with no bundler, and the harness loads it through its own module loader. 0.2.0 added an `import` of a sibling module to it, and the bundle then loads without registering itself. The interruption logic is back inside the client; `lib/corte.js` remains as the copy that can be tested outside a browser. - Three tests now guard that pairing: the two copies must agree on their four constants, they must decide identically on the cases that matter, and `client.js` must contain no `import` line at all. Every test passed on the broken build — they import the module directly in Node, where imports are fine. That is the actual lesson, and it is written down in the traps document: if the tests do not load the code the way production loads it, they are not testing that. ## [0.2.0] - 2026-09-02 **You can interrupt it now.** Talking to the agent while it was thinking, or while it was reading a reply aloud, did nothing at all: the turn detector was paused the moment a message was sent and did not come back until the whole reply had been read. On a long answer that is half a minute of deafness. Found by using it. - **Echo cancellation is now actually requested.** It never was: the microphone was opened bare and the browser left to decide — and it decides differently depending on how the device is asked for. Noise suppression and automatic gain too, both for the microphone and for the detector, which opens its own. - **While the agent is thinking, the ear stays open.** Nothing is being spoken then, so there is no echo to defend against; it was closed for no reason. It now closes when the first word is actually spoken. - **You can talk over the reply and it stops.** The threshold is not guessed, it is measured: for the first half second of every reply the microphone listens, and what it hears *is* the echo, because nobody has spoken yet. To count as a voice, sound must clear that floor by 3× and hold for a third of a second. It adapts itself to headphones or speakers, and re-measures on every reply. A door slam or a cough is too short to trigger it. - **One turn at a time, and the newest wins.** With the ear open, speaking could start a second turn on top of the first — two transcriptions, two drafts and two sends. Each turn now takes a number; the superseded one may no longer write the draft, send, or read aloud. Eleven new tests over the interruption decision, including the two cases that would ruin the feature: the echo must not trigger it, and neither must a bang. ## [0.1.2] - 2026-09-01 Documentation only; no change to what the plugin does. - **The install warning about `msedge-tts` is not a warning under pnpm — it is an error, and it fails the whole command.** Since a harness profile uses pnpm, this is the case that actually matters, and 0.1.1 only described the npm one. pnpm leaves `msedge-tts: set this to true or false` undecided in the profile's `pnpm-workspace.yaml`, and until somebody decides, *every* install in that profile fails — other people's plugins included. The READMEs now say so, with the fix, in all three languages. - The Chinese version of that note shipped double-encoded in 0.1.1 and was unreadable. Repaired. - Four tests over the published documents: none double-encoded, every relative link resolves, the three languages of the traps document link to each other, and the pnpm fix appears in all three READMEs rather than only in English. ## [0.1.1] - 2026-09-01 Packaging only; no change to what the plugin does. - The package description said the name twice — a leftover from the rename. - The link to the traps document is now absolute. That file is not shipped inside the package, so a relative link went nowhere when the README was read on npm rather than on GitHub. ## [0.1.0] - 2026-09-01 First public release. Voice for the DeepSeek Harness web UI: talk to the agent and hear the reply out loud, with a companion window that floats above other applications and global hotkeys that work while you are somewhere else. ### Added - **Two ways of speaking**, and they are different: one press, one whole turn (push to talk, the text lands in the message box, you send it) and the open hands-free conversation that keeps listening until you stop it. - **Hands-free knows a voice from a noise**: the turn ends on measured silence from Silero v5, never on a loudness meter or a timer. - **Read-aloud as it arrives**: a reply is spoken sentence by sentence while the agent is still writing it. Code blocks are named, not spelled out. - **Three speaking engines**: 104 neural voices from Microsoft's read-aloud service (45 Spanish, 47 English, 12 Chinese), local Piper voices, and the built-in system voice as the always-available fallback. Nothing is ever downloaded on the user's behalf. - **Reading speed from half to double**, applied to all three engines, set from the companion window's menu. - **Two ring tones**: a rising note when the conversation opens, a falling one when it closes, so you can tell from the sound that it is listening. - **The companion window**: a floating bar that is the controls (speak, conversation, menu, close), carries the state in its own colour, drags without stretching, and leaves no ghost on screen. Global hotkeys F8-F11, assignable in the menu. - **Interface in three languages**: Spanish, English and Simplified Chinese, for the page and for the window (the window's language is chosen independently from the language you dictate in). - **The transcription key never reaches the browser**: it is resolved from the harness credential store on every call, never cached, never logged. - **Every HTTP route checks its caller**: loopback socket plus an Origin that must name this same server on this same port. The window's page makes no network calls at all; its main process relays against a closed list of paths. - **The turn detector's files are served by the harness itself** against a fixed six-name list; a file name can never become a path. - 50 tests covering the parts where a mistake is expensive: the guard, voice name escaping, the reply splitter, the log fallback, the window's request allowlist, the read-aloud sentence splitter and the speed steps.