myaiforone 1.1.63 → 1.1.64
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/public/voice-mode.html +35 -5
package/package.json
CHANGED
package/public/voice-mode.html
CHANGED
|
@@ -909,27 +909,57 @@ async function playTtsForText(text){
|
|
|
909
909
|
|
|
910
910
|
function playBlob(blob){
|
|
911
911
|
return new Promise((resolve) => {
|
|
912
|
+
let done = false;
|
|
913
|
+
const finish = () => { if (done) return; done = true; try { URL.revokeObjectURL(url); } catch {}; resolve(); };
|
|
912
914
|
if (audioElement){
|
|
913
915
|
try { audioElement.pause(); } catch {}
|
|
914
916
|
try { URL.revokeObjectURL(audioElement.src); } catch {}
|
|
915
917
|
}
|
|
916
918
|
const url = URL.createObjectURL(blob);
|
|
917
919
|
audioElement = new Audio(url);
|
|
918
|
-
audioElement.onended =
|
|
919
|
-
audioElement.onerror =
|
|
920
|
-
audioElement.
|
|
920
|
+
audioElement.onended = finish;
|
|
921
|
+
audioElement.onerror = finish;
|
|
922
|
+
audioElement.onpause = () => { // iOS Safari sometimes fires pause instead of ended at completion
|
|
923
|
+
if (audioElement && audioElement.currentTime >= (audioElement.duration || 0) - 0.25) finish();
|
|
924
|
+
};
|
|
925
|
+
// Hard ceiling: once we know the duration, force-resolve a bit after it would naturally end.
|
|
926
|
+
// Covers iOS Safari case where playback never actually starts or 'ended' is never fired.
|
|
927
|
+
let ceilingTimer = setTimeout(finish, 60000); // absolute fallback if metadata never loads
|
|
928
|
+
audioElement.onloadedmetadata = () => {
|
|
929
|
+
const dur = isFinite(audioElement.duration) ? audioElement.duration : 0;
|
|
930
|
+
if (dur > 0){
|
|
931
|
+
clearTimeout(ceilingTimer);
|
|
932
|
+
ceilingTimer = setTimeout(finish, Math.ceil(dur * 1000) + 2000);
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
audioElement.play().catch(finish);
|
|
921
936
|
});
|
|
922
937
|
}
|
|
923
938
|
|
|
924
939
|
function speakBrowser(text){
|
|
925
940
|
return new Promise((resolve) => {
|
|
926
941
|
if (!window.speechSynthesis){ resolve(); return; }
|
|
942
|
+
let done = false;
|
|
943
|
+
const finish = () => { if (done) return; done = true; clearInterval(poll); resolve(); };
|
|
927
944
|
window.speechSynthesis.cancel();
|
|
928
945
|
const u = new SpeechSynthesisUtterance(text);
|
|
929
946
|
u.rate = 1.05;
|
|
930
|
-
u.onend =
|
|
931
|
-
u.onerror =
|
|
947
|
+
u.onend = finish;
|
|
948
|
+
u.onerror = finish;
|
|
932
949
|
window.speechSynthesis.speak(u);
|
|
950
|
+
// iOS Safari frequently drops the 'end' event on long utterances — poll the speaking flag.
|
|
951
|
+
// Allow a small grace period for synthesis to actually start before treating !speaking as done.
|
|
952
|
+
let started = false;
|
|
953
|
+
const startedBy = Date.now() + 1500;
|
|
954
|
+
const poll = setInterval(() => {
|
|
955
|
+
const sp = window.speechSynthesis;
|
|
956
|
+
if (!sp){ finish(); return; }
|
|
957
|
+
if (sp.speaking) started = true;
|
|
958
|
+
if (started && !sp.speaking && !sp.pending) finish();
|
|
959
|
+
else if (!started && Date.now() > startedBy && !sp.speaking && !sp.pending) finish(); // never started
|
|
960
|
+
}, 250);
|
|
961
|
+
// Absolute hard ceiling so the UI can never get stuck.
|
|
962
|
+
setTimeout(finish, Math.max(15000, Math.min(180000, text.length * 90)));
|
|
933
963
|
});
|
|
934
964
|
}
|
|
935
965
|
|