@flowingspring/dsh-voco 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QUAN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,31 +1,56 @@
1
1
  # `@flowingspring/dsh-voco`
2
-
3
- English | [中文](README.zh.md)
4
-
5
- Browser PCM uses lightweight local silence detection and is uploaded as WAV to SiliconFlow after 1.5 seconds of silence.
6
-
7
- The `dsh-voco` patch-layer bundle attaches provider transport directly to the current source Session and restores recent conversation from durable `voice/utterance-end` events. A lightweight frontend model answers ordinary conversation directly and delegates only tool-requiring work. Delegations from one source Session continuously reuse one ordinary task Agent Session. `voice/agent-binding-state` persists the Voice conversation, background Agent, workspace, last task, last-used time, and status; restart recovery prefers that complete record while remaining compatible with `voice/task-session-bound`. Full results and progress remain in the task UI, while an independent model rewrites the final result and user-actionable questions or warnings against the original request. Progress updates stay in the folded task trace and do not create extra Voice-window messages or TTS requests. The background Agent cannot directly specify spoken text. Root-owned audio continues across navigation, and the browser history index exposes saved Voice Sessions in the sidebar.
8
-
9
- ## Model Experience
10
-
11
- ### Voice profile composition
12
-
13
- #### What the model sees
14
-
15
- Voice-initiated work reaches the source Session's fixed background task Agent only as an accepted `realtime_delegation` envelope and exact-id updates. That task Agent alone receives the scoped `send_voice_message` backend tool for structured `progress | result | warning | error | question` events; the bridge creates or resumes the exact target directly, so no project-listing tool is added. The local provider owns speech input and output while the bridge exposes only task orchestration tools.
16
-
17
- #### Token effect
18
-
19
- The lightweight frontend route consumes one model call per new utterance. Delegated work additionally consumes task-model tokens for execution, backend reporting, and auxiliary event rewrites; local VAD, ASR, and TTS add no per-minute voice API charge.
20
-
21
- #### KV Cache effect
22
-
23
- Only accepted commands extend the continuously reused task Agent history. Durable binding state restores that exact Agent Session after restart, while recent Voice Session utterances restore the local conversation.
24
-
25
- ## Known Limitations and Deferred Work
26
-
27
- - The shipped provider is local CPU speech; the service seam keeps model details out of the assistant consumer.
28
- - Raw audio remains process-local; a fresh provider connection restores bounded context from durable completed utterance text.
29
- - An active task is persisted as `interrupted` when the service stops. Recovery reports its last spoken progress without automatically replaying the task or any side-effecting command.
30
- - The filtered Voice history index is browser-local; clearing site data does not delete the underlying Sessions.
31
- - The browser client surface targets the dsh Web UI: it is emitted by the copied dsh client tsdown preset and loads through the dsh web runtime's `window.__ModuleLoader__` contract. The server-side packages are transport-agnostic, but the microphone/playback UI is not a standalone browser plugin.
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@flowingspring/dsh-voco.svg)](https://www.npmjs.com/package/@flowingspring/dsh-voco)
4
+ [![license](https://img.shields.io/npm/l/@flowingspring/dsh-voco.svg)](https://github.com/lgquan/dsh-voco/blob/master/LICENSE)
5
+
6
+ English | [中文](README.zh.md)
7
+
8
+ Persistent, interruptible voice conversations for the DSH Web UI. Speak naturally, get an immediate conversational response, and delegate workspace tasks to a durable background Agent Session without losing context.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ dsh plugin --profile web add @flowingspring/dsh-voco
14
+ dsh web
15
+ ```
16
+
17
+ Install the DSH CLI first if needed:
18
+
19
+ ```sh
20
+ npm install -g @deepseek-ai/dsh
21
+ ```
22
+
23
+ ## Configure speech recognition
24
+
25
+ Set a [SiliconFlow](https://siliconflow.cn/) API key in the DSH environment:
26
+
27
+ ```dotenv
28
+ SILICONFLOW_API_KEY=your-key
29
+ ```
30
+
31
+ The plugin uses `XingChenAGI/XingChenASR-V3.2-Ultra` for cloud speech recognition and Edge TTS with `zh-CN-XiaoxiaoNeural` for speech output. Browser audio is uploaded only after lightweight local speech and silence detection.
32
+
33
+ ## Highlights
34
+
35
+ - Keeps one background Agent Session bound to each Voice Session, including after DSH restarts.
36
+ - Answers ordinary conversation directly and delegates only work that needs tools.
37
+ - Speaks a short contextual acknowledgement before starting delegated Agent work.
38
+ - Keeps full task reports in the task UI while speaking a concise, purpose-written result.
39
+ - Supports interruption, browser navigation, reconnects, and restored conversation history.
40
+ - Ships the server and browser surfaces as one public npm package.
41
+
42
+ ## Configuration
43
+
44
+ The default utterance boundary is 1.5 seconds of continuous silence. Advanced settings such as `silenceDurationMs`, `speechThreshold`, `minSpeechDurationMs`, and `maxUtteranceMs` are available in the plugin profile configuration.
45
+
46
+ ## Requirements and limitations
47
+
48
+ - The microphone and playback surface targets the DSH Web UI and is not a framework-independent browser plugin.
49
+ - Speech recognition requires network access and a SiliconFlow API key.
50
+ - Voice responses currently use the Chinese Xiaoxiao Edge TTS voice by default.
51
+
52
+ Source, development instructions, and issue tracking are available in the [GitHub repository](https://github.com/lgquan/dsh-voco).
53
+
54
+ ## License
55
+
56
+ [MIT](LICENSE)
package/README.zh.md CHANGED
@@ -1,31 +1,56 @@
1
1
  # `@flowingspring/dsh-voco`
2
-
3
- [English](README.md) | 中文
4
-
5
- 浏览器 PCM 只在本地做轻量静音检测,连续静音三秒后以 WAV 上传硅基流动完成识别。
6
-
7
- `dsh-voco` 的 `voice` profile patch-layer bundle。它把 provider transport 直接挂到当前来源 Session,并从持久化的 `voice/utterance-end` 事件恢复最近对话。轻量前台模型直接回答普通对话,只委派需要工具的工作。同一来源 Session 的委派持续复用并持久绑定一个普通 Task Agent Session;`voice/agent-binding-state` 保存语音会话、后台 Agent、工作区、最后任务、最近使用时间和状态,服务重启后优先从该记录恢复,并兼容旧的 `voice/task-session-bound`。完整结果和进度保留在任务界面;独立模型结合用户原话重写最终结果以及需要用户处理的问题或警告。进度只显示在折叠任务详情中,不会额外创建语音消息或 TTS 请求。后台 Agent 不能直接指定播报文本。root 持有的音频在跳转期间持续运行,浏览器历史索引从侧栏展示已保存的 Voice Session。
8
-
9
- ## 模型体验
10
-
11
- ### 语音 profile 组合
12
-
13
- #### 模型看到什么
14
-
15
- 语音发起的工作只以已接受的 `realtime_delegation` 信封与准确 id 更新到达来源 Session 所绑定的固定后台 Task Agent。只有该 Task Agent 收到作用域内的 `send_voice_message` 后台工具,用于发送结构化的 `progress | result | warning | error | question` 事件;桥接层直接创建或恢复准确目标,因此不增加 project 列举工具。本地 provider 负责语音输入输出,桥接层只暴露任务编排工具。
16
-
17
- #### Token 影响
18
-
19
- 每个新的前台话语会消耗一次轻量模型路由调用。被委派的工作还会消耗任务执行、后台回报和辅助事件改写的模型 token;本地 VAD、ASR、TTS 不产生按分钟的语音 API 费用。
20
-
21
- #### KV Cache 影响
22
-
23
- 只有已接受的 command 扩展持续复用的 Task Agent 历史。持久绑定状态会在重启后恢复同一个 Agent Session;Voice Session 的最近话语用于恢复本地语音对话。
24
-
25
- ## 已知限制与后续工作
26
-
27
- - 随附的 provider 是本地 CPU 语音;service seam 保证 assistant consumer 不依赖模型细节。
28
- - 原始音频仍限于当前进程;新的 provider 连接会从已完成的持久 utterance 文本恢复有界上下文。
29
- - 服务停止时,活跃任务会持久标记为 `interrupted`。恢复时只告知最后一次已播报进度,不会自动重放任务或任何有副作用的命令。
30
- - 筛选后的语音历史索引只属于当前浏览器;清除站点数据不会删除底层 Session。
31
- - 浏览器客户端界面面向 dsh Web UI:它由复制而来的 dsh client tsdown 预设构建,并通过 dsh web 运行时的 `window.__ModuleLoader__` 契约加载。服务端包与传输无关,但麦克风/播放 UI 不是独立浏览器插件。
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@flowingspring/dsh-voco.svg)](https://www.npmjs.com/package/@flowingspring/dsh-voco)
4
+ [![许可证](https://img.shields.io/npm/l/@flowingspring/dsh-voco.svg)](https://github.com/lgquan/dsh-voco/blob/master/LICENSE)
5
+
6
+ [English](README.md) | 中文
7
+
8
+ 面向 DSH Web UI 的可恢复、可打断语音对话插件。你可以自然说出需求、立即获得口语回复,并把需要工具的工作委派给持续复用的后台 Agent Session,而不会丢失任务上下文。
9
+
10
+ ## 安装
11
+
12
+ ```sh
13
+ dsh plugin --profile web add @flowingspring/dsh-voco
14
+ dsh web
15
+ ```
16
+
17
+ 如果尚未安装 DSH 命令行:
18
+
19
+ ```sh
20
+ npm install -g @deepseek-ai/dsh
21
+ ```
22
+
23
+ ## 配置语音识别
24
+
25
+ DSH 的运行环境中设置[硅基流动](https://siliconflow.cn/) API Key:
26
+
27
+ ```dotenv
28
+ SILICONFLOW_API_KEY=你的密钥
29
+ ```
30
+
31
+ 插件使用 `XingChenAGI/XingChenASR-V3.2-Ultra` 完成云端语音识别,并通过 Edge TTS `zh-CN-XiaoxiaoNeural` 音色输出语音。浏览器只在本地做轻量起音和静音检测,确认一句话结束后才上传音频。
32
+
33
+ ## 主要功能
34
+
35
+ - 每个 Voice Session 持续绑定一个后台 Agent Session,重启 DSH 后也能恢复。
36
+ - 普通聊天直接回答,只有需要工具的工作才委派给后台 Agent。
37
+ - 委派任务启动前立即播报一句贴合当前请求的简短确认语。
38
+ - 完整任务报告保留在任务界面,语音只播报专门生成的简洁结果。
39
+ - 支持语音打断、页面切换、断线重连以及历史对话恢复。
40
+ - 服务端和浏览器界面统一通过一个公开 npm 包发行。
41
+
42
+ ## 配置项
43
+
44
+ 默认以连续静音 1.5 秒作为一句话的边界。`silenceDurationMs`、`speechThreshold`、`minSpeechDurationMs` 和 `maxUtteranceMs` 等高级参数可在插件 profile 配置中调整。
45
+
46
+ ## 要求与限制
47
+
48
+ - 麦克风和播放界面面向 DSH Web UI,并不是框架无关的浏览器插件。
49
+ - 云端语音识别需要网络连接及硅基流动 API Key。
50
+ - 语音回复目前默认使用 Edge TTS 的中文晓晓音色。
51
+
52
+ 源码、开发说明和问题反馈请前往 [GitHub 仓库](https://github.com/lgquan/dsh-voco)。
53
+
54
+ ## 许可证
55
+
56
+ [MIT](LICENSE)
package/lib/client.js CHANGED
@@ -18,9 +18,9 @@ window.__ModuleLoader__.load({
18
18
  document.head.appendChild(tag);
19
19
  }
20
20
  var VoiceControl_module_css_default = {
21
- "active": "PapC8q_active",
22
21
  "voice-pulse": "PapC8q_voice-pulse",
23
- "button": "PapC8q_button"
22
+ "button": "PapC8q_button",
23
+ "active": "PapC8q_active"
24
24
  };
25
25
  //#endregion
26
26
  //#region ../ui-voice/src/client/VoiceControl.tsx
@@ -89,17 +89,17 @@ window.__ModuleLoader__.load({
89
89
  document.head.appendChild(tag);
90
90
  }
91
91
  var VoiceHistoryAction_module_css_default = {
92
- "list": "hhHVeG_list",
93
- "empty": "hhHVeG_empty",
92
+ "title": "hhHVeG_title",
94
93
  "panel": "hhHVeG_panel",
95
- "wave": "hhHVeG_wave",
96
- "meta": "hhHVeG_meta",
97
94
  "row": "hhHVeG_row",
98
- "close": "hhHVeG_close",
99
- "rowText": "hhHVeG_rowText",
100
- "title": "hhHVeG_title",
95
+ "trigger": "hhHVeG_trigger",
96
+ "wave": "hhHVeG_wave",
101
97
  "header": "hhHVeG_header",
102
- "trigger": "hhHVeG_trigger"
98
+ "empty": "hhHVeG_empty",
99
+ "rowText": "hhHVeG_rowText",
100
+ "close": "hhHVeG_close",
101
+ "list": "hhHVeG_list",
102
+ "meta": "hhHVeG_meta"
103
103
  };
104
104
  //#endregion
105
105
  //#region ../ui-voice/src/client/VoiceHistoryAction.tsx
@@ -211,23 +211,23 @@ window.__ModuleLoader__.load({
211
211
  document.head.appendChild(tag);
212
212
  }
213
213
  var VoiceNodeViews_module_css_default = {
214
+ "taskTitle": "KSbXkW_taskTitle",
214
215
  "bubble": "KSbXkW_bubble",
215
- "taskCancel": "KSbXkW_taskCancel",
216
- "meta": "KSbXkW_meta",
217
- "voiceBadge": "KSbXkW_voiceBadge",
218
216
  "taskInput": "KSbXkW_taskInput",
219
- "taskSummary": "KSbXkW_taskSummary",
220
- "taskDetails": "KSbXkW_taskDetails",
221
217
  "taskStatus": "KSbXkW_taskStatus",
218
+ "taskDetails": "KSbXkW_taskDetails",
222
219
  "taskDot": "KSbXkW_taskDot",
223
- "taskTitle": "KSbXkW_taskTitle",
224
- "taskChevron": "KSbXkW_taskChevron",
225
220
  "taskActions": "KSbXkW_taskActions",
226
- "miniWave": "KSbXkW_miniWave",
227
221
  "taskCard": "KSbXkW_taskCard",
222
+ "taskSummary": "KSbXkW_taskSummary",
223
+ "miniWave": "KSbXkW_miniWave",
224
+ "utterance": "KSbXkW_utterance",
225
+ "meta": "KSbXkW_meta",
228
226
  "taskLink": "KSbXkW_taskLink",
227
+ "taskCancel": "KSbXkW_taskCancel",
228
+ "voiceBadge": "KSbXkW_voiceBadge",
229
229
  "taskUpdate": "KSbXkW_taskUpdate",
230
- "utterance": "KSbXkW_utterance"
230
+ "taskChevron": "KSbXkW_taskChevron"
231
231
  };
232
232
  //#endregion
233
233
  //#region ../ui-voice/src/client/VoiceNodeViews.tsx
@@ -355,12 +355,12 @@ window.__ModuleLoader__.load({
355
355
  document.head.appendChild(tag);
356
356
  }
357
357
  var VoiceOverlay_module_css_default = {
358
- "voice-wave": "AAuDqa_voice-wave",
359
- "stop": "AAuDqa_stop",
360
- "status": "AAuDqa_status",
361
358
  "wave": "AAuDqa_wave",
362
359
  "root": "AAuDqa_root",
363
- "link": "AAuDqa_link"
360
+ "link": "AAuDqa_link",
361
+ "stop": "AAuDqa_stop",
362
+ "status": "AAuDqa_status",
363
+ "voice-wave": "AAuDqa_voice-wave"
364
364
  };
365
365
  //#endregion
366
366
  //#region ../ui-voice/src/client/VoiceOverlay.tsx
@@ -431,6 +431,11 @@ window.__ModuleLoader__.load({
431
431
  generation = 0;
432
432
  frame;
433
433
  pendingText = [];
434
+ onConversationStarted;
435
+ conversationStarted = false;
436
+ constructor(options = {}) {
437
+ this.onConversationStarted = options.onConversationStarted;
438
+ }
434
439
  /** Return the identity-stable snapshot until a voice fact changes. */
435
440
  getSnapshot = () => this.snapshot;
436
441
  /** Subscribe to state and frame-coalesced transcript changes. */
@@ -447,6 +452,7 @@ window.__ModuleLoader__.load({
447
452
  async start(sessionId) {
448
453
  const generation = ++this.generation;
449
454
  this.pendingText.splice(0);
455
+ this.conversationStarted = false;
450
456
  const contexts = createAudioContexts();
451
457
  await this.closeTransport();
452
458
  if (generation !== this.generation) {
@@ -584,7 +590,9 @@ window.__ModuleLoader__.load({
584
590
  return;
585
591
  }
586
592
  if (type === "transcription.completed") {
587
- this.setText(stringOf(event.utteranceId), stringOf(event.text), "immediate");
593
+ const text = stringOf(event.text);
594
+ this.setText(stringOf(event.utteranceId), text, "immediate");
595
+ if (text.trim() !== "") this.markConversationStarted();
588
596
  return;
589
597
  }
590
598
  if (type === "output_text.started") {
@@ -598,6 +606,7 @@ window.__ModuleLoader__.load({
598
606
  if (type === "output_text.done") {
599
607
  const text = stringOf(event.text);
600
608
  if (text !== "") this.setText(stringOf(event.utteranceId), text, "immediate");
609
+ if (text.trim() !== "") this.markConversationStarted();
601
610
  return;
602
611
  }
603
612
  if (type === "output_audio.started") {
@@ -610,6 +619,13 @@ window.__ModuleLoader__.load({
610
619
  }
611
620
  if (type === "error") this.handleUnexpectedClose();
612
621
  }
622
+ markConversationStarted() {
623
+ if (this.conversationStarted) return;
624
+ const sessionId = this.snapshot.sessionId;
625
+ if (sessionId === void 0) return;
626
+ this.conversationStarted = true;
627
+ this.onConversationStarted?.(sessionId);
628
+ }
613
629
  publish(snapshot) {
614
630
  if (this.frame !== void 0) {
615
631
  cancelAnimationFrame(this.frame);
@@ -743,7 +759,7 @@ window.__ModuleLoader__.load({
743
759
  return;
744
760
  }
745
761
  const record = recordOf(event);
746
- if (record?.type === "transcription.started" || record?.type === "response.interrupted") stopPlayback();
762
+ if (record?.type === "response.interrupted") stopPlayback();
747
763
  if (record?.type === "output_audio.done") {
748
764
  outputDone = true;
749
765
  notifyPlaybackEnded();
@@ -1357,9 +1373,19 @@ registerProcessor('dsh-pcm-capture', DshPcmCaptureProcessor)
1357
1373
  "sessions",
1358
1374
  "locale"
1359
1375
  ];
1376
+ /** Refresh the Host-backed Session list when the runtime exposes its baseline pull. */
1377
+ function refreshSessions(ctx) {
1378
+ const service = ctx.sessions;
1379
+ if (typeof service.refresh !== "function") return;
1380
+ service.refresh.call(ctx.sessions).catch((cause) => {
1381
+ console.warn("voice session list refresh failed:", cause);
1382
+ });
1383
+ }
1360
1384
  /** Mount root transport controls and durable Voice conversation renderers. @param ctx - browser context. */
1361
1385
  function apply(ctx) {
1362
- const controller = new VoiceController();
1386
+ const controller = new VoiceController({ onConversationStarted: () => {
1387
+ refreshSessions(ctx);
1388
+ } });
1363
1389
  const history = new VoiceHistoryStore();
1364
1390
  const textSubmit = new VoiceTextSubmitBridge(controller, (sessionId) => {
1365
1391
  const scope = ctx.sessions.scope(sessionId);