| <!DOCTYPE html> |
| <html lang="hi"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Cartesia AI TTS + Voice Clone</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;600;700&display=swap'); |
| body { |
| font-family: 'Plus Jakarta+Sans', sans-serif; |
| background-color: #050505; |
| color: #ffffff; |
| } |
| .cartesia-card { |
| background: rgba(20, 20, 20, 0.8); |
| border: 1px solid #333; |
| backdrop-filter: blur(10px); |
| } |
| .pulse-red { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); animation: pulse-red 2s infinite; } |
| @keyframes pulse-red { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } } |
| .tab-btn { padding: 12px 20px; border: none; background: rgba(50,50,50,0.5); color: #999; cursor: pointer; transition: all 0.3s; border-bottom: 2px solid transparent; } |
| .tab-btn.active { background: rgba(70,70,70,0.7); color: #fff; border-bottom-color: #3b82f6; } |
| .tab-content { display: none; } |
| .tab-content.active { display: block; } |
| .credit-badge { |
| display: inline-block; |
| padding: 6px 14px; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| border-radius: 20px; |
| font-size: 0.75rem; |
| font-weight: bold; |
| } |
| .upload-drag { |
| border: 2px dashed #374151; |
| border-radius: 1rem; |
| padding: 2rem; |
| text-align: center; |
| cursor: pointer; |
| transition: all 0.3s; |
| } |
| .upload-drag.dragover { |
| border-color: #3b82f6; |
| background: rgba(59, 130, 246, 0.1); |
| } |
| </style> |
| </head> |
| <body class="p-4 md:p-10 min-h-screen"> |
| <div class="max-w-6xl mx-auto"> |
| |
| <div class="flex flex-col md:flex-row justify-between items-center mb-10 gap-4"> |
| <div> |
| <h1 class="text-4xl font-extrabold tracking-tight text-white">Cartesia <span class="text-blue-500">AI</span></h1> |
| <p class="text-gray-400">TTS + Voice Cloning 🎤</p> |
| </div> |
| <div class="space-y-2 w-full md:w-auto"> |
| <div class="flex items-center gap-3 cartesia-card p-2 px-4 rounded-full justify-between md:justify-start"> |
| <div class="flex items-center gap-3"> |
| <span id="statusDot" class="h-3 w-3 rounded-full bg-red-500 pulse-red flex-shrink-0"></span> |
| <span id="statusText" class="text-sm font-medium text-gray-300 uppercase">Disconnected</span> |
| </div> |
| <div id="creditArea" class="hidden flex items-center gap-2"> |
| <span class="text-xs text-gray-400">|</span> |
| <div class="credit-badge" id="creditBadge">💰 Loading...</div> |
| </div> |
| </div> |
| <div id="creditRefreshArea" class="hidden text-center md:text-right"> |
| <button id="refreshCreditsBtn" class="text-xs text-gray-500 hover:text-gray-300">🔄 Refresh Credits</button> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-12 gap-8"> |
| |
| <div class="lg:col-span-4 space-y-6"> |
| |
| <div class="cartesia-card p-6 rounded-3xl"> |
| <h2 class="text-lg font-bold mb-4">🔑 API Settings</h2> |
| <div class="space-y-4"> |
| <div> |
| <label class="block text-xs font-bold text-gray-500 mb-1">API KEY</label> |
| <input type="password" id="apiKey" placeholder="sk_car_..." |
| class="w-full bg-black border border-gray-800 rounded-xl p-3 text-sm focus:border-blue-500 outline-none"> |
| <div class="flex gap-2 mt-2"> |
| <button id="toggleShowKey" class="text-xs text-gray-500 hover:text-gray-300 flex-1">👁️ Show</button> |
| <button id="clearKeyBtn" class="text-xs text-gray-500 hover:text-red-400 flex-1">❌ Clear</button> |
| </div> |
| </div> |
| <button id="connectBtn" class="w-full bg-white text-black font-bold py-3 rounded-xl hover:bg-gray-200"> |
| Connect Account |
| </button> |
| <p id="savedKeyIndicator" class="text-xs text-green-500 hidden">✓ Connected</p> |
| </div> |
| </div> |
|
|
| |
| <div class="cartesia-card p-6 rounded-3xl"> |
| <h2 class="text-lg font-bold mb-4">🎙️ Voice Engine</h2> |
| <div class="space-y-4"> |
| <div> |
| <label class="block text-xs font-bold text-gray-500 mb-1">SELECT VOICE</label> |
| <select id="voiceSelect" class="w-full bg-black border border-gray-800 rounded-xl p-3 text-sm outline-none focus:border-blue-500"> |
| <option value="">Select...</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-xs font-bold text-gray-500 mb-1">CUSTOM VOICE ID</label> |
| <input type="text" id="customVoiceId" placeholder="Voice ID..." |
| class="w-full bg-black border border-gray-800 rounded-xl p-3 text-sm outline-none focus:border-blue-500"> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="lg:col-span-8 space-y-6"> |
| |
| <div id="logBox" class="hidden cartesia-card p-4 rounded-2xl text-sm border-l-4"> |
| <p id="logMessage"></p> |
| </div> |
|
|
| |
| <div class="cartesia-card rounded-3xl overflow-hidden"> |
| <div class="flex border-b border-gray-800"> |
| <button class="tab-btn active" data-tab="tts">⚡ Text to Speech</button> |
| <button class="tab-btn" data-tab="clone">🎤 Voice Clone</button> |
| </div> |
|
|
| |
| <div id="tts-tab" class="tab-content active p-8"> |
| <label class="block text-xs font-bold text-gray-500 mb-2 text-center">YOUR TEXT</label> |
| <textarea id="textInput" rows="6" placeholder="Yahan likhen..." |
| class="w-full bg-transparent border-b border-gray-800 text-2xl text-center focus:border-blue-500 outline-none resize-none mb-8"></textarea> |
| |
| <div class="flex flex-col items-center gap-6"> |
| <button id="generateBtn" class="bg-blue-600 hover:bg-blue-500 text-white px-10 py-4 rounded-full font-bold text-lg shadow-lg shadow-blue-900/40 disabled:opacity-50"> |
| <span id="btnIcon">⚡</span> Convert to Speech |
| </button> |
|
|
| <div id="audioArea" class="w-full hidden text-center"> |
| <audio id="audioPlayer" controls class="w-full"></audio> |
| <div class="flex gap-2 justify-center mt-3"> |
| <button id="downloadBtn" class="text-xs bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded">⬇️ Download</button> |
| <button id="shareBtn" class="text-xs bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded">📋 Copy</button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="clone-tab" class="tab-content p-8"> |
| <div class="space-y-6"> |
| |
| <div> |
| <label class="block text-xs font-bold text-gray-500 mb-3">📁 AUDIO SAMPLE (30+ secs)</label> |
| <div class="upload-drag" id="uploadArea"> |
| <input type="file" id="audioFile" accept="audio/*" class="hidden"> |
| <p class="text-sm text-gray-400 mb-2">Click ya drag karo</p> |
| <p class="text-xs text-gray-600">WAV, MP3, 30-60 secs</p> |
| <div id="uploadedFileName" class="mt-2 text-xs text-green-400 hidden">✓ Selected</div> |
| </div> |
| </div> |
|
|
| |
| <label class="flex items-center gap-2 cursor-pointer"> |
| <input type="checkbox" id="removeNoise" class="w-4 h-4"> |
| <span class="text-sm text-gray-400">🔇 Remove Background Noise</span> |
| </label> |
|
|
| |
| <div> |
| <label class="block text-xs font-bold text-gray-500 mb-2">VOICE NAME</label> |
| <input type="text" id="voiceName" placeholder="MyVoice, Deepu, etc..." |
| class="w-full bg-black border border-gray-800 rounded-xl p-3 text-sm outline-none focus:border-blue-500"> |
| </div> |
|
|
| |
| <button id="cloneBtn" class="w-full bg-purple-600 hover:bg-purple-500 text-white px-6 py-3 rounded-xl font-bold text-sm shadow-lg shadow-purple-900/40 disabled:opacity-50"> |
| 🎤 Create Voice |
| </button> |
|
|
| |
| <div> |
| <label class="block text-xs font-bold text-gray-500 mb-2">TEST TEXT</label> |
| <textarea id="testText" rows="3" placeholder="Test karne ke liye..." |
| class="w-full bg-black border border-gray-800 rounded-xl p-3 text-sm outline-none focus:border-blue-500 resize-none"></textarea> |
| </div> |
|
|
| <button id="testVoiceBtn" class="w-full bg-green-600 hover:bg-green-500 text-white px-6 py-3 rounded-xl font-bold text-sm shadow-lg disabled:opacity-50"> |
| ▶️ Test Voice |
| </button> |
|
|
| <div id="testAudioArea" class="hidden text-center"> |
| <audio id="testAudioPlayer" controls class="w-full"></audio> |
| <p class="text-xs text-gray-500 mt-2">Apka cloned voice!</p> |
| </div> |
|
|
| |
| <div id="voiceDetailsArea" class="hidden bg-gray-900 p-4 rounded-lg border border-gray-800"> |
| <p class="text-xs text-green-400 mb-2">✓ VOICE CREATED!</p> |
| <p class="text-sm text-gray-300 mb-2">ID: <code id="clonedVoiceId" class="bg-black px-2 py-1 rounded text-xs">...</code></p> |
| <button id="copyVoiceIdBtn" class="text-xs text-gray-400 hover:text-gray-200">📋 Copy ID</button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const apiKeyInput = document.getElementById('apiKey'); |
| const connectBtn = document.getElementById('connectBtn'); |
| const voiceSelect = document.getElementById('voiceSelect'); |
| const customVoiceId = document.getElementById('customVoiceId'); |
| const textInput = document.getElementById('textInput'); |
| const generateBtn = document.getElementById('generateBtn'); |
| const logBox = document.getElementById('logBox'); |
| const logMessage = document.getElementById('logMessage'); |
| const statusDot = document.getElementById('statusDot'); |
| const statusText = document.getElementById('statusText'); |
| const audioArea = document.getElementById('audioArea'); |
| const audioPlayer = document.getElementById('audioPlayer'); |
| const toggleShowKey = document.getElementById('toggleShowKey'); |
| const clearKeyBtn = document.getElementById('clearKeyBtn'); |
| const savedKeyIndicator = document.getElementById('savedKeyIndicator'); |
| const downloadBtn = document.getElementById('downloadBtn'); |
| const shareBtn = document.getElementById('shareBtn'); |
| const creditArea = document.getElementById('creditArea'); |
| const creditBadge = document.getElementById('creditBadge'); |
| const refreshCreditsBtn = document.getElementById('refreshCreditsBtn'); |
| const creditRefreshArea = document.getElementById('creditRefreshArea'); |
| |
| |
| const uploadArea = document.getElementById('uploadArea'); |
| const audioFile = document.getElementById('audioFile'); |
| const uploadedFileName = document.getElementById('uploadedFileName'); |
| const removeNoise = document.getElementById('removeNoise'); |
| const voiceName = document.getElementById('voiceName'); |
| const cloneBtn = document.getElementById('cloneBtn'); |
| const testText = document.getElementById('testText'); |
| const testVoiceBtn = document.getElementById('testVoiceBtn'); |
| const testAudioArea = document.getElementById('testAudioArea'); |
| const testAudioPlayer = document.getElementById('testAudioPlayer'); |
| const voiceDetailsArea = document.getElementById('voiceDetailsArea'); |
| const clonedVoiceId = document.getElementById('clonedVoiceId'); |
| const copyVoiceIdBtn = document.getElementById('copyVoiceIdBtn'); |
| |
| |
| const CARTESIA_API_URL = "https://api.cartesia.ai"; |
| const STORAGE_KEY = "cartesia_api_key"; |
| let currentClonedVoiceId = null; |
| |
| |
| document.querySelectorAll('.tab-btn').forEach(btn => { |
| btn.addEventListener('click', () => { |
| document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); |
| document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); |
| btn.classList.add('active'); |
| document.getElementById(btn.dataset.tab + '-tab').classList.add('active'); |
| }); |
| }); |
| |
| |
| function initializeApp() { |
| const savedKey = localStorage.getItem(STORAGE_KEY); |
| if (savedKey) { |
| apiKeyInput.value = savedKey; |
| showSavedIndicator(); |
| } |
| } |
| |
| function showSavedIndicator() { |
| savedKeyIndicator.classList.remove('hidden'); |
| setTimeout(() => savedKeyIndicator.classList.add('hidden'), 3000); |
| } |
| |
| function updateLog(msg, type = 'info') { |
| logBox.classList.remove('hidden'); |
| logMessage.textContent = msg; |
| if (type === 'error') { |
| logBox.className = "cartesia-card p-4 rounded-2xl text-sm border-l-4 border-red-500 text-red-400"; |
| } else if (type === 'success') { |
| logBox.className = "cartesia-card p-4 rounded-2xl text-sm border-l-4 border-green-500 text-green-400"; |
| } else { |
| logBox.className = "cartesia-card p-4 rounded-2xl text-sm border-l-4 border-blue-500 text-blue-400"; |
| } |
| } |
| |
| |
| async function fetchCredits(key) { |
| try { |
| let credits = null; |
| |
| |
| try { |
| const creditsResponse = await fetch(`${CARTESIA_API_URL}/account/billing/credits`, { |
| headers: { |
| "X-API-Key": key, |
| "Cartesia-Version": "2024-06-10" |
| } |
| }); |
| |
| if (creditsResponse.ok) { |
| const creditsData = await creditsResponse.json(); |
| credits = creditsData.credits || creditsData.remaining_credits || creditsData.balance || 0; |
| console.log("Credits from /account/billing/credits:", credits); |
| } |
| } catch (e1) { |
| console.log("Endpoint 1 failed, trying endpoint 2"); |
| |
| try { |
| const accountResponse = await fetch(`${CARTESIA_API_URL}/account`, { |
| headers: { |
| "X-API-Key": key, |
| "Cartesia-Version": "2024-06-10" |
| } |
| }); |
| |
| if (accountResponse.ok) { |
| const accountData = await accountResponse.json(); |
| credits = accountData.credits || accountData.remaining_credits || accountData.balance || 0; |
| console.log("Credits from /account:", credits); |
| } |
| } catch (e2) { |
| console.log("Both endpoints failed", e1, e2); |
| } |
| } |
| |
| if (credits !== null) { |
| const displayCredits = typeof credits === 'number' ? credits.toFixed(0) : credits; |
| creditBadge.textContent = `💰 ${displayCredits} Credits`; |
| creditArea.classList.remove('hidden'); |
| creditRefreshArea.classList.remove('hidden'); |
| console.log("Credits displayed:", displayCredits); |
| } else { |
| creditBadge.textContent = `💰 Unable to fetch`; |
| creditArea.classList.remove('hidden'); |
| creditRefreshArea.classList.remove('hidden'); |
| } |
| } catch (e) { |
| console.log("Credit fetch error:", e); |
| creditBadge.textContent = `💰 Error`; |
| creditArea.classList.remove('hidden'); |
| creditRefreshArea.classList.remove('hidden'); |
| } |
| } |
| |
| |
| toggleShowKey.addEventListener('click', () => { |
| const type = apiKeyInput.type === 'password' ? 'text' : 'password'; |
| apiKeyInput.type = type; |
| toggleShowKey.textContent = type === 'password' ? '👁️ Show' : '👁️ Hide'; |
| }); |
| |
| |
| clearKeyBtn.addEventListener('click', () => { |
| apiKeyInput.value = ''; |
| localStorage.removeItem(STORAGE_KEY); |
| updateLog("API Key clear!", "info"); |
| statusDot.className = "h-3 w-3 rounded-full bg-red-500 pulse-red"; |
| statusText.textContent = "DISCONNECTED"; |
| creditArea.classList.add('hidden'); |
| creditRefreshArea.classList.add('hidden'); |
| }); |
| |
| |
| async function fetchVoices() { |
| const key = apiKeyInput.value.trim(); |
| if (!key) return updateLog("API Key daalo pehle", "error"); |
| |
| connectBtn.disabled = true; |
| connectBtn.textContent = "Connecting..."; |
| |
| try { |
| |
| const voicesResponse = await fetch(`${CARTESIA_API_URL}/voices`, { |
| headers: { |
| "X-API-Key": key, |
| "Cartesia-Version": "2024-06-10" |
| } |
| }); |
| |
| if (!voicesResponse.ok) throw new Error("API Key invalid!"); |
| |
| const voices = await voicesResponse.json(); |
| localStorage.setItem(STORAGE_KEY, key); |
| |
| voiceSelect.innerHTML = '<option value="">Select voice...</option>'; |
| voices.forEach(voice => { |
| const option = document.createElement('option'); |
| option.value = voice.id; |
| option.textContent = `${voice.name} (${voice.language})`; |
| voiceSelect.appendChild(option); |
| }); |
| |
| |
| fetchCredits(key); |
| |
| statusDot.className = "h-3 w-3 rounded-full bg-green-500 shadow-lg shadow-green-900"; |
| statusText.textContent = "CONNECTED"; |
| updateLog("Connected! Voices loaded!", "success"); |
| showSavedIndicator(); |
| |
| } catch (err) { |
| updateLog(err.message, "error"); |
| statusDot.className = "h-3 w-3 rounded-full bg-red-500 pulse-red"; |
| statusText.textContent = "ERROR"; |
| } finally { |
| connectBtn.disabled = false; |
| connectBtn.textContent = "Connect Account"; |
| } |
| } |
| |
| connectBtn.addEventListener('click', fetchVoices); |
| |
| |
| if (refreshCreditsBtn) { |
| refreshCreditsBtn.addEventListener('click', async () => { |
| const key = apiKeyInput.value.trim(); |
| if (!key) return updateLog("API Key daalo pehle", "error"); |
| refreshCreditsBtn.textContent = "⏳ Loading..."; |
| await fetchCredits(key); |
| refreshCreditsBtn.textContent = "🔄 Refresh Credits"; |
| }); |
| } |
| |
| |
| uploadArea.addEventListener('click', () => audioFile.click()); |
| uploadArea.addEventListener('dragover', (e) => { |
| e.preventDefault(); |
| uploadArea.classList.add('dragover'); |
| }); |
| uploadArea.addEventListener('dragleave', () => { |
| uploadArea.classList.remove('dragover'); |
| }); |
| uploadArea.addEventListener('drop', (e) => { |
| e.preventDefault(); |
| uploadArea.classList.remove('dragover'); |
| audioFile.files = e.dataTransfer.files; |
| if (audioFile.files.length) { |
| uploadedFileName.classList.remove('hidden'); |
| uploadedFileName.textContent = `✓ ${audioFile.files[0].name}`; |
| } |
| }); |
| |
| audioFile.addEventListener('change', () => { |
| if (audioFile.files.length) { |
| uploadedFileName.classList.remove('hidden'); |
| uploadedFileName.textContent = `✓ ${audioFile.files[0].name}`; |
| } |
| }); |
| |
| |
| cloneBtn.addEventListener('click', async () => { |
| const key = apiKeyInput.value.trim(); |
| const name = voiceName.value.trim(); |
| const file = audioFile.files[0]; |
| |
| if (!key) return updateLog("API Key missing!", "error"); |
| if (!file) return updateLog("Audio file select karo", "error"); |
| if (!name) return updateLog("Voice ko name do", "error"); |
| |
| cloneBtn.disabled = true; |
| cloneBtn.textContent = "⏳ Creating..."; |
| |
| try { |
| const reader = new FileReader(); |
| reader.onload = async (e) => { |
| const base64Audio = btoa(String.fromCharCode.apply(null, new Uint8Array(e.target.result))); |
| |
| const body = { |
| name: name, |
| samples: [{ |
| text: "This is a voice sample", |
| audio: base64Audio |
| }] |
| }; |
| |
| if (removeNoise.checked) { |
| body.enhance_audio = true; |
| } |
| |
| const response = await fetch(`${CARTESIA_API_URL}/voices/clone`, { |
| method: 'POST', |
| headers: { |
| "X-API-Key": key, |
| "Cartesia-Version": "2024-06-10", |
| "Content-Type": "application/json" |
| }, |
| body: JSON.stringify(body) |
| }); |
| |
| if (!response.ok) { |
| const error = await response.json(); |
| throw new Error(error.message || "Clone failed"); |
| } |
| |
| const voiceData = await response.json(); |
| currentClonedVoiceId = voiceData.id; |
| clonedVoiceId.textContent = voiceData.id; |
| voiceDetailsArea.classList.remove('hidden'); |
| customVoiceId.value = voiceData.id; |
| updateLog("✓ Voice cloned! Test kar sakte ho!", "success"); |
| }; |
| reader.readAsArrayBuffer(file); |
| |
| } catch (err) { |
| updateLog(err.message, "error"); |
| } finally { |
| cloneBtn.disabled = false; |
| cloneBtn.textContent = "🎤 Create Voice"; |
| } |
| }); |
| |
| |
| copyVoiceIdBtn.addEventListener('click', () => { |
| navigator.clipboard.writeText(currentClonedVoiceId); |
| copyVoiceIdBtn.textContent = "✓ Copied!"; |
| setTimeout(() => copyVoiceIdBtn.textContent = "📋 Copy ID", 2000); |
| }); |
| |
| |
| testVoiceBtn.addEventListener('click', async () => { |
| const key = apiKeyInput.value.trim(); |
| const voiceId = customVoiceId.value.trim() || voiceSelect.value; |
| const text = testText.value.trim(); |
| |
| if (!key) return updateLog("API Key missing!", "error"); |
| if (!voiceId) return updateLog("Voice select karo", "error"); |
| if (!text) return updateLog("Text likho", "error"); |
| |
| testVoiceBtn.disabled = true; |
| testVoiceBtn.textContent = "⏳ Processing..."; |
| |
| try { |
| const response = await fetch(`${CARTESIA_API_URL}/tts/bytes`, { |
| method: 'POST', |
| headers: { |
| "X-API-Key": key, |
| "Cartesia-Version": "2024-06-10", |
| "Content-Type": "application/json" |
| }, |
| body: JSON.stringify({ |
| model_id: "sonic-english", |
| transcript: text, |
| voice: { mode: "id", id: voiceId }, |
| output_format: { container: "wav", encoding: "pcm_f32le", sample_rate: 44100 } |
| }) |
| }); |
| |
| if (!response.ok) throw new Error("TTS failed"); |
| |
| const blob = await response.blob(); |
| testAudioPlayer.src = URL.createObjectURL(blob); |
| testAudioArea.classList.remove('hidden'); |
| updateLog("Test audio ready!", "success"); |
| testAudioPlayer.play(); |
| |
| } catch (err) { |
| updateLog(err.message, "error"); |
| } finally { |
| testVoiceBtn.disabled = false; |
| testVoiceBtn.textContent = "▶️ Test Voice"; |
| } |
| }); |
| |
| |
| generateBtn.addEventListener('click', async () => { |
| const key = apiKeyInput.value.trim(); |
| const voiceId = customVoiceId.value.trim() || voiceSelect.value; |
| const text = textInput.value.trim(); |
| |
| if (!key) return updateLog("API Key missing!", "error"); |
| if (!voiceId) return updateLog("Voice select karo", "error"); |
| if (!text) return updateLog("Text likho", "error"); |
| |
| generateBtn.disabled = true; |
| generateBtn.textContent = "⌛ Processing..."; |
| audioArea.classList.add('hidden'); |
| |
| try { |
| const response = await fetch(`${CARTESIA_API_URL}/tts/bytes`, { |
| method: 'POST', |
| headers: { |
| "X-API-Key": key, |
| "Cartesia-Version": "2024-06-10", |
| "Content-Type": "application/json" |
| }, |
| body: JSON.stringify({ |
| model_id: "sonic-english", |
| transcript: text, |
| voice: { mode: "id", id: voiceId }, |
| output_format: { container: "wav", encoding: "pcm_f32le", sample_rate: 44100 } |
| }) |
| }); |
| |
| if (!response.ok) throw new Error("TTS failed"); |
| |
| const blob = await response.blob(); |
| audioPlayer.src = URL.createObjectURL(blob); |
| audioArea.classList.remove('hidden'); |
| updateLog("Audio ready!", "success"); |
| audioPlayer.play(); |
| |
| } catch (err) { |
| updateLog(err.message, "error"); |
| } finally { |
| generateBtn.disabled = false; |
| generateBtn.textContent = "⚡ Convert to Speech"; |
| } |
| }); |
| |
| |
| downloadBtn.addEventListener('click', () => { |
| const src = audioPlayer.src; |
| if (!src) return; |
| const a = document.createElement('a'); |
| a.href = src; |
| a.download = `audio-${Date.now()}.wav`; |
| a.click(); |
| updateLog("Download started!", "success"); |
| }); |
| |
| |
| shareBtn.addEventListener('click', () => { |
| const src = audioPlayer.src; |
| if (!src) return; |
| navigator.clipboard.writeText(src); |
| updateLog("Link copied!", "success"); |
| shareBtn.textContent = "✓ Copied!"; |
| setTimeout(() => shareBtn.textContent = "📋 Copy", 2000); |
| }); |
| |
| window.addEventListener('load', initializeApp); |
| </script> |
| </body> |
| </html> |
|
|