dsh-live-voice 0.2.1 → 0.2.3
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/CHANGELOG.md +43 -1
- package/README.md +29 -21
- package/lib/client.js +457 -60
- package/lib/server.js +2 -0
- package/package.json +3 -2
- package/scripts/build.ts +9 -1
- package/src/client/components.ts +171 -23
- package/src/client/index.ts +68 -30
- package/src/client/releases.ts +117 -0
- package/src/client/styles.ts +3 -1
- package/src/core/coordinator.ts +54 -7
- package/src/core/settings.ts +12 -0
- package/src/engines/recognition/browser.ts +38 -0
- package/src/engines/recognition/whisper-http.ts +80 -29
package/lib/client.js
CHANGED
|
@@ -114,7 +114,9 @@ var defaultSettings = Object.freeze({
|
|
|
114
114
|
recognitionProcessLocally: true,
|
|
115
115
|
recognitionAutoInstall: true,
|
|
116
116
|
voiceDetectionPreset: "natural",
|
|
117
|
+
recognitionMaxUtteranceSeconds: 60,
|
|
117
118
|
microphoneEnabled: true,
|
|
119
|
+
holdToTalkEnabled: true,
|
|
118
120
|
announceAssistantMessages: true,
|
|
119
121
|
interruptSpeechOnUserMessage: false,
|
|
120
122
|
sendingMode: "manual",
|
|
@@ -150,7 +152,9 @@ function normalizeSettings(value) {
|
|
|
150
152
|
recognitionProcessLocally: typeof source.recognitionProcessLocally === "boolean" ? source.recognitionProcessLocally : defaultSettings.recognitionProcessLocally,
|
|
151
153
|
recognitionAutoInstall: typeof source.recognitionAutoInstall === "boolean" ? source.recognitionAutoInstall : defaultSettings.recognitionAutoInstall,
|
|
152
154
|
voiceDetectionPreset: Object.hasOwn(voiceDetectionPresets, source.voiceDetectionPreset) ? source.voiceDetectionPreset : defaultSettings.voiceDetectionPreset,
|
|
155
|
+
recognitionMaxUtteranceSeconds: Number.isInteger(source.recognitionMaxUtteranceSeconds) && source.recognitionMaxUtteranceSeconds >= 10 && source.recognitionMaxUtteranceSeconds <= 300 ? source.recognitionMaxUtteranceSeconds : defaultSettings.recognitionMaxUtteranceSeconds,
|
|
153
156
|
microphoneEnabled: typeof source.microphoneEnabled === "boolean" ? source.microphoneEnabled : defaultSettings.microphoneEnabled,
|
|
157
|
+
holdToTalkEnabled: typeof source.holdToTalkEnabled === "boolean" ? source.holdToTalkEnabled : defaultSettings.holdToTalkEnabled,
|
|
154
158
|
announceAssistantMessages: typeof source.announceAssistantMessages === "boolean" ? source.announceAssistantMessages : defaultSettings.announceAssistantMessages,
|
|
155
159
|
interruptSpeechOnUserMessage: typeof source.interruptSpeechOnUserMessage === "boolean" ? source.interruptSpeechOnUserMessage : defaultSettings.interruptSpeechOnUserMessage,
|
|
156
160
|
sendingMode: source.sendingMode === "automatic" ? "queue" : ["manual", "queue", "steer"].includes(source.sendingMode) ? source.sendingMode : defaultSettings.sendingMode,
|
|
@@ -273,6 +277,7 @@ var VoiceCoordinator = class {
|
|
|
273
277
|
conversation: false,
|
|
274
278
|
listening: false,
|
|
275
279
|
recognizing: false,
|
|
280
|
+
pendingTranscriptions: 0,
|
|
276
281
|
muted: normalizeSettings(settings).microphoneEnabled === false,
|
|
277
282
|
speaking: false,
|
|
278
283
|
paused: false,
|
|
@@ -287,6 +292,7 @@ var VoiceCoordinator = class {
|
|
|
287
292
|
};
|
|
288
293
|
this.autoSendTimer = null;
|
|
289
294
|
this.autoSendDraft = null;
|
|
295
|
+
this.holdToTalkRelease = false;
|
|
290
296
|
this.interruptionTimer = null;
|
|
291
297
|
this.interruptionTranscriptConfirmed = false;
|
|
292
298
|
this.interruptionPausedSpeech = false;
|
|
@@ -404,6 +410,27 @@ var VoiceCoordinator = class {
|
|
|
404
410
|
startDictation() {
|
|
405
411
|
return this.startListening(false);
|
|
406
412
|
}
|
|
413
|
+
startHoldToTalk() {
|
|
414
|
+
this.holdToTalkRelease = false;
|
|
415
|
+
return this.startListening(false);
|
|
416
|
+
}
|
|
417
|
+
async releaseHoldToTalk() {
|
|
418
|
+
if (this.disposed || !this.snapshot.listening && !this.snapshot.starting) return;
|
|
419
|
+
this.holdToTalkRelease = true;
|
|
420
|
+
if (this.snapshot.starting) return;
|
|
421
|
+
await this.recognition.finish?.();
|
|
422
|
+
if (this.snapshot.pendingTranscriptions === 0) this._finishHoldToTalk();
|
|
423
|
+
}
|
|
424
|
+
_finishHoldToTalk() {
|
|
425
|
+
if (!this.holdToTalkRelease) return;
|
|
426
|
+
this.holdToTalkRelease = false;
|
|
427
|
+
const draft = this.composer.getDraft();
|
|
428
|
+
if (!draft.trim()) {
|
|
429
|
+
void this.stopListening();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
this.scheduleAutoSend(draft, { force: true, stopAfter: true });
|
|
433
|
+
}
|
|
407
434
|
startConversation() {
|
|
408
435
|
return this.startListening(true);
|
|
409
436
|
}
|
|
@@ -483,6 +510,14 @@ var VoiceCoordinator = class {
|
|
|
483
510
|
onResult: (result) => {
|
|
484
511
|
if (valid()) this.onResult(result);
|
|
485
512
|
},
|
|
513
|
+
onProcessingChange: ({ pending = 0 } = {}) => {
|
|
514
|
+
if (!valid()) return;
|
|
515
|
+
const count = Number.isSafeInteger(pending) && pending >= 0 ? pending : 0;
|
|
516
|
+
this.patch({ pendingTranscriptions: count });
|
|
517
|
+
if (count > 0) this.cancelAutoSend();
|
|
518
|
+
else if (this.holdToTalkRelease) this._finishHoldToTalk();
|
|
519
|
+
else this.maybeScheduleAutoSend();
|
|
520
|
+
},
|
|
486
521
|
onActivity: (active) => {
|
|
487
522
|
if (!valid()) return;
|
|
488
523
|
if (active) {
|
|
@@ -507,6 +542,7 @@ var VoiceCoordinator = class {
|
|
|
507
542
|
return;
|
|
508
543
|
}
|
|
509
544
|
this.patch({ listening: true, starting: false });
|
|
545
|
+
if (this.holdToTalkRelease) void this.releaseHoldToTalk();
|
|
510
546
|
this._drain();
|
|
511
547
|
} catch (error) {
|
|
512
548
|
try {
|
|
@@ -619,7 +655,7 @@ var VoiceCoordinator = class {
|
|
|
619
655
|
}
|
|
620
656
|
const next = this.transcript.update(this.composer.getDraft(), final, true);
|
|
621
657
|
this.composer.setDraft(next);
|
|
622
|
-
|
|
658
|
+
this.maybeScheduleAutoSend(next);
|
|
623
659
|
}
|
|
624
660
|
if (interim) {
|
|
625
661
|
this.cancelAutoSend();
|
|
@@ -630,7 +666,12 @@ var VoiceCoordinator = class {
|
|
|
630
666
|
this.patch({ recognizing: !!interim });
|
|
631
667
|
this._drain();
|
|
632
668
|
}
|
|
633
|
-
|
|
669
|
+
maybeScheduleAutoSend(draft = this.composer.getDraft()) {
|
|
670
|
+
if (this.snapshot.settings.sendingMode === "manual" || this.snapshot.recognizing || this.snapshot.pendingTranscriptions > 0)
|
|
671
|
+
return;
|
|
672
|
+
this.scheduleAutoSend(draft);
|
|
673
|
+
}
|
|
674
|
+
scheduleAutoSend(draft, { force = false, stopAfter = false } = {}) {
|
|
634
675
|
this.cancelAutoSend();
|
|
635
676
|
if (!draft.trim() || typeof this.composer.submit !== "function") return;
|
|
636
677
|
const delay = this.snapshot.settings.autoSendDelaySeconds * 1e3;
|
|
@@ -641,11 +682,14 @@ var VoiceCoordinator = class {
|
|
|
641
682
|
const expected = this.autoSendDraft;
|
|
642
683
|
this.autoSendDraft = null;
|
|
643
684
|
this.patch({ autoSendAt: null });
|
|
644
|
-
if (!this.disposed && this.snapshot.settings.sendingMode !== "manual" && expected === this.composer.getDraft()) {
|
|
685
|
+
if (!this.disposed && (force || this.snapshot.settings.sendingMode !== "manual") && expected === this.composer.getDraft()) {
|
|
645
686
|
try {
|
|
646
|
-
this.composer.submit(
|
|
687
|
+
this.composer.submit(
|
|
688
|
+
this.snapshot.settings.sendingMode === "steer" && !force ? "steer" : "queue"
|
|
689
|
+
);
|
|
647
690
|
this.transcript.reset();
|
|
648
691
|
this.recognition.reset?.();
|
|
692
|
+
if (stopAfter) void this.stopListening();
|
|
649
693
|
} catch (error) {
|
|
650
694
|
this.patch({ error: message(error) });
|
|
651
695
|
}
|
|
@@ -662,10 +706,11 @@ var VoiceCoordinator = class {
|
|
|
662
706
|
if (this.autoSendDraft !== null && draft !== this.autoSendDraft) this.cancelAutoSend();
|
|
663
707
|
}
|
|
664
708
|
stopListening() {
|
|
709
|
+
this.holdToTalkRelease = false;
|
|
665
710
|
this.cancelAutoSend();
|
|
666
711
|
++this.epoch;
|
|
667
712
|
this.inputController?.abort();
|
|
668
|
-
this.patch({ listening: false, recognizing: false, starting: false });
|
|
713
|
+
this.patch({ listening: false, recognizing: false, pendingTranscriptions: 0, starting: false });
|
|
669
714
|
return this._input(async () => {
|
|
670
715
|
try {
|
|
671
716
|
await this._releaseInput();
|
|
@@ -1389,7 +1434,10 @@ var BrowserRecognitionEngine = class {
|
|
|
1389
1434
|
restarts: 0,
|
|
1390
1435
|
timer: null,
|
|
1391
1436
|
recognition: null,
|
|
1392
|
-
speaking: false
|
|
1437
|
+
speaking: false,
|
|
1438
|
+
finishing: false,
|
|
1439
|
+
finishPromise: null,
|
|
1440
|
+
resolveFinish: null
|
|
1393
1441
|
};
|
|
1394
1442
|
this.session = session;
|
|
1395
1443
|
const cancelled3 = new Promise((resolve, reject) => {
|
|
@@ -1487,6 +1535,12 @@ var BrowserRecognitionEngine = class {
|
|
|
1487
1535
|
session.recognition = null;
|
|
1488
1536
|
this._activity(session, false);
|
|
1489
1537
|
if (this.session !== session) return;
|
|
1538
|
+
if (session.finishing) {
|
|
1539
|
+
this.session = null;
|
|
1540
|
+
session.signal?.removeEventListener("abort", session.cancel);
|
|
1541
|
+
session.resolveFinish?.();
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1490
1544
|
if (session.restarts >= this.maxRestarts) {
|
|
1491
1545
|
this._fail(
|
|
1492
1546
|
session,
|
|
@@ -1515,6 +1569,33 @@ var BrowserRecognitionEngine = class {
|
|
|
1515
1569
|
};
|
|
1516
1570
|
recognition.start();
|
|
1517
1571
|
}
|
|
1572
|
+
finish() {
|
|
1573
|
+
const session = this.session;
|
|
1574
|
+
if (!session) return Promise.resolve();
|
|
1575
|
+
if (session.finishPromise) return session.finishPromise;
|
|
1576
|
+
session.finishing = true;
|
|
1577
|
+
session.finishPromise = new Promise((resolve) => {
|
|
1578
|
+
session.resolveFinish = resolve;
|
|
1579
|
+
});
|
|
1580
|
+
if (session.timer !== null) {
|
|
1581
|
+
(this.globals.clearTimeout ?? globalThis.clearTimeout)(session.timer);
|
|
1582
|
+
session.timer = null;
|
|
1583
|
+
this.session = null;
|
|
1584
|
+
session.resolveFinish();
|
|
1585
|
+
return session.finishPromise;
|
|
1586
|
+
}
|
|
1587
|
+
try {
|
|
1588
|
+
session.recognition?.stop();
|
|
1589
|
+
if (!session.recognition) {
|
|
1590
|
+
this.session = null;
|
|
1591
|
+
session.resolveFinish();
|
|
1592
|
+
}
|
|
1593
|
+
} catch {
|
|
1594
|
+
this.session = null;
|
|
1595
|
+
session.resolveFinish();
|
|
1596
|
+
}
|
|
1597
|
+
return session.finishPromise;
|
|
1598
|
+
}
|
|
1518
1599
|
reset() {
|
|
1519
1600
|
const session = this.session;
|
|
1520
1601
|
const recognition = session?.recognition;
|
|
@@ -1549,6 +1630,7 @@ var BrowserRecognitionEngine = class {
|
|
|
1549
1630
|
} catch {
|
|
1550
1631
|
}
|
|
1551
1632
|
this._activity(session, false);
|
|
1633
|
+
session.resolveFinish?.();
|
|
1552
1634
|
}
|
|
1553
1635
|
};
|
|
1554
1636
|
|
|
@@ -1585,12 +1667,18 @@ function encodeMonoPcm16Wav(samples, inputRate) {
|
|
|
1585
1667
|
return buffer;
|
|
1586
1668
|
}
|
|
1587
1669
|
var WhisperHttpRecognitionEngine = class {
|
|
1588
|
-
constructor({
|
|
1670
|
+
constructor({
|
|
1671
|
+
globals = globalThis,
|
|
1672
|
+
meter,
|
|
1673
|
+
voiceDetectionPreset = "natural",
|
|
1674
|
+
maxUtteranceSeconds = 60
|
|
1675
|
+
} = {}) {
|
|
1589
1676
|
this.g = globals;
|
|
1590
1677
|
this.meter = meter;
|
|
1591
1678
|
this.session = null;
|
|
1592
1679
|
this.lang = "pt-BR";
|
|
1593
1680
|
this.voiceDetectionPreset = voiceDetectionPreset;
|
|
1681
|
+
this.maxUtteranceSeconds = maxUtteranceSeconds;
|
|
1594
1682
|
this.route = ROUTE;
|
|
1595
1683
|
}
|
|
1596
1684
|
get segmentation() {
|
|
@@ -1616,7 +1704,14 @@ var WhisperHttpRecognitionEngine = class {
|
|
|
1616
1704
|
};
|
|
1617
1705
|
}
|
|
1618
1706
|
}
|
|
1619
|
-
async start({
|
|
1707
|
+
async start({
|
|
1708
|
+
lang = this.lang,
|
|
1709
|
+
signal,
|
|
1710
|
+
onResult,
|
|
1711
|
+
onActivity,
|
|
1712
|
+
onError,
|
|
1713
|
+
onProcessingChange
|
|
1714
|
+
} = {}) {
|
|
1620
1715
|
await this.stop();
|
|
1621
1716
|
if (signal?.aborted) throw abortError3();
|
|
1622
1717
|
const context = this.meter?.context, source = this.meter?.source;
|
|
@@ -1636,16 +1731,24 @@ var WhisperHttpRecognitionEngine = class {
|
|
|
1636
1731
|
samples: 0,
|
|
1637
1732
|
voiced: false,
|
|
1638
1733
|
silence: 0,
|
|
1639
|
-
|
|
1734
|
+
transcriptionQueue: [],
|
|
1735
|
+
activeRequest: null,
|
|
1736
|
+
draining: false,
|
|
1640
1737
|
onResult,
|
|
1641
1738
|
onActivity,
|
|
1642
1739
|
onError,
|
|
1740
|
+
onProcessingChange,
|
|
1643
1741
|
lang,
|
|
1644
1742
|
signal
|
|
1645
1743
|
};
|
|
1646
1744
|
this.session = session;
|
|
1647
1745
|
const valid = () => this.session === session && !signal?.aborted;
|
|
1648
|
-
const
|
|
1746
|
+
const notifyProcessing = () => session.onProcessingChange?.({
|
|
1747
|
+
queued: session.transcriptionQueue.length,
|
|
1748
|
+
active: !!session.activeRequest,
|
|
1749
|
+
pending: session.transcriptionQueue.length + (session.activeRequest ? 1 : 0)
|
|
1750
|
+
});
|
|
1751
|
+
const enqueue = () => {
|
|
1649
1752
|
if (!session.voiced || session.samples < context.sampleRate * 0.25) {
|
|
1650
1753
|
session.chunks = [];
|
|
1651
1754
|
session.samples = 0;
|
|
@@ -1663,29 +1766,46 @@ var WhisperHttpRecognitionEngine = class {
|
|
|
1663
1766
|
session.samples = 0;
|
|
1664
1767
|
session.voiced = false;
|
|
1665
1768
|
session.silence = 0;
|
|
1666
|
-
|
|
1667
|
-
|
|
1769
|
+
session.transcriptionQueue.push(samples);
|
|
1770
|
+
notifyProcessing();
|
|
1771
|
+
void drain();
|
|
1772
|
+
};
|
|
1773
|
+
const drain = async () => {
|
|
1774
|
+
if (session.draining || !valid()) return;
|
|
1775
|
+
session.draining = true;
|
|
1668
1776
|
try {
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
"
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1777
|
+
while (valid() && session.transcriptionQueue.length) {
|
|
1778
|
+
const samples = session.transcriptionQueue.shift();
|
|
1779
|
+
const request = new AbortController();
|
|
1780
|
+
session.activeRequest = request;
|
|
1781
|
+
notifyProcessing();
|
|
1782
|
+
try {
|
|
1783
|
+
const response = await this.g.fetch(this.route + "/transcribe", {
|
|
1784
|
+
method: "POST",
|
|
1785
|
+
credentials: "same-origin",
|
|
1786
|
+
headers: {
|
|
1787
|
+
"content-type": "audio/wav",
|
|
1788
|
+
"x-dlv-client-id": session.operation,
|
|
1789
|
+
"x-dlv-operation-id": id(),
|
|
1790
|
+
"x-dlv-language": lang
|
|
1791
|
+
},
|
|
1792
|
+
body: encodeMonoPcm16Wav(samples, context.sampleRate),
|
|
1793
|
+
signal: request.signal
|
|
1794
|
+
});
|
|
1795
|
+
const json = await response.json();
|
|
1796
|
+
if (!response.ok || !json?.ok)
|
|
1797
|
+
throw new Error(json?.error?.message || "HTTP transcription failed.");
|
|
1798
|
+
if (valid() && json.value.text) onResult?.({ final: json.value.text, interim: "" });
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
if (error.name !== "AbortError" && valid()) onError?.(error);
|
|
1801
|
+
} finally {
|
|
1802
|
+
if (session.activeRequest === request) session.activeRequest = null;
|
|
1803
|
+
notifyProcessing();
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1687
1806
|
} finally {
|
|
1688
|
-
session.
|
|
1807
|
+
session.draining = false;
|
|
1808
|
+
notifyProcessing();
|
|
1689
1809
|
}
|
|
1690
1810
|
};
|
|
1691
1811
|
processor.onaudioprocess = (event) => {
|
|
@@ -1701,13 +1821,24 @@ var WhisperHttpRecognitionEngine = class {
|
|
|
1701
1821
|
}
|
|
1702
1822
|
session.chunks.push(data);
|
|
1703
1823
|
session.samples += data.length;
|
|
1704
|
-
if (session.voiced && session.silence > context.sampleRate * (this.segmentation.silenceMs / 1e3) || session.samples > context.sampleRate *
|
|
1705
|
-
|
|
1824
|
+
if (session.voiced && session.silence > context.sampleRate * (this.segmentation.silenceMs / 1e3) || session.samples > context.sampleRate * this.maxUtteranceSeconds)
|
|
1825
|
+
enqueue();
|
|
1706
1826
|
};
|
|
1707
1827
|
source.connect(processor);
|
|
1828
|
+
session.finish = enqueue;
|
|
1708
1829
|
session.abort = () => this.stop();
|
|
1709
1830
|
signal?.addEventListener("abort", session.abort, { once: true });
|
|
1710
1831
|
}
|
|
1832
|
+
finish() {
|
|
1833
|
+
const session = this.session;
|
|
1834
|
+
if (!session) return;
|
|
1835
|
+
session.processor.onaudioprocess = null;
|
|
1836
|
+
try {
|
|
1837
|
+
this.meter?.source?.disconnect(session.processor);
|
|
1838
|
+
} catch {
|
|
1839
|
+
}
|
|
1840
|
+
session.finish?.();
|
|
1841
|
+
}
|
|
1711
1842
|
async stop() {
|
|
1712
1843
|
const session = this.session;
|
|
1713
1844
|
if (!session) return;
|
|
@@ -1723,8 +1854,10 @@ var WhisperHttpRecognitionEngine = class {
|
|
|
1723
1854
|
session.gain?.disconnect();
|
|
1724
1855
|
} catch {
|
|
1725
1856
|
}
|
|
1726
|
-
|
|
1727
|
-
session.
|
|
1857
|
+
session.transcriptionQueue = [];
|
|
1858
|
+
session.activeRequest?.abort();
|
|
1859
|
+
session.activeRequest = null;
|
|
1860
|
+
session.onProcessingChange?.({ queued: 0, active: false, pending: 0 });
|
|
1728
1861
|
}
|
|
1729
1862
|
};
|
|
1730
1863
|
|
|
@@ -2150,6 +2283,100 @@ function createQwenSettings(React2) {
|
|
|
2150
2283
|
};
|
|
2151
2284
|
}
|
|
2152
2285
|
|
|
2286
|
+
// src/client/releases.ts
|
|
2287
|
+
var CURRENT_VERSION = "0.2.3";
|
|
2288
|
+
var TESTED_DSH_VERSION = "0.1.6-alpha.2";
|
|
2289
|
+
var REPOSITORY_URL = "https://github.com/victorwads/dsh-live-voice";
|
|
2290
|
+
var RELEASES_URL = `${REPOSITORY_URL}/releases`;
|
|
2291
|
+
var LATEST_RELEASE_API_URL = "https://api.github.com/repos/victorwads/dsh-live-voice/releases/latest";
|
|
2292
|
+
var RELEASE_CHECK_STORAGE_KEY = "dsh-live-voice.latest-release-check";
|
|
2293
|
+
var RELEASE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2294
|
+
var LIVE_VOICE_BADGE_URL = "https://cdn.simpleicons.org/npm/white";
|
|
2295
|
+
var DSH_BADGE_URL = "https://cdn.simpleicons.org/deepseek/white";
|
|
2296
|
+
var TESTED_DSH_RELEASE_URL = `https://github.com/deepseek-ai/deepseek-harness/releases/tag/v${TESTED_DSH_VERSION}`;
|
|
2297
|
+
function versionParts(version) {
|
|
2298
|
+
const normalized = String(version || "").trim().replace(/^v/i, "").split("+", 1)[0];
|
|
2299
|
+
const [core = "", prerelease = ""] = normalized.split("-", 2);
|
|
2300
|
+
const numbers = core.split(".").map((part) => {
|
|
2301
|
+
const match = part.match(/^\d+/);
|
|
2302
|
+
return match ? Number(match[0]) : 0;
|
|
2303
|
+
});
|
|
2304
|
+
while (numbers.length < 3) numbers.push(0);
|
|
2305
|
+
return { numbers, prerelease: prerelease ? prerelease.split(".") : [] };
|
|
2306
|
+
}
|
|
2307
|
+
function compareIdentifier(left, right) {
|
|
2308
|
+
const leftNumber = /^\d+$/.test(left) ? Number(left) : null;
|
|
2309
|
+
const rightNumber = /^\d+$/.test(right) ? Number(right) : null;
|
|
2310
|
+
if (leftNumber !== null && rightNumber !== null) return Math.sign(leftNumber - rightNumber);
|
|
2311
|
+
if (leftNumber !== null) return -1;
|
|
2312
|
+
if (rightNumber !== null) return 1;
|
|
2313
|
+
return left.localeCompare(right);
|
|
2314
|
+
}
|
|
2315
|
+
function compareVersions(left, right) {
|
|
2316
|
+
const a = versionParts(left);
|
|
2317
|
+
const b = versionParts(right);
|
|
2318
|
+
for (let index = 0; index < Math.max(a.numbers.length, b.numbers.length); index++) {
|
|
2319
|
+
const difference = (a.numbers[index] || 0) - (b.numbers[index] || 0);
|
|
2320
|
+
if (difference) return Math.sign(difference);
|
|
2321
|
+
}
|
|
2322
|
+
if (!a.prerelease.length && !b.prerelease.length) return 0;
|
|
2323
|
+
if (!a.prerelease.length) return 1;
|
|
2324
|
+
if (!b.prerelease.length) return -1;
|
|
2325
|
+
for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index++) {
|
|
2326
|
+
if (a.prerelease[index] === void 0) return -1;
|
|
2327
|
+
if (b.prerelease[index] === void 0) return 1;
|
|
2328
|
+
const difference = compareIdentifier(a.prerelease[index], b.prerelease[index]);
|
|
2329
|
+
if (difference) return difference;
|
|
2330
|
+
}
|
|
2331
|
+
return 0;
|
|
2332
|
+
}
|
|
2333
|
+
function readCached(storage, now) {
|
|
2334
|
+
try {
|
|
2335
|
+
const cached = JSON.parse(storage?.getItem(RELEASE_CHECK_STORAGE_KEY) || "null");
|
|
2336
|
+
if (cached && Number.isFinite(cached.checkedAt) && now - cached.checkedAt >= 0 && now - cached.checkedAt < RELEASE_CHECK_INTERVAL_MS)
|
|
2337
|
+
return cached;
|
|
2338
|
+
} catch {
|
|
2339
|
+
}
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
function writeCached(storage, value) {
|
|
2343
|
+
try {
|
|
2344
|
+
storage?.setItem(RELEASE_CHECK_STORAGE_KEY, JSON.stringify(value));
|
|
2345
|
+
} catch {
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
async function checkLatestRelease({
|
|
2349
|
+
fetchImpl = globalThis.window?.fetch?.bind(globalThis.window),
|
|
2350
|
+
storage = globalThis.window?.localStorage,
|
|
2351
|
+
now = Date.now()
|
|
2352
|
+
} = {}) {
|
|
2353
|
+
const cached = readCached(storage, now);
|
|
2354
|
+
if (cached) return cached;
|
|
2355
|
+
const attempted = { checkedAt: now, release: null };
|
|
2356
|
+
writeCached(storage, attempted);
|
|
2357
|
+
if (typeof fetchImpl !== "function") return attempted;
|
|
2358
|
+
try {
|
|
2359
|
+
const response = await fetchImpl(LATEST_RELEASE_API_URL, {
|
|
2360
|
+
headers: { Accept: "application/vnd.github+json" }
|
|
2361
|
+
});
|
|
2362
|
+
if (!response.ok) return attempted;
|
|
2363
|
+
const body = await response.json();
|
|
2364
|
+
if (typeof body?.tag_name !== "string" || !body.tag_name.trim()) return attempted;
|
|
2365
|
+
const release = {
|
|
2366
|
+
tag: body.tag_name.trim(),
|
|
2367
|
+
url: typeof body.html_url === "string" && body.html_url.startsWith("https://github.com/") ? body.html_url : RELEASES_URL
|
|
2368
|
+
};
|
|
2369
|
+
const result = { checkedAt: now, release };
|
|
2370
|
+
writeCached(storage, result);
|
|
2371
|
+
return result;
|
|
2372
|
+
} catch {
|
|
2373
|
+
return attempted;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
function hasNewerRelease(release, currentVersion = CURRENT_VERSION) {
|
|
2377
|
+
return Boolean(release?.tag && compareVersions(release.tag, currentVersion) > 0);
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2153
2380
|
// src/client/components.ts
|
|
2154
2381
|
function createComponents(React2) {
|
|
2155
2382
|
const h = React2.createElement;
|
|
@@ -2443,6 +2670,17 @@ function createComponents(React2) {
|
|
|
2443
2670
|
const settings = state.settings || {};
|
|
2444
2671
|
const capabilities = state.capabilities || {};
|
|
2445
2672
|
const tabsId = React2.useId();
|
|
2673
|
+
const [latestRelease, setLatestRelease] = React2.useState(null);
|
|
2674
|
+
React2.useEffect(() => {
|
|
2675
|
+
let active = true;
|
|
2676
|
+
void checkLatestRelease().then((result) => {
|
|
2677
|
+
if (active) setLatestRelease(result.release);
|
|
2678
|
+
});
|
|
2679
|
+
return () => {
|
|
2680
|
+
active = false;
|
|
2681
|
+
};
|
|
2682
|
+
}, []);
|
|
2683
|
+
const updateAvailable = hasNewerRelease(latestRelease);
|
|
2446
2684
|
const tabs = [
|
|
2447
2685
|
{ id: "conversation", label: "Conversation" },
|
|
2448
2686
|
{ id: "speech", label: "Speech" },
|
|
@@ -2535,7 +2773,69 @@ function createComponents(React2) {
|
|
|
2535
2773
|
return h(
|
|
2536
2774
|
"section",
|
|
2537
2775
|
{ className: "dlv-settings", "aria-label": "Live Voice settings" },
|
|
2538
|
-
h(
|
|
2776
|
+
h(
|
|
2777
|
+
"div",
|
|
2778
|
+
{ className: "dlv-settings-heading" },
|
|
2779
|
+
h("h3", null, "Live Voice"),
|
|
2780
|
+
h(
|
|
2781
|
+
"div",
|
|
2782
|
+
{ className: "dlv-version-badges", "aria-label": "Version information" },
|
|
2783
|
+
h(
|
|
2784
|
+
"a",
|
|
2785
|
+
{
|
|
2786
|
+
className: "dlv-shields-badge",
|
|
2787
|
+
href: RELEASES_URL,
|
|
2788
|
+
target: "_blank",
|
|
2789
|
+
rel: "noreferrer",
|
|
2790
|
+
"aria-label": `DSH Live Voice v${CURRENT_VERSION}. Open releases`,
|
|
2791
|
+
title: `DSH Live Voice v${CURRENT_VERSION}`
|
|
2792
|
+
},
|
|
2793
|
+
h("img", { src: LIVE_VOICE_BADGE_URL, alt: "" }),
|
|
2794
|
+
h("span", null, `v${CURRENT_VERSION}`)
|
|
2795
|
+
),
|
|
2796
|
+
h(
|
|
2797
|
+
"a",
|
|
2798
|
+
{
|
|
2799
|
+
className: "dlv-shields-badge",
|
|
2800
|
+
href: TESTED_DSH_RELEASE_URL,
|
|
2801
|
+
target: "_blank",
|
|
2802
|
+
rel: "noreferrer",
|
|
2803
|
+
"aria-label": `Compatible with DSH v${TESTED_DSH_VERSION}. Open release`,
|
|
2804
|
+
title: `Compatible with DSH v${TESTED_DSH_VERSION}`
|
|
2805
|
+
},
|
|
2806
|
+
h("img", { src: DSH_BADGE_URL, alt: "" }),
|
|
2807
|
+
h("span", null, `v${TESTED_DSH_VERSION}`)
|
|
2808
|
+
)
|
|
2809
|
+
),
|
|
2810
|
+
h("span", { className: "dlv-heading-divider", "aria-hidden": true }),
|
|
2811
|
+
updateAvailable ? h(
|
|
2812
|
+
"a",
|
|
2813
|
+
{
|
|
2814
|
+
className: "dlv-version-badge dlv-update-badge",
|
|
2815
|
+
href: latestRelease.url,
|
|
2816
|
+
target: "_blank",
|
|
2817
|
+
rel: "noreferrer",
|
|
2818
|
+
"aria-label": `Update available: ${latestRelease.tag}. Open release`,
|
|
2819
|
+
title: `Update available: ${latestRelease.tag}`
|
|
2820
|
+
},
|
|
2821
|
+
h("span", { className: "dlv-update-icon", "aria-hidden": true }, "\u2191"),
|
|
2822
|
+
"Update available"
|
|
2823
|
+
) : null,
|
|
2824
|
+
updateAvailable ? h("span", { className: "dlv-heading-divider", "aria-hidden": true }) : null,
|
|
2825
|
+
h(
|
|
2826
|
+
"a",
|
|
2827
|
+
{
|
|
2828
|
+
className: "dlv-version-badge dlv-star-badge",
|
|
2829
|
+
href: REPOSITORY_URL,
|
|
2830
|
+
target: "_blank",
|
|
2831
|
+
rel: "noreferrer",
|
|
2832
|
+
"aria-label": "Star DSH Live Voice on GitHub",
|
|
2833
|
+
title: "Star DSH Live Voice on GitHub"
|
|
2834
|
+
},
|
|
2835
|
+
h("span", { className: "dlv-star-icon", "aria-hidden": true }, "\u2605"),
|
|
2836
|
+
"Star Us on GitHub"
|
|
2837
|
+
)
|
|
2838
|
+
),
|
|
2539
2839
|
onClose ? h(Button, { label: "Close voice settings", icon: "close", onClick: onClose }) : null,
|
|
2540
2840
|
h(
|
|
2541
2841
|
"div",
|
|
@@ -2744,11 +3044,28 @@ function createComponents(React2) {
|
|
|
2744
3044
|
event.target.value === "browser" && settings.recognitionLang === "auto" ? { recognitionEngine: "browser", recognitionLang: "pt-BR" } : { recognitionEngine: event.target.value }
|
|
2745
3045
|
)
|
|
2746
3046
|
},
|
|
2747
|
-
h("option", { value: "
|
|
2748
|
-
h("option", { value: "
|
|
2749
|
-
h("option", { value: "whisper-http" }, "Whisper
|
|
3047
|
+
h("option", { value: "browser" }, "Browser SpeechRecognition \u2014 Default option"),
|
|
3048
|
+
h("option", { value: "qwen-http" }, "Qwen3 ASR \u2014 HTTP API"),
|
|
3049
|
+
h("option", { value: "whisper-http" }, "Whisper \u2014 HTTP API"),
|
|
3050
|
+
h(
|
|
3051
|
+
"optgroup",
|
|
3052
|
+
{ label: "Coming Soon \u2014 vote on repo issues" },
|
|
3053
|
+
h("option", { value: "webgpu", disabled: true }, "Browser WebGPU Inference \u2014 Soon"),
|
|
3054
|
+
h(
|
|
3055
|
+
"option",
|
|
3056
|
+
{ value: "sherpa-onnx", disabled: true },
|
|
3057
|
+
"sherpa-onnx Streaming \u2014 Soon"
|
|
3058
|
+
),
|
|
3059
|
+
h("option", { value: "parakeet", disabled: true }, "NVIDIA Parakeet \u2014 Soon"),
|
|
3060
|
+
h("option", { value: "voxtral", disabled: true }, "Voxtral Realtime \u2014 Soon")
|
|
3061
|
+
)
|
|
2750
3062
|
)
|
|
2751
3063
|
),
|
|
3064
|
+
h(
|
|
3065
|
+
"small",
|
|
3066
|
+
{ className: "dlv-recognition-engine-description" },
|
|
3067
|
+
settings.recognitionEngine === "qwen-http" ? "HTTP API at the configured base URL (default: http://127.0.0.1:8080/). Compatible with POST /v1/audio/transcriptions." : settings.recognitionEngine === "whisper-http" ? "HTTP API at the configured DSH host URL (default: http://127.0.0.1:8080/inference). Audio uses the authenticated DSH host transcription route." : "Uses the browser SpeechRecognition API. This is the default option."
|
|
3068
|
+
),
|
|
2752
3069
|
field("Input device", "inputDeviceId", deviceOptions("audioinput", "Microphone")),
|
|
2753
3070
|
settings.recognitionEngine === "browser" ? h(
|
|
2754
3071
|
"small",
|
|
@@ -2894,6 +3211,28 @@ function createComponents(React2) {
|
|
|
2894
3211
|
null,
|
|
2895
3212
|
"Controls how long a pause must last before captured speech is sent for recognition."
|
|
2896
3213
|
),
|
|
3214
|
+
h(
|
|
3215
|
+
"label",
|
|
3216
|
+
{ className: "dlv-setting-field" },
|
|
3217
|
+
"Maximum continuous speech (seconds)",
|
|
3218
|
+
h("input", {
|
|
3219
|
+
type: "number",
|
|
3220
|
+
min: 10,
|
|
3221
|
+
max: 300,
|
|
3222
|
+
step: 1,
|
|
3223
|
+
value: settings.recognitionMaxUtteranceSeconds ?? 60,
|
|
3224
|
+
onChange: (event) => {
|
|
3225
|
+
const value = Number(event.target.value);
|
|
3226
|
+
if (Number.isInteger(value) && value >= 10 && value <= 300)
|
|
3227
|
+
invoke("updateSettings", { recognitionMaxUtteranceSeconds: value });
|
|
3228
|
+
}
|
|
3229
|
+
}),
|
|
3230
|
+
h(
|
|
3231
|
+
"small",
|
|
3232
|
+
null,
|
|
3233
|
+
"If speech never pauses, start a new transcription chunk after this duration. Default: 60 seconds."
|
|
3234
|
+
)
|
|
3235
|
+
),
|
|
2897
3236
|
h(
|
|
2898
3237
|
"div",
|
|
2899
3238
|
{
|
|
@@ -2961,6 +3300,21 @@ function createComponents(React2) {
|
|
|
2961
3300
|
{ key: "interrupt-message-description", className: "dlv-setting-description" },
|
|
2962
3301
|
settings.interruptSpeechOnUserMessage ? "Sending or steering a new user message stops current or paused assistant speech." : "Sending another message does not stop the assistant audio you are already hearing."
|
|
2963
3302
|
),
|
|
3303
|
+
h(
|
|
3304
|
+
"label",
|
|
3305
|
+
{ key: "hold-to-talk", className: "dlv-check" },
|
|
3306
|
+
h("input", {
|
|
3307
|
+
type: "checkbox",
|
|
3308
|
+
checked: settings.holdToTalkEnabled !== false,
|
|
3309
|
+
onChange: (event) => invoke("updateSettings", { holdToTalkEnabled: event.target.checked })
|
|
3310
|
+
}),
|
|
3311
|
+
" Hold Control to talk"
|
|
3312
|
+
),
|
|
3313
|
+
h(
|
|
3314
|
+
"p",
|
|
3315
|
+
{ key: "hold-to-talk-description", className: "dlv-setting-description" },
|
|
3316
|
+
"While a composer is open, hold Control anywhere on the page to capture speech. Release it to flush queued transcription, wait the configured send delay, queue the message, and close voice capture. Press Escape while holding to cancel."
|
|
3317
|
+
),
|
|
2964
3318
|
field("Listening mode", "mode", [
|
|
2965
3319
|
{ value: "speaker", label: "Speakers \u2014 gated listening" },
|
|
2966
3320
|
{ value: "headphones", label: "Headphones \u2014 open microphone" }
|
|
@@ -3066,13 +3420,15 @@ var styles = `
|
|
|
3066
3420
|
.dlv-error{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:13px;max-width:720px;margin:8px auto}
|
|
3067
3421
|
.dlv-settings{box-sizing:border-box;padding:18px;width:100%;display:grid;gap:16px;max-width:640px;color:var(--dsw-alias-label-primary)}
|
|
3068
3422
|
.dlv-settings h3,.dlv-settings p{margin:0}
|
|
3423
|
+
.dlv-settings-heading{display:flex;align-items:center;gap:8px;min-width:0}.dlv-settings-heading h3{white-space:nowrap}.dlv-heading-divider{height:1px;min-width:12px;flex:1;background:var(--dsw-alias-border-l1)}.dlv-settings-heading .dlv-star-badge{flex:none}
|
|
3424
|
+
.dlv-version-badges{display:flex;flex:none;gap:5px;align-items:center}.dlv-shields-badge{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;height:20px;gap:4px;padding:3px 5px;border:1px solid var(--dsw-alias-border-l1);border-radius:5px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:10px;font-weight:650;line-height:12px;text-decoration:none}.dlv-shields-badge img{display:block;width:12px;height:12px;max-width:100%}.dlv-shields-badge:focus-visible{border-radius:4px;outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}.dlv-version-badge{display:inline-flex;align-items:center;min-height:24px;box-sizing:border-box;padding:3px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:650;line-height:16px;text-decoration:none}.dlv-version-badges a.dlv-version-badge:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}.dlv-star-badge{padding:4px 10px;color:var(--dsw-alias-label-primary)}.dlv-update-icon{margin-right:4px;font-size:14px;line-height:1}.dlv-star-icon{margin-right:4px;color:#f59e0b;font-size:14px;line-height:1;text-shadow:0 0 8px rgba(245,158,11,.28)}.dlv-version-badges a.dlv-star-badge:hover{border-color:#f59e0b;background:rgba(245,158,11,.08)}.dlv-update-badge{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}
|
|
3069
3425
|
.dlv-settings-tabs{border-bottom:.5px solid var(--dsw-alias-border-l2);display:flex;align-items:flex-end;gap:22px;overflow-x:auto}.dlv-settings-tab{position:relative;flex:none;padding:7px 1px 9px;border:0;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:13px;line-height:20px;cursor:pointer}.dlv-settings-tab:hover,.dlv-settings-tab[data-active=true]{color:var(--dsw-alias-label-primary)}.dlv-settings-tab[data-active=true]:after,.dlv-settings-tab:focus-visible:after{content:"";position:absolute;right:0;bottom:-1px;left:0;height:2px;border-radius:2px 2px 0 0;background:var(--dsw-alias-label-primary)}.dlv-settings-tab:focus-visible{border-radius:2px;outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;color:var(--dsw-alias-label-primary)}.dlv-settings-panel{min-width:0;padding-top:2px}.dlv-settings-panel[hidden]{display:none}.dlv-settings-group{margin:0;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;min-width:0;display:grid;gap:14px;padding:16px}.dlv-settings-group legend{padding:0 6px;font-weight:600;color:var(--dsw-alias-label-primary)}.dlv-settings-card-body{display:grid;gap:12px;padding:12px 0}.dlv-settings-subcard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;min-width:0}.dlv-settings-subcard>summary{cursor:pointer;font-weight:650;list-style:none;display:flex;align-items:center;justify-content:space-between;padding:10px}.dlv-settings-subcard>summary::-webkit-details-marker{display:none}.dlv-settings-subcard>summary:after{content:"\u203A";transform:rotate(90deg);transition:transform .15s}.dlv-settings-subcard:not([open])>summary:after{transform:rotate(0)}.dlv-settings-subcard-body{display:grid;gap:10px;padding:0 10px 10px}
|
|
3070
3426
|
.dlv-settings label{display:grid;gap:6px;font-size:14px}.dlv-settings label.dlv-check{display:flex;align-items:center;gap:8px}.dlv-settings label.dlv-check input{width:auto}
|
|
3071
3427
|
.dlv-settings :is(input,select,textarea){box-sizing:border-box;width:100%;padding:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:inherit;font:inherit}
|
|
3072
3428
|
.dlv-settings small,.dlv-settings p{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}
|
|
3073
3429
|
.dlv-preset-group{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.dlv-settings label.dlv-preset{display:flex;align-items:flex-start;gap:8px;padding:10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;cursor:pointer}.dlv-settings label.dlv-preset:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.dlv-settings label.dlv-preset input{width:auto;margin-top:3px}.dlv-preset span{display:grid;gap:3px}.dlv-vad-summary{font-weight:500}
|
|
3074
3430
|
.dlv-settings-actions{display:flex;flex-wrap:wrap;gap:8px}.dlv-settings-actions button{padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:inherit;cursor:pointer}.dlv-settings-actions button:disabled{opacity:.45;cursor:default}
|
|
3075
|
-
@media(max-width:480px){.dlv-preset-group{grid-template-columns:1fr}.dlv-pill{flex-wrap:wrap}.dlv-wave{flex-basis:90px}.dlv-status{flex-basis:100px}}
|
|
3431
|
+
@media(max-width:480px){.dlv-settings-heading{gap:6px}.dlv-heading-divider{min-width:4px}.dlv-settings-heading .dlv-star-badge{padding:3px 6px;font-size:10px}.dlv-preset-group{grid-template-columns:1fr}.dlv-pill{flex-wrap:wrap}.dlv-wave{flex-basis:90px}.dlv-status{flex-basis:100px}}
|
|
3076
3432
|
`;
|
|
3077
3433
|
|
|
3078
3434
|
// src/client/chat.ts
|
|
@@ -3120,12 +3476,22 @@ function apply(ctx) {
|
|
|
3120
3476
|
let disposed = false;
|
|
3121
3477
|
let voiceModeActive = false;
|
|
3122
3478
|
const ownership = new VoiceOwnership();
|
|
3479
|
+
const recognitionSettingKeys = /* @__PURE__ */ new Set([
|
|
3480
|
+
"recognitionEngine",
|
|
3481
|
+
"recognitionProcessLocally",
|
|
3482
|
+
"recognitionAutoInstall",
|
|
3483
|
+
"voiceDetectionPreset",
|
|
3484
|
+
"recognitionMaxUtteranceSeconds"
|
|
3485
|
+
]);
|
|
3486
|
+
const changesRecognition = (next) => Object.keys(next).some((key) => recognitionSettingKeys.has(key));
|
|
3123
3487
|
const recognitionFor = (settings, meter) => settings.recognitionEngine === "qwen-http" ? new QwenHttpRecognitionEngine({
|
|
3124
3488
|
meter,
|
|
3125
|
-
voiceDetectionPreset: settings.voiceDetectionPreset
|
|
3489
|
+
voiceDetectionPreset: settings.voiceDetectionPreset,
|
|
3490
|
+
maxUtteranceSeconds: settings.recognitionMaxUtteranceSeconds
|
|
3126
3491
|
}) : settings.recognitionEngine === "whisper-http" ? new WhisperHttpRecognitionEngine({
|
|
3127
3492
|
meter,
|
|
3128
|
-
voiceDetectionPreset: settings.voiceDetectionPreset
|
|
3493
|
+
voiceDetectionPreset: settings.voiceDetectionPreset,
|
|
3494
|
+
maxUtteranceSeconds: settings.recognitionMaxUtteranceSeconds
|
|
3129
3495
|
}) : new BrowserRecognitionEngine({
|
|
3130
3496
|
processLocally: settings.recognitionProcessLocally,
|
|
3131
3497
|
autoInstallLocalPack: settings.recognitionAutoInstall
|
|
@@ -3202,7 +3568,8 @@ function apply(ctx) {
|
|
|
3202
3568
|
if (capture.index < capture.interaction.questions.length) {
|
|
3203
3569
|
capture.answering = false;
|
|
3204
3570
|
const text = pendingQuestionSpeech(capture.interaction, capture.index);
|
|
3205
|
-
if (text)
|
|
3571
|
+
if (text)
|
|
3572
|
+
run(entry.controller, entry.controller.speak(text, capture.interaction.key));
|
|
3206
3573
|
} else {
|
|
3207
3574
|
entry.questionCapture = null;
|
|
3208
3575
|
entry.controller.patch({ answeringQuestion: false });
|
|
@@ -3303,7 +3670,7 @@ function apply(ctx) {
|
|
|
3303
3670
|
return original(...args);
|
|
3304
3671
|
};
|
|
3305
3672
|
}
|
|
3306
|
-
for (const method of ["startDictation", "startConversation", "speak"]) {
|
|
3673
|
+
for (const method of ["startDictation", "startHoldToTalk", "startConversation", "speak"]) {
|
|
3307
3674
|
const original = controller[method].bind(controller);
|
|
3308
3675
|
controller[method] = (...args) => {
|
|
3309
3676
|
if (disposed || entry.closed || !entry.refs) return Promise.resolve();
|
|
@@ -3414,13 +3781,11 @@ function apply(ctx) {
|
|
|
3414
3781
|
let settingsRevision = 0;
|
|
3415
3782
|
c.updateSettings = async (next) => {
|
|
3416
3783
|
const revision = ++settingsRevision;
|
|
3417
|
-
const previousEngine = c.getSnapshot().settings.recognitionEngine;
|
|
3418
3784
|
update(next);
|
|
3419
3785
|
browser.lang = c.getSnapshot().settings.lang;
|
|
3420
3786
|
qwen.lang = c.getSnapshot().settings.lang;
|
|
3421
3787
|
const settings2 = c.getSnapshot().settings;
|
|
3422
|
-
if (
|
|
3423
|
-
c.replaceRecognition(recognitionFor(settings2, c.meter));
|
|
3788
|
+
if (changesRecognition(next)) c.replaceRecognition(recognitionFor(settings2, c.meter));
|
|
3424
3789
|
localStorage.setItem("dsh-live-voice.settings", JSON.stringify(settings2));
|
|
3425
3790
|
run(c, c.refreshCapabilities());
|
|
3426
3791
|
const active = [...controllers.values()];
|
|
@@ -3431,7 +3796,7 @@ function apply(ctx) {
|
|
|
3431
3796
|
if (revision !== settingsRevision || disposed || c.disposed) return;
|
|
3432
3797
|
for (const entry of controllers.values()) {
|
|
3433
3798
|
if (entry.closed) continue;
|
|
3434
|
-
if (
|
|
3799
|
+
if (changesRecognition(next))
|
|
3435
3800
|
entry.controller.replaceRecognition(recognitionFor(settings2, entry.controller.meter));
|
|
3436
3801
|
entry.applySettings(settings2);
|
|
3437
3802
|
run(entry.controller, entry.controller.refreshCapabilities());
|
|
@@ -3548,19 +3913,47 @@ function apply(ctx) {
|
|
|
3548
3913
|
for (const entry of controllers.values())
|
|
3549
3914
|
run(entry.controller, entry.controller.endConversation());
|
|
3550
3915
|
};
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
return;
|
|
3916
|
+
let holdToTalk = null;
|
|
3917
|
+
const candidate = () => {
|
|
3554
3918
|
const candidates = [...controllers.values()].filter(
|
|
3555
3919
|
(entry) => entry.buttons > 0 && entry.composers.size > 0
|
|
3556
3920
|
);
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
);
|
|
3921
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
3922
|
+
};
|
|
3923
|
+
const releaseHoldToTalk = () => {
|
|
3924
|
+
const entry = holdToTalk;
|
|
3925
|
+
holdToTalk = null;
|
|
3926
|
+
if (!entry || entry.closed) return;
|
|
3927
|
+
run(entry.controller, entry.controller.releaseHoldToTalk());
|
|
3928
|
+
};
|
|
3929
|
+
const onKeyDown = (event) => {
|
|
3930
|
+
if (disposed || event.defaultPrevented || event.repeat) return;
|
|
3931
|
+
if (event.key === "Escape" && holdToTalk) {
|
|
3932
|
+
const entry2 = holdToTalk;
|
|
3933
|
+
holdToTalk = null;
|
|
3934
|
+
event.preventDefault();
|
|
3935
|
+
run(entry2.controller, entry2.controller.cancelDictation());
|
|
3936
|
+
return;
|
|
3937
|
+
}
|
|
3938
|
+
if (event.ctrlKey && event.shiftKey && event.code === "Space") {
|
|
3939
|
+
const entry2 = candidate();
|
|
3940
|
+
if (!entry2) return;
|
|
3941
|
+
event.preventDefault();
|
|
3942
|
+
const c = entry2.controller;
|
|
3943
|
+
run(
|
|
3944
|
+
c,
|
|
3945
|
+
c.getSnapshot().listening || c.getSnapshot().starting ? c.stopListening() : c.startDictation()
|
|
3946
|
+
);
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
if (event.key !== "Control" || event.altKey || event.metaKey || event.shiftKey) return;
|
|
3950
|
+
const entry = candidate();
|
|
3951
|
+
if (!entry || !entry.controller.getSnapshot().settings.holdToTalkEnabled) return;
|
|
3952
|
+
holdToTalk = entry;
|
|
3953
|
+
run(entry.controller, entry.controller.startHoldToTalk());
|
|
3954
|
+
};
|
|
3955
|
+
const onKeyUp = (event) => {
|
|
3956
|
+
if (event.key === "Control") releaseHoldToTalk();
|
|
3564
3957
|
};
|
|
3565
3958
|
const refreshCapabilities = () => {
|
|
3566
3959
|
for (const entry of controllers.values())
|
|
@@ -3573,7 +3966,9 @@ function apply(ctx) {
|
|
|
3573
3966
|
window.speechSynthesis?.addEventListener?.("voiceschanged", refreshCapabilities);
|
|
3574
3967
|
navigator.mediaDevices?.addEventListener?.("devicechange", refreshCapabilities);
|
|
3575
3968
|
document.addEventListener("visibilitychange", visibilityChanged);
|
|
3576
|
-
document.addEventListener("keydown",
|
|
3969
|
+
document.addEventListener("keydown", onKeyDown);
|
|
3970
|
+
document.addEventListener("keyup", onKeyUp);
|
|
3971
|
+
window.addEventListener("blur", releaseHoldToTalk);
|
|
3577
3972
|
window.addEventListener("pagehide", stop);
|
|
3578
3973
|
return () => {
|
|
3579
3974
|
disposed = true;
|
|
@@ -3581,7 +3976,9 @@ function apply(ctx) {
|
|
|
3581
3976
|
window.speechSynthesis?.removeEventListener?.("voiceschanged", refreshCapabilities);
|
|
3582
3977
|
navigator.mediaDevices?.removeEventListener?.("devicechange", refreshCapabilities);
|
|
3583
3978
|
document.removeEventListener("visibilitychange", visibilityChanged);
|
|
3584
|
-
document.removeEventListener("keydown",
|
|
3979
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
3980
|
+
document.removeEventListener("keyup", onKeyUp);
|
|
3981
|
+
window.removeEventListener("blur", releaseHoldToTalk);
|
|
3585
3982
|
window.removeEventListener("pagehide", stop);
|
|
3586
3983
|
for (const entry of controllers.values()) retire(entry);
|
|
3587
3984
|
};
|