dankvoice: segmented sequential speech (ported bundle vocalizer) #26

Merged
dank merged 1 commits from fix/dankvoice-segmented-speech into main 2026-10-19 01:25:04 -07:00
Owner

What

Long answers now speak as sequential, sentence-sized synthesis requests
instead of one monolithic POST /v1/audio/speech that the agent_end
handler awaited.

Two bugs died here:

  1. The 30 s handler timeout. agent_end awaited one whole-answer
    synthesis + spawn; the harness caps extension handlers at 30000 ms, so any
    slow answer produced Extension "..." error: handler timed out after 30000ms while the audio (still) played late or not at all.
  2. The stall itself. One frank-tts request for an entire answer is the
    request most likely to outlive any timeout budget.

How

Instead of hand-rolling a chunker, the utterance-segmentation state machine
is ported from the built-in speak-aloud vocalizer (minified class fHe

  • helpers in the agent bundle's dist/cli.js) — the known-working stock
    implementation:
  • strong boundaries are [.!?…] runs followed by whitespace — !!!!!
    and ... stay one boundary, never a pile of cuts;
  • boundaries after e.g./etc./Mr. or inside inline code don't count;
    3.5 and 1,5 survive (a boundary needs trailing whitespace);
  • line starts classified: tables and fenced code swallowed whole (never
    read aloud), heading/bullet markers vanish, ordered markers speak their
    number ("1, …"), quote markers strip;
  • per-segment cleanup: links → label, URLs → host, deep paths → basename,
    emphasis markup dies;
  • run-ons break at commas/word wraps before 280 chars.

Playback is strictly sequential: segment N+1 is synthesized only after
segment N's player process exits
— audio can't stack. agent_end and
/say <text> fire-and-forget the chain (it legitimately outlives the
handler timeout), and a run token retires superseded chains: /say stop,
disarming, or a newer utterance kills the current player and skips the
pending segments. DANKVOICE_MAX is now a per-answer budget over whole
leading segments; the hand-rolled stripForSpeech truncator is deleted.

No LLM "humanization" rewrite is performed — the bundle's speech.enhanced
rewrites with the tiny/smol model; dankvoice speaks mechanically cleaned
text and lets the cloned frank-tts voices carry the expression.

Verify

  • bats tests/dankvoice.bats — 17/17, incl. new scenarios: three-sentence
    utterance → exactly 3 sequential requests/plays; Model 3.5 … Version 1,5
    → exactly 2 (no decimal splits); budget 60 → only the leading segment;
    markdown fixture's fenced code and URL never reach the endpoint.
  • Full suite bats tests/ — 53 tests, 0 failures.
  • Docs: omp-docs/core/dankvoice.md § Segmentation + log entry.

cwd: /home/blank/.omp/wt/wt-20261018-204442-7c8a99d · session: 01a1525d-0a05-7615-ba60-58e9b20495d1

## What Long answers now speak as **sequential, sentence-sized synthesis requests** instead of one monolithic `POST /v1/audio/speech` that the `agent_end` handler awaited. Two bugs died here: 1. **The 30 s handler timeout.** `agent_end` awaited one whole-answer synthesis + spawn; the harness caps extension handlers at 30000 ms, so any slow answer produced `Extension "..." error: handler timed out after 30000ms` while the audio (still) played late or not at all. 2. **The stall itself.** One frank-tts request for an entire answer is the request most likely to outlive any timeout budget. ## How Instead of hand-rolling a chunker, the utterance-segmentation state machine is **ported from the built-in speak-aloud vocalizer** (minified class `fHe` + helpers in the agent bundle's `dist/cli.js`) — the known-working stock implementation: * strong boundaries are `[.!?…]` **runs** followed by whitespace — `!!!!!` and `...` stay one boundary, never a pile of cuts; * boundaries after `e.g.`/`etc.`/`Mr.` or inside inline code don't count; `3.5` and `1,5` survive (a boundary needs trailing whitespace); * line starts classified: tables and fenced code **swallowed whole** (never read aloud), heading/bullet markers vanish, ordered markers speak their number ("1, …"), quote markers strip; * per-segment cleanup: links → label, URLs → host, deep paths → basename, emphasis markup dies; * run-ons break at commas/word wraps before 280 chars. Playback is strictly sequential: segment N+1 is synthesized only **after segment N's player process exits** — audio can't stack. `agent_end` and `/say <text>` fire-and-forget the chain (it legitimately outlives the handler timeout), and a run token retires superseded chains: `/say stop`, disarming, or a newer utterance kills the current player and skips the pending segments. `DANKVOICE_MAX` is now a per-answer budget over whole leading segments; the hand-rolled `stripForSpeech` truncator is deleted. No LLM "humanization" rewrite is performed — the bundle's `speech.enhanced` rewrites with the tiny/smol model; dankvoice speaks mechanically cleaned text and lets the cloned frank-tts voices carry the expression. ## Verify * `bats tests/dankvoice.bats` — 17/17, incl. new scenarios: three-sentence utterance → exactly 3 sequential requests/plays; `Model 3.5 … Version 1,5` → exactly 2 (no decimal splits); budget 60 → only the leading segment; markdown fixture's fenced code and URL never reach the endpoint. * Full suite `bats tests/` — 53 tests, 0 failures. * Docs: `omp-docs/core/dankvoice.md` § Segmentation + log entry. --- cwd: /home/blank/.omp/wt/wt-20261018-204442-7c8a99d · session: 01a1525d-0a05-7615-ba60-58e9b20495d1
dank added 1 commit 2026-10-19 01:21:59 -07:00
agent_end awaited one whole-answer synthesis; long answers hit the
harness 30s extension-handler timeout (handler timed out after 30000ms)
and the single frank-tts request was the stall. Port the built-in
speak-aloud vocalizer's utterance segmenter (fHe in dist/cli.js) so
segmentation matches the known-working stock path: [.!?…]-run
boundaries, abbreviation/decimal/inline-code guards, fenced code and
tables swallowed, markup cleaned. Play segment N to player exit before
synthesizing N+1 (audio never stacks); fire-and-forget the chain so
utterances outlive the handler timeout; a run token retires superseded
chains. DANKVOICE_MAX becomes a per-answer budget of whole segments;
stripForSpeech is gone. Refs #23 follow-up.
dank merged commit 0075ca3bbf into main 2026-10-19 01:25:04 -07:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dank/command-center#26