@olares/lares-mobile 0.15.30 → 0.15.31
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 +2 -2
- package/src/App.vue +33 -2
- package/src/settings/TtsSettings.vue +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olares/lares-mobile",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.31",
|
|
4
4
|
"description": "Lares chat component for LarePass",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"vue": "^3.2.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@olares/lares-core": "0.15.
|
|
31
|
+
"@olares/lares-core": "0.15.31",
|
|
32
32
|
"markdown-it": "^14.3.0"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/App.vue
CHANGED
|
@@ -127,6 +127,7 @@ import { createVoiceConversation } from "@olares/lares-core/voice/conversation";
|
|
|
127
127
|
import { createUnifiedConversation } from "@olares/lares-core/voice/unified-conversation";
|
|
128
128
|
import { createAnswerPlayer } from "./chat/playback.js";
|
|
129
129
|
import { createReplySpeaker, createSentencePlayer, readoutState, speakable } from "@olares/lares-core/voice/speech";
|
|
130
|
+
import { createScreenWakeLock } from "@olares/lares-core/voice/wake-lock";
|
|
130
131
|
import { prepareBrowserCapture } from "@olares/lares-core/voice/browser-capture";
|
|
131
132
|
import { playAudioStream, createStreamingCapture } from "@olares/lares-core/voice/stream-client";
|
|
132
133
|
import { voiceHeaders, wavBlob } from "@olares/lares-core/voice/client";
|
|
@@ -561,6 +562,9 @@ export default {
|
|
|
561
562
|
this.intake = new FileIntake((file, sessionId, options) => this.runtime.upload(file, options, sessionId));
|
|
562
563
|
this.imageUploadControllers = new Map();
|
|
563
564
|
this.player = createAnswerPlayer();
|
|
565
|
+
// A locked screen suspends the WebView and cuts a long answer short; hold
|
|
566
|
+
// the screen for as long as a readout is actually playing.
|
|
567
|
+
this.readoutWake = createScreenWakeLock();
|
|
564
568
|
this.voicePanel = createVoicePanel({
|
|
565
569
|
armOutput: () => this.player?.unlock(),
|
|
566
570
|
create: (options) => this.createMobileVoiceConversation(options),
|
|
@@ -607,6 +611,7 @@ export default {
|
|
|
607
611
|
halt: () => this.player?.stop(),
|
|
608
612
|
silence: (source) => void this.runtime.interruptSpeech?.(source),
|
|
609
613
|
fail: (err) => this.showSpeakProblem(err),
|
|
614
|
+
onState: (active) => this.setReadoutAwake(active),
|
|
610
615
|
});
|
|
611
616
|
|
|
612
617
|
this.bindIntake(this.sessionId);
|
|
@@ -624,12 +629,30 @@ export default {
|
|
|
624
629
|
this.conversationUnsub = subscribeConversationSettings((section) => {
|
|
625
630
|
this.busyEnter = section.busyEnter;
|
|
626
631
|
});
|
|
632
|
+
this.voiceReadoutPaused = false;
|
|
627
633
|
this.voiceVisibilityHandler = () => {
|
|
628
|
-
if (document.hidden) {
|
|
634
|
+
if (document.hidden) {
|
|
635
|
+
// A locked screen or an app switch is a pause, not the end of the
|
|
636
|
+
// answer: stopping here marks the utterance read and it never resumes.
|
|
637
|
+
if (this.reader.canPause && this.reader.pause()) this.voiceReadoutPaused = true;
|
|
638
|
+
void this.voicePanel.pause().catch(() => {});
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (this.readoutActive) void this.readoutWake?.hold();
|
|
642
|
+
if (this.voiceReadoutPaused) { this.voiceReadoutPaused = false; this.reader.resume(); }
|
|
643
|
+
};
|
|
644
|
+
this.voicePageHideHandler = () => {
|
|
645
|
+
if (this.reader.canPause && this.reader.pause()) this.voiceReadoutPaused = true;
|
|
646
|
+
void this.voicePanel.pause().catch(() => {});
|
|
647
|
+
};
|
|
648
|
+
this.voicePageShowHandler = () => {
|
|
649
|
+
if (this.voiceReadoutPaused) { this.voiceReadoutPaused = false; this.reader.resume(); }
|
|
629
650
|
};
|
|
630
651
|
this.voiceOfflineHandler = () => { this.reader.stop(); void this.voicePanel.pause().catch(() => {}); };
|
|
631
652
|
window.addEventListener("offline", this.voiceOfflineHandler);
|
|
632
653
|
document.addEventListener("visibilitychange", this.voiceVisibilityHandler);
|
|
654
|
+
window.addEventListener("pagehide", this.voicePageHideHandler);
|
|
655
|
+
window.addEventListener("pageshow", this.voicePageShowHandler);
|
|
633
656
|
window.addEventListener("keydown", this.onVoiceShortcut, true);
|
|
634
657
|
this.voiceClock = setInterval(() => {
|
|
635
658
|
if (this.voicePhase === "recording") this.voiceElapsed += 1;
|
|
@@ -647,6 +670,9 @@ export default {
|
|
|
647
670
|
beforeUnmount() {
|
|
648
671
|
window.removeEventListener("offline", this.voiceOfflineHandler);
|
|
649
672
|
document.removeEventListener("visibilitychange", this.voiceVisibilityHandler);
|
|
673
|
+
window.removeEventListener("pagehide", this.voicePageHideHandler);
|
|
674
|
+
window.removeEventListener("pageshow", this.voicePageShowHandler);
|
|
675
|
+
this.readoutWake?.release();
|
|
650
676
|
window.removeEventListener("keydown", this.onVoiceShortcut, true);
|
|
651
677
|
clearInterval(this.voiceClock);
|
|
652
678
|
this.captureLog();
|
|
@@ -1340,7 +1366,7 @@ export default {
|
|
|
1340
1366
|
send: (value) => conversation.speak(value),
|
|
1341
1367
|
play: (sentence) => sentence.bluetooth === "played" ? Promise.resolve() : this.player.play(wavBlob(sentence.audio)),
|
|
1342
1368
|
halt: () => this.player.stop(), silence: () => {},
|
|
1343
|
-
onState: (playing) => conversation?.playbackState(playing),
|
|
1369
|
+
onState: (playing) => { conversation?.playbackState(playing); this.setReadoutAwake(playing); },
|
|
1344
1370
|
onOutput: (value) => readoutState.publish(value?.status === "error" ? null : value),
|
|
1345
1371
|
fail: (error) => { void conversation.controller.disconnect(error?.code || error?.message || "voice_playback_failed"); },
|
|
1346
1372
|
});
|
|
@@ -1368,6 +1394,11 @@ export default {
|
|
|
1368
1394
|
onError: (error) => { this.voiceError = error.message; },
|
|
1369
1395
|
});
|
|
1370
1396
|
},
|
|
1397
|
+
setReadoutAwake(active) {
|
|
1398
|
+
this.readoutActive = Boolean(active);
|
|
1399
|
+
if (this.readoutActive) void this.readoutWake?.hold();
|
|
1400
|
+
else this.readoutWake?.release();
|
|
1401
|
+
},
|
|
1371
1402
|
showSpeakProblem(err) {
|
|
1372
1403
|
this.speakNotice = speakProblemText(this.t, err);
|
|
1373
1404
|
},
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
<p v-if="view.errors.ttsModel" class="lares-tts__alert" role="alert">{{ t('settings.saveFailed') }}</p>
|
|
18
18
|
<p v-if="!state.model" class="lares-tts__hint">{{ t('preferences.modelsMissing') }}</p>
|
|
19
19
|
<p v-else-if="state.loading" class="lares-tts__hint" role="status">{{ t('tts.loading') }}</p>
|
|
20
|
+
<div v-if="state.stale" class="lares-tts__status" role="status">
|
|
21
|
+
<p>{{ t('tts.stale') }}</p>
|
|
22
|
+
<button type="button" @click="controller.load()">{{ t('dictation.retry') }}</button>
|
|
23
|
+
</div>
|
|
20
24
|
<div v-else-if="state.error" class="lares-tts__status" role="alert">
|
|
21
25
|
<p>{{ t('tts.loadFailed') }}</p>
|
|
22
26
|
<button type="button" @click="controller.load()">{{ t('dictation.retry') }}</button>
|