@jskit-ai/assistant-voice 0.1.9 → 0.1.12
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
|
@@ -82,13 +82,18 @@ The host's `avatar` slot receives state, mouthLevel, mouthPose, avatar and bindi
|
|
|
82
82
|
|
|
83
83
|
Opening a target does not start capture. Tap Talk for hands-free, or hold it for
|
|
84
84
|
push-to-talk and release to send; connection setup is automatic.
|
|
85
|
-
Pause
|
|
85
|
+
Pause stops new microphone input immediately and retains its buffered audio for
|
|
86
|
+
the original final flush before recognition finishes the current utterance.
|
|
86
87
|
Completed words follow the captured review policy. Pending delivery does not disable
|
|
87
88
|
the microphone: tap can resume the existing continuous capture or start another
|
|
88
89
|
one while preserving the prior pending transcript. A busy Pause or hold release
|
|
89
|
-
mutes newer capture
|
|
90
|
-
admission settles. Resuming first keeps the
|
|
90
|
+
mutes newer capture without discarding its buffered tail and finishes it through
|
|
91
|
+
the original owner after that prior admission settles. Resuming first keeps the
|
|
92
|
+
capture and its retained tail open. No pending words are
|
|
91
93
|
replaced or appended. The icon reflects actual unmuted capture, not readiness.
|
|
94
|
+
When the daemon rejects a stale endpoint commit, its exact turn and revision
|
|
95
|
+
release only that commit. A waiting Pause can then finalize the same recording;
|
|
96
|
+
an obsolete acknowledgement cannot release a newer commit or submit provisional words.
|
|
92
97
|
Automatic sends keep the call layout fixed. A failed send exposes the retained
|
|
93
98
|
transcript for review and retry with the same message identity. An application
|
|
94
99
|
can call `beginTranscriptEdit(messageId)` for its exact unsent preview; this
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@fastify/websocket": "^11.2.0",
|
|
12
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
13
|
-
"@jskit-ai/assistant-runtime": "0.1.
|
|
14
|
-
"@jskit-ai/assistant-voice": "0.1.
|
|
15
|
-
"@jskit-ai/connectors-catalog": "0.1.
|
|
16
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
17
|
-
"@jskit-ai/kernel": "0.1.
|
|
18
|
-
"@jskit-ai/realtime": "0.1.
|
|
19
|
-
"@jskit-ai/shell-web": "0.1.
|
|
12
|
+
"@jskit-ai/assistant-core": "0.1.212",
|
|
13
|
+
"@jskit-ai/assistant-runtime": "0.1.205",
|
|
14
|
+
"@jskit-ai/assistant-voice": "0.1.12",
|
|
15
|
+
"@jskit-ai/connectors-catalog": "0.1.51",
|
|
16
|
+
"@jskit-ai/http-runtime": "0.1.232",
|
|
17
|
+
"@jskit-ai/kernel": "0.1.234",
|
|
18
|
+
"@jskit-ai/realtime": "0.1.231",
|
|
19
|
+
"@jskit-ai/shell-web": "0.1.238",
|
|
20
20
|
"@mdi/js": "^7.4.47",
|
|
21
21
|
"@tanstack/vue-query": "^5.90.5",
|
|
22
22
|
"fastify": "^5.8.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-voice",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Duplex voice conversations with application-owned targets, authorization and speech services.",
|
|
6
6
|
"scripts": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"examples"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
21
|
+
"@jskit-ai/assistant-core": "0.1.212",
|
|
22
22
|
"@mdi/js": "^7.4.47",
|
|
23
23
|
"ws": "^8.20.1"
|
|
24
24
|
},
|
|
@@ -12,8 +12,10 @@ class AssistantVoiceCapture extends AudioWorkletProcessor {
|
|
|
12
12
|
this.port.onmessage = (event) => {
|
|
13
13
|
if (event.data?.type === "mute") {
|
|
14
14
|
this.muted = event.data.muted;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (!event.data.preserveBuffered) {
|
|
16
|
+
this.frames = [];
|
|
17
|
+
this.length = 0;
|
|
18
|
+
}
|
|
17
19
|
return;
|
|
18
20
|
}
|
|
19
21
|
if (event.data?.type !== "flush") return;
|
|
@@ -108,12 +110,13 @@ async function createMicrophoneCapture({ onLevel, onPcm, targetSampleRate = 16_0
|
|
|
108
110
|
silence.gain.value = 0;
|
|
109
111
|
let finishFlush = null;
|
|
110
112
|
let muted = false;
|
|
113
|
+
let preserveBuffered = false;
|
|
111
114
|
capture.port.onmessage = (event) => {
|
|
112
115
|
if (event.data?.type === "flushed") {
|
|
113
116
|
finishFlush?.();
|
|
114
117
|
return;
|
|
115
118
|
}
|
|
116
|
-
if (muted) return;
|
|
119
|
+
if (muted && !preserveBuffered) return;
|
|
117
120
|
const samples = event.data instanceof Float32Array
|
|
118
121
|
? event.data
|
|
119
122
|
: new Float32Array(event.data);
|
|
@@ -154,11 +157,14 @@ async function createMicrophoneCapture({ onLevel, onPcm, targetSampleRate = 16_0
|
|
|
154
157
|
}
|
|
155
158
|
});
|
|
156
159
|
return Object.freeze({
|
|
157
|
-
setMuted(value) {
|
|
158
|
-
|
|
160
|
+
setMuted(value, { preserveBuffered: retainTail = false } = {}) {
|
|
161
|
+
const nextPreserveBuffered = Boolean(value) && retainTail;
|
|
162
|
+
if (muted === Boolean(value) && preserveBuffered === nextPreserveBuffered) return;
|
|
163
|
+
const keepBuffered = nextPreserveBuffered || (!value && preserveBuffered);
|
|
159
164
|
muted = Boolean(value);
|
|
165
|
+
preserveBuffered = nextPreserveBuffered;
|
|
160
166
|
for (const track of stream.getTracks()) track.enabled = !muted;
|
|
161
|
-
capture.port.postMessage({ type: "mute", muted });
|
|
167
|
+
capture.port.postMessage({ type: "mute", muted, ...(keepBuffered ? { preserveBuffered: true } : {}) });
|
|
162
168
|
if (muted) onLevel?.(0);
|
|
163
169
|
},
|
|
164
170
|
close() {
|
|
@@ -409,10 +409,10 @@ export function useVoiceConversation(binding, { socketUrl, createTransport = use
|
|
|
409
409
|
if (recording === next) { next.started = started; if (!started) recording = null; }
|
|
410
410
|
} catch (cause) { if (recording === next) { recording = null; error.value = cause.message; } }
|
|
411
411
|
}
|
|
412
|
-
async function toggleMicrophoneMuted() {
|
|
412
|
+
async function toggleMicrophoneMuted({ preserveBuffered = false } = {}) {
|
|
413
413
|
// A person's explicit microphone choice supersedes an Edit-owned pause.
|
|
414
414
|
if (pendingTranscript.value) delete pendingTranscript.value.editPausedCaptureId;
|
|
415
|
-
voice.setMicrophoneMuted(!microphoneMuted.value);
|
|
415
|
+
voice.setMicrophoneMuted(!microphoneMuted.value, { preserveBuffered });
|
|
416
416
|
if (!microphoneMuted.value && recording) delete recording.finishAfterPending;
|
|
417
417
|
clearTimeout(stopTimer);
|
|
418
418
|
if (microphoneMuted.value && starting.value) await cancelRecording();
|
|
@@ -455,7 +455,7 @@ export function useVoiceConversation(binding, { socketUrl, createTransport = use
|
|
|
455
455
|
try {
|
|
456
456
|
if (voice.listening.value && (pendingTranscript.value || sending.value || committing)) {
|
|
457
457
|
if (recording) recording.finishAfterPending = true;
|
|
458
|
-
voice.setMicrophoneMuted(true);
|
|
458
|
+
voice.setMicrophoneMuted(true, { preserveBuffered: true });
|
|
459
459
|
return;
|
|
460
460
|
}
|
|
461
461
|
if (voice.listening.value) await voice.stopListening();
|
|
@@ -474,7 +474,7 @@ export function useVoiceConversation(binding, { socketUrl, createTransport = use
|
|
|
474
474
|
// Pause must finish the newer recording after the existing admission,
|
|
475
475
|
// without replacing the one pending transcript or stopping output.
|
|
476
476
|
if (!microphoneMuted.value && recording) recording.finishAfterPending = true;
|
|
477
|
-
await toggleMicrophoneMuted();
|
|
477
|
+
await toggleMicrophoneMuted({ preserveBuffered: true });
|
|
478
478
|
return;
|
|
479
479
|
}
|
|
480
480
|
if (committing) return;
|
|
@@ -482,10 +482,10 @@ export function useVoiceConversation(binding, { socketUrl, createTransport = use
|
|
|
482
482
|
if (capturing.value && !voice.listening.value) return;
|
|
483
483
|
if (live.value && callMode.value === "hands-free") {
|
|
484
484
|
if (!microphoneMuted.value && voice.listening.value) {
|
|
485
|
-
//
|
|
486
|
-
//
|
|
485
|
+
// Stop new input immediately, retaining the captured tail for the
|
|
486
|
+
// original Stop drain. Resuming starts a fresh recording.
|
|
487
|
+
voice.setMicrophoneMuted(true, { preserveBuffered: true });
|
|
487
488
|
await voice.stopListening();
|
|
488
|
-
voice.setMicrophoneMuted(true);
|
|
489
489
|
} else if (microphoneMuted.value) await toggleMicrophoneMuted();
|
|
490
490
|
else await startRecording(false, true);
|
|
491
491
|
return;
|
|
@@ -608,7 +608,11 @@ export function useVoiceConversation(binding, { socketUrl, createTransport = use
|
|
|
608
608
|
if (live.value && !microphoneMuted.value && voice.partialTranscript.value === text) stopSpeech();
|
|
609
609
|
}, 120);
|
|
610
610
|
});
|
|
611
|
-
watch([pendingTranscript, sending, voice.utteranceReset], () => {
|
|
611
|
+
watch([pendingTranscript, sending, voice.utteranceReset, voice.utteranceStale], () => {
|
|
612
|
+
const stale = voice.utteranceStale.value;
|
|
613
|
+
if (committing && stale?.turnId === committing.turnId && stale.revision === committing.revision) {
|
|
614
|
+
committing = null;
|
|
615
|
+
}
|
|
612
616
|
if (!disposed && live.value && !pushHolding.value && !changingCallMode.value &&
|
|
613
617
|
!pendingTranscript.value && !sending.value && !committing && microphoneMuted.value &&
|
|
614
618
|
recording?.finishAfterPending && voice.listening.value) {
|
|
@@ -41,6 +41,7 @@ function useVoiceTransport({ socketUrl, speechEnabled = true, interruptSpeechOnL
|
|
|
41
41
|
const endpoint = ref(null);
|
|
42
42
|
const completedUtterance = ref(null);
|
|
43
43
|
const utteranceReset = ref(null);
|
|
44
|
+
const utteranceStale = ref(null);
|
|
44
45
|
const partialTranscript = ref("");
|
|
45
46
|
const muted = ref(false);
|
|
46
47
|
const microphoneMuted = ref(false);
|
|
@@ -63,6 +64,7 @@ function useVoiceTransport({ socketUrl, speechEnabled = true, interruptSpeechOnL
|
|
|
63
64
|
let lastPongAt = 0;
|
|
64
65
|
let lastSpeechProgressAt = 0;
|
|
65
66
|
let microphone = null;
|
|
67
|
+
let preserveBufferedMicrophone = false;
|
|
66
68
|
const openingMicrophones = new Set();
|
|
67
69
|
let microphoneClaim = null;
|
|
68
70
|
const speechClaim = Symbol("voice playback");
|
|
@@ -444,6 +446,8 @@ function useVoiceTransport({ socketUrl, speechEnabled = true, interruptSpeechOnL
|
|
|
444
446
|
utteranceReset.value = { turnId: message.turnId, revision: message.revision };
|
|
445
447
|
transcript.value = "";
|
|
446
448
|
partialTranscript.value = "";
|
|
449
|
+
} else {
|
|
450
|
+
utteranceStale.value = { turnId: message.turnId, revision: message.revision };
|
|
447
451
|
}
|
|
448
452
|
return;
|
|
449
453
|
}
|
|
@@ -629,6 +633,7 @@ function useVoiceTransport({ socketUrl, speechEnabled = true, interruptSpeechOnL
|
|
|
629
633
|
}
|
|
630
634
|
microphoneClaim = Symbol("voice microphone");
|
|
631
635
|
microphoneOwner = microphoneClaim;
|
|
636
|
+
preserveBufferedMicrophone = false;
|
|
632
637
|
const turnId = voiceTurnId("listen");
|
|
633
638
|
pendingListenTurnId = turnId;
|
|
634
639
|
if (interruptSpeechOnListen) stopSpeaking();
|
|
@@ -647,7 +652,7 @@ function useVoiceTransport({ socketUrl, speechEnabled = true, interruptSpeechOnL
|
|
|
647
652
|
}
|
|
648
653
|
},
|
|
649
654
|
onPcm(frame) {
|
|
650
|
-
if (!microphoneMuted.value && socket?.readyState === WEBSOCKET_OPEN && activeListenTurnId.value === turnId) {
|
|
655
|
+
if ((!microphoneMuted.value || preserveBufferedMicrophone) && socket?.readyState === WEBSOCKET_OPEN && activeListenTurnId.value === turnId) {
|
|
651
656
|
if (socket.bufferedAmount > 32 * 1024) {
|
|
652
657
|
connectionLost("The microphone connection fell behind. Review the unfinished recording before sending.");
|
|
653
658
|
return;
|
|
@@ -843,9 +848,10 @@ function useVoiceTransport({ socketUrl, speechEnabled = true, interruptSpeechOnL
|
|
|
843
848
|
muted.value = !muted.value;
|
|
844
849
|
}
|
|
845
850
|
|
|
846
|
-
function setMicrophoneMuted(value) {
|
|
851
|
+
function setMicrophoneMuted(value, { preserveBuffered = false } = {}) {
|
|
847
852
|
microphoneMuted.value = Boolean(value);
|
|
848
|
-
|
|
853
|
+
preserveBufferedMicrophone = microphoneMuted.value && preserveBuffered;
|
|
854
|
+
microphone?.setMuted(microphoneMuted.value, { preserveBuffered });
|
|
849
855
|
if (microphoneMuted.value) inputLevel.value = 0;
|
|
850
856
|
}
|
|
851
857
|
|
|
@@ -900,6 +906,7 @@ function useVoiceTransport({ socketUrl, speechEnabled = true, interruptSpeechOnL
|
|
|
900
906
|
captureState,
|
|
901
907
|
completedUtterance,
|
|
902
908
|
utteranceReset,
|
|
909
|
+
utteranceStale,
|
|
903
910
|
endpoint,
|
|
904
911
|
finishUtterance,
|
|
905
912
|
finishCurrentPhrase,
|