myaiforone 1.1.64 → 1.1.65

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.64",
3
+ "version": "1.1.65",
4
4
  "type": "module",
5
5
  "description": "Routes messages from phone channels to project-specific Claude Code agents",
6
6
  "bin": {
@@ -426,7 +426,8 @@ let currentJobId = null; // in-flight chat job id
426
426
  let lastUserText = ''; // last transcribed user input
427
427
  let lastAgentText = ''; // last agent response (full)
428
428
  let lastAudioBlob = null; // cached TTS audio Blob for re-reading
429
- let audioElement = null; // HTMLAudioElement currently playing TTS
429
+ let audioElement = null; // HTMLAudioElement currently playing TTS (reused — keeps iOS user-activation)
430
+ let audioUnlocked = false; // iOS Safari: have we primed audio in a user gesture yet?
430
431
 
431
432
  // ─── Voice config (decides server-side vs browser path) ──
432
433
  async function loadVoiceConfig(){
@@ -621,8 +622,41 @@ async function newSession(){
621
622
  }
622
623
  }
623
624
 
625
+ // ─── iOS Safari audio unlock ──────────────────────────
626
+ // Must be called SYNCHRONOUSLY from a user-gesture handler (e.g. the tap on the big button).
627
+ // After this primes successfully once, later async-triggered audio.play() and
628
+ // speechSynthesis.speak() calls are permitted by iOS for the lifetime of the page.
629
+ function unlockAudio(){
630
+ if (audioUnlocked) return;
631
+ try {
632
+ // 1) Warm an HTMLAudioElement we'll reuse for blob playback.
633
+ // Tiny silent WAV — plays instantly, satisfies the user-activation requirement.
634
+ if (!audioElement){
635
+ audioElement = new Audio();
636
+ audioElement.preload = 'auto';
637
+ audioElement.playsInline = true;
638
+ audioElement.setAttribute('playsinline', '');
639
+ }
640
+ audioElement.src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQAAAAA=';
641
+ const p = audioElement.play();
642
+ if (p && typeof p.then === 'function') p.catch(() => {});
643
+ } catch {}
644
+ try {
645
+ // 2) Warm speechSynthesis with an empty utterance.
646
+ if (window.speechSynthesis){
647
+ const u = new SpeechSynthesisUtterance('');
648
+ u.volume = 0;
649
+ window.speechSynthesis.speak(u);
650
+ }
651
+ } catch {}
652
+ audioUnlocked = true;
653
+ }
654
+
624
655
  // ─── Big button dispatcher ────────────────────────────
625
656
  function onBigBtn(){
657
+ // Run audio-unlock FIRST and synchronously — iOS only counts this as a user-gesture
658
+ // call if it happens before any await.
659
+ unlockAudio();
626
660
  if (state === 'idle') startListening();
627
661
  else if (state === 'listening') stopListeningAndSend();
628
662
  else if (state === 'processing') abortJob();
@@ -910,13 +944,19 @@ async function playTtsForText(text){
910
944
  function playBlob(blob){
911
945
  return new Promise((resolve) => {
912
946
  let done = false;
947
+ const url = URL.createObjectURL(blob);
913
948
  const finish = () => { if (done) return; done = true; try { URL.revokeObjectURL(url); } catch {}; resolve(); };
914
- if (audioElement){
949
+ // IMPORTANT: reuse the existing audioElement (warmed by unlockAudio on the tap),
950
+ // so iOS Safari still considers it user-activated. Creating `new Audio()` here
951
+ // produces an un-activated element that iOS silently refuses to play.
952
+ if (!audioElement){
953
+ audioElement = new Audio();
954
+ audioElement.preload = 'auto';
955
+ audioElement.playsInline = true;
956
+ audioElement.setAttribute('playsinline', '');
957
+ } else {
915
958
  try { audioElement.pause(); } catch {}
916
- try { URL.revokeObjectURL(audioElement.src); } catch {}
917
959
  }
918
- const url = URL.createObjectURL(blob);
919
- audioElement = new Audio(url);
920
960
  audioElement.onended = finish;
921
961
  audioElement.onerror = finish;
922
962
  audioElement.onpause = () => { // iOS Safari sometimes fires pause instead of ended at completion
@@ -932,7 +972,10 @@ function playBlob(blob){
932
972
  ceilingTimer = setTimeout(finish, Math.ceil(dur * 1000) + 2000);
933
973
  }
934
974
  };
935
- audioElement.play().catch(finish);
975
+ audioElement.src = url;
976
+ try { audioElement.currentTime = 0; } catch {}
977
+ const p = audioElement.play();
978
+ if (p && typeof p.catch === 'function') p.catch(finish);
936
979
  });
937
980
  }
938
981