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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myaiforone",
3
- "version": "1.1.63",
3
+ "version": "1.1.64",
4
4
  "type": "module",
5
5
  "description": "Routes messages from phone channels to project-specific Claude Code agents",
6
6
  "bin": {
@@ -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 = () => { try { URL.revokeObjectURL(url); } catch {}; resolve(); };
919
- audioElement.onerror = () => { resolve(); };
920
- audioElement.play().catch(() => resolve());
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 = () => resolve();
931
- u.onerror = () => resolve();
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