dsh-live-voice 0.7.1 → 0.7.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/lib/asr/asr.worker.js +1 -1
- package/lib/asr/manifest.json +1 -1
- package/lib/client.js +63 -20
- package/lib/server.js +2 -2
- package/lib/vad/manifest.json +2 -2
- package/lib/vad/vad.worker.js +1 -1
- package/package.json +1 -1
- package/src/app/ARCHITECTURE.md +1 -0
- package/src/app/client/apply.tsx +17 -3
- package/src/modules/core/coordinator.ts +64 -14
package/lib/asr/asr.worker.js
CHANGED
|
@@ -36682,7 +36682,7 @@ async function createQwenRuntime(options) {
|
|
|
36682
36682
|
}
|
|
36683
36683
|
|
|
36684
36684
|
// src/modules/recognition/local/assets.ts
|
|
36685
|
-
var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.
|
|
36685
|
+
var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.2" : "test") + "/";
|
|
36686
36686
|
|
|
36687
36687
|
// src/modules/recognition/local/modelCatalog.ts
|
|
36688
36688
|
var browserAsrModels = [
|
package/lib/asr/manifest.json
CHANGED
package/lib/client.js
CHANGED
|
@@ -152,7 +152,7 @@ var SessionSpeechMemory = class {
|
|
|
152
152
|
};
|
|
153
153
|
|
|
154
154
|
// src/modules/recognition/vad/assets.ts
|
|
155
|
-
var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.
|
|
155
|
+
var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.2" : "test") + "/";
|
|
156
156
|
async function sileroVadAvailable(fetchImpl = globalThis.fetch) {
|
|
157
157
|
try {
|
|
158
158
|
const response = await fetchImpl(sileroVadAssetBase() + "availability", {
|
|
@@ -3507,7 +3507,7 @@ async function removeBrowserModel(key, storage = caches) {
|
|
|
3507
3507
|
}
|
|
3508
3508
|
|
|
3509
3509
|
// src/modules/recognition/local/assets.ts
|
|
3510
|
-
var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.
|
|
3510
|
+
var browserAsrAssetBase = () => "/api/dsh-live-voice/asr/" + encodeURIComponent(true ? "0.7.2" : "test") + "/";
|
|
3511
3511
|
|
|
3512
3512
|
// src/modules/recognition/local/runtimeIdentity.ts
|
|
3513
3513
|
var workerProtocol = "asr-worker-v3-ort-1.31.0-dev.20260914-8d85527a0";
|
|
@@ -10662,9 +10662,9 @@ var VoiceCoordinator = class _VoiceCoordinator {
|
|
|
10662
10662
|
return await this.recognition.suspendCapture() !== false;
|
|
10663
10663
|
});
|
|
10664
10664
|
}
|
|
10665
|
-
async _resumeInputCapture() {
|
|
10665
|
+
async _resumeInputCapture(valid = () => true) {
|
|
10666
10666
|
return this._input(async () => {
|
|
10667
|
-
if (!this.snapshot.listening || typeof this.recognition.resumeCapture !== "function")
|
|
10667
|
+
if (!valid() || !this.snapshot.listening || typeof this.recognition.resumeCapture !== "function")
|
|
10668
10668
|
return false;
|
|
10669
10669
|
return await this.recognition.resumeCapture() !== false;
|
|
10670
10670
|
});
|
|
@@ -11458,22 +11458,53 @@ var VoiceCoordinator = class _VoiceCoordinator {
|
|
|
11458
11458
|
getSpeechProgress() {
|
|
11459
11459
|
return this.engines[this.snapshot.speechEngine]?.getPlaybackProgress?.() ?? null;
|
|
11460
11460
|
}
|
|
11461
|
+
async _restorePausedSpeechInput(epoch, control) {
|
|
11462
|
+
const valid = () => !this.disposed && !this.navigationPaused && epoch === this.speechEpoch && control === this.controlEpoch && this.snapshot.paused && this.snapshot.settings.mode === "speaker";
|
|
11463
|
+
if (!valid()) return;
|
|
11464
|
+
try {
|
|
11465
|
+
if (this._hasSuspendedInputCapture()) {
|
|
11466
|
+
await this._resumeInputCapture(valid);
|
|
11467
|
+
} else if (this.snapshot.conversation && !this.snapshot.listening && !this.snapshot.starting && !this.snapshot.inputRecovery) {
|
|
11468
|
+
await this._startInput(true);
|
|
11469
|
+
}
|
|
11470
|
+
} catch (error) {
|
|
11471
|
+
if (valid()) this.patch({ inputError: message(error), error: message(error) });
|
|
11472
|
+
}
|
|
11473
|
+
}
|
|
11474
|
+
async _gateResumedSpeechInput() {
|
|
11475
|
+
if (this.snapshot.settings.mode !== "speaker") return;
|
|
11476
|
+
const suspended = await this._suspendInputCapture();
|
|
11477
|
+
if (!suspended) {
|
|
11478
|
+
await this.stopListening();
|
|
11479
|
+
if (this.inputReleaseError) throw this.inputReleaseError;
|
|
11480
|
+
}
|
|
11481
|
+
}
|
|
11461
11482
|
async pauseSpeech(automatic = false) {
|
|
11462
11483
|
if (this.snapshot.speechRecovery?.retrying) this._cancelRecovery("speech", true);
|
|
11463
11484
|
if (!automatic) {
|
|
11464
11485
|
this._cancelInterruptionResume();
|
|
11465
11486
|
this.interruptionPausedSpeech = false;
|
|
11466
11487
|
}
|
|
11467
|
-
if (this.disposed
|
|
11488
|
+
if (this.disposed) return;
|
|
11489
|
+
if (this.snapshot.paused) {
|
|
11490
|
+
const epoch2 = this.speechEpoch;
|
|
11491
|
+
const control2 = ++this.controlEpoch;
|
|
11492
|
+
await this.inputTail;
|
|
11493
|
+
await this._restorePausedSpeechInput(epoch2, control2);
|
|
11494
|
+
return;
|
|
11495
|
+
}
|
|
11468
11496
|
if (!this.snapshot.speaking && this.snapshot.speechRunActive) {
|
|
11469
11497
|
this._cancelAssistantSpeechTimer();
|
|
11498
|
+
const control2 = ++this.controlEpoch;
|
|
11470
11499
|
this.patch({ paused: true });
|
|
11500
|
+
await this._restorePausedSpeechInput(this.speechEpoch, control2);
|
|
11471
11501
|
return;
|
|
11472
11502
|
}
|
|
11473
11503
|
if (!this.snapshot.speaking) return;
|
|
11474
11504
|
if (this.speechLaunchPending) {
|
|
11475
|
-
++this.controlEpoch;
|
|
11505
|
+
const control2 = ++this.controlEpoch;
|
|
11476
11506
|
this.patch({ paused: true });
|
|
11507
|
+
await this._restorePausedSpeechInput(this.speechEpoch, control2);
|
|
11477
11508
|
return;
|
|
11478
11509
|
}
|
|
11479
11510
|
const epoch = this.speechEpoch;
|
|
@@ -11484,6 +11515,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
|
|
|
11484
11515
|
if (epoch === this.speechEpoch && control === this.controlEpoch && this.snapshot.speaking && result !== false) {
|
|
11485
11516
|
this.patch({ paused: true });
|
|
11486
11517
|
this.activeRecordingTimeline?.("assistant-pause");
|
|
11518
|
+
await this._restorePausedSpeechInput(epoch, control);
|
|
11487
11519
|
}
|
|
11488
11520
|
} catch (error) {
|
|
11489
11521
|
if (epoch === this.speechEpoch) this.patch({ error: message(error) });
|
|
@@ -11503,12 +11535,11 @@ var VoiceCoordinator = class _VoiceCoordinator {
|
|
|
11503
11535
|
if (this.speechLaunchPending) {
|
|
11504
11536
|
const epoch2 = this.speechEpoch;
|
|
11505
11537
|
const control2 = ++this.controlEpoch;
|
|
11506
|
-
|
|
11507
|
-
await this.
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
}
|
|
11538
|
+
try {
|
|
11539
|
+
await this._gateResumedSpeechInput();
|
|
11540
|
+
} catch (error) {
|
|
11541
|
+
if (epoch2 === this.speechEpoch) this.patch({ error: message(error) });
|
|
11542
|
+
return;
|
|
11512
11543
|
}
|
|
11513
11544
|
if (epoch2 === this.speechEpoch && control2 === this.controlEpoch)
|
|
11514
11545
|
this.patch({ paused: false });
|
|
@@ -11517,19 +11548,19 @@ var VoiceCoordinator = class _VoiceCoordinator {
|
|
|
11517
11548
|
const epoch = this.speechEpoch;
|
|
11518
11549
|
const control = ++this.controlEpoch;
|
|
11519
11550
|
try {
|
|
11520
|
-
|
|
11521
|
-
await this.stopListening();
|
|
11522
|
-
if (this.inputReleaseError) throw this.inputReleaseError;
|
|
11523
|
-
}
|
|
11551
|
+
await this._gateResumedSpeechInput();
|
|
11524
11552
|
if (epoch !== this.speechEpoch || control !== this.controlEpoch) return;
|
|
11525
11553
|
const result = await this.engines[this.snapshot.speechEngine || this.snapshot.settings.engine].resume();
|
|
11526
11554
|
if (epoch === this.speechEpoch && control === this.controlEpoch && result !== false) {
|
|
11527
11555
|
this.patch({ paused: false });
|
|
11528
11556
|
this.activeRecordingTimeline?.("assistant-resume");
|
|
11529
11557
|
this._prefetchSpeech();
|
|
11558
|
+
} else {
|
|
11559
|
+
await this._restorePausedSpeechInput(epoch, control);
|
|
11530
11560
|
}
|
|
11531
11561
|
} catch (error) {
|
|
11532
11562
|
if (epoch === this.speechEpoch) this.patch({ error: message(error) });
|
|
11563
|
+
await this._restorePausedSpeechInput(epoch, control);
|
|
11533
11564
|
}
|
|
11534
11565
|
}
|
|
11535
11566
|
async speak(text, messageId = null) {
|
|
@@ -11624,6 +11655,7 @@ var VoiceCoordinator = class _VoiceCoordinator {
|
|
|
11624
11655
|
if (gating) timeline?.("input-stop-complete");
|
|
11625
11656
|
}
|
|
11626
11657
|
if (epoch !== this.speechEpoch || this.disposed) return;
|
|
11658
|
+
if (this.snapshot.paused) await this._restorePausedSpeechInput(epoch, this.controlEpoch);
|
|
11627
11659
|
const queued = item;
|
|
11628
11660
|
if (queued?.preparing) await queued.preparing;
|
|
11629
11661
|
if (epoch !== this.speechEpoch || this.disposed) return;
|
|
@@ -14769,7 +14801,7 @@ function createDeviceOptions(devices, kind, systemDefault, fallback) {
|
|
|
14769
14801
|
var import_react39 = __toESM(require("react"), 1);
|
|
14770
14802
|
|
|
14771
14803
|
// src/modules/settings/services/releases.ts
|
|
14772
|
-
var CURRENT_VERSION = "0.7.
|
|
14804
|
+
var CURRENT_VERSION = "0.7.2";
|
|
14773
14805
|
var TESTED_DSH_VERSION = "0.2.2-rc.1";
|
|
14774
14806
|
var REPOSITORY_URL = "https://github.com/victorwads/dsh-live-voice";
|
|
14775
14807
|
var RELEASES_URL = `${REPOSITORY_URL}/releases`;
|
|
@@ -18081,7 +18113,7 @@ function apply(ctx) {
|
|
|
18081
18113
|
entry.chatListeners.add(listener);
|
|
18082
18114
|
return () => entry.chatListeners.delete(listener);
|
|
18083
18115
|
};
|
|
18084
|
-
entry.readChat = entry.chat.getSnapshot
|
|
18116
|
+
entry.readChat = () => entry.chat.getSnapshot();
|
|
18085
18117
|
const refresh = (reset = false) => {
|
|
18086
18118
|
if (disposed || entry.closed) return;
|
|
18087
18119
|
const snapshot = entry.readChat();
|
|
@@ -18109,10 +18141,20 @@ function apply(ctx) {
|
|
|
18109
18141
|
};
|
|
18110
18142
|
entry.refreshSpeech = refresh;
|
|
18111
18143
|
refresh();
|
|
18112
|
-
|
|
18144
|
+
const publishChat = () => {
|
|
18113
18145
|
refresh();
|
|
18114
18146
|
for (const listener of entry.chatListeners) listener();
|
|
18115
|
-
}
|
|
18147
|
+
};
|
|
18148
|
+
entry.unsubscribe = entry.chat.subscribe(publishChat);
|
|
18149
|
+
entry.reconnectChat = () => {
|
|
18150
|
+
if (disposed || entry.closed) return;
|
|
18151
|
+
const chat = ctx.uiConversation.binding(sessionId).target("chat");
|
|
18152
|
+
if (chat === entry.chat) return;
|
|
18153
|
+
entry.unsubscribe?.();
|
|
18154
|
+
entry.chat = chat;
|
|
18155
|
+
entry.unsubscribe = chat.subscribe(publishChat);
|
|
18156
|
+
publishChat();
|
|
18157
|
+
};
|
|
18116
18158
|
const pendingInteractions = ctx.uiSession.pendingInteractions;
|
|
18117
18159
|
const refreshPendingQuestion = (baseline = false) => {
|
|
18118
18160
|
if (disposed || entry.closed || !pendingInteractions) return;
|
|
@@ -18289,6 +18331,7 @@ function apply(ctx) {
|
|
|
18289
18331
|
import_react75.default.useLayoutEffect(() => {
|
|
18290
18332
|
if (disposed) return;
|
|
18291
18333
|
const current = get(sessionId);
|
|
18334
|
+
current.reconnectChat();
|
|
18292
18335
|
current.refs++;
|
|
18293
18336
|
if (kind === "buttons") current.buttons++;
|
|
18294
18337
|
setEntry(current);
|
package/lib/server.js
CHANGED
|
@@ -2052,7 +2052,7 @@ var VAD_ASSET_TYPES = {
|
|
|
2052
2052
|
};
|
|
2053
2053
|
var MAX_VAD_ASSET_BYTES = 32 * 1024 * 1024;
|
|
2054
2054
|
function registerVadAssetRoutes(ctx, options = {}) {
|
|
2055
|
-
const version = options.version ?? (true ? "0.7.
|
|
2055
|
+
const version = options.version ?? (true ? "0.7.2" : "test");
|
|
2056
2056
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9.+-]{0,127}$/.test(version))
|
|
2057
2057
|
throw new Error("Invalid VAD asset version");
|
|
2058
2058
|
const directory = options.directory ?? fileURLToPath(new URL("./vad/", import.meta.url));
|
|
@@ -2146,7 +2146,7 @@ import { lstat as lstat2, readFile as readFile5 } from "node:fs/promises";
|
|
|
2146
2146
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2147
2147
|
import { join as join8 } from "node:path";
|
|
2148
2148
|
function registerBrowserAsrAssets(ctx, options = {}) {
|
|
2149
|
-
const version = options.version ?? (true ? "0.7.
|
|
2149
|
+
const version = options.version ?? (true ? "0.7.2" : "test");
|
|
2150
2150
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9.+-]{0,127}$/.test(version)) throw new Error("invalid-version");
|
|
2151
2151
|
const base = "/api/dsh-live-voice/asr/" + encodeURIComponent(version) + "/";
|
|
2152
2152
|
const directory = options.directory ?? fileURLToPath2(new URL("./asr/", import.meta.url));
|
package/lib/vad/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"pluginVersion": "0.7.
|
|
3
|
+
"pluginVersion": "0.7.2",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@ricky0123/vad-web": "0.0.31",
|
|
6
6
|
"onnxruntime-web": "1.22.0"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"vad.worker.js": {
|
|
43
43
|
"bytes": 79337,
|
|
44
|
-
"sha256": "
|
|
44
|
+
"sha256": "6b83547e105d6a61b37f3647687021afcab2162d707071e7bffd9a6ffecade58"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
}
|
package/lib/vad/vad.worker.js
CHANGED
|
@@ -1632,7 +1632,7 @@ var vo = Ke;
|
|
|
1632
1632
|
Object.defineProperty(I.versions, "web", { value: $t, enumerable: true });
|
|
1633
1633
|
|
|
1634
1634
|
// src/modules/recognition/vad/assets.ts
|
|
1635
|
-
var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.
|
|
1635
|
+
var sileroVadAssetBase = () => "/api/dsh-live-voice/vad/" + encodeURIComponent(true ? "0.7.2" : "test") + "/";
|
|
1636
1636
|
|
|
1637
1637
|
// src/modules/recognition/vad/StreamingResampler.ts
|
|
1638
1638
|
var StreamingResampler = class {
|
package/package.json
CHANGED
package/src/app/ARCHITECTURE.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
`src/app` is the composition boundary between DSH and the voice-domain modules. Client and server entry points remain separate; host-only Node dependencies must never enter the browser bundle.
|
|
4
4
|
|
|
5
5
|
- `client/apply.tsx` injects DSH client services, creates session controllers and voice ownership, wires engines and settings, and disposes resources. `client/slotDefinitions.ts` and `client/registerSlots.tsx` own the established slot IDs, order, and registration. Preserve those contracts when changing placement. Microphone mode stop invalidates pending input ownership and application continuity without ending current playback; explicit takeover and full End retain their separate handoff/lifecycle semantics.
|
|
6
|
+
- Retained session entries re-resolve the current DSH chat target on committed slot attachment. A replaced target releases its old subscription and republishes through stable readers/listeners without resetting session speech memory; detached-session admission still prevents background playback.
|
|
6
7
|
- Composer detachment parks eligible recording-backed microphone controllers in the existing session registry rather than retiring them. Their capture clock pauses, recognition requests continue, and final results stay on the originating controller. Returning uses the existing ownership barrier before resuming capture and delivering queued results against the latest draft. Explicit stop clears parked intent; plugin disposal retires both mounted and parked entries. This memory is page-lifetime only, not reload recovery.
|
|
7
8
|
- `server/apply.ts` composes host engines and providers. `server/registerRoutes.ts` registers authenticated same-origin routes; do not bypass their validation or expose host access directly to browser code.
|
|
8
9
|
- `client/transcriptionLeaveGuard.ts` owns one activity-scoped native `beforeunload` listener across microphone, meeting sources and Settings test capture. It covers acquisition, listening and unsettled transcription, not playback or voice-mode preference alone. Cancelling the browser dialog does not stop capture; existing `pagehide` cleanup runs only after leaving. Idle and disposed apps remove the listener. Browser wording, user-activation requirements and mobile/forced-close limitations apply; this is not refresh recovery.
|
package/src/app/client/apply.tsx
CHANGED
|
@@ -690,7 +690,8 @@ export function apply(ctx) {
|
|
|
690
690
|
entry.chatListeners.add(listener);
|
|
691
691
|
return () => entry.chatListeners.delete(listener);
|
|
692
692
|
};
|
|
693
|
-
|
|
693
|
+
// Stable React store readers must follow a replacement DSH session binding.
|
|
694
|
+
entry.readChat = () => entry.chat.getSnapshot();
|
|
694
695
|
const refresh = (reset = false) => {
|
|
695
696
|
if (disposed || entry.closed) return;
|
|
696
697
|
const snapshot = entry.readChat();
|
|
@@ -729,10 +730,22 @@ export function apply(ctx) {
|
|
|
729
730
|
};
|
|
730
731
|
entry.refreshSpeech = refresh;
|
|
731
732
|
refresh();
|
|
732
|
-
|
|
733
|
+
const publishChat = () => {
|
|
733
734
|
refresh();
|
|
734
735
|
for (const listener of entry.chatListeners) listener();
|
|
735
|
-
}
|
|
736
|
+
};
|
|
737
|
+
entry.unsubscribe = entry.chat.subscribe(publishChat);
|
|
738
|
+
entry.reconnectChat = () => {
|
|
739
|
+
if (disposed || entry.closed) return;
|
|
740
|
+
const chat = ctx.uiConversation.binding(sessionId).target('chat');
|
|
741
|
+
if (chat === entry.chat) return;
|
|
742
|
+
entry.unsubscribe?.();
|
|
743
|
+
entry.chat = chat;
|
|
744
|
+
entry.unsubscribe = chat.subscribe(publishChat);
|
|
745
|
+
// Subscribe first: activation may materialize the target synchronously.
|
|
746
|
+
// Existing speech memory and detached-session admission rules still apply.
|
|
747
|
+
publishChat();
|
|
748
|
+
};
|
|
736
749
|
// DSH 0.1.6 may omit this legacy store. Keep the remaining voice controls
|
|
737
750
|
// available while structured-question integration is unavailable.
|
|
738
751
|
const pendingInteractions = ctx.uiSession.pendingInteractions;
|
|
@@ -927,6 +940,7 @@ export function apply(ctx) {
|
|
|
927
940
|
// Allocation and subscriptions happen only for committed mounts. Suspended
|
|
928
941
|
// or abandoned renders never acquire a session, probe engines, or retain it.
|
|
929
942
|
const current = get(sessionId);
|
|
943
|
+
current.reconnectChat();
|
|
930
944
|
current.refs++;
|
|
931
945
|
if (kind === 'buttons') current.buttons++;
|
|
932
946
|
setEntry(current);
|
|
@@ -408,9 +408,13 @@ export class VoiceCoordinator {
|
|
|
408
408
|
return (await this.recognition.suspendCapture()) !== false;
|
|
409
409
|
});
|
|
410
410
|
}
|
|
411
|
-
async _resumeInputCapture() {
|
|
411
|
+
async _resumeInputCapture(valid = () => true) {
|
|
412
412
|
return this._input(async () => {
|
|
413
|
-
if (
|
|
413
|
+
if (
|
|
414
|
+
!valid() ||
|
|
415
|
+
!this.snapshot.listening ||
|
|
416
|
+
typeof this.recognition.resumeCapture !== 'function'
|
|
417
|
+
)
|
|
414
418
|
return false;
|
|
415
419
|
return (await this.recognition.resumeCapture()) !== false;
|
|
416
420
|
});
|
|
@@ -1370,23 +1374,67 @@ export class VoiceCoordinator {
|
|
|
1370
1374
|
getSpeechProgress() {
|
|
1371
1375
|
return this.engines[this.snapshot.speechEngine]?.getPlaybackProgress?.() ?? null;
|
|
1372
1376
|
}
|
|
1377
|
+
async _restorePausedSpeechInput(epoch, control) {
|
|
1378
|
+
const valid = () =>
|
|
1379
|
+
!this.disposed &&
|
|
1380
|
+
!this.navigationPaused &&
|
|
1381
|
+
epoch === this.speechEpoch &&
|
|
1382
|
+
control === this.controlEpoch &&
|
|
1383
|
+
this.snapshot.paused &&
|
|
1384
|
+
this.snapshot.settings.mode === 'speaker';
|
|
1385
|
+
if (!valid()) return;
|
|
1386
|
+
try {
|
|
1387
|
+
if (this._hasSuspendedInputCapture()) {
|
|
1388
|
+
await this._resumeInputCapture(valid);
|
|
1389
|
+
} else if (
|
|
1390
|
+
this.snapshot.conversation &&
|
|
1391
|
+
!this.snapshot.listening &&
|
|
1392
|
+
!this.snapshot.starting &&
|
|
1393
|
+
!this.snapshot.inputRecovery
|
|
1394
|
+
) {
|
|
1395
|
+
await this._startInput(true);
|
|
1396
|
+
}
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
if (valid()) this.patch({ inputError: message(error), error: message(error) });
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
async _gateResumedSpeechInput() {
|
|
1402
|
+
if (this.snapshot.settings.mode !== 'speaker') return;
|
|
1403
|
+
// Preserve recording-backed capture; native engines retain safe stop/restart.
|
|
1404
|
+
const suspended = await this._suspendInputCapture();
|
|
1405
|
+
if (!suspended) {
|
|
1406
|
+
await this.stopListening();
|
|
1407
|
+
if (this.inputReleaseError) throw this.inputReleaseError;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1373
1410
|
async pauseSpeech(automatic = false) {
|
|
1374
1411
|
if (this.snapshot.speechRecovery?.retrying) this._cancelRecovery('speech', true);
|
|
1375
1412
|
if (!automatic) {
|
|
1376
1413
|
this._cancelInterruptionResume();
|
|
1377
1414
|
this.interruptionPausedSpeech = false;
|
|
1378
1415
|
}
|
|
1379
|
-
if (this.disposed
|
|
1416
|
+
if (this.disposed) return;
|
|
1417
|
+
if (this.snapshot.paused) {
|
|
1418
|
+
// Reaffirming pause invalidates a resume whose capture gate is still pending.
|
|
1419
|
+
const epoch = this.speechEpoch;
|
|
1420
|
+
const control = ++this.controlEpoch;
|
|
1421
|
+
await this.inputTail;
|
|
1422
|
+
await this._restorePausedSpeechInput(epoch, control);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1380
1425
|
if (!this.snapshot.speaking && this.snapshot.speechRunActive) {
|
|
1381
1426
|
this._cancelAssistantSpeechTimer();
|
|
1427
|
+
const control = ++this.controlEpoch;
|
|
1382
1428
|
this.patch({ paused: true });
|
|
1429
|
+
await this._restorePausedSpeechInput(this.speechEpoch, control);
|
|
1383
1430
|
return;
|
|
1384
1431
|
}
|
|
1385
1432
|
if (!this.snapshot.speaking) return;
|
|
1386
1433
|
if (this.speechLaunchPending) {
|
|
1387
1434
|
// Preparation has no player to pause yet; retain an explicit launch gate.
|
|
1388
|
-
++this.controlEpoch;
|
|
1435
|
+
const control = ++this.controlEpoch;
|
|
1389
1436
|
this.patch({ paused: true });
|
|
1437
|
+
await this._restorePausedSpeechInput(this.speechEpoch, control);
|
|
1390
1438
|
return;
|
|
1391
1439
|
}
|
|
1392
1440
|
const epoch = this.speechEpoch;
|
|
@@ -1403,6 +1451,7 @@ export class VoiceCoordinator {
|
|
|
1403
1451
|
) {
|
|
1404
1452
|
this.patch({ paused: true });
|
|
1405
1453
|
this.activeRecordingTimeline?.('assistant-pause');
|
|
1454
|
+
await this._restorePausedSpeechInput(epoch, control);
|
|
1406
1455
|
}
|
|
1407
1456
|
} catch (error) {
|
|
1408
1457
|
if (epoch === this.speechEpoch) this.patch({ error: message(error) });
|
|
@@ -1422,12 +1471,11 @@ export class VoiceCoordinator {
|
|
|
1422
1471
|
if (this.speechLaunchPending) {
|
|
1423
1472
|
const epoch = this.speechEpoch;
|
|
1424
1473
|
const control = ++this.controlEpoch;
|
|
1425
|
-
|
|
1426
|
-
await this.
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
}
|
|
1474
|
+
try {
|
|
1475
|
+
await this._gateResumedSpeechInput();
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
if (epoch === this.speechEpoch) this.patch({ error: message(error) });
|
|
1478
|
+
return;
|
|
1431
1479
|
}
|
|
1432
1480
|
if (epoch === this.speechEpoch && control === this.controlEpoch)
|
|
1433
1481
|
this.patch({ paused: false });
|
|
@@ -1436,10 +1484,7 @@ export class VoiceCoordinator {
|
|
|
1436
1484
|
const epoch = this.speechEpoch;
|
|
1437
1485
|
const control = ++this.controlEpoch;
|
|
1438
1486
|
try {
|
|
1439
|
-
|
|
1440
|
-
await this.stopListening();
|
|
1441
|
-
if (this.inputReleaseError) throw this.inputReleaseError;
|
|
1442
|
-
}
|
|
1487
|
+
await this._gateResumedSpeechInput();
|
|
1443
1488
|
if (epoch !== this.speechEpoch || control !== this.controlEpoch) return;
|
|
1444
1489
|
const result =
|
|
1445
1490
|
await this.engines[this.snapshot.speechEngine || this.snapshot.settings.engine].resume();
|
|
@@ -1447,9 +1492,12 @@ export class VoiceCoordinator {
|
|
|
1447
1492
|
this.patch({ paused: false });
|
|
1448
1493
|
this.activeRecordingTimeline?.('assistant-resume');
|
|
1449
1494
|
this._prefetchSpeech();
|
|
1495
|
+
} else {
|
|
1496
|
+
await this._restorePausedSpeechInput(epoch, control);
|
|
1450
1497
|
}
|
|
1451
1498
|
} catch (error) {
|
|
1452
1499
|
if (epoch === this.speechEpoch) this.patch({ error: message(error) });
|
|
1500
|
+
await this._restorePausedSpeechInput(epoch, control);
|
|
1453
1501
|
}
|
|
1454
1502
|
}
|
|
1455
1503
|
async speak(text, messageId = null) {
|
|
@@ -1548,6 +1596,8 @@ export class VoiceCoordinator {
|
|
|
1548
1596
|
if (gating) timeline?.('input-stop-complete');
|
|
1549
1597
|
}
|
|
1550
1598
|
if (epoch !== this.speechEpoch || this.disposed) return;
|
|
1599
|
+
// A pause can arrive while the initial capture gate is still settling.
|
|
1600
|
+
if (this.snapshot.paused) await this._restorePausedSpeechInput(epoch, this.controlEpoch);
|
|
1551
1601
|
const queued = item;
|
|
1552
1602
|
if (queued?.preparing) await queued.preparing;
|
|
1553
1603
|
if (epoch !== this.speechEpoch || this.disposed) return;
|