Clone / index.html
Offex's picture
Update index.html
439a1ef verified
<!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">
<!-- 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>