@flowingspring/dsh-voco 0.3.6 → 0.3.8

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.
@@ -5,6 +5,8 @@ import { existsSync } from "node:fs";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { loadEnvFile } from "node:process";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
9
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
8
10
  import { mkdtemp, readFile, rm } from "node:fs/promises";
9
11
  import { tmpdir } from "node:os";
10
12
  import { EdgeTTS } from "node-edge-tts";
@@ -70,6 +72,9 @@ var LocalSession = class {
70
72
  async start() {
71
73
  await this.backend.start((event) => this.receive(event));
72
74
  }
75
+ beginManualUtterance() {
76
+ this.backend.beginManualUtterance();
77
+ }
73
78
  appendAudio(audio) {
74
79
  this.backend.appendAudio(audio);
75
80
  }
@@ -312,6 +317,8 @@ var NodeSpeechBackend = class {
312
317
  preRoll = [];
313
318
  preRollBytes = 0;
314
319
  active;
320
+ manual = false;
321
+ manualMaxed = false;
315
322
  recognitionQueue = Promise.resolve();
316
323
  synthesisQueue = Promise.resolve();
317
324
  synthesisGeneration = 0;
@@ -343,8 +350,16 @@ var NodeSpeechBackend = class {
343
350
  emit({ type: "ready" });
344
351
  return Promise.resolve();
345
352
  }
353
+ beginManualUtterance() {
354
+ if (this.closed) return;
355
+ this.active = void 0;
356
+ this.preRoll.splice(0);
357
+ this.preRollBytes = 0;
358
+ this.manual = true;
359
+ this.manualMaxed = false;
360
+ }
346
361
  appendAudio(audio) {
347
- if (this.closed || audio.byteLength === 0) return;
362
+ if (this.closed || audio.byteLength === 0 || this.manualMaxed) return;
348
363
  if (audio.byteLength % 2 !== 0) {
349
364
  this.emit?.({
350
365
  type: "error",
@@ -367,14 +382,19 @@ var NodeSpeechBackend = class {
367
382
  this.active.silenceBytes = 0;
368
383
  if (!this.active.confirmed && this.active.voicedBytes >= this.bytesFor(this.config.minSpeechDurationMs ?? 250)) this.confirmUtterance();
369
384
  } else this.active.silenceBytes += frame.byteLength;
370
- if (!this.active.confirmed && this.active.silenceBytes >= this.bytesFor(this.config.minSpeechDurationMs ?? 250)) {
385
+ if (!this.manual && !this.active.confirmed && this.active.silenceBytes >= this.bytesFor(this.config.minSpeechDurationMs ?? 250)) {
371
386
  this.active = void 0;
372
387
  return;
373
388
  }
374
- if (this.active.silenceBytes >= this.bytesFor(this.config.silenceDurationMs) || this.active.totalBytes >= this.bytesFor(this.config.maxUtteranceMs)) this.finishUtterance();
389
+ if (!this.manual && this.active.silenceBytes >= this.bytesFor(this.config.silenceDurationMs) || this.active.totalBytes >= this.bytesFor(this.config.maxUtteranceMs)) {
390
+ this.finishUtterance();
391
+ if (this.manual) this.manualMaxed = true;
392
+ }
375
393
  }
376
394
  commitAudio() {
377
395
  this.finishUtterance();
396
+ this.manual = false;
397
+ this.manualMaxed = false;
378
398
  }
379
399
  synthesize(responseId, text) {
380
400
  const generation = this.synthesisGeneration;
@@ -433,6 +453,8 @@ var NodeSpeechBackend = class {
433
453
  this.synthesisGeneration += 1;
434
454
  this.synthesisResponses.clear();
435
455
  this.active = void 0;
456
+ this.manual = false;
457
+ this.manualMaxed = false;
436
458
  this.preRoll.splice(0);
437
459
  await Promise.allSettled([this.recognitionQueue, this.synthesisQueue]);
438
460
  this.emit?.({
@@ -539,8 +561,10 @@ function errorMessage(error) {
539
561
  /** Silence-gated SiliconFlow cloud ASR with Edge TTS. */
540
562
  const name = "voice-local";
541
563
  const inject = ["voice"];
564
+ const VOICE_LOCAL_SETTINGS_NAMESPACE = settingsNamespace("voice-local");
565
+ const SILICONFLOW_API_KEY_REF = credentialRef("SILICONFLOW_API_KEY");
542
566
  const Config = z.object({
543
- apiKey: z.string(),
567
+ apiKey: z.string().role("secret"),
544
568
  endpoint: z.string().default("https://api.siliconflow.cn/v1/audio/transcriptions"),
545
569
  model: z.string().default("XingChenAGI/XingChenASR-V3.2-Ultra"),
546
570
  interactionMode: z.union(["speech-shell", "frontend-agent"]).default("speech-shell"),
@@ -575,26 +599,34 @@ function loadProjectEnv() {
575
599
  }
576
600
  }
577
601
  function apply(ctx, config = {}) {
578
- loadProjectEnv();
602
+ if (ctx.get("credentials") === void 0) loadProjectEnv();
603
+ let current = () => config;
604
+ installSettingsSection(ctx, VOICE_LOCAL_SETTINGS_NAMESPACE, Config, config, {
605
+ setSource: (source) => {
606
+ current = source;
607
+ },
608
+ onChange: () => {}
609
+ });
579
610
  return ctx.voice.registerProvider({
580
611
  id: "local",
581
612
  available: () => true,
582
613
  connect: async ({ voiceSessionId, emit }) => {
614
+ const active = current();
583
615
  const session = new LocalSession(new NodeSpeechBackend({
584
- apiKey: config.apiKey ?? process.env.SILICONFLOW_API_KEY ?? "",
585
- endpoint: config.endpoint ?? "https://api.siliconflow.cn/v1/audio/transcriptions",
586
- model: config.model ?? "XingChenAGI/XingChenASR-V3.2-Ultra",
587
- requestTimeoutMs: config.requestTimeoutMs ?? 6e4,
588
- inputSampleRate: config.inputSampleRate ?? 16e3,
589
- outputSampleRate: config.outputSampleRate ?? 48e3,
590
- ttsRate: config.ttsRate ?? "+20%",
591
- silenceDurationMs: config.silenceDurationMs ?? 1500,
592
- speechThreshold: config.speechThreshold ?? .015,
593
- minSpeechDurationMs: config.minSpeechDurationMs ?? 250,
594
- preRollMs: config.preRollMs ?? 400,
595
- trailingSilenceMs: config.trailingSilenceMs ?? 200,
596
- maxUtteranceMs: config.maxUtteranceMs ?? 6e4
597
- }), emit, voiceSessionId, config.interactionMode ?? "speech-shell");
616
+ apiKey: await resolveSiliconFlowApiKey(ctx, active),
617
+ endpoint: active.endpoint ?? "https://api.siliconflow.cn/v1/audio/transcriptions",
618
+ model: active.model ?? "XingChenAGI/XingChenASR-V3.2-Ultra",
619
+ requestTimeoutMs: active.requestTimeoutMs ?? 6e4,
620
+ inputSampleRate: active.inputSampleRate ?? 16e3,
621
+ outputSampleRate: active.outputSampleRate ?? 48e3,
622
+ ttsRate: active.ttsRate ?? "+20%",
623
+ silenceDurationMs: active.silenceDurationMs ?? 1500,
624
+ speechThreshold: active.speechThreshold ?? .015,
625
+ minSpeechDurationMs: active.minSpeechDurationMs ?? 250,
626
+ preRollMs: active.preRollMs ?? 400,
627
+ trailingSilenceMs: active.trailingSilenceMs ?? 200,
628
+ maxUtteranceMs: active.maxUtteranceMs ?? 6e4
629
+ }), emit, voiceSessionId, active.interactionMode ?? "speech-shell");
598
630
  try {
599
631
  await session.start();
600
632
  return session;
@@ -605,5 +637,12 @@ function apply(ctx, config = {}) {
605
637
  }
606
638
  });
607
639
  }
640
+ /** Resolve the key per connection so a settings-page write reaches the next attempt. */
641
+ async function resolveSiliconFlowApiKey(ctx, config) {
642
+ if (config.apiKey !== void 0 && config.apiKey.trim() !== "") return config.apiKey;
643
+ const credentials = ctx.get("credentials");
644
+ if (credentials !== void 0) return (await credentials.resolve(SILICONFLOW_API_KEY_REF))?.value ?? "";
645
+ return process.env.SILICONFLOW_API_KEY ?? "";
646
+ }
608
647
  //#endregion
609
- export { Config, LocalSession, NodeSpeechBackend, SiliconFlowAsr, apply, inject, name, pcm16MonoWav };
648
+ export { Config, LocalSession, NodeSpeechBackend, SILICONFLOW_API_KEY_REF, SiliconFlowAsr, VOICE_LOCAL_SETTINGS_NAMESPACE, apply, inject, name, pcm16MonoWav, resolveSiliconFlowApiKey };
@@ -213,7 +213,8 @@ async function attach(ctx, socket, rawUrl) {
213
213
  return;
214
214
  }
215
215
  const type = control.type;
216
- if (type === "audio.commit") voice.commitAudio(session.id);
216
+ if (type === "audio.push-to-talk.start") voice.beginManualUtterance(session.id);
217
+ else if (type === "audio.commit") voice.commitAudio(session.id);
217
218
  else if (type === "response.interrupt") voice.interruptResponse(session.id);
218
219
  else if (type === "playback.ended") voice.playbackEnded(session.id);
219
220
  else if (type === "task.cancel") {
@@ -169,6 +169,10 @@ var VoiceRuntime = class extends Service {
169
169
  appendAudio(id, audio) {
170
170
  this.requireAttachedSession(id).provider.appendAudio(audio);
171
171
  }
172
+ /** Begin a manually committed push-to-talk utterance. */
173
+ beginManualUtterance(id) {
174
+ this.requireAttachedSession(id).provider.beginManualUtterance();
175
+ }
172
176
  /**
173
177
  * Commit current microphone input.
174
178
  * @param id - voice session.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flowingspring/dsh-voco",
3
3
  "description": "Persistent voice conversations for DSH with cloud speech recognition, Edge TTS, and background Agent delegation",
4
- "version": "0.3.6",
4
+ "version": "0.3.8",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -65,6 +65,8 @@
65
65
  "@deepseek-ai/dsh-client-connection",
66
66
  "@deepseek-ai/dsh-client-locale",
67
67
  "@deepseek-ai/dsh-client-runtime",
68
+ "@deepseek-ai/dsh-client-ui-settings",
69
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
68
70
  "@deepseek-ai/dsh-client-ui-conversation",
69
71
  "@deepseek-ai/dsh-client-ui-layout",
70
72
  "@deepseek-ai/dsh-client-ui-sidebar"
@@ -86,15 +88,19 @@
86
88
  "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.1",
87
89
  "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
88
90
  "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.1",
91
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
92
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.1-rc.2",
89
93
  "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.1",
90
94
  "@deepseek-ai/dsh-client-ui-layout": "^0.1.1-rc.1",
91
95
  "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.1-rc.1",
92
96
  "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
93
97
  "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.1",
94
98
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
99
+ "@deepseek-ai/dsh-credentials": "^0.1.1-rc.2",
95
100
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
96
101
  "@deepseek-ai/dsh-session": "^0.1.1-rc.1",
97
102
  "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.1",
103
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
98
104
  "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.1",
99
105
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
100
106
  "@deepseek-ai/dsh-workspace": "^0.1.1-rc.1",
@@ -109,15 +115,19 @@
109
115
  "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.1",
110
116
  "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
111
117
  "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.1",
118
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
119
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.1-rc.2",
112
120
  "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.1",
113
121
  "@deepseek-ai/dsh-client-ui-layout": "^0.1.1-rc.1",
114
122
  "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.1-rc.1",
115
123
  "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
116
124
  "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.1",
117
125
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
126
+ "@deepseek-ai/dsh-credentials": "^0.1.1-rc.2",
118
127
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
119
128
  "@deepseek-ai/dsh-session": "^0.1.1-rc.1",
120
129
  "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.1",
130
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
121
131
  "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.1",
122
132
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
123
133
  "@deepseek-ai/dsh-workspace": "^0.1.1-rc.1",
@@ -125,11 +135,11 @@
125
135
  "@types/ws": "^8.18.1",
126
136
  "react": "^18.2.0",
127
137
  "react-dom": "^18.2.0",
138
+ "@flowingspring/dsh-voice": "0.1.2",
128
139
  "@flowingspring/dsh-voice-local": "0.1.0",
129
- "@flowingspring/dsh-client-ui-voice": "0.1.2",
130
140
  "@flowingspring/dsh-llm-tool-call-compat": "0.1.0",
131
- "@flowingspring/dsh-voice": "0.1.2",
132
141
  "@flowingspring/dsh-voice-web": "0.1.2",
142
+ "@flowingspring/dsh-client-ui-voice": "0.1.2",
133
143
  "@flowingspring/dsh-voice-assistant": "0.1.2"
134
144
  }
135
145
  }