diff --git a/public/src/tts-modal.js b/public/src/tts-modal.js index 3c23d3b..e73da00 100644 --- a/public/src/tts-modal.js +++ b/public/src/tts-modal.js @@ -17,7 +17,7 @@ import { initializeChunkFiveFont } from "./ui-themes.js"; import { getUIText } from "./ui-translations.js"; -import { handleTTS, downloadAudio, generateTitleForTTS, getTTSMode, setTTSMode, primeAudioPlayback } from "./tts.js"; +import { handleTTS, downloadAudio, generateTitleForTTS, getTTSMode, setTTSMode, primeAudioPlayback, normalizeModelVoice } from "./tts.js"; import { VOICES_API_URL } from "./config.js"; // Function to fetch and populate voice dropdown @@ -356,14 +356,15 @@ export function openTTSModal() { const speed = parseFloat(speedSlider.value); // Parse model:voice format (e.g., "tts-1-f5:aria" or legacy "aria") + // Normalize legacy/qwen entries so stale vault data doesn't 503. let model = 'tts-1-f5'; let voice = selectedValue; - if (selectedValue.includes(':')) { const parts = selectedValue.split(':'); model = parts[0]; voice = parts[1]; } + ({ model, voice } = normalizeModelVoice(model, voice)); try { const currentMode = getTTSMode(); diff --git a/public/src/tts.js b/public/src/tts.js index fcfb9be..2f7261c 100644 --- a/public/src/tts.js +++ b/public/src/tts.js @@ -327,20 +327,42 @@ async function fetchTTSChunk(chunkText, voice, model) { } } -// Silent 1-frame MP3 (data URI). Used to "prime" audio output inside a user -// gesture handler so iOS Safari unlocks audio playback before async work runs. -// Without this, audio.play() called after `await` rejects with NotAllowedError. -const SILENT_MP3_DATA_URI = - "data:audio/mp3;base64,SUQzBAAAAAABEVRYWFgAAAAtAAADY29tbWVudABCaWdTb3VuZEJhbmsuY29tIC8gTGFTb25vdGhlcXVlLm9yZwBURU5DAAAAHQAAA1N3aXRjaCBQbHVzIMKpIE5DSCBTb2Z0d2FyZQBUSVQyAAAABgAAAzIyMzUAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAAAAD/80DEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//MUZAAAAAGkAAAAAAAAA0gAAAAAVVVV"; +// Legacy voice names from the deprecated Qwen-only era. Anyone with these in +// their vault would otherwise hit 503 since the backend routes "tts-1" → qwen +// and qwen-tts is not installed in the F5 sidecar. +const LEGACY_OPENAI_VOICES = new Set([ + "alloy", "echo", "fable", "onyx", "nova", "shimmer", + "amber", "breeze", "coral", "dawn", "ember", "frost", + "glow", "haze", "jade", "kite", "lark", "mist", "nectar", +]); -// Call inside a user-gesture handler before doing any async work, so iOS Safari -// unlocks audio output for subsequent .play() calls after `await`. +// Normalize a model+voice pair: remap legacy / unknown models to F5, and +// remap legacy OpenAI voice names to F5's "aria" default. +export function normalizeModelVoice(model, voice) { + const isQwen = !model || model === "tts-1" || model.includes("qwen"); + const normModel = isQwen ? "tts-1-f5" : model; + const normVoice = LEGACY_OPENAI_VOICES.has(voice) ? "aria" : voice; + return { model: normModel, voice: normVoice }; +} + +// Call inside a user-gesture handler before doing any async work to unlock iOS +// Safari audio output for subsequent .play() calls after `await`. Uses an +// AudioContext + zero-gain silent buffer (NOT a "silent" MP3 data URI, which +// can contain encoder artifacts the user hears as a pop/buzz before playback). +let _audioCtx = null; export function primeAudioPlayback() { try { - const a = new Audio(SILENT_MP3_DATA_URI); - a.volume = 0; - const p = a.play(); - if (p && p.catch) p.catch(() => {}); + const Ctx = window.AudioContext || window.webkitAudioContext; + if (!Ctx) return; + if (!_audioCtx) _audioCtx = new Ctx(); + if (_audioCtx.state === "suspended") _audioCtx.resume(); + const buffer = _audioCtx.createBuffer(1, 1, _audioCtx.sampleRate || 22050); + const source = _audioCtx.createBufferSource(); + const gain = _audioCtx.createGain(); + gain.gain.value = 0; + source.buffer = buffer; + source.connect(gain).connect(_audioCtx.destination); + source.start(0); } catch (e) { console.warn("primeAudioPlayback failed:", e); } diff --git a/public/src/uncloseai-embed-modal.js b/public/src/uncloseai-embed-modal.js index ca7c620..0ea3294 100644 --- a/public/src/uncloseai-embed-modal.js +++ b/public/src/uncloseai-embed-modal.js @@ -18,6 +18,7 @@ import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/es/highlight.min.js"; import { setSystemMessageAppend, isTTSEnabled } from "./config.js"; +import { normalizeModelVoice } from "./tts.js"; import { extractWebpageContent, fitPageContent, buildPageAnalysisPrompt, parsePageAnalysis, buildAnalysisContext } from "./content.js"; import { computePageIntelligence, formatPageIntelligence } from "./page-intelligence.js"; import { loadSiteJourney, recordPageVisit, formatJourneyContext, savePageSummary, getRelevantPageSummaries, saveConversationSummary, saveSiteLinks } from "./storage.js"; @@ -54,15 +55,17 @@ async function ensureCryptoJS() { // Get USE_CUSTOM_STYLING from window or default const _USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false; -// Helper function to parse model:voice format +// Helper function to parse model:voice format. Normalizes legacy/qwen models +// and legacy OpenAI voice names so stale vault entries don't hit 503. function parseVoiceSelection(selectedValue) { - // Check if format is "model:voice" or legacy "voice" + let model = "tts-1-f5"; + let voice = selectedValue; if (selectedValue.includes(":")) { - const [model, voice] = selectedValue.split(":"); - return { model, voice }; + const [m, v] = selectedValue.split(":"); + model = m; + voice = v; } - // Legacy format - assume tts-1-f5 model - return { model: "tts-1-f5", voice: selectedValue }; + return normalizeModelVoice(model, voice); } // Import required functions dynamically to avoid circular dependencies @@ -1498,7 +1501,7 @@ async function openUncloseaiEmbeddedModalNew() { userTtsBtn.onclick = async () => { const modalVoiceSelect = document.getElementById("hermes-voice-select"); - const selectedVoice = modalVoiceSelect?.value || "tts-1:alloy"; + const selectedVoice = modalVoiceSelect?.value || "tts-1-f5:aria"; // If audio is already playing, pause it if (userAudio && !userAudio.paused) { @@ -1692,7 +1695,7 @@ async function openUncloseaiEmbeddedModalNew() { const modalVoiceSelect = document.getElementById( "hermes-voice-select", ); - const selectedVoice = modalVoiceSelect?.value || "tts-1:alloy"; + const selectedVoice = modalVoiceSelect?.value || "tts-1-f5:aria"; // If audio is already playing, pause it if (currentAudio && !currentAudio.paused) { @@ -2167,7 +2170,7 @@ You have complete knowledge of this page content and can reference any details, const modalVoiceSelect = document.getElementById( "hermes-voice-select", ); - const selectedVoice = modalVoiceSelect?.value || "tts-1:alloy"; + const selectedVoice = modalVoiceSelect?.value || "tts-1-f5:aria"; // If audio is already playing, pause it if (historicalUserAudio && !historicalUserAudio.paused) { @@ -2331,7 +2334,7 @@ You have complete knowledge of this page content and can reference any details, const modalVoiceSelect = document.getElementById( "hermes-voice-select", ); - const selectedVoice = modalVoiceSelect?.value || "tts-1:alloy"; + const selectedVoice = modalVoiceSelect?.value || "tts-1-f5:aria"; // If audio is already playing, pause it if (historicalAudio && !historicalAudio.paused) { @@ -2800,7 +2803,7 @@ You have complete knowledge of this page content and can reference any details, const modalVoiceSelect = document.getElementById( "hermes-voice-select", ); - const selectedVoice = modalVoiceSelect?.value || "tts-1:alloy"; + const selectedVoice = modalVoiceSelect?.value || "tts-1-f5:aria"; // If audio is already playing, pause it if (introAudio && !introAudio.paused) {