diff --git a/public/src/tts-modal.js b/public/src/tts-modal.js index 45358e3..06fb3fb 100644 --- a/public/src/tts-modal.js +++ b/public/src/tts-modal.js @@ -11,7 +11,7 @@ async function populateVoiceDropdown(voiceSelection) { const cacheTimeKey = 'uncloseai_voices_cache_time'; const cacheVersionKey = 'uncloseai_voices_cache_version'; const cacheExpiration = 60000; // 1 minute in milliseconds - const currentVersion = '2'; // Increment this when voice models change + const currentVersion = '3'; // Increment this when voice models change (v3: 20 voices) try { const cachedTime = localStorage.getItem(cacheTimeKey); @@ -49,27 +49,62 @@ async function populateVoiceDropdown(voiceSelection) { const data = await response.json(); + // Transform API response format to frontend format + // API returns: {data: [{id: "tts-1-qwen", voices: ["alloy", ...]}]} + // Frontend expects: [{model: "tts-1-qwen", voice: "alloy"}, ...] + let voices = []; + if (data.data && Array.isArray(data.data)) { + data.data.forEach(modelInfo => { + if (modelInfo.voices && Array.isArray(modelInfo.voices)) { + modelInfo.voices.forEach(voice => { + voices.push({ model: modelInfo.id, voice: voice }); + }); + } + }); + } else if (data.voices && Array.isArray(data.voices)) { + // Legacy format support + voices = data.voices; + } + + console.log(`Loaded ${voices.length} voices from API`); + // Cache the response with version try { - localStorage.setItem(cacheKey, JSON.stringify(data.voices)); + localStorage.setItem(cacheKey, JSON.stringify(voices)); localStorage.setItem(cacheTimeKey, Date.now().toString()); localStorage.setItem(cacheVersionKey, currentVersion); } catch (error) { console.warn('Cache write failed:', error); } - renderVoiceOptions(voiceSelection, data.voices); - return data.voices; + renderVoiceOptions(voiceSelection, voices); + return voices; } catch (error) { console.error('Failed to fetch voices:', error); - // Fallback to default voices + // Fallback to default voices (20 voices from Qwen3-TTS) const defaultVoices = [ - { model: 'tts-1', voice: 'alloy' }, - { model: 'tts-1', voice: 'echo' }, - { model: 'tts-1', voice: 'fable' }, - { model: 'tts-1', voice: 'onyx' }, - { model: 'tts-1', voice: 'nova' }, - { model: 'tts-1', voice: 'shimmer' } + // Standard OpenAI-compatible voices + { model: 'tts-1-qwen', voice: 'alloy' }, + { model: 'tts-1-qwen', voice: 'echo' }, + { model: 'tts-1-qwen', voice: 'fable' }, + { model: 'tts-1-qwen', voice: 'onyx' }, + { model: 'tts-1-qwen', voice: 'nova' }, + { model: 'tts-1-qwen', voice: 'shimmer' }, + // Extended voices + { model: 'tts-1-qwen', voice: 'amber' }, + { model: 'tts-1-qwen', voice: 'breeze' }, + { model: 'tts-1-qwen', voice: 'coral' }, + { model: 'tts-1-qwen', voice: 'dawn' }, + { model: 'tts-1-qwen', voice: 'ember' }, + { model: 'tts-1-qwen', voice: 'frost' }, + { model: 'tts-1-qwen', voice: 'glow' }, + { model: 'tts-1-qwen', voice: 'haze' }, + { model: 'tts-1-qwen', voice: 'ivy' }, + { model: 'tts-1-qwen', voice: 'jade' }, + { model: 'tts-1-qwen', voice: 'kite' }, + { model: 'tts-1-qwen', voice: 'lark' }, + { model: 'tts-1-qwen', voice: 'mist' }, + { model: 'tts-1-qwen', voice: 'nectar' } ]; renderVoiceOptions(voiceSelection, defaultVoices); return defaultVoices;