Download index.html from Offex/Clone: direct link, hf CLI and curl.
- Browser
- Download file 30.7 kB
-
https://huggingface.co/spaces/Offex/Clone/resolve/main/index.html
- Command line
-
hf download hf://spaces/Offex/Clone/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Offex/Clone/resolve/main/index.html
30.7 kB
| <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"> | |
| <!-- Header --> | |
| <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"> | |
| <!-- Left Column --> | |
| <div class="lg:col-span-4 space-y-6"> | |
| <!-- API Settings --> | |
| <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> | |
| <!-- Voice Selection --> | |
| <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> | |
| <!-- Right Column - Tabs --> | |
| <div class="lg:col-span-8 space-y-6"> | |
| <!-- Log --> | |
| <div id="logBox" class="hidden cartesia-card p-4 rounded-2xl text-sm border-l-4"> | |
| <p id="logMessage"></p> | |
| </div> | |
| <!-- Tabs --> | |
| <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> | |
| <!-- TTS Tab --> | |
| <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> | |
| <!-- Clone Tab --> | |
| <div id="clone-tab" class="tab-content p-8"> | |
| <div class="space-y-6"> | |
| <!-- Audio Upload --> | |
| <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> | |
| <!-- BG Noise --> | |
| <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> | |
| <!-- Voice Name --> | |
| <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> | |
| <!-- Create Button --> | |
| <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> | |
| <!-- Test Text --> | |
| <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> | |
| <!-- Voice Details --> | |
| <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> | |
| // Elements | |
| 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'); | |
| // Clone elements | |
| 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'); | |
| // Constants | |
| const CARTESIA_API_URL = "https://api.cartesia.ai"; | |
| const STORAGE_KEY = "cartesia_api_key"; | |
| let currentClonedVoiceId = null; | |
| // Tab switching | |
| 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'); | |
| }); | |
| }); | |
| // Initialize | |
| 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"; | |
| } | |
| } | |
| // Fetch and display credits | |
| async function fetchCredits(key) { | |
| try { | |
| let credits = null; | |
| // Try endpoint 1 | |
| 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 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'); | |
| } | |
| } | |
| // Toggle key visibility | |
| toggleShowKey.addEventListener('click', () => { | |
| const type = apiKeyInput.type === 'password' ? 'text' : 'password'; | |
| apiKeyInput.type = type; | |
| toggleShowKey.textContent = type === 'password' ? '👁️ Show' : '👁️ Hide'; | |
| }); | |
| // Clear key | |
| 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'); | |
| }); | |
| // Connect and fetch voices + credits | |
| async function fetchVoices() { | |
| const key = apiKeyInput.value.trim(); | |
| if (!key) return updateLog("API Key daalo pehle", "error"); | |
| connectBtn.disabled = true; | |
| connectBtn.textContent = "Connecting..."; | |
| try { | |
| // Fetch voices | |
| 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); | |
| }); | |
| // Fetch credits | |
| 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); | |
| // Refresh credits button | |
| 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"; | |
| }); | |
| } | |
| // File upload | |
| 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}`; | |
| } | |
| }); | |
| // Clone voice | |
| 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"; | |
| } | |
| }); | |
| // Copy voice ID | |
| copyVoiceIdBtn.addEventListener('click', () => { | |
| navigator.clipboard.writeText(currentClonedVoiceId); | |
| copyVoiceIdBtn.textContent = "✓ Copied!"; | |
| setTimeout(() => copyVoiceIdBtn.textContent = "📋 Copy ID", 2000); | |
| }); | |
| // Test voice | |
| 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"; | |
| } | |
| }); | |
| // TTS Generation | |
| 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"; | |
| } | |
| }); | |
| // Download | |
| 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"); | |
| }); | |
| // Share | |
| 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> | |