@olares/lares-core 0.15.23 → 0.15.24
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/package.json +1 -1
- package/voice/controller.js +3 -2
- package/voice/panel.js +28 -9
- package/voice/presentation.js +8 -6
package/package.json
CHANGED
package/voice/controller.js
CHANGED
|
@@ -157,14 +157,15 @@ export function createVoiceController({ input, agent, output, voiceId = mintRpcI
|
|
|
157
157
|
change({ connection: code ? "paused" : "closed", error: code });
|
|
158
158
|
await Promise.allSettled([cancelInput(), stopPlayback()]);
|
|
159
159
|
},
|
|
160
|
-
async resume({ shouldResume = () => true } = {}) {
|
|
160
|
+
async resume({ shouldResume = () => true, resumeListening = !manualTurns } = {}) {
|
|
161
161
|
const resumedEpoch = epoch;
|
|
162
162
|
if (state.connection !== "paused") return;
|
|
163
163
|
const running = state.sessionId ? await agent.running(state.sessionId) : false;
|
|
164
164
|
if (resumedEpoch !== epoch || !shouldResume()) return;
|
|
165
165
|
clearTimers();
|
|
166
166
|
change({ connection: "open", agent: running ? "running" : "idle", error: null });
|
|
167
|
-
touch();
|
|
167
|
+
touch();
|
|
168
|
+
if (resumeListening) void listen();
|
|
168
169
|
},
|
|
169
170
|
};
|
|
170
171
|
return controller;
|
package/voice/panel.js
CHANGED
|
@@ -55,16 +55,28 @@ export function createVoicePanel({ create, complete, loadConfig = async () => ({
|
|
|
55
55
|
if (processing === abort) { processing = null; renderDraft(); }
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
|
-
const
|
|
58
|
+
const readoutWanted = async () => {
|
|
59
|
+
if (readAloudPreference === true) return true;
|
|
60
|
+
if (readAloudPreference === false) return false;
|
|
61
|
+
return (await loadConfig())?.speak === true;
|
|
62
|
+
};
|
|
63
|
+
const prepare = async (token, { readout = false } = {}) => {
|
|
64
|
+
const armed = () => token === intent && (readout || state.visible);
|
|
65
|
+
if (readout && !await readoutWanted()) return;
|
|
59
66
|
if (call?.controller.getSnapshot().connection === "paused") {
|
|
60
|
-
await call.resume({ shouldResume: (
|
|
61
|
-
if (
|
|
67
|
+
await call.resume({ shouldResume: armed, ...(readout ? { resumeListening: false } : {}) });
|
|
68
|
+
if (armed() && readout) await panel.setReadAloud(true);
|
|
69
|
+
else if (armed() && state.mode === "continuous" && typeof readAloudPreference === "boolean") await panel.setReadAloud(readAloudPreference);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (call?.controller.getSnapshot().connection === "open") {
|
|
73
|
+
if (readout && readAloudPreference === true) await panel.setReadAloud(true);
|
|
62
74
|
return;
|
|
63
75
|
}
|
|
64
|
-
if (call?.controller.getSnapshot().connection === "open") return;
|
|
65
76
|
await end(); config = state.mode === "single" && config ? config : structuredClone(await loadConfig());
|
|
66
|
-
if (
|
|
77
|
+
if (!armed()) return;
|
|
67
78
|
if (state.mode === "continuous" && typeof readAloudPreference === "boolean") config = { ...config, speak: readAloudPreference };
|
|
79
|
+
if (readout && config.speak !== true) return;
|
|
68
80
|
const initialReadAloud = state.mode === "continuous" && config.speak === true;
|
|
69
81
|
const target = state.sessionId, epoch = ++generation;
|
|
70
82
|
const conversation = await create({ mode: state.mode, sessionId: target, config, preparation: inputPreparation,
|
|
@@ -73,7 +85,7 @@ export function createVoicePanel({ create, complete, loadConfig = async () => ({
|
|
|
73
85
|
const raw = [state.raw, text].filter(Boolean).join(" "); emit({ raw, draft: raw, edited: false }); save();
|
|
74
86
|
}, onDevice(device) { if (epoch === generation) emit({ device }); },
|
|
75
87
|
});
|
|
76
|
-
if (
|
|
88
|
+
if (!armed()) { await conversation.stop(); return; }
|
|
77
89
|
call = conversation; appliedReadAloud = initialReadAloud;
|
|
78
90
|
unsubscribe = call.controller.subscribe(() => {
|
|
79
91
|
if (epoch !== generation) return;
|
|
@@ -82,11 +94,12 @@ export function createVoicePanel({ create, complete, loadConfig = async () => ({
|
|
|
82
94
|
emit({ snapshot, ...(snapshot.inputLevel == null ? {} : { levels: [...state.levels.slice(-19), snapshot.inputLevel] }),
|
|
83
95
|
...(snapshot.capture === "recording" && snapshot.inputReady && !state.startedAt ? { startedAt: Date.now(), elapsedMs: 0 } : snapshot.capture !== "recording" && state.startedAt ? { startedAt: null, elapsedMs: Date.now() - state.startedAt } : {}) });
|
|
84
96
|
});
|
|
85
|
-
await call.start({ sessionId: target, continuous: state.mode === "continuous", manual: state.mode === "continuous" && manualTurns, shouldStart:
|
|
86
|
-
if (
|
|
97
|
+
await call.start({ sessionId: target, continuous: state.mode === "continuous", manual: state.mode === "continuous" && (manualTurns || readout), shouldStart: armed });
|
|
98
|
+
if (!armed()) await call.controller.cancelRecording();
|
|
87
99
|
else {
|
|
88
100
|
readAloudReady = true;
|
|
89
|
-
if (
|
|
101
|
+
if (readout) await panel.setReadAloud(true);
|
|
102
|
+
else if (state.mode === "continuous" && typeof readAloudPreference === "boolean") await panel.setReadAloud(readAloudPreference);
|
|
90
103
|
}
|
|
91
104
|
};
|
|
92
105
|
const pause = async () => {
|
|
@@ -165,6 +178,12 @@ export function createVoicePanel({ create, complete, loadConfig = async () => ({
|
|
|
165
178
|
close() { intent++; cancelPreparation(); cancelProcessing(); emit({ visible: false, holding: false, cancelArmed: false }); if (state.mode === "continuous") void call?.controller.cancelRecording(); return enqueue(pause); },
|
|
166
179
|
pause() { intent++; cancelPreparation(); cancelProcessing(); emit({ holding: false, cancelArmed: false }); if (state.mode === "continuous") void call?.controller.cancelRecording(); return enqueue(pause); },
|
|
167
180
|
start() { armOutput(); const token = ++intent; return enqueue(() => prepare(token)); },
|
|
181
|
+
openReadout() {
|
|
182
|
+
if (state.mode !== "continuous") return Promise.resolve();
|
|
183
|
+
armOutput();
|
|
184
|
+
const token = ++intent;
|
|
185
|
+
return enqueue(() => prepare(token, { readout: true }));
|
|
186
|
+
},
|
|
168
187
|
finish() {
|
|
169
188
|
if (state.busy) return Promise.resolve();
|
|
170
189
|
const token = intent;
|
package/voice/presentation.js
CHANGED
|
@@ -7,25 +7,26 @@ export function voicePanelPresentation(view) {
|
|
|
7
7
|
const preparing = view.busy || capture === "recording" && view.snapshot?.inputReady === false;
|
|
8
8
|
const recording = capture === "recording" && !preparing;
|
|
9
9
|
const reported = view.error || view.snapshot?.error;
|
|
10
|
+
// Recovery and idle endings are not failures. Hold and send open readout again.
|
|
10
11
|
// A muted call is not a listen session. "Resume to listen again" is the readout copy.
|
|
11
|
-
const error = reported === "voice_disconnected" && view.snapshot?.outputMuted ? "voice_background" : reported;
|
|
12
|
+
const error = QUIET_ENDINGS.has(reported) ? null : reported === "voice_disconnected" && view.snapshot?.outputMuted ? "voice_background" : reported;
|
|
12
13
|
const phase = view.cancelArmed ? "cancel" : error ? "error" : processing ? "processing" : transcribing ? "transcribing" : preparing ? "preparing" : recording ? "recording" : preview ? "preview" : view.snapshot && view.snapshot.playback !== "idle" ? "speaking" : view.snapshot?.agent === "running" ? "working" : "waiting";
|
|
13
14
|
const status = error ? `error.${error}` : ({ cancel: "gesture.cancelRelease", processing: "dictation.processing", transcribing: "mic.transcribing", preparing: "mic.preparing", recording: "call.listening", preview: "dictation.preview", speaking: "dictation.speaking", working: "call.working", waiting: view.mode === "continuous" ? "gesture.waiting" : "dictation.preview" })[phase];
|
|
14
15
|
return { phase, status, preview, recording, pending: processing || transcribing || preparing, error };
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
+
const QUIET_ENDINGS = new Set(["voice_recovery_expired", "voice_idle_timeout"]);
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* A continuous call that has failed is one composer banner.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* A paused session can still be resumed. Recovery and idle endings are quiet:
|
|
23
|
+
* hold-to-speak and send open readout again, without a banner.
|
|
23
24
|
*/
|
|
24
25
|
export function voiceCallFailure(view) {
|
|
25
26
|
if (!view?.visible || view.mode !== "continuous") return null;
|
|
26
27
|
const ui = voicePanelPresentation(view);
|
|
27
28
|
if (!ui.error) return null;
|
|
28
|
-
const recoverable = view.snapshot?.connection === "paused" && !
|
|
29
|
+
const recoverable = view.snapshot?.connection === "paused" && !QUIET_ENDINGS.has(ui.error);
|
|
29
30
|
return { code: ui.error, action: recoverable ? "resume" : "restart" };
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -45,7 +46,8 @@ export function voiceDictationPresentation(view, now = Date.now()) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export function voiceIndicatorPresentation(view, settingsError = null) {
|
|
48
|
-
const
|
|
49
|
+
const callError = view.error || view.snapshot?.error;
|
|
50
|
+
const error = (QUIET_ENDINGS.has(callError) ? null : callError) || view.readAloudError || settingsError;
|
|
49
51
|
const continuous = view.mode === "continuous";
|
|
50
52
|
const active = view.visible && (view.snapshot?.connection === "open" || view.busy && !view.snapshot);
|
|
51
53
|
return {
|