AtropinolTT/dsh-guide-dog
Guide Dog for DSH — MiniMax multimodal plugin: image/video/music/speech generation & vision tools, voice mode, voice input. Real-time call mode implemented: streaming voice conversation, unified floating UI, adaptive prompts, zh/en i18n. Accessibility features planned for testing & rollout. Featured in the dsh-recommend plugin registry.
Listed
4
Vision
Bundle verified
Preview
What it does
MiniMax-powered multimodal plugin: real-time voice call mode (streaming conversation, floating dock UI), voice mode and mic voice input, plus image/video/music/speech generation and vision inspection tools.
Best for
- DSH users who need MiniMax-backed real-time voice conversations with VAD or push-to-talk controls.
- Workflows requiring image, video, music, or speech generation alongside vision inspection.
- Models without native vision input that need local Web UI previews and visual review tools.
Not ideal for
- Text-only workflows that do not need voice, media generation, or visual inspection.
- Deployments that cannot provide the MiniMax/mmx route required for its multimodal functions.
- Users seeking the documented accessibility mode now; those accessibility features are planned rather than shipped.
README
Guide Dog for DSH, powered by MiniMax
| English | 简体中文 |
A dynamic Cordis plugin that gives DeepSeek Harness multimodal superpowers through the mmx CLI (MiniMax):
-
Eyes for DeepSeek — MiniMax VLM (
guide_dog_vision/guide_dog_inspect) describes images, so a model with no native vision input (e.g. DeepSeek) can still review frontend designs, figures, screenshots, and generated images. -
Hands for generation — images (
image-01), video (MiniMax-H3/ Hailuo), speech (MiniMax TTS), music (music-3.0), text (MiniMax-M3), and web search. -
Web UI preview & playback — every generated file is served same-origin at
/guide-dog/media/<file>and rendered inline in the conversation tool cards (<img>,<audio controls>,<video controls>), plus a Guide Dog settings page with auth status and a speak tester. -
Skill integration —
guide_dog_speakreuses your existingaudio-conversationandspeech-mmxskill pipelines (text transform, CJK auto-detect, per-language voices, host playback), and falls back to rawmmx speech synthesizewhen the skill scripts are absent. -
Automatic invocation — a mounted system-prompt section
(
guide-dog-vision, order 110) tells the agent to auto-invoke the inspection tools for any job needing visual checks, especially when the active model cannot see images. - Call mode (Phase 2, shipped) — hands-free, real-time voice conversation in the web UI: VAD / push-to-talk turn-taking, streaming sentence-level TTS with barge-in, consensus-first protection for write commands, progress announcements, and a unified floating dual-pill UI at the composer with zh/en i18n (details in “Phase 2 — call mode” below).
-
Accessibility mode (Phase 3, planned) — an
a11yconfig block is reserved (auto-narration, vision-cloud, summary-first); accessibility features are next on the roadmap and will be tested and rolled out after the call-mode shakeout.
Featured
Guide Dog is featured in dsh-recommend, a community-curated plugin directory for DSH. It passed the project’s certification review and carries the gold certified badge at the top of this README; the score badge updates automatically on every registry sync.
Files
| File | Purpose |
|---|---|
plugin-host.js |
Host half — source of record (tools, RPC, media route, prompt section, voice mode) |
plugin-client.js |
Client half — source of record (tool cards, settings page, voice cluster) |
bundle/ |
Static web-profile bundle generated from the two halves (deploy/convert_bundle.py) |
deploy/ |
convert_bundle.py (source → bundle) and publish.py (bundle → ~/.dsh/dsh-guide-dog + web profile registration) |
package.json |
Root manifest declaring the dsh.bundle (makes the repo installable via dsh plugin add) |
README.md |
This file |
README.zh-CN.md |
Simplified-Chinese version of this README |
Install
Install straight from this repository with the standard DSH plugin command —
the root package.json declares the dsh.bundle manifest, so the profile’s
pnpm layer resolves the package itself (no npm publish required):
dsh plugin --profile web add github:AtropinolTT/dsh-guide-dog
Deploy (static web-profile bundle — current)
- Edit the source of record:
plugin-host.js/plugin-client.js. -
python3 deploy/convert_bundle.py— regeneratebundle/lib/. -
python3 deploy/publish.py— copy to~/.dsh/dsh-guide-dog, idempotently register in~/.dsh/profiles/web(dependency link +bundlesentry + node_modules symlink), remove the superseded autoload bundle. -
Restart DSH (
dsh web) — bundles are parsed at startup.
No dynamic plugin, no approval cards, no per-session instances: after a DSH restart the tools and voice UI come back with the profile itself. Full details and pitfalls in the “Restart recovery” section below.
plugin-source.js is a legacy dynamic-era artifact (both halves concatenated);
kept for reference, not used by the current deploy flow.
Tools
| Tool | Args | Returns |
|---|---|---|
guide_dog_speak |
text*, voice (auto), speed, language, playOnHost
|
{ok, url, voice, bytes} mp3 |
guide_dog_image |
prompt*, aspectRatio, n, width, height, seed, promptOptimizer, watermark
|
{ok, urls[], files[]} |
guide_dog_video |
prompt*, model (MiniMax-H3 default), image, subjectImage, duration, ratio
|
{ok, url, taskId} mp4 (polls until done) |
guide_dog_vision |
image*, prompt
|
{ok, answer} VLM description |
guide_dog_inspect |
image*, focus (general/frontend/figure/screenshot/ocr), prompt
|
{ok, answer, focus} structured review |
guide_dog_voices |
language |
{ok, voices[]} |
guide_dog_music |
prompt*, lyrics, instrumental, vocals, genre, mood, model
|
{ok, url} mp3 |
guide_dog_text |
message*, system, model, maxTokens, temperature
|
{ok, text} |
guide_dog_search |
q* |
{ok, results[]} (max 10) |
* required
Auto-invoke contract (visual checks)
While the plugin runs, a system-prompt section instructs the agent:
- For visual checks (frontend design review, figure/plot/chart generation,
screenshots, UI mockups, generated-image QA) it MUST call
guide_dog_inspect(structured) orguide_dog_vision(general) on the produced image file before finalizing — never claim to have seen an image it has not inspected. - Generated media is served to the user at
/guide-dog/media/<file>; the agent must include the returnedurlfields so the user can preview. - Speech requests route to
guide_dog_speak.
Example visual-check flow on DeepSeek:
1. (agent) create figure/screenshot file, e.g. chart.png
2. (agent) guide_dog_inspect { image: "chart.png", focus: "figure" }
→ structured review of axes/labels/readability/encoding
3. (agent) iterate the figure, re-inspect, then finalize with the url
4. (user) previews chart.png in the web UI card
Media store & serving
- Media lives in
~/.dsh/guide-dog/.guide-dog/media— the global store underGLOBAL_ROOT = ~/.dsh/guide-dog(one instance for the whole web profile since 2026-08-16; no longer the per-workspace sandbox root — see “Restart recovery” below). - Served by a same-origin prefix route
/guide-dog/mediawith:- extension allowlist (
jpg/jpeg/png/gif/webp/mp3/wav/m4a/ogg/mp4/webm), - basename-only lookup + traversal guard,
-
Accept-Ranges: byteswith real byte-range responses (video seeking), - 404/405/413/416 as appropriate.
- extension allowlist (
-
.index.jsonkeeps metadata (prompt,voice,ts,kind) for the settings gallery (guide-dog/list-mediaRPC). A corrupt index is rebuilt from the directory. - Files persist across plugin restarts; stopping/removing the plugin only removes the runtime registrations, never the files.
Skill integration (audio-conversation / speech-mmx)
guide_dog_speak honors the exact pipeline of your two skills:
-
~/.agents/skills/audio-conversation/scripts/transform.py— markdown/code/URL stripping (falls back to a built-in JS transform when absent). - CJK auto-detect → per-language voice defaults
(
English_Trustworthy_Man/Chinese (Mandarin)_Gentle_Youth), same as the skill env contract. Explicitvoiceoverrides;languageboosts accents. -
~/.agents/skills/speech-mmx/scripts/mmx_tts.py speak --input … --out …(falls back tommx speech synthesize). - Browser playback via the returned mp3 URL. With
playOnHost: truethe host speakers play it too — one file at a time (previous playback is terminated first), mirroring the skill’s latest-only rule.
Env vars of the skills that still apply when set in the dsh process
environment: AUDIO_CONVERSATION_VOICE(_EN/_ZH), AUDIO_CONVERSATION_SPEED,
AUDIO_CONVERSATION_DIR, AUDIO_CONVERSATION_NO_PLAY, AUDIO_CONVERSATION_KEEP_FILES,
TTS_GEN. Turn files keep the turn-NNN.mp3 naming convention.
Settings page
Settings → Guide Dog (id guide-dog):
-
Auth —
mmx auth statusresult with the key masked (sk-c…xxxx); never logged in full. - Voice mode — global default on/off radios (per-session override lives on the small speaker button at the input’s bottom-left).
- Voice input — STT engine select (whisper / sherpa / minimax), recognition language (auto/zh/en), input device select (defaults to the system default), and auto-send-after-recognition checkbox.
- STT — faster-whisper availability + version/python, and the whisper model select (base/small).
-
Speak tester — text + voice selector (from
guide-dog/voices), plays the mp3 in the browser.
Phase 1 — voice mode & voice input
Feature list
-
Voice mode (host event-driven) — a host
session/eventlistener watchesassistant/messageevents, extracts the reply text (event.data.contentblocks withtype === 'text'), checks whether voice mode is effective for that session (session override else global default), and enqueues the TTS result ({url, key}) or error into a per-sessionvoiceQueue. The client polls the queue every second and plays it with a module-levelAudioobject, or shows a bottom-right toast + beep for 6s. -
Voice cluster —
conversation.input.leftentryguide-dog-voice(order 30) at the input box’s bottom-left, themed with DSH tokens (--dsw-alias-*), inheriting the app font:- small speaker icon — click toggles the per-session voice-mode override
(
guide-dog/set-configwithvoiceMode.sessions); hover tooltip shows “Voice mode: on/off · Global default: on/off”. - language dropdown — recognition language detection (auto/zh/en).
- mic icon — record → transcribe → insert (feather-style SVG; recording state pulses red with a second counter).
- small speaker icon — click toggles the per-session voice-mode override
(
-
Session-scoped playback — playback runs on a module-level
Audioobject, so switching sessions never replays or interrupts it: the current clip plays to the end unless a new playback task (a fresh queue entry from any session) overrides it. -
Mic voice input — the mic in the cluster: MediaRecorder with 1s
timeslices, live second counter, maxSeconds auto-stop, language from the
dropdown, and transcribe via
guide-dog/transcribe. Recognized text is inserted into the input box withinputActions.setDraft(text)(auto-send viainputActions.submit()when configured). Error states:mic_denied,no_device,empty_speech,stt_failed,stt_timeout,engine_unavailable,insert_failed(never silent). -
Recorder page — sandboxed clients that cannot record in-page get a
🎙 Open recorder pagelink to the standalone/guide-dog/recorderpage (GET serves a self-contained HTML recorder; POST/guide-dog/transcribe-uploadaccepts rawaudio/webm, 20 MB cap, and runs the sametranscribeImpl). -
Settings controls — the Phase 1 config blocks above, backed by
guide-dog/get-config/guide-dog/set-config/guide-dog/status.
config.json schema
Lives at ~/.dsh/guide-dog/.guide-dog/config.json (auto-created from
defaults; all keys optional, deep-merged over the defaults):
{
"voiceMode": { "default": false, "sessions": { "<sessionId>": true } },
"voiceInput": {
"autoSend": false,
"engine": "whisper",
"language": "auto",
"maxSeconds": 60,
"whisper": { "python": "python3", "model": "small" }
},
"tts": {
"voiceEn": "English_expressive_narrator",
"voiceZh": "Chinese (Mandarin)_Gentle_Youth",
"speed": 0.95,
"format": "mp3"
}
}
-
voiceMode.sessionsmaps a session id to a boolean override;defaultis the fallback. The speaker button at the input’s bottom-left toggles the current session’s override. -
voiceInput.engine:whisper(only engine implemented;sherpa/minimaxare reserved — selecting them returnsengine_unavailable). -
voiceInput.maxSecondsforces the mic recording to stop.
STT engine (faster-whisper)
The whisper engine shells out to a bundled Python script
(.guide-dog/scripts/whisper_transcribe.py) using faster-whisper:
pip install faster-whisper # needs Python 3.8+; installs torch cpu wheels
python3 -c "import faster_whisper; print(faster_whisper.__version__)"
The host probes availability at startup and writes the result to
.guide-dog/status.json (whisperAvailable, whisperVersion, whisperPython),
shown in the Settings → STT row. Model choices: base (fast) / small
(accurate); first run downloads the model weights.
Verification
node --check plugin-host.js && node --check plugin-client.js # syntax
curl -s http://127.0.0.1:3080/guide-dog/recorder | head -5 # recorder page serves HTML
curl -s -X POST http://127.0.0.1:3080/guide-dog/api/guide-dog/status \
-H 'content-type: application/json' -d '{}' | head -5 # status RPC (compat layer)
cat ~/.dsh/guide-dog/.guide-dog/status.json # whisper probe result
Manual checks (after deploy): click the speaker button (voice mode on, turns green) → send a message → the assistant reply is spoken automatically; switch sessions mid-playback → the clip continues to the end and is NOT replayed; use the mic button → recognized text appears in the input box; Settings → Guide Dog shows the Voice mode / Voice input / STT blocks.
Phase 2 — call mode
Feature list
-
WebSocket-free dual channel — uplink is a whole-clip POST
/guide-dog/call-transcribe(webm/opus, ≤20MB, reuses the Phase 1transcribeImpland the local whisper pipeline) →{ok, text, language, durationMs}; downlink isGET /guide-dog/tts-streamover a chunked HTTP stream (the host spawnsmmx speech synthesize --stream --format pcm --sample-rate 24000per sentence and pipes stdout incrementally intores.write; the client reads the stream withfetch().body.getReader()→ PCM→WAV → seamless Web Audio scheduling). No new WebSocket protocol surface on the transport layer; browser and CLI reuse the same pipeline. -
Automatic VAD + push-to-talk (PTT) — default VAD (
call.mode='vad'): MediaRecorder (audio/webm;codecs=opus, 250ms timeslice, continuous recording) + a parallel AnalyserNode energy detector (RMS ≥vad.threshold; silence forvad.silenceMsends an utterance;vad.minSpeechMsminimum speech;vad.maxSegmentSecondsper-segment cap) — speak-pause-speak automatically becomes two turns; the panel can switch topttpush-to-talk (hold the mic to talk, release to send; VAD parameters do not participate in endpointing, only in interruption monitoring). -
Consensus-first (core interaction paradigm) — active only when
call/a11y is on; typing mode keeps the Phase 1 behavior: a prompt soft
constraint (
guide_dog_call_consensussystemPrompt variable, conversational wording: understand intent first, ask when unclear, explain before writing/modifying and wait for the user’s go-ahead) plus a mechanical hard guarantee (tools/pre-executewaterfall interception: write/edit and destructive-bash heuristics rm/mv/cp/truncate/dd/overwriting-redirect/git push etc. → unconfirmed returns{kind:'deny', reason: 'needs_voice_confirmation'}and the model asks by voice; user confirmation keywords hit → released for this turn; before every execution the host TTS-broadcasts a one-sentence summary built from the tool args (not through the model), then opens aconsensus.summaryWindowMsinterruption window; speech inside the window aborts the execution — the tool has physically not started). Interceptor failure → deny and announce “consensus check failed” (better to block wrongly than to allow wrongly, spec §6.8). -
Progress announcements (minimalist principle, RC10) — only useful
information is announced:
agent/status(running → “processing”),tools/result(tool name → phrase: write/edit → “modifying files”, web_search → “searching the web”, guide_dog_image/video/music/speak → “generating media”, bash only for destructive commands (the same DESTRUCTIVE_BASH_RE as consensus) → “running a command”; read/grep/glob/ skill/non-destructive bash/unknown tools stay silent),agent/error(“processing failed: -
Streaming TTS — reply text is split per sentence (
stream.sentenceSplitcharset。!?.!?\n;stream.maxSentenceCharsforce-truncates over-long sentences) and synthesized sentence by sentence; each sentence gets a fresh one-time token viaguide-dog/tts-token(single-use, 5-minute validity, bound to sessionId); pre-synthesis between sentences (while the current sentence plays, the client requests the next sentence’s stream ahead of time and appends decoded frames seamlessly on the playback-time chain — the next sentence continues before the previous one finishes; long replies are read in full with ≤400ms gaps). Measured ~600ms to first byte for short Chinese sentences, satisfying the “first audio <1.5s” criterion. Only the turn’s final message is played (RC13): intermediate assistant messages (with tool-call blocks) are not enqueued — playing near-identical text per step was the root cause of “the same content repeated”; intermediate steps are covered by progress announcements. A terminal tool turn (the last message still has tool calls) is covered by the turn/end fallback that plays the buffered text — never silent. -
Barge-in — VAD detects user speech during playback (≥
vad.interruptMinMs300ms to avoid false triggers) → the browser immediately stops playback and clears the unplayed buffer → the stop is a 10ms fade-out (RC13) —src.stop()hard cuts click at sentence boundaries. The first transcript segment after the interruption goes straight to the current turn via theinterruptRPC (agent.steer, RC11) instead of queueing as a new turn → abort the currenttts-streamfetch → the new speech naturally becomes the next turn (Pipecat InterruptionFrame semantics). -
Voice commands — call transcriptions that hit the command table (pause,
resume, repeat/say-again, slower/faster, look-at-screen [Phase 3 stub])
execute locally and are not submitted to the conversation (stop/continue
are consensus confirmation words, not in the command table — they pass
through to the agent unchanged);
guide-dog/call-commandRPC provides host-side commands such asclear-queue. -
Dual-channel mutual exclusion (RC13) — text already spoken on the host
speakers via
guide_dog_speak(playOnHost=true)is not replayed through the voice-mode/call queue channels (consumed on use, the same text blocked once) — eliminates the “host + browser double sound”. - Fault tolerance — a stream interruption auto-reconnects once (at most one retry per (sid,text) within 5s, no retry on 429; a fresh token per sentence; failure toast “playback interrupted”); STT failure does not submit + beep + toast; TTS failure still lands the text + failure beep + panel error state (never silent); consensus-interceptor failure denies conservatively and announces the reason. Session ownership for call transcription/interruption/polling is captured once when the call starts (RC13) — multi-session switching no longer cross-talks.
RC14 fixes (2026-08-17): announcement content selection + queue tail-truncation + progress dedupe + double-play pinpointing
-
Announcement sanitization (
sanitizeSpeechText, F1) — before enqueueing, reply text goes through a markdown/URL/emoji strip:[title](url)keeps the title, drops the URL; bare URLs (https?://,www.) are removed entirely; leading list/quote markers (-/+/*/>) and leading ordered-list markers (1.1、1)) are stripped; markdown markers such as**bold**and backticks are stripped; emoji ranges (U+1F000-U+1FAFFetc.) are stripped. Call mode reads only human language — no URLs/**/-/📢 metacharacters, so URL fragments like “thepaper/newsD/weather.com” are no longer read out in pieces. -
Smart sentence splitting (
splitSentences, F2) — in the default separators'。!?.!?\n',.follows a smart rule: split only when.is followed by whitespace + an uppercase letter/digit/CJK ('Hello. Next'→ 2 sentences;'8.17 的上海'stays 1; dots inside URLs are never split). The Chinese separator set gains;;so…;no longer cuts one Chinese reply into two halves. -
Queue cap 40 with tail truncation (
VOICE_QUEUE_MAX, F3) — raised 10→40, drop from the tail to keep content: on overflowwhile (q.length > VOICE_QUEUE_MAX) q.pop()(first-in content wins; the oldsplice(0, …)head-removal strategy cut the main content first while keeping URL fragments). announce/hb progress still usepop()(unshifted to the queue head, progress first). -
30s progress-phrase dedupe window (F4) —
announce’sprogressDedupecooldown extended 4s→30s: web_search results ~4.3s apart no longer announce “searching the web” three times. TheprogressDedupefunction body is untouched;repro-progress.jssemantics preserved. -
Dual-channel mutual exclusion by sanitized text (F5) —
wasHostSpoken/markHostSpokenuniformly usesanitizeSpeechText-processed text as the key: all threewasHostSpokencall sites (downlink, turn-end flush, voice-mode) match on the sanitized key;speakImplregisters both the raw and the sanitized key after a successfulplayOnHost(double key) so any downstream channel dedupes correctly. Known edge: when transform.py rewrites the text the two keys may differ slightly (acceptable edge). -
Diagnostic instrumentation (F6, one-shot retest to pinpoint “reading
twice”) — zero behavior change, logs only:
- host (
[gd-host], visible in the DSH terminal):enqueue from=downlink|turnend|voice-mode|consensus|announce|heartbeat n=... qlen=...,shift key=... remain=...,skip host-spoken sid=... text=...,QUEUE-DUP text=.... - client (
[gd], browser DevTools):playStreamEntry ... times=...(accumulates perentry.key || entry.texton each play),PLAY-SUMMARY key=N | ...(when the queue empties, summarizes all current counts then clears). - Retest calibration (basis for the RC15 direction):
QUEUE-DUP→ host double-enqueue;PLAY-SUMMARY key=2→ client double-play; neither but still twice → tts-stream double audio write;enqueue from=same source twice with the same text → event replay.
- host (
RC15 fixes (2026-08-17): persistent player + gesture unlock + failed-entry requeue + event-replay dedupe
-
Persistent voice player (
playVoiceEntry, F1) — voice-mode playback changed from “new Audio() + temporary URL each time” to fetch + Blob + single-element reuse: the whole clip is fetched once into aBlob, bound viaURL.createObjectURLto a single<audio>element; later entries only replacesrcand the playback callbacks — no more repeated Audio-object creation/destruction, which eliminates theERR_CONTENT_LENGTH_MISMATCHretry storm (each Audio rebuild replays the pre-mismatch portion; with long audio this looked like “repeated replay + stutter”). -
Gesture unlock + blocked-pending retry (F2) — under the browser
autoplay policy the first play may be blocked (
play()rejected): enter a “pending playback” state, bind the first user gesture (click/keydown/touchstart, capture phase, persistent listener) and continue automatically; blocked entries are no longer dropped — they replay after the gesture.stopCurrentnow correctly releasesbusyand requeues (prevents a busy deadlock from swallowing entries). -
Failed-entry requeue RPC (
voice-requeue, F3) — on playback failure (decode/network/blocked) the client calls the hostvoice-requeueRPC to re-enqueue the entry (requeueEntrypure function: new text inserts, duplicate text skips, tail pop truncates), max 3 retries per entry (attemptsmap) — no more lost content. -
Event-replay 10s text-window dedupe (
replayDup, F4) — host enqueue for the “call downlink + voice mode” channels gains a 10s last-text dedupe window (lastStreamText/lastVoiceTextmaps): the same text enqueued again within 10s is skipped ([gd-host] skip replay text=/[gd-host] skip voice-dup text=instrumentation). Root cause: event replay on voice mode + call downlink enqueued the same text twice — the male-voice reply repeated “7 times” because this window was missing. -
url-entry play counts (F5) —
PLAY-SUMMARYsummary logs cover voice-mode entries (playCountskeyed byentry.key || entry.url, summarized and cleared when the queue empties) — url-entry play counts are trackable; retest pinpointing no longer relies on guessing. -
Build marker — the client build tag was bumped to
rc15-20260817at the time (plugin-client.jssource andbundle/lib/client.jsin sync; visible in the DevTools console after a hard refresh; since superseded — the current tag isrc20-20260817).
config.json schema (Phase 2: call / a11y)
New keys added on top of the Phase 1 config
(~/.dsh/guide-dog/.guide-dog/config.json); all optional, deep-merged over
defaults (spec §4 copy):
{
"call": {
"mode": "vad",
"vad": {
"method": "energy",
"threshold": 0.02,
"silenceMs": 700,
"minSpeechMs": 300,
"maxSegmentSeconds": 60,
"interruptMinMs": 300
},
"stream": {
"format": "pcm",
"sampleRate": 24000,
"sentenceSplit": "。!?.!?\n",
"maxSentenceChars": 200
},
"voice": "English_expressive_narrator",
"speed": 1.0,
"progress": true,
"consensus": { "enabled": true, "summaryWindowMs": 3000 }
},
"a11y": {
"enabled": false,
"autoNarrate": true,
"visionCloud": true,
"summaryFirst": true
}
}
-
call.mode:vad(default, automatic endpointing)ptt(push-to-talk). -
call.vad.method:energy(Phase 2 v1, RMS energy threshold; raisethresholdin noisy environments) → upgrade slotsilero(web-vad browser WASM) /sherpa(VAD+ASR integrated). -
call.stream.format/sampleRate:mmx speech synthesize --streamargs (s16le mono PCM; 24000 is the explicit override — mmx’s own default is 32000). -
call.consensus:enabledtoggles consensus-first (effective only in call/a11y);summaryWindowMsis the window that waits for the user to interrupt after the summary announcement. -
a11y: Phase 3 accessibility-mode config (this stage onlyenabledparticipates in the call streaming/consensus decisions;autoNarrate/visionCloud/summaryFirstare reserved for Phase 3).
Routes (Phase 2)
| Method & Path | Purpose |
|---|---|
POST /guide-dog/call-transcribe |
Uplink: whole-clip audio (the client sends a raw audio/webm body with an x-session-id header; the host base64s the whole body and hands it to whisper), hard cap ≤20MB; the host reuses the Phase 1 transcribeImpl → {ok, text, language, durationMs}
|
GET /guide-dog/tts-stream?token=…&sid=…&text=<sentence> |
Downlink: chunked PCM audio stream (content-type: audio/pcm, cache-control: no-store); requires a one-time token issued by guide-dog/tts-token — missing/wrong token → 403, in-flight stream for that session → 429 |
RPC-style endpoints (tts-token / call-active / call-command) go through
the same JSON POST compatibility layer; their physical URL is
/guide-dog/api/guide-dog/<name> (double prefix, same as the Phase 1
guide-dog/status example) — see the three new rows in the RPC surface table
below.
Verification
node --check bundle/lib/index.js && node --check bundle/lib/client.js # bundle syntax ×2
curl -s -X POST http://127.0.0.1:3080/guide-dog/call-transcribe \
-H 'content-type: application/json' -d '{}' | head -5 # uplink route reachable (empty audio → error JSON)
curl -s -o /dev/null -w '%{http_code}\n' \
'http://127.0.0.1:3080/guide-dog/tts-stream?token=bad&sid=x&text=hi' # invalid token → 403
Manual acceptance checklist (full criteria in
specs/2026-08-14-guide-dog-v2-design.md §6.9; verify item by item after
deploying and restarting DSH):
- VAD: speak-pause-speak becomes two separate turns; silence detection does
not cut wrongly (
thresholdadjustable). - Turn loop: voice → transcription → submit → agent execution (incl. tool calls) → reply read aloud; an end-to-end “use voice to have the agent generate an image / search” completes.
- Barge-in: speaking during playback stops it; the next turn works.
- Progress announcements: at least one stage announcement while the agent runs a tool.
- Stream safety: non-allowlisted Origin and missing/wrong tokens rejected; recovery after a dropped-stream reconnect.
- Full streaming: long replies read in full; “repeat / pause / slower” commands work; first-audio latency ≤1.5s, playback gaps ≤400ms (measured).
- Consensus-first: voice “change X in README to Y” → not executed
immediately → voice confirmation → after confirmation a short summary is
heard before every write; speaking during the summary aborts that
execution and the user’s speech becomes a new turn; an unconfirmed
write/edit is blocked (check the
tools/pre-executeinterception path). - Ambiguous intent (e.g. “change that file” without context) → the agent asks the key question by voice instead of guessing; the user asking “why change it?” → the agent explains by voice.
- PTT: hold to talk / release to send; the mode switch takes effect in VAD mode.
RPC surface (Client → Host)
| Method | Args | Returns |
|---|---|---|
guide-dog/speak |
{text, voice?, speed?, language?, playOnHost?} |
{ok, url, file, voice, bytes} |
guide-dog/list-media |
{limit?} |
[{name, kind, prompt, voice, ts, bytes, url}] |
guide-dog/auth-status |
— | {ok, method, source, keyMasked} |
guide-dog/voices |
{language?} |
{ok, voices[]} |
guide-dog/get-config |
— |
{ok, config} (merged defaults) |
guide-dog/set-config |
{patch} |
{ok} / {ok:false, error}
|
guide-dog/status |
— |
{ok, status} (whisper probe + probeAt) |
guide-dog/transcribe |
{audioB64, mime, sessionId?, language?} |
{ok, text, language, durationMs} / {ok:false, error}
|
guide-dog/beep |
— |
{ok, dataUri} (WAV beep data URI) |
guide-dog/voice-queue |
{sessionId} |
{ok, entry} — pops one entry (play/error) or null
|
guide-dog/tts-token |
{sessionId} |
{ok, token} — one-time stream token (5 min, single-use, bound to session) |
guide-dog/call-active |
{sessionId, kind ('session'\|'speaking'), active} |
{ok} — session persistence vs instantaneous speaking flag (C4) |
guide-dog/call-command |
{sessionId, cmd} |
{ok} — host-side call commands (clear-queue …) |
Security notes
- Media dir inside the workspace root → no sandbox widening required.
- The route serves only plugin-owned media with allowlisted extensions.
- The MiniMax API key stays in mmx’s own config (
~/.mmx/config.json); the plugin never reads or forwards it. - Host playback uses the raw
subprocessservice (players must outlive the sandbox’s--die-with-parentbwrap profile); each new playback terminates the previous one.
Troubleshooting
-
mmxnot found / auth missing — tool returns{ok:false, error}; the settings page shows the auth problem. Fix:npm install -g mmx-cliandmmx auth login --api-key sk-…(orexport MINIMAX_API_KEY=…). -
Sandbox denial — the tool error reports
denied: true; keep media inside the workspace (the plugin already does). -
MiniMax-H3returns “TokenPlan or Credit does not yet support the MiniMax-H3 model family” — the account’s MiniMax plan does not include the H3 model family. Usemodel: "MiniMax-Hailuo-2.3"(legacy V1) or upgrade the plan. The plugin surfaces the API error verbatim (the message may appear in Chinese), so this is visible in the tool result. -
Video never finishes — the poll loop honors the call’s abort signal and
times out after 15 minutes; re-run with a shorter
durationor differentmodel. -
Cards show generic JSON — the client half did not load; check that the
bundle client route
/plugins/dsh-guide-dog/client.jsreturns 200 after a DSH restart, and refresh the page. - Stop / update — everything (tools, route, prompt section, cards, settings entry) is disposed automatically; media files remain.
mmx output-shape notes (verified against mmx 1.0.19)
-
--quietchanges per-command JSON shapes:speech voicesprints a flat array of voice-id strings,text chatprints only the reply content (so the plugin runs text chat without--quiet), whileauth status/search querykeep their objects. -
video generate --asyncalways prints{taskId}(raw stdout write). - H3 (V2) task results carry
content.url; the plugin downloads it withcurl. Legacy V1 tasks returnfile_id, downloaded viammx video download --file-id. - File-writing commands (
image generate --out-dir,music generate --out,speech synthesize --out,video download --out) may print nothing parseable; the plugin treats exit 0 as success and verifies the file viafs.stat.
Restart recovery (static web-profile bundle)
Since 2026-08-16 Guide Dog ships as a static bundle mounted in the web
profile — one global host half + one client half, exactly like the published
dsh-better-sidebar. No dynamic plugin, no per-session gdog-* instances,
no approval cards: after a DSH restart the tools and the voice UI come back
with the profile itself.
The source of record stays the two dynamic-plugin halves
(plugin-host.js / plugin-client.js); deploy/convert_bundle.py
regenerates bundle/lib/ from them:
-
host half (
bundle/lib/index.js, ESMname/apply): a tiny compatibility layer replaces the dynamic sandbox’sharness— tool definitions are registered via the globaltoolsregistry (visible to every session), and the formerharness.handleRPCs (guide-dog/*) become JSON POST routes under/guide-dog/api/. The per-workspace sandbox root is replaced by the global store~/.dsh/guide-dog/(config, media, scripts). -
client half (
bundle/lib/client.js): awindow.__ModuleLoader__.load({id, factory})CJS factory like the published bundles;require('react')from the platform seed, self-managed<style>tag instead of the sandboxstyles, andhost.callbecomes same-originfetchagainst the JSON routes.
Deploy once after any plugin change:
-
python3 deploy/convert_bundle.py— regeneratebundle/lib/. -
python3 deploy/publish.py— copies the bundle to~/.dsh/dsh-guide-dog(outside workspaces), idempotently registers it in the web profile (~/.dsh/profiles/web: dependency link +bundlesentry +node_modulessymlink) and removes the supersededdsh-guide-dog-autoloadbundle (which otherwise keeps deploying per-session dynamic instances). - Restart DSH (
dsh web) — bundles are parsed at startup, so a restart is required after any change.
Legacy history: the earlier auto-deployer (autoload/) — a host bundle that
watched agent/created and define+runed a fresh gdog-* dynamic plugin
per session — is retained in the repo and still published to
~/.dsh/guide-dog-deploy / ~/.dsh/guide-dog-autoload for rollback, but
nothing consumes it once removed from the profile.
Profile pitfall (observed 2026-08-15): dsh web is an alias for
--profile web — the GUI runs the web profile. Registering a bundle
only in another profile (e.g. cc-tui) silently does nothing for the GUI;
deploy/publish.py always targets ~/.dsh/profiles/web.
Service-scope pitfall (observed 2026-08-16 — root cause #3): the
dynamicCordisRunner and agents services are registered on agent-scoped
contexts, not on the global/profile context a bundle’s apply(ctx) runs in.
ctx.get('dynamicCordisRunner') there returns undefined, so an early
if (!runner || !agents) return in apply bailed out before the
agent/created listener was even registered — the bundle loaded fine
(verified via dsh web --dump-default-config) yet never deployed. The fix
resolves both services through the event payload’s agent.ctx
(Agent exposes readonly ctx: Context; probe-verified that both services
are visible there), with a global-ctx fallback for hosts that register them
globally. Debugging aid: a temporary dynamic probe plugin (inject:
['dynamicCordisRunner', 'agents']) sees both services in its (agent-scoped)
apply ctx — that asymmetry is the signature of this pitfall.
The bundle shape mirrors the published dsh-better-sidebar precedent:
dsh.bundle.patch → cordis.patch.yml with a single insert row, named
exports (export const name + export function apply(ctx)), no default
export, host-only (no dsh.client block needed).
Phase 2 backlog (deferred from the V4-Pro final review)
-
M9 — mic
onstopclosure holds a staleinputActionswhen switching sessions mid-recording; re-check recorder ownership before transcribing. - M10 — the media route buffers the entire file in memory to satisfy range requests; stream only the requested byte range (matters once Phase 2 streaming TTS/playback lands).
-
M11 —
setVoiceOverriderebuilds the wholevoiceMode.sessionsmap from possibly-stale config, so concurrent session toggles can clobber each other; move to per-key merge (host-side patch) or refresh cfg before write.
Frequently Asked QuestionsFAQ
Use the verified command dsh plugin --profile default add github:AtropinolTT/dsh-guide-dog in a DSH-enabled shell. The command resolves the public package metadata and keeps the plugin attached to the catalog identity shown on this page.
Compatibility follows the bundle and profile status shown above. If a profile is not detected, keep the plugin disabled there and check the repository documentation before enabling it in production.
The GitHub link and activity metadata are the source of truth for releases and maintenance. Revisit this page after a new release to confirm the catalog has observed the latest version.