// TTSFM Playground JavaScript // Global variables let currentAudioBlob = null; let currentFormat = 'mp3'; let batchResults = []; // Initialize playground document.addEventListener('DOMContentLoaded', function() { initializePlayground(); }); // Check authentication status and show/hide API key field async function checkAuthStatus() { try { const response = await fetch('/api/auth-status'); const data = await response.json(); const apiKeySection = document.getElementById('api-key-section'); if (apiKeySection) { if (data.api_key_required) { // Show API key field and mark as required apiKeySection.style.display = 'block'; const apiKeyInput = document.getElementById('api-key-input'); const label = apiKeySection.querySelector('label'); if (apiKeyInput) { apiKeyInput.required = true; apiKeyInput.placeholder = 'Enter your API key (required)'; } if (label) { label.innerHTML = '' + (window.currentLocale === 'zh' ? 'API密钥(必需)' : 'API Key (Required)'); } // Update form text const formText = apiKeySection.querySelector('.form-text'); if (formText) { formText.innerHTML = 'API key protection is enabled - this field is required'; } } else { // Hide API key field or mark as optional apiKeySection.style.display = 'none'; } } } catch (error) { console.warn('Could not check auth status:', error); // If we can't check, assume API key might be required and show the field const apiKeySection = document.getElementById('api-key-section'); if (apiKeySection) { apiKeySection.style.display = 'block'; } } } function initializePlayground() { console.log('Initializing playground...'); checkAuthStatus(); loadVoices(); loadFormats(); updateCharCount(); setupEventListeners(); console.log('Playground initialization complete'); // Initialize tooltips if Bootstrap is available if (typeof bootstrap !== 'undefined') { const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl); }); } } function setupEventListeners() { console.log('Setting up event listeners...'); // Form and input events const textInput = document.getElementById('text-input'); if (textInput) { textInput.addEventListener('input', updateCharCount); console.log('Text input event listener added'); } else { console.error('Text input element not found!'); } // Add form submit event listener with better error handling const form = document.getElementById('tts-form'); if (form) { form.addEventListener('submit', function(event) { console.log('Form submit event triggered'); event.preventDefault(); // Prevent default form submission event.stopPropagation(); // Stop event bubbling generateSpeech(event); return false; // Additional prevention }); } else { console.error('TTS form not found!'); } const maxLengthInput = document.getElementById('max-length-input'); if (maxLengthInput) { maxLengthInput.addEventListener('input', updateCharCount); console.log('Max length input event listener added'); } else { console.error('Max length input element not found!'); } const autoCombineCheck = document.getElementById('auto-combine-check'); if (autoCombineCheck) { autoCombineCheck.addEventListener('change', updateAutoCombineStatus); } // Enhanced button events const validateBtn = document.getElementById('validate-text-btn'); if (validateBtn) { validateBtn.addEventListener('click', validateText); console.log('Validate button event listener added'); } else { console.error('Validate button not found!'); } const randomBtn = document.getElementById('random-text-btn'); if (randomBtn) { randomBtn.addEventListener('click', loadRandomText); console.log('Random text button event listener added'); } else { console.error('Random text button not found!'); } const downloadBtn = document.getElementById('download-btn'); if (downloadBtn) { downloadBtn.addEventListener('click', downloadAudio); console.log('Download button event listener added'); } else { console.error('Download button not found!'); } // Add direct click event listener for generate button as backup const generateBtn = document.getElementById('generate-btn'); if (generateBtn) { generateBtn.addEventListener('click', function(event) { console.log('Generate button clicked directly'); event.preventDefault(); event.stopPropagation(); generateSpeech(event); return false; }); } // New button events const clearTextBtn = document.getElementById('clear-text-btn'); if (clearTextBtn) { clearTextBtn.addEventListener('click', clearText); } const resetFormBtn = document.getElementById('reset-form-btn'); if (resetFormBtn) { resetFormBtn.addEventListener('click', resetForm); } const replayBtn = document.getElementById('replay-btn'); if (replayBtn) { replayBtn.addEventListener('click', replayAudio); } const shareBtn = document.getElementById('share-btn'); if (shareBtn) { shareBtn.addEventListener('click', shareAudio); } // API Key visibility toggle const toggleApiKeyBtn = document.getElementById('toggle-api-key-visibility'); if (toggleApiKeyBtn) { toggleApiKeyBtn.addEventListener('click', toggleApiKeyVisibility); } // Voice and format selection events const voiceSelect = document.getElementById('voice-select'); if (voiceSelect) { voiceSelect.addEventListener('change', updateVoiceInfo); console.log('Voice select event listener added'); } else { console.error('Voice select element not found!'); } const formatSelect = document.getElementById('format-select'); if (formatSelect) { formatSelect.addEventListener('change', updateFormatInfo); console.log('Format select event listener added'); } else { console.error('Format select element not found!'); } // Example text buttons document.querySelectorAll('.use-example').forEach(button => { button.addEventListener('click', function() { document.getElementById('text-input').value = this.dataset.text; updateCharCount(); // Add visual feedback this.classList.add('btn-success'); setTimeout(() => { this.classList.remove('btn-success'); this.classList.add('btn-outline-primary'); }, 1000); }); }); // Keyboard shortcuts document.addEventListener('keydown', function(e) { // Ctrl/Cmd + Enter to generate speech if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); document.getElementById('generate-btn').click(); } // Escape to clear results if (e.key === 'Escape') { clearResults(); } }); // Initialize auto-combine status updateAutoCombineStatus(); } async function loadVoices() { try { // Prepare headers for API key if available (OpenAI compatible format) const headers = {}; const apiKeyInput = document.getElementById('api-key-input'); if (apiKeyInput && apiKeyInput.value.trim()) { headers['Authorization'] = `Bearer ${apiKeyInput.value.trim()}`; } const response = await fetch('/api/voices', { headers }); const data = await response.json(); const select = document.getElementById('voice-select'); select.innerHTML = ''; data.voices.forEach(voice => { const option = document.createElement('option'); option.value = voice.id; option.textContent = `${voice.name} - ${voice.description}`; select.appendChild(option); }); // Select default voice select.value = 'alloy'; } catch (error) { console.error('Failed to load voices:', error); console.log('Failed to load voices. Please refresh the page.'); } } async function loadFormats() { try { // Prepare headers for API key if available (OpenAI compatible format) const headers = {}; const apiKeyInput = document.getElementById('api-key-input'); if (apiKeyInput && apiKeyInput.value.trim()) { headers['Authorization'] = `Bearer ${apiKeyInput.value.trim()}`; } const response = await fetch('/api/formats', { headers }); const data = await response.json(); const select = document.getElementById('format-select'); select.innerHTML = ''; data.formats.forEach(format => { const option = document.createElement('option'); option.value = format.id; option.textContent = `${format.name} - ${format.description}`; select.appendChild(option); }); // Select default format select.value = 'mp3'; updateFormatInfo(); } catch (error) { console.error('Failed to load formats:', error); console.log('Failed to load formats. Please refresh the page.'); } } function updateCharCount() { const textInput = document.getElementById('text-input'); const maxLengthInput = document.getElementById('max-length-input'); const charCountElement = document.getElementById('char-count'); if (!textInput || !maxLengthInput || !charCountElement) { console.warn('Required elements not found for updateCharCount'); return; } const text = textInput.value; const maxLength = parseInt(maxLengthInput.value) || 4096; const charCount = text.length; charCountElement.textContent = charCount.toLocaleString(); // Update length status with better visual feedback const statusElement = document.getElementById('length-status'); if (statusElement) { const percentage = (charCount / maxLength) * 100; if (charCount > maxLength) { statusElement.innerHTML = 'Exceeds limit'; } else if (percentage > 80) { statusElement.innerHTML = 'Near limit'; } else if (percentage > 50) { statusElement.innerHTML = 'Good'; } else { statusElement.innerHTML = 'OK'; } } updateGenerateButton(); updateAutoCombineStatus(); } function updateGenerateButton() { const text = document.getElementById('text-input').value; const maxLength = parseInt(document.getElementById('max-length-input').value) || 4096; const autoCombineCheck = document.getElementById('auto-combine-check'); const autoCombine = autoCombineCheck ? autoCombineCheck.checked : false; const generateBtn = document.getElementById('generate-btn'); if (!generateBtn) { console.warn('Generate button not found'); return; } const btnText = generateBtn.querySelector('.btn-text'); if (!btnText) { console.warn('Button text element not found'); return; } if (text.length > maxLength && autoCombine) { btnText.innerHTML = 'Generate Speech (Auto-Combine)'; generateBtn.classList.add('btn-warning'); generateBtn.classList.remove('btn-primary'); } else { btnText.innerHTML = 'Generate Speech'; generateBtn.classList.add('btn-primary'); generateBtn.classList.remove('btn-warning'); } } async function validateText() { const text = document.getElementById('text-input').value.trim(); const maxLength = parseInt(document.getElementById('max-length-input').value) || 4096; if (!text) { console.log('Please enter some text to validate'); return; } const validateBtn = document.getElementById('validate-text-btn'); setLoading(validateBtn, true); try { const response = await fetch('/api/validate-text', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, max_length: maxLength }) }); const data = await response.json(); const resultDiv = document.getElementById('validation-result'); if (data.is_valid) { resultDiv.innerHTML = `
Text is valid! (${data.text_length.toLocaleString()} characters)
`; } else { resultDiv.innerHTML = `
Text exceeds limit! (${data.text_length.toLocaleString()}/${data.max_length.toLocaleString()} characters)
Suggested chunks: ${data.suggested_chunks}
Preview of chunks:
${data.chunk_preview.map((chunk, i) => `
Chunk ${i+1}:
${chunk}
`).join('')}
`; } resultDiv.classList.remove('d-none'); resultDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (error) { console.error('Validation failed:', error); console.log('Failed to validate text. Please try again.'); } finally { setLoading(validateBtn, false); } } function updateAutoCombineStatus() { const autoCombineCheck = document.getElementById('auto-combine-check'); const statusBadge = document.getElementById('auto-combine-status'); const textInput = document.getElementById('text-input'); const maxLength = parseInt(document.getElementById('max-length-input').value) || 4096; if (!autoCombineCheck || !statusBadge) return; const isAutoCombineEnabled = autoCombineCheck.checked; const textLength = textInput.value.length; const isLongText = textLength > maxLength; // Show/hide status badge if (isAutoCombineEnabled && isLongText) { statusBadge.classList.remove('d-none'); statusBadge.classList.add('bg-success'); statusBadge.classList.remove('bg-warning'); statusBadge.innerHTML = 'Auto-combine enabled'; } else if (!isAutoCombineEnabled && isLongText) { statusBadge.classList.remove('d-none'); statusBadge.classList.add('bg-warning'); statusBadge.classList.remove('bg-success'); statusBadge.innerHTML = 'Long text detected'; } else { statusBadge.classList.add('d-none'); } // Remove the recursive call to updateCharCount() - this was causing infinite recursion } async function generateSpeech(event) { console.log('generateSpeech function called'); // Prevent default form submission behavior if (event) { event.preventDefault(); event.stopPropagation(); } const button = document.getElementById('generate-btn'); const audioResult = document.getElementById('audio-result'); // Get form data const formData = getFormData(); if (!validateFormData(formData)) { console.log('Form validation failed'); return false; } // Show loading state setLoading(button, true); clearResults(); try { console.log('Starting speech generation...'); // Always use the unified endpoint with auto-combine await generateUnifiedSpeech(formData); console.log('Speech generation completed successfully'); } catch (error) { console.error('Generation failed:', error); console.log(`Failed to generate speech: ${error.message}`); } finally { setLoading(button, false); } return false; // Ensure form doesn't submit } function getFormData() { return { text: document.getElementById('text-input').value.trim(), voice: document.getElementById('voice-select').value, format: document.getElementById('format-select').value, instructions: document.getElementById('instructions-input').value.trim(), maxLength: parseInt(document.getElementById('max-length-input').value) || 4096, validateLength: document.getElementById('validate-length-check').checked, autoCombine: document.getElementById('auto-combine-check').checked, apiKey: document.getElementById('api-key-input').value.trim() }; } function validateFormData(formData) { if (!formData.text || !formData.voice || !formData.format) { console.log('Please fill in all required fields'); return false; } if (formData.text.length > formData.maxLength && formData.validateLength && !formData.autoCombine) { console.log(`Text is too long (${formData.text.length} characters). Enable auto-combine or reduce text length.`); return false; } return true; } function clearResults() { document.getElementById('audio-result').classList.add('d-none'); const batchResult = document.getElementById('batch-result'); if (batchResult) { batchResult.classList.add('d-none'); } document.getElementById('validation-result').classList.add('d-none'); } // Utility functions function setLoading(button, loading) { if (loading) { button.classList.add('loading'); button.disabled = true; } else { button.classList.remove('loading'); button.disabled = false; } } // New unified function using OpenAI-compatible endpoint with auto-combine async function generateUnifiedSpeech(formData) { const audioResult = document.getElementById('audio-result'); // Prepare headers const headers = { 'Content-Type': 'application/json' }; // Add API key if provided (OpenAI compatible format) if (formData.apiKey) { headers['Authorization'] = `Bearer ${formData.apiKey}`; } const response = await fetch('/v1/audio/speech', { method: 'POST', headers: headers, body: JSON.stringify({ model: 'gpt-4o-mini-tts', input: formData.text, voice: formData.voice, response_format: formData.format, instructions: formData.instructions || undefined, auto_combine: formData.autoCombine, max_length: formData.maxLength }) }); if (!response.ok) { const errorData = await response.json(); const errorMessage = errorData.error?.message || errorData.error || `HTTP ${response.status}`; throw new Error(errorMessage); } // Get audio data const audioBlob = await response.blob(); currentAudioBlob = audioBlob; currentFormat = formData.format; // Create audio URL and setup player const audioUrl = URL.createObjectURL(audioBlob); const audioPlayer = document.getElementById('audio-player'); audioPlayer.src = audioUrl; // Get response headers for enhanced display const chunksCount = response.headers.get('X-Chunks-Combined') || '1'; const autoCombineUsed = response.headers.get('X-Auto-Combine') === 'true'; const originalLength = response.headers.get('X-Original-Text-Length'); // Use enhanced display function with new metadata displayAudioResult(audioBlob, formData.format, formData.voice, formData.text, { chunksCount, autoCombineUsed, originalLength }); console.log('Speech generated successfully! Click play to listen.'); if (autoCombineUsed && chunksCount > 1) { console.log(`Auto-combine feature combined ${chunksCount} chunks into a single audio file.`); } // Auto-play if user prefers if (localStorage.getItem('autoPlay') === 'true') { audioPlayer.play().catch(() => { // Auto-play blocked, that's fine }); } } // Legacy function for backward compatibility async function generateSingleSpeech(formData) { // Use the new unified function await generateUnifiedSpeech(formData); } function downloadAudio() { if (!currentAudioBlob) { console.log('No audio to download'); return; } const url = URL.createObjectURL(currentAudioBlob); const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); downloadFromUrl(url, `ttsfm-speech-${timestamp}.${currentFormat}`); URL.revokeObjectURL(url); } function downloadFromUrl(url, filename) { const a = document.createElement('a'); a.href = url; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } // New enhanced functions function clearText() { document.getElementById('text-input').value = ''; updateCharCount(); clearResults(); console.log('Text cleared successfully'); } function loadRandomText() { const randomTexts = [ // News & Information "Breaking news: Scientists have discovered a revolutionary new method for generating incredibly natural synthetic speech using advanced neural networks and machine learning algorithms.", "Weather update: Today will be partly cloudy with temperatures reaching 75 degrees Fahrenheit. Light winds from the southwest at 5 to 10 miles per hour.", "Technology report: The latest advancements in artificial intelligence are revolutionizing how we interact with digital devices and services.", // Educational & Informative "The human brain contains approximately 86 billion neurons, each connected to thousands of others, creating a complex network that enables consciousness, memory, and thought.", "Photosynthesis is the process by which plants convert sunlight, carbon dioxide, and water into glucose and oxygen, forming the foundation of most life on Earth.", "The speed of light in a vacuum is exactly 299,792,458 meters per second, making it one of the fundamental constants of physics.", // Creative & Storytelling "Once upon a time, in a land far away, there lived a wise old wizard who could speak to the stars and understand their ancient secrets.", "The mysterious lighthouse stood alone on the rocky cliff, its beacon cutting through the fog like a sword of light, guiding lost ships safely home.", "In the depths of the enchanted forest, where sunbeams danced through emerald leaves, a young adventurer discovered a hidden path to destiny.", // Business & Professional "Our quarterly results demonstrate strong growth across all market segments, with revenue increasing by 23% compared to the same period last year.", "The new product launch exceeded expectations, capturing 15% market share within the first six months and establishing our brand as an industry leader.", "We are committed to sustainable business practices that benefit our customers, employees, and the environment for generations to come.", // Technical & Programming "The TTSFM package provides a comprehensive API for text-to-speech generation with support for multiple voices and audio formats.", "Machine learning algorithms process vast amounts of data to identify patterns and make predictions with remarkable accuracy.", "Cloud computing has transformed how businesses store, process, and access their data, enabling scalability and flexibility like never before.", // Conversational & Casual "Welcome to TTSFM! Experience the future of text-to-speech technology with our premium AI voices.", "Good morning! Today is a beautiful day to learn something new and explore the possibilities of text-to-speech technology.", "Have you ever wondered what it would be like if your computer could speak with perfect human-like intonation and emotion?" ]; const randomText = randomTexts[Math.floor(Math.random() * randomTexts.length)]; document.getElementById('text-input').value = randomText; updateCharCount(); console.log('Random text loaded successfully'); } function resetForm() { // Reset form to default values document.getElementById('text-input').value = 'Welcome to TTSFM! Experience the future of text-to-speech technology with our premium AI voices. Generate natural, expressive speech for any application.'; document.getElementById('voice-select').value = 'alloy'; document.getElementById('format-select').value = 'mp3'; document.getElementById('instructions-input').value = ''; document.getElementById('max-length-input').value = '4096'; document.getElementById('validate-length-check').checked = true; const autoCombineCheck = document.getElementById('auto-combine-check'); if (autoCombineCheck) { autoCombineCheck.checked = true; } updateCharCount(); updateGenerateButton(); clearResults(); console.log('Form reset to default values'); } function replayAudio() { const audioPlayer = document.getElementById('audio-player'); if (audioPlayer && audioPlayer.src) { audioPlayer.currentTime = 0; audioPlayer.play().catch(() => { console.log('Unable to replay audio. Please check your browser settings.'); }); } } function shareAudio() { if (navigator.share && currentAudioBlob) { const file = new File([currentAudioBlob], `ttsfm-speech.${currentFormat}`, { type: `audio/${currentFormat}` }); navigator.share({ title: 'TTSFM Generated Speech', text: 'Check out this speech generated with TTSFM!', files: [file] }).catch(() => { // Fallback to copying link copyAudioLink(); }); } else { copyAudioLink(); } } function copyAudioLink() { const audioPlayer = document.getElementById('audio-player'); if (audioPlayer && audioPlayer.src) { navigator.clipboard.writeText(audioPlayer.src).then(() => { console.log('Audio link copied to clipboard!'); }).catch(() => { console.log('Unable to copy link. Please try downloading the audio instead.'); }); } } function updateVoiceInfo() { const voiceSelect = document.getElementById('voice-select'); const previewBtn = document.getElementById('preview-voice-btn'); if (voiceSelect.value) { previewBtn.disabled = false; previewBtn.onclick = () => previewVoice(voiceSelect.value); } else { previewBtn.disabled = true; } } function updateFormatInfo() { const formatSelect = document.getElementById('format-select'); const formatInfo = document.getElementById('format-info'); const formatDescriptions = { 'mp3': '🎵 MP3 - Good quality, small file size. Best for web and general use.', 'opus': '📻 OPUS - Excellent quality, small file size. Best for streaming and VoIP.', 'aac': '📱 AAC - Good quality, medium file size. Best for Apple devices and streaming.', 'flac': '💿 FLAC - Lossless quality, large file size. Best for archival and high-quality audio.', 'wav': '🎧 WAV - Lossless quality, large file size. Best for professional audio production.', 'pcm': '🔊 PCM - Raw audio data, large file size. Best for audio processing.' }; if (formatInfo && formatSelect.value) { formatInfo.textContent = formatDescriptions[formatSelect.value] || 'High-quality audio format'; } } function previewVoice(voiceId) { // This would typically play a short preview of the voice console.log(`Voice preview for ${voiceId} - Feature coming soon!`); } // Enhanced audio result display with auto-combine metadata function displayAudioResult(audioBlob, format, voice, text, metadata = {}) { const audioResult = document.getElementById('audio-result'); const audioPlayer = document.getElementById('audio-player'); const audioInfo = document.getElementById('audio-info'); // Create audio URL and setup player const audioUrl = URL.createObjectURL(audioBlob); audioPlayer.src = audioUrl; // Update audio stats const sizeKB = (audioBlob.size / 1024).toFixed(1); document.getElementById('audio-size').textContent = `${sizeKB} KB`; document.getElementById('audio-format').textContent = format.toUpperCase(); document.getElementById('audio-voice').textContent = voice.charAt(0).toUpperCase() + voice.slice(1); // Update audio info safely without innerHTML // Clear existing content audioInfo.textContent = ''; // Create and append icon element const icon = document.createElement('i'); icon.className = 'fas fa-check-circle text-success me-1'; audioInfo.appendChild(icon); // Create info text with auto-combine details let infoText = `Generated successfully • ${sizeKB} KB • ${format.toUpperCase()}`; if (metadata.autoCombineUsed && metadata.chunksCount > 1) { infoText += ` • Auto-combined ${metadata.chunksCount} chunks`; // Add a special badge for auto-combine const badge = document.createElement('span'); badge.className = 'badge bg-primary ms-2'; badge.innerHTML = 'Auto-combined'; audioInfo.appendChild(document.createTextNode(infoText)); audioInfo.appendChild(badge); } else { // Create and append text content (safely escaped) const textNode = document.createTextNode(infoText); audioInfo.appendChild(textNode); } // Show result with animation audioResult.classList.remove('d-none'); audioResult.classList.add('fade-in'); // Update duration when metadata loads audioPlayer.addEventListener('loadedmetadata', function() { const duration = Math.round(audioPlayer.duration); document.getElementById('audio-duration').textContent = `${duration}s`; }, { once: true }); // Scroll to result audioResult.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } // API Key visibility toggle function function toggleApiKeyVisibility() { const apiKeyInput = document.getElementById('api-key-input'); const eyeIcon = document.getElementById('api-key-eye-icon'); if (apiKeyInput.type === 'password') { apiKeyInput.type = 'text'; eyeIcon.className = 'fas fa-eye-slash'; } else { apiKeyInput.type = 'password'; eyeIcon.className = 'fas fa-eye'; } } // Export functions for use in HTML window.clearText = clearText; window.loadRandomText = loadRandomText; window.resetForm = resetForm; window.toggleApiKeyVisibility = toggleApiKeyVisibility;