dsh-live-voice 0.0.2 → 0.1.0

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/README.md CHANGED
@@ -100,7 +100,7 @@ Speech processing can run locally, but the DSH language model may still be remot
100
100
 
101
101
  ## Qwen3 HTTP engine
102
102
 
103
- When a compatible Qwen3 speech API is already running on the DSH host, choose **Qwen3 ASR — local MLX server** under Speech recognition and **Qwen3 TTS — local MLX server** under Speech output. Configure its loopback base URL in **DSH Settings → Live Voice**. The plugin supports the OminiX-API contract and standard OpenAI-style speech endpoints at `GET /health`, `POST /v1/audio/transcriptions`, and `POST /v1/audio/speech`; it does not install, start, stop, or manage that external service or its model weights.
103
+ When a compatible Qwen3 speech API is already running on the DSH host, choose **Qwen3 ASR — local MLX server** under Speech recognition and **Qwen3 TTS — local MLX server** under Speech output. Configure its base URL in **DSH Settings → Live Voice**. The Qwen server may use any HTTP or HTTPS base URL reachable from the DSH host. The plugin supports the OminiX-API contract and standard OpenAI-style speech endpoints at `GET /health`, `POST /v1/audio/transcriptions`, and `POST /v1/audio/speech`; it does not install, start, stop, or manage that external service or its model weights.
104
104
 
105
105
  ## License
106
106
 
package/lib/client.js CHANGED
@@ -435,8 +435,15 @@ var VoiceCoordinator = class {
435
435
  const expected = this.autoSendDraft;
436
436
  this.autoSendDraft = null;
437
437
  this.patch({ autoSendAt: null });
438
- if (!this.disposed && this.snapshot.settings.sendingMode === "automatic" && expected === this.composer.getDraft())
439
- this.composer.submit();
438
+ if (!this.disposed && this.snapshot.settings.sendingMode === "automatic" && expected === this.composer.getDraft()) {
439
+ try {
440
+ this.composer.submit();
441
+ this.transcript.reset();
442
+ this.recognition.reset?.();
443
+ } catch (error) {
444
+ this.patch({ error: message(error) });
445
+ }
446
+ }
440
447
  }, delay);
441
448
  }
442
449
  cancelAutoSend() {
@@ -1271,6 +1278,26 @@ var BrowserRecognitionEngine = class {
1271
1278
  };
1272
1279
  recognition.start();
1273
1280
  }
1281
+ reset() {
1282
+ const session = this.session;
1283
+ const recognition = session?.recognition;
1284
+ if (!session || !recognition) return false;
1285
+ this._detach(recognition);
1286
+ session.recognition = null;
1287
+ try {
1288
+ recognition.abort();
1289
+ } catch {
1290
+ }
1291
+ this._activity(session, false);
1292
+ if (this.session !== session || session.signal?.aborted) return false;
1293
+ try {
1294
+ this._begin(session);
1295
+ return true;
1296
+ } catch (error) {
1297
+ this._fail(session, error);
1298
+ return false;
1299
+ }
1300
+ }
1274
1301
  async stop() {
1275
1302
  const session = this.session;
1276
1303
  if (!session) return;
@@ -1854,7 +1881,7 @@ function createQwenSettings(React2) {
1854
1881
  h(
1855
1882
  "p",
1856
1883
  null,
1857
- "Host-wide settings for the resident Qwen3 ASR + TTS server. Only unauthenticated loopback HTTP is allowed; the browser reaches it through authenticated DSH routes."
1884
+ "Host-wide settings for the Qwen3 ASR + TTS server. Enter any HTTP or HTTPS base URL reachable from the DSH host. The browser accesses it through authenticated DSH routes."
1858
1885
  ),
1859
1886
  field("Qwen API base URL", "baseUrl"),
1860
1887
  field("Request timeout (ms)", "timeoutMs", "number"),
package/lib/server.js CHANGED
@@ -473,14 +473,14 @@ var defaultSettings = Object.freeze({
473
473
  });
474
474
 
475
475
  // src/engines/qwen-http-host.ts
476
- var LOOPBACK2 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
477
476
  var clean2 = (value) => String(value ?? "").trim();
478
477
  function resolveQwenBaseUrl(value = process.env.DSH_LIVE_VOICE_QWEN_URL || "http://127.0.0.1:8080/") {
479
478
  const url = new URL(value);
480
- if (url.protocol !== "http:" || !LOOPBACK2.has(url.hostname) || url.username || url.password || url.hash)
481
- throw new Error("Qwen API URL must be an unauthenticated loopback http URL.");
479
+ if (!["http:", "https:"].includes(url.protocol))
480
+ throw new Error("Qwen API URL must use HTTP or HTTPS.");
482
481
  url.pathname = url.pathname.replace(/\/*$/, "/");
483
482
  url.search = "";
483
+ url.hash = "";
484
484
  return url;
485
485
  }
486
486
  function validateQwenConfig(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-live-voice",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "typecheck": "tsc --noEmit",
@@ -113,7 +113,7 @@ export function createQwenSettings(React) {
113
113
  h(
114
114
  'p',
115
115
  null,
116
- 'Host-wide settings for the resident Qwen3 ASR + TTS server. Only unauthenticated loopback HTTP is allowed; the browser reaches it through authenticated DSH routes.',
116
+ 'Host-wide settings for the Qwen3 ASR + TTS server. Enter any HTTP or HTTPS base URL reachable from the DSH host. The browser accesses it through authenticated DSH routes.',
117
117
  ),
118
118
  field('Qwen API base URL', 'baseUrl'),
119
119
  field('Request timeout (ms)', 'timeoutMs', 'number'),
@@ -317,8 +317,18 @@ export class VoiceCoordinator {
317
317
  !this.disposed &&
318
318
  this.snapshot.settings.sendingMode === 'automatic' &&
319
319
  expected === this.composer.getDraft()
320
- )
321
- this.composer.submit();
320
+ ) {
321
+ try {
322
+ this.composer.submit();
323
+ // Web Speech keeps a cumulative native result list for the lifetime of
324
+ // one recognition instance. Start a fresh instance at the turn boundary
325
+ // so the sent utterance cannot prefix the next one.
326
+ this.transcript.reset();
327
+ this.recognition.reset?.();
328
+ } catch (error) {
329
+ this.patch({ error: message(error) });
330
+ }
331
+ }
322
332
  }, delay);
323
333
  }
324
334
  cancelAutoSend() {
@@ -6,22 +6,16 @@ import { randomUUID } from 'node:crypto';
6
6
  import { defaultQwenVoice, isQwenVoice } from '../core/settings.ts';
7
7
  import { validateMonoPcm16Wav } from './recognition/whisper-http-host.ts';
8
8
 
9
- const LOOPBACK = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
10
9
  const clean = (value) => String(value ?? '').trim();
11
10
  export function resolveQwenBaseUrl(
12
11
  value = process.env.DSH_LIVE_VOICE_QWEN_URL || 'http://127.0.0.1:8080/',
13
12
  ) {
14
13
  const url = new URL(value);
15
- if (
16
- url.protocol !== 'http:' ||
17
- !LOOPBACK.has(url.hostname) ||
18
- url.username ||
19
- url.password ||
20
- url.hash
21
- )
22
- throw new Error('Qwen API URL must be an unauthenticated loopback http URL.');
14
+ if (!['http:', 'https:'].includes(url.protocol))
15
+ throw new Error('Qwen API URL must use HTTP or HTTPS.');
23
16
  url.pathname = url.pathname.replace(/\/*$/, '/');
24
17
  url.search = '';
18
+ url.hash = '';
25
19
  return url;
26
20
  }
27
21
  export function validateQwenConfig(value) {
@@ -19,6 +19,7 @@ const notify = (callback, value) => {
19
19
  * not when permission is granted. Native asynchronous errors go to onError(Error).
20
20
  * onResult({interim,final}) contains the current interim string and NEW final text only.
21
21
  * onActivity(boolean) reports speech activity, not microphone permission/readiness.
22
+ * reset() replaces active native capture so its cumulative result list starts empty.
22
23
  * stop() aborts capture, discards pending results, removes handlers and cancels restarts.
23
24
  * Consecutive no-progress restarts are bounded; speech or nonempty results reset the budget.
24
25
  * Native start acceptance alone is not progress. Restart delay backs off up to 30 seconds.
@@ -272,6 +273,28 @@ export class BrowserRecognitionEngine {
272
273
  recognition.start();
273
274
  }
274
275
 
276
+ reset() {
277
+ const session = this.session;
278
+ const recognition = session?.recognition;
279
+ if (!session || !recognition) return false;
280
+ this._detach(recognition);
281
+ session.recognition = null;
282
+ try {
283
+ recognition.abort();
284
+ } catch {
285
+ /* Browser already ended capture. */
286
+ }
287
+ this._activity(session, false);
288
+ if (this.session !== session || session.signal?.aborted) return false;
289
+ try {
290
+ this._begin(session);
291
+ return true;
292
+ } catch (error) {
293
+ this._fail(session, error);
294
+ return false;
295
+ }
296
+ }
297
+
275
298
  async stop() {
276
299
  const session = this.session;
277
300
  if (!session) return;