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 +1 -1
- package/lib/client.js +30 -3
- package/lib/server.js +3 -3
- package/package.json +1 -1
- package/src/client/qwen-settings.ts +1 -1
- package/src/core/coordinator.ts +12 -2
- package/src/engines/qwen-http-host.ts +3 -9
- package/src/engines/recognition/browser.ts +23 -0
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
481
|
-
throw new Error("Qwen API URL must
|
|
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
|
@@ -113,7 +113,7 @@ export function createQwenSettings(React) {
|
|
|
113
113
|
h(
|
|
114
114
|
'p',
|
|
115
115
|
null,
|
|
116
|
-
'Host-wide settings for the
|
|
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'),
|
package/src/core/coordinator.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|