Skip to content

Releases

dsh-tts 0.4.16-local.5 - speak inline code instead of deleting it

Choose a tag to compare

@YevheniiMiachyn YevheniiMiachyn released this 23 Sep 23:49

dsh-tts 0.4.16-local.5 — speak inline code instead of deleting it

Replies were spoken with holes in them. stripForSpeech replaced every inline
`span` with a single space, so identifiers vanished from the audio while the
words around them survived. Heard live, from a real reply:

Everything the restart needed to pick up is live: now reads True (it was false
before), the installed plugin is , and the Akeno voice re-registered.

Three things were missing from that sentence — a setting name, a version, and the words
that connected them — and the result still sounded like a sentence, which is why it read
as a glitch rather than a bug.

The fix

Inline spans are unwrapped (`x` → x) instead of deleted. Deleting a fenced
block is right — nobody wants a listing read aloud — but an inline span is a single
identifier inside prose, and reading it literally is what a person does when reading the
sentence aloud. Fenced blocks still become "code block, N lines", and skipCode: false
still leaves all markup untouched.

One rule was added to protectAbbreviations: a dot preceded by a letter or digit and
followed by a digit. The existing rule only covered digits-dot-digits, so it stopped at
0.4.16 and left the final dot of 0.4.16-local.4 — the sentence cutter then broke the
identifier in two and spoke "0.4.16-local. 4". A sentence boundary never has a digit
immediately after the dot without a space, so this cannot swallow one.

Both changes sit on the shared cleanText path, so live sentence flush and the durable
settlement cannot disagree about what a sentence says.

Verified artifact

field value
commit 3cd846a (master)
tag v0.4.16-local.5
artifact goodandready-dsh-tts-0.4.16-local.5.tgz
artifact SHA256 F74C5DB607AD699BA9F22C3B1C9FADC807FC62D587FD19B1B7FF23D7B592A793
artifact size 114 309 bytes, 37 files
lib/text.js SHA256 9CC43E5348AF91A215964FF8C17705680EF65F99E178C5AE138AC68FE52D758E
tests 93 pass / 0 fail
parity all production patches intact

lib/client.js is byte-identical to local.4 in this release — the browser half is
untouched, so the recorded client hash stays valid across both versions and only the host
half changes. A restart is still required: lib/text.js is loaded at boot.

Regression coverage

test/spoken-code.test.mjs (8 tests) asserts the invariant that matters — no hole in the
sentence
(!/is\s+,/) — rather than only the presence of the missing text, and it uses
the real sentence that was heard garbled. It also pins the fenced-block notice line count,
skipCode: false, several spans in one sentence, spans at sentence edges, and that an
unwrapped version survives sentence splitting as one piece.

Negative-controlled against git show HEAD:lib/text.js: the pre-patch module speaks the
garbled sentence above and fails three of four checks; the patched module passes all four.

dsh-tts 0.4.16-local.4 - barge-in silences the rest of the turn

Choose a tag to compare

@YevheniiMiachyn YevheniiMiachyn released this 23 Sep 23:42

dsh-tts 0.4.16-local.4 — barge-in silences the rest of the turn

Speaking over the assistant by pressing the microphone stopped it sometimes:
reliably for a reply that had already finished streaming, and not for a longer one
still being generated.

The defect

Aborting in-flight synthesis and dropping the queued slots stops speech now. It
does not stop speech resuming — the agent loop keeps emitting frames for the same
turn after the interruption, and every sentence completed afterwards was flushed,
claimed and synthesized as usual. The reply picked up again a moment later,
mid-thought.

Three separate holes fed the same symptom:

hole effect
abortAll aborted controllers and dropped the queue but left the ownership cursor alive the next completed sentence of the same turn was spoken
nothing ever dispatched dsh:tts:start / dsh:tts:stop dsh-voice listened for both and nothing sent them, so its isTtsSpeaking was permanently false and its own barge-in path was unreachable
the host barge-in POST was gated on player.liveEnabled with live sentence streaming off, a piece still synthesizing settled into the queue and was collected by the next poll

The fix

lib/live.js — per-turn silence. abortAll(sid) now also records the turn in a
mutedTurns map, taken from every live cursor and from the turn currently
streaming, so a barge-in that lands before the first flushed sentence — or between
two steps of a multi-step turn — still counts. speak() returns early for a muted
turn, which is safe precisely because drainPieces claims raw before returning
pieces: the silenced text stays owned by the step, so the settlement reconcile can
never read it as unsaid and speak it from the durable path afterwards. The claim
advances; only the audio is suppressed.

onSettledMessage() checks the mute first. The cursor can hold zero pieces when
the barge-in beat the first sentence, and the existing pieces === 0 fallback would
have handed the entire message back to be spoken — the loudest possible version of
the talk-over. Mutes are deliberately not pruned by turn order, because a settlement
can arrive after the next turn has already started streaming; they are bounded by a
cap instead, and the muted turn consumes its own entry when it settles.

lib/client-src/20-player.js — one hard-stop path. announceSpeech() dispatches
dsh:tts:start / dsh:tts:stop on edges only, so isTtsSpeaking is now truthful.
bargeInNow() holds the single implementation and no longer gates the host POST on
live mode, because the durable path has a pending queue too. dsh:tts:cancel is now
listened for as well as dispatched, and is authoritative — the voice plugin has
already decided that the user talking wins.

Deployment requirement

isTtsSpeaking being truthful activates dsh-voice's turn-taking gate. With the plugin
defaults (bargeIn: false, gatedTurnTaking: true) pressing the microphone during
speech now refuses with Assistant speaking (gated mode) instead of barging in. The
profile therefore sets bargeIn: true, pairing with the bargeIn: true this plugin
already reads from settings.yaml.

Verified artifact

field value
commit 0d4ca887f4fb7680b8f426eb36dd99532e6c2d4b (master)
tag v0.4.16-local.4 → target 0d4ca887…
artifact goodandready-dsh-tts-0.4.16-local.4.tgz
artifact SHA256 EFD257D4D39F1CFB879AE4B89143CBC6FBDB46B124D4FD02EBFD074A2B4F7FB9
artifact size 113 880 bytes, 37 files
lib/live.js SHA256 EF3D5145A7B741AD2E9BC6657CFD1DD439C169A57566297F37B90E18AB829897
lib/client.js SHA256 8827A7399EDF227B798153E65BF68A191E8D5045A73BE930C69F12660C6344D3
tests 85 pass / 0 fail
parity all production patches intact (scripts/verify-parity.mjs)

The installed plugin's lib/live.js and lib/client.js hashes must equal the fork's;
the -local.4 tarball was installed with pnpm and the whole profile was
snapshot-diffed before and after, with only @goodandready/dsh-tts changing.

Regression coverage

test/bargein.test.mjs (12 tests) drives the engine with the exact window this defect
lived in — text arriving after the barge-in — and asserts that the piece already
audible stays audible, that later text is claimed but never spoken in both the
pieces > 0 and pieces === 0 shapes, that later steps and later frames of the
interrupted turn stay silent, that a duplicate settlement cannot resurrect it, that a
following turn speaks again, and that the mute map stays capped.

test/bargein-wiring.test.mjs (4 tests) reads the built lib/client.js for the
browser half so a rebase cannot quietly drop it. Every assertion was
negative-controlled against the pre-patch bundle: 6 of 7 checks fail there, 7 of 7
pass here.

Known boundary

A barge-in that arrives before the interrupted turn has emitted its first stream frame
— a sub-second window between sending a message and the model starting to generate —
has nothing to silence, and that reply speaks normally. Everything from the first
frame onward is covered.

dsh-tts-live v0.4.16-live.1 — Live Sentence TTS

Choose a tag to compare

@YevheniiMiachyn YevheniiMiachyn released this 23 Sep 05:31

Pre-release. This build is production-usable and has been validated on a real
local DSH voice stack, but it will keep being exercised through normal daily use
before it is declared fully stable.

This is a fork of GooDAnDReaDY/dsh-tts (MIT),
based on upstream 0.4.16 (npm tarball shasum 0516c0c9613efe5ca666738179ba34570802b92e).

It does not replace or renumber upstream releases: the package version stays
0.4.16-local.3 and v0.4.16-live.1 is this fork's own tag series. This is not
upstream 0.4.17.

Highlights

  • Live sentence-level speech — driven by the process-local agent/assistant-stream
    event, the only genuinely token-level hook in DeepSeek Harness.
  • First-sentence early flush — the first sentence can be synthesized before
    assistant/message settles, so audio starts while the model is still writing.
  • Ordered synthesis — one piece per sentence, published in order with stable ids, so
    the client cannot reorder or double-play audio.
  • Tool / multi-step ownership — explicit session/turn/step ownership on every queued
    and settled piece; reasoning text and tool-call JSON are never spoken.
  • Barge-in cancellation — host-side POST /dsh-tts/bargein drops pending audio and
    aborts in-flight synthesis; a late re-settlement cannot resurrect cancelled audio.
  • Durable-message reconciliation — when the settled message arrives, already spoken
    text is matched and only the unsaid tail is published, so nothing is said twice.

Fixes

  • Duplicate-tail publication — ownership now claims an exact raw text range
    (claimedEnd) instead of a re-rendered "pieces joined by one space" string, so a
    single synthesis can never be published twice.
  • WAV / custom TTS support — the custom and openai providers request
    response_format: 'wav' and report audio/wav, matching local OpenAI-compatible
    TTS servers.
  • Abort propagation — a bare abort identifier meant the cancellation listener was
    never attached, so caller cancellation never reached in-flight synthesis.
  • Browser SpeechSynthesis fallback suppressed — the OS-voice fallback used to speak
    provider failures out loud in the wrong voice, masking real errors.
  • Provider timeout raised from 10 s to 120 s for slow local cold-start synthesis.

Configuration

Live mode is opt-in and off by default. All keys live under dsh-tts: in
settings.yaml:

dsh-tts:
  liveSentenceStreaming: true   # master switch
  liveMinCharsFirst: 12         # minimum characters before the first flush
  liveMinChars: 48              # minimum characters for later flushes
  liveMaxChars: 0               # hard cap per piece; 0 -> falls back to sentenceChars
  livePollMs: 150               # pending-queue poll interval while speaking
  liveFlushOnToolCall: true     # flush the remainder before a tool call
  streamingEnabled: false       # independent of live mode; kept false in the validated setup

Validation

  • 69/69 plugin tests (npm test), 208/208 benchmark-harness selftests
  • cache-clean CONTROL/LIVE validation — live speech begins before message settlement
    on 5/5 measured turns (median −1220 ms; the control arm never did), and the
    first-flushable-sentence → first-synthesis queue wait improved from 1175 ms to 15 ms
  • targeted 30-turn multi-step validation — zero duplicate speech, zero out-of-order
    pieces, zero unowned pieces, full sentence coverage
  • successful production promotion, then re-verified in production: every published
    piece owned by the expected turn/step, all WAV, no duplicates, nothing left reserved,
    and a real barge-in that left nothing to resurrect

Known limitations

  • Shared-GPU contention between the LLM and the local TTS engine. Live streaming
    improves scheduling — speech starts earlier — but it does not make the model or the
    synthesizer faster.
  • It does not solve model TTFT. It removes the wait after the first sentence exists.
  • Inline-code speech is cosmetic. With skipCode: true inline code spans are deleted
    on both the live and the settled path, so a code-heavy sentence can be spoken with gaps.
    Pre-existing upstream behaviour; live mode simply speaks more sentences, so it is heard
    more often.
  • Performance varies with the local model, TTS engine and GPU configuration. The
    figures above come from one specific local setup and are not a general benchmark.
  • Continued daily-use testing — hence the pre-release flag.

Install

The runtime in this tag is byte-identical to the validated commit
9c4dd72f97d50dd9c561dde5c122bda983cc30c3; package version 0.4.16-local.3.

git clone https://github.com/YevheniiMiachyn/dsh-tts-live.git
npm pack            # inside the clone, to produce a frozen tarball
dsh plugin --profile web add file:/path/to/dsh-tts-0.4.16-local.3.tgz

Prefer a file: tarball for production installs. link:/junction installs are fine for
scratch profiles but are not what this release was validated against.

Release artifact

The validated production tarball is intentionally not attached. Its SHA256 does match
the recorded value (464429BC2E0A0ACC8C8AF91B8C47E8D9D39E10CFF51CBF0C320E07947E1600A7)
and it contains no credentials, but its bundled scripts/verify-parity.mjs still carries
two lines containing a local machine path, so it fails this fork's own publication rule.
Use the automatically generated source archive instead: the runtime (lib/) in it is
byte-identical, and npm pack at this tag reproduces a tarball — the only differences
being the genericized default path in that one dev script and the README.

License and attribution

MIT. Upstream copyright Copyright (c) 2026 GooDAnDReaDY is preserved verbatim in
LICENSE. Not affiliated with or endorsed by the upstream
author. See PATCHES.md for the per-patch ledger and
LIVE-SENTENCE.md for the design notes and defect log.