sesame_openai / static /voice-cloning.html
karumati's picture
yo
01115c6
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Voice Cloning</title>
<style>
:root {
/* Base colors */
--bg-color: #ffffff;
--text-color: #333333;
--card-bg: #ffffff;
--card-border: #e2e8f0;
--card-shadow: rgba(0,0,0,0.1);
/* Primary colors */
--primary-color: #4f46e5;
--primary-hover: #4338ca;
--primary-light: rgba(79, 70, 229, 0.1);
/* Secondary colors */
--secondary-color: #6b7280;
--secondary-hover: #4b5563;
/* Accent colors */
--success-color: #10b981;
--success-hover: #059669;
--danger-color: #ef4444;
--danger-hover: #dc2626;
--warning-color: #f59e0b;
--warning-hover: #d97706;
/* Input elements */
--input-bg: #ffffff;
--input-border: #d1d5db;
--input-focus-border: #4f46e5;
--input-focus-shadow: rgba(79, 70, 229, 0.2);
/* Voice cards */
--voice-card-bg: #f9fafb;
--voice-card-border: #e5e7eb;
--voice-card-shadow: rgba(0,0,0,0.05);
/* Tabs */
--tab-border: #e5e7eb;
--tab-text: #4b5563;
--tab-active: #4f46e5;
--tab-active-bg: rgba(79, 70, 229, 0.1);
/* Toggle */
--toggle-bg: #e5e7eb;
--toggle-active: #4f46e5;
--toggle-circle: #ffffff;
/* Status indicators */
--status-success-bg: #dcfce7;
--status-success-text: #166534;
--status-error-bg: #fee2e2;
--status-error-text: #b91c1c;
--status-warning-bg: #fff7ed;
--status-warning-text: #c2410c;
--status-info-bg: #eff6ff;
--status-info-text: #1e40af;
}
[data-theme="dark"] {
/* Base colors */
--bg-color: #111827;
--text-color: #f3f4f6;
--card-bg: #1f2937;
--card-border: #374151;
--card-shadow: rgba(0,0,0,0.3);
/* Primary colors */
--primary-color: #6366f1;
--primary-hover: #4f46e5;
--primary-light: rgba(99, 102, 241, 0.2);
/* Secondary colors */
--secondary-color: #9ca3af;
--secondary-hover: #6b7280;
/* Accent colors - slightly brighter for dark theme */
--success-color: #34d399;
--success-hover: #10b981;
--danger-color: #f87171;
--danger-hover: #ef4444;
--warning-color: #fbbf24;
--warning-hover: #f59e0b;
/* Input elements */
--input-bg: #374151;
--input-border: #4b5563;
--input-focus-border: #6366f1;
--input-focus-shadow: rgba(99, 102, 241, 0.3);
/* Voice cards */
--voice-card-bg: #1f2937;
--voice-card-border: #374151;
--voice-card-shadow: rgba(0,0,0,0.2);
/* Tabs */
--tab-border: #374151;
--tab-text: #9ca3af;
--tab-active: #6366f1;
--tab-active-bg: rgba(99, 102, 241, 0.2);
/* Toggle */
--toggle-bg: #4b5563;
--toggle-active: #6366f1;
--toggle-circle: #e5e7eb;
/* Status indicators */
--status-success-bg: rgba(16, 185, 129, 0.2);
--status-success-text: #34d399;
--status-error-bg: rgba(239, 68, 68, 0.2);
--status-error-text: #f87171;
--status-warning-bg: rgba(245, 158, 11, 0.2);
--status-warning-text: #fbbf24;
--status-info-bg: rgba(59, 130, 246, 0.2);
--status-info-text: #60a5fa;
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
overflow-x: hidden;
width: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
max-width: 100%;
padding: 16px;
line-height: 1.6;
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
.container {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 0 8px;
}
h1, h2, h3 {
color: var(--text-color);
margin-top: 0;
}
h1 {
font-size: 1.8rem;
margin-bottom: 1rem;
text-align: center;
}
h2 {
font-size: 1.4rem;
margin-bottom: 1rem;
}
.card {
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 6px var(--card-shadow);
background-color: var(--card-bg);
transition: box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 6px 12px var(--card-shadow);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 6px;
font-weight: 500;
font-size: 0.95rem;
}
input, textarea, select {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--input-border);
border-radius: 8px;
background-color: var(--input-bg);
color: var(--text-color);
font-size: 1rem;
transition: border-color 0.3s, box-shadow 0.3s;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--input-focus-border);
box-shadow: 0 0 0 3px var(--input-focus-shadow);
}
/* File input styling */
input[type="file"] {
padding: 8px;
background-color: var(--input-bg);
border: 1px dashed var(--input-border);
border-radius: 8px;
cursor: pointer;
}
input[type="file"]:hover {
border-color: var(--primary-color);
}
button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
padding: 12px 20px;
cursor: pointer;
font-weight: 600;
font-size: 1rem;
transition: background-color 0.3s, transform 0.1s;
width: 100%;
}
button:hover {
background-color: var(--primary-hover);
}
button:active {
transform: translateY(1px);
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-row {
display: flex;
gap: 10px;
}
.btn-row button {
flex: 1;
}
.voice-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.voice-card {
border: 1px solid var(--voice-card-border);
border-radius: 10px;
padding: 16px;
background-color: var(--voice-card-bg);
box-shadow: 0 2px 6px var(--voice-card-shadow);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.voice-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px var(--voice-card-shadow);
}
.controls {
display: flex;
gap: 8px;
margin-top: 12px;
}
.voice-name {
font-weight: 600;
font-size: 18px;
margin: 0 0 8px 0;
color: var(--primary-color);
}
.btn-danger {
background-color: var(--danger-color);
}
.btn-danger:hover {
background-color: var(--danger-hover);
}
.btn-secondary {
background-color: var(--secondary-color);
}
.btn-secondary:hover {
background-color: var(--secondary-hover);
}
#audio-preview {
margin-top: 20px;
width: 100%;
border-radius: 8px;
background-color: var(--card-bg);
}
/* Status indicators */
.status-indicator {
padding: 12px 16px;
margin: 16px 0;
border-radius: 8px;
font-weight: 500;
display: flex;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
max-height: 0;
overflow: hidden;
}
.status-indicator.show {
opacity: 1;
max-height: 100px;
}
.status-indicator.success {
background-color: var(--status-success-bg);
color: var(--status-success-text);
}
.status-indicator.error {
background-color: var(--status-error-bg);
color: var(--status-error-text);
}
.status-indicator.warning {
background-color: var(--status-warning-bg);
color: var(--status-warning-text);
}
.status-indicator.info {
background-color: var(--status-info-bg);
color: var(--status-info-text);
}
.status-indicator svg {
margin-right: 8px;
flex-shrink: 0;
}
/* Tabs */
.tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid var(--tab-border);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none; /* Hide scrollbar for Firefox */
}
.tabs::-webkit-scrollbar {
display: none; /* Hide scrollbar for Chrome/Safari */
}
.tabs button {
background-color: transparent;
color: var(--tab-text);
border: none;
padding: 12px 16px;
margin-right: 8px;
cursor: pointer;
position: relative;
font-weight: 600;
border-radius: 8px 8px 0 0;
white-space: nowrap;
width: auto;
flex-shrink: 0;
}
.tabs button.active {
color: var(--tab-active);
background-color: var(--tab-active-bg);
}
.tabs button.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background-color: var(--tab-active);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Theme toggle switch */
.theme-switch-wrapper {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 16px;
}
.theme-switch {
display: inline-block;
height: 28px;
position: relative;
width: 54px;
}
.theme-switch input {
display: none;
}
.slider {
background-color: var(--toggle-bg);
bottom: 0;
cursor: pointer;
left: 0;
position: absolute;
right: 0;
top: 0;
transition: .4s;
border-radius: 28px;
}
.slider:before {
background-color: var(--toggle-circle);
bottom: 4px;
content: "";
height: 20px;
left: 4px;
position: absolute;
transition: .4s;
width: 20px;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
input:checked + .slider {
background-color: var(--toggle-active);
}
input:checked + .slider:before {
transform: translateX(26px);
}
.theme-icon {
width: 16px;
height: 16px;
display: inline-block;
margin: 0 8px;
font-size: 16px;
line-height: 1;
}
/* Progress bar */
.progress-bar {
width: 100%;
height: 12px;
background-color: var(--input-bg);
border-radius: 6px;
margin-top: 12px;
overflow: hidden;
box-shadow: inset 0 1px 3px var(--card-shadow);
}
.progress-fill {
height: 100%;
background: linear-gradient(to right, var(--primary-color), var(--primary-hover));
width: 0%;
transition: width 0.5s ease-in-out;
border-radius: 6px;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
-45deg,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%
);
background-size: 16px 16px;
animation: progress-animation 1s linear infinite;
border-radius: 6px;
}
@keyframes progress-animation {
0% {
background-position: 0 0;
}
100% {
background-position: 16px 0;
}
}
/* Divider */
.divider {
margin: 24px 0;
border-top: 1px solid var(--card-border);
position: relative;
}
.divider-text {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
background-color: var(--bg-color);
padding: 0 12px;
color: var(--secondary-color);
font-size: 0.9rem;
}
/* Small text */
small {
color: var(--secondary-color);
display: block;
margin-top: 6px;
font-size: 0.85rem;
}
/* Range slider styling */
input[type="range"] {
-webkit-appearance: none;
height: 8px;
border-radius: 4px;
background: var(--input-bg);
outline: none;
padding: 0;
margin: 10px 0;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
input[type="range"]::-ms-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
#temperature-value {
display: inline-block;
width: 40px;
text-align: center;
font-weight: 600;
color: var(--primary-color);
}
/* Toast notifications */
.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
max-width: 100%;
width: 300px;
}
.toast {
padding: 12px 16px;
margin-bottom: 12px;
border-radius: 8px;
color: white;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: space-between;
animation: toast-in 0.3s ease forwards;
max-width: 100%;
}
.toast.success {
background-color: var(--success-color);
}
.toast.error {
background-color: var(--danger-color);
}
.toast.warning {
background-color: var(--warning-color);
}
.toast.info {
background-color: var(--primary-color);
}
.toast-close {
background: none;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
opacity: 0.8;
width: auto;
padding: 0 0 0 12px;
}
.toast-close:hover {
opacity: 1;
background: none;
}
@keyframes toast-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Loading spinner */
.spinner {
display: inline-block;
width: 20px;
height: 20px;
margin-right: 8px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Mobile responsiveness */
@media (max-width: 640px) {
body {
padding: 12px 8px;
}
h1 {
font-size: 1.6rem;
}
.card {
padding: 16px;
}
.voice-list {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
}
.controls button {
width: 100%;
}
.tabs button {
padding: 8px 12px;
font-size: 0.9rem;
}
}
/* Checkbox styling */
.checkbox-group {
margin-bottom: 20px;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
font-weight: 500;
font-size: 0.95rem;
}
input[type="checkbox"] {
width: auto;
margin-right: 10px;
height: 18px;
width: 18px;
cursor: pointer;
accent-color: var(--primary-color);
}
.checkbox-text {
position: relative;
top: 1px;
}
</style>
</head>
<body>
<div class="container">
<div class="theme-switch-wrapper">
<span class="theme-icon">☀️</span>
<label class="theme-switch" for="checkbox">
<input type="checkbox" id="checkbox" />
<div class="slider"></div>
</label>
<span class="theme-icon">🌙</span>
</div>
<h1>Voice Cloning</h1>
<div class="tabs">
<button id="tab-clone" class="active">Clone Voice</button>
<button id="tab-voices">My Voices</button>
<button id="tab-generate">Generate Speech</button>
</div>
<!-- Status indicator -->
<div id="status-message" class="status-indicator">
<!-- Content will be dynamically inserted -->
</div>
<div id="clone-tab" class="tab-content active">
<div class="card">
<h2>Clone a New Voice</h2>
<form id="clone-form">
<div class="form-group">
<label for="voice-name">Voice Name</label>
<input type="text" id="voice-name" name="name" required placeholder="e.g. My Voice">
</div>
<div class="form-group">
<label for="audio-file">Voice Sample (2-3 minute audio recording)</label>
<input type="file" id="audio-file" name="audio_file" required accept="audio/*">
<small>For best results, provide a clear recording with minimal background noise.</small>
</div>
<div class="form-group">
<label for="transcript">Transcript (Optional)</label>
<textarea id="transcript" name="transcript" rows="4" placeholder="Exact transcript of your audio sample..."></textarea>
<small>Adding a transcript helps improve voice accuracy.</small>
</div>
<div class="form-group">
<label for="description">Description (Optional)</label>
<input type="text" id="description" name="description" placeholder="A description of this voice">
</div>
<button type="submit">Clone Voice</button>
</form>
</div>
<div class="divider">
<span class="divider-text">OR</span>
</div>
<div class="card">
<h2>Clone Voice from YouTube</h2>
<form id="youtube-clone-form">
<div class="form-group">
<label for="youtube-url">YouTube URL</label>
<input type="url" id="youtube-url" name="youtube_url" required placeholder="https://www.youtube.com/watch?v=...">
</div>
<div class="form-group">
<label for="youtube-voice-name">Voice Name</label>
<input type="text" id="youtube-voice-name" name="voice_name" required placeholder="e.g. YouTube Voice">
</div>
<div class="form-group">
<label for="start-time">Start Time (seconds)</label>
<input type="number" id="start-time" name="start_time" min="0" value="0">
</div>
<div class="form-group">
<label for="duration">Duration (seconds)</label>
<input type="number" id="duration" name="duration" min="10" max="600" value="180">
<small>Recommended: 2-3 minutes of clear speech</small>
</div>
<div class="form-group">
<label for="youtube-description">Description (Optional)</label>
<input type="text" id="youtube-description" name="description" placeholder="A description of this voice">
</div>
<button type="submit">Clone from YouTube</button>
</form>
<div id="youtube-progress" style="display: none; margin-top: 16px;">
<p>Processing YouTube video... <span id="progress-status">Downloading</span></p>
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
</div>
</div>
</div>
<div id="voices-tab" class="tab-content">
<h2>My Cloned Voices</h2>
<div id="voice-list" class="voice-list">
<!-- Voice cards will be added here -->
</div>
</div>
<div id="generate-tab" class="tab-content">
<div class="card">
<h2>Generate Speech with Cloned Voice</h2>
<form id="generate-form">
<div class="form-group">
<label for="voice-select">Select Voice</label>
<select id="voice-select" name="voice" required>
<option value="">Select a voice</option>
<!-- Voice options will be added here -->
</select>
</div>
<div class="form-group">
<label for="generate-text">Text to Speak</label>
<textarea id="generate-text" name="text" rows="4" required placeholder="Enter text to synthesize with the selected voice..."></textarea>
</div>
<div class="form-group">
<label>Temperature: <span id="temperature-value">0.7</span></label>
<input type="range" id="temperature" name="temperature" min="0.5" max="1.0" step="0.05" value="0.7">
<small>Lower values (0.5-0.7) produce more consistent speech, higher values (0.8-1.0) produce more varied speech.</small>
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="use-streaming" name="use_streaming">
<span class="checkbox-text">Use streaming mode</span>
</label>
<small>Stream audio as it's generated for faster start and lower latency.</small>
</div>
<button type="submit">Generate Speech</button>
</form>
<audio id="audio-preview" controls style="display: none;"></audio>
</div>
</div>
</div>
<!-- Toast notifications container -->
<div class="toast-container" id="toast-container"></div>
<script>
// Theme toggle functionality
const toggleSwitch = document.querySelector('#checkbox');
const html = document.querySelector('html');
// Check for saved theme preference or use system preference
function getThemePreference() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
return savedTheme;
}
// Check if system prefers dark mode
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
// Apply the theme
function setTheme(theme) {
html.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
toggleSwitch.checked = theme === 'dark';
}
// Initialize theme
setTheme(getThemePreference());
// Listen for toggle changes
toggleSwitch.addEventListener('change', function(e) {
if (e.target.checked) {
setTheme('dark');
} else {
setTheme('light');
}
});
// Toast notification system
function showToast(message, type = 'info', duration = 5000) {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<span>${message}</span>
<button class="toast-close">&times;</button>
`;
container.appendChild(toast);
// Auto remove after duration
const timeout = setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
container.removeChild(toast);
}, 300);
}, duration);
// Manual close
toast.querySelector('.toast-close').addEventListener('click', () => {
clearTimeout(timeout);
toast.style.opacity = '0';
setTimeout(() => {
container.removeChild(toast);
}, 300);
});
}
// Status indicator functions
function showStatus(message, type) {
const statusElem = document.getElementById('status-message');
statusElem.className = `status-indicator ${type} show`;
let icon = '';
switch(type) {
case 'success':
icon = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 12l2 2 4-4M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
break;
case 'error':
icon = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
break;
case 'warning':
icon = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
break;
case 'info':
icon = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
break;
}
statusElem.innerHTML = icon + message;
// Auto-hide after 5 seconds
setTimeout(() => {
statusElem.className = 'status-indicator';
}, 5000);
}
function hideStatus() {
const statusElem = document.getElementById('status-message');
statusElem.className = 'status-indicator';
}
// Tab functionality
const tabs = document.querySelectorAll('.tabs button');
const tabContents = document.querySelectorAll('.tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// Remove active class from all tabs
tabs.forEach(t => t.classList.remove('active'));
tabContents.forEach(tc => tc.classList.remove('active'));
// Add active class to clicked tab
tab.classList.add('active');
// Show corresponding tab content
const tabId = tab.id.replace('tab-', '');
document.getElementById(`${tabId}-tab`).classList.add('active');
// Hide any status messages when changing tabs
hideStatus();
});
});
// Temperature slider
const temperatureSlider = document.getElementById('temperature');
const temperatureValue = document.getElementById('temperature-value');
temperatureSlider.addEventListener('input', () => {
temperatureValue.textContent = temperatureSlider.value;
});
// Load voices
async function loadVoices() {
try {
const response = await fetch('/v1/voice-cloning/voices');
const data = await response.json();
const voiceList = document.getElementById('voice-list');
const voiceSelect = document.getElementById('voice-select');
// Clear existing content
voiceList.innerHTML = '';
// Clear voice select options but keep the first one
while (voiceSelect.options.length > 1) {
voiceSelect.remove(1);
}
if (data.voices && data.voices.length > 0) {
data.voices.forEach(voice => {
// Add to voice list
const voiceCard = document.createElement('div');
voiceCard.className = 'voice-card';
voiceCard.innerHTML = `
<h3 class="voice-name">${voice.name}</h3>
<p>${voice.description || 'No description'}</p>
<p>Created: ${new Date(voice.created_at * 1000).toLocaleString()}</p>
<div class="controls">
<button class="btn-secondary preview-voice" data-id="${voice.id}">Preview</button>
<button class="btn-danger delete-voice" data-id="${voice.id}">Delete</button>
</div>
`;
voiceList.appendChild(voiceCard);
// Add to voice select
const option = document.createElement('option');
option.value = voice.id;
option.textContent = voice.name;
voiceSelect.appendChild(option);
});
// Add event listeners for preview and delete buttons
document.querySelectorAll('.preview-voice').forEach(button => {
button.addEventListener('click', previewVoice);
});
document.querySelectorAll('.delete-voice').forEach(button => {
button.addEventListener('click', deleteVoice);
});
showStatus(`Loaded ${data.voices.length} voices successfully`, 'success');
} else {
voiceList.innerHTML = '<p>No cloned voices yet. Create one in the "Clone Voice" tab.</p>';
}
} catch (error) {
console.error('Error loading voices:', error);
showStatus('Failed to load voices', 'error');
}
}
// Preview voice
async function previewVoice(event) {
const button = event.target;
const originalText = button.textContent;
button.disabled = true;
button.innerHTML = '<div class="spinner"></div> Loading...';
const voiceId = button.dataset.id;
const audioPreview = document.getElementById('audio-preview');
try {
const response = await fetch(`/v1/voice-cloning/voices/${voiceId}/preview`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: "This is a preview of my cloned voice. I hope you like how it sounds!"
})
});
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
audioPreview.src = url;
audioPreview.style.display = 'block';
// Switch to the generate tab
document.getElementById('tab-generate').click();
// Set the voice in the select
document.getElementById('voice-select').value = voiceId;
audioPreview.play();
showToast('Voice preview loaded', 'success');
} else {
showToast('Failed to preview voice', 'error');
}
} catch (error) {
console.error('Error previewing voice:', error);
showToast('Error previewing voice', 'error');
} finally {
button.disabled = false;
button.textContent = originalText;
}
}
// Delete voice
async function deleteVoice(event) {
if (!confirm('Are you sure you want to delete this voice? This cannot be undone.')) {
return;
}
const button = event.target;
const originalText = button.textContent;
button.disabled = true;
button.innerHTML = '<div class="spinner"></div> Deleting...';
const voiceId = button.dataset.id;
try {
const response = await fetch(`/v1/voice-cloning/voices/${voiceId}`, {
method: 'DELETE'
});
if (response.ok) {
showToast('Voice deleted successfully', 'success');
loadVoices();
} else {
showToast('Failed to delete voice', 'error');
}
} catch (error) {
console.error('Error deleting voice:', error);
showToast('Error deleting voice', 'error');
} finally {
button.disabled = false;
button.textContent = originalText;
}
}
// Clone voice form submission
document.getElementById('clone-form').addEventListener('submit', async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const submitButton = event.target.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.disabled = true;
submitButton.innerHTML = '<div class="spinner"></div> Cloning Voice...';
showStatus('Processing your audio sample...', 'info');
try {
const response = await fetch('/v1/voice-cloning/clone', {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
showStatus('Voice cloned successfully!', 'success');
showToast('Voice cloned successfully!', 'success');
event.target.reset();
// Switch to the voices tab
document.getElementById('tab-voices').click();
loadVoices();
} else {
const error = await response.json();
showStatus(`Failed to clone voice: ${error.detail}`, 'error');
showToast('Failed to clone voice', 'error');
}
} catch (error) {
console.error('Error cloning voice:', error);
showStatus('Error processing your request', 'error');
showToast('Error cloning voice', 'error');
} finally {
submitButton.disabled = false;
submitButton.textContent = originalText;
}
});
// YouTube voice cloning form submission
document.getElementById('youtube-clone-form').addEventListener('submit', async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const youtubeUrl = formData.get('youtube_url');
const voiceName = formData.get('voice_name');
const startTime = parseInt(formData.get('start_time'));
const duration = parseInt(formData.get('duration'));
const description = formData.get('description');
const progressDiv = document.getElementById('youtube-progress');
const progressStatus = document.getElementById('progress-status');
const progressFill = document.querySelector('.progress-fill');
const submitButton = event.target.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.disabled = true;
submitButton.innerHTML = '<div class="spinner"></div> Processing...';
showStatus('Starting YouTube download...', 'info');
// Show progress bar
progressDiv.style.display = 'block';
progressFill.style.width = '10%';
progressStatus.textContent = 'Downloading audio...';
// Simulate progress updates (since we can't get real-time updates easily)
let progress = 10;
const progressInterval = setInterval(() => {
if (progress < 90) {
progress += 5;
progressFill.style.width = `${progress}%`;
if (progress > 30 && progress < 60) {
progressStatus.textContent = 'Generating transcript...';
} else if (progress >= 60) {
progressStatus.textContent = 'Cloning voice...';
}
}
}, 1000);
try {
const response = await fetch('/v1/voice-cloning/clone-from-youtube', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
youtube_url: youtubeUrl,
voice_name: voiceName,
start_time: startTime,
duration: duration,
description: description
})
});
clearInterval(progressInterval);
if (response.ok) {
progressFill.style.width = '100%';
progressStatus.textContent = 'Complete!';
const result = await response.json();
showStatus('Voice cloned successfully from YouTube!', 'success');
showToast('Voice cloned from YouTube!', 'success');
event.target.reset();
// Switch to the voices tab
document.getElementById('tab-voices').click();
loadVoices();
} else {
const error = await response.json();
showStatus(`Failed to clone voice from YouTube: ${error.detail}`, 'error');
showToast('Failed to clone voice from YouTube', 'error');
progressDiv.style.display = 'none';
}
} catch (error) {
console.error('Error cloning voice from YouTube:', error);
showStatus('Error processing YouTube video', 'error');
showToast('Error cloning voice from YouTube', 'error');
progressDiv.style.display = 'none';
} finally {
clearInterval(progressInterval);
submitButton.disabled = false;
submitButton.textContent = originalText;
}
});
// Generate speech form submission
document.getElementById('generate-form').addEventListener('submit', async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const voiceId = formData.get('voice');
const text = formData.get('text');
const temperature = formData.get('temperature');
const useStreaming = formData.get('use_streaming') === 'on';
if (!voiceId) {
showToast('Please select a voice', 'warning');
return;
}
const submitButton = event.target.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.disabled = true;
submitButton.innerHTML = '<div class="spinner"></div> Generating...';
showStatus(useStreaming ? 'Streaming speech...' : 'Generating speech...', 'info');
try {
const audioPreview = document.getElementById('audio-preview');
if (useStreaming) {
// For streaming, we need to handle the response differently to play audio as it arrives
try {
// Reset audio element
audioPreview.style.display = 'block';
// Prepare the request
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: "csm-1b",
input: text,
voice: voiceId,
response_format: "mp3",
temperature: parseFloat(temperature),
speed: 1.0
})
};
// Create a unique URL for this request to avoid caching issues
const timestamp = new Date().getTime();
const streamingUrl = `/v1/audio/speech/streaming?t=${timestamp}`;
// Fetch from streaming endpoint
const response = await fetch(streamingUrl, requestOptions);
if (response.ok) {
// Create a blob URL for immediate playback
const blob = await response.blob();
const url = URL.createObjectURL(blob);
// Set the audio source and play immediately
audioPreview.src = url;
audioPreview.autoplay = true;
// Event listeners for success/failure
audioPreview.onplay = () => {
showStatus('Speech streamed successfully', 'success');
showToast('Speech streaming playback started', 'success');
};
audioPreview.onerror = (e) => {
console.error('Audio playback error:', e);
showStatus('Error playing streamed audio', 'error');
showToast('Streaming playback error', 'error');
};
} else {
const error = await response.json();
showStatus(`Failed to stream speech: ${error.detail || 'Unknown error'}`, 'error');
showToast('Failed to stream speech', 'error');
}
} catch (error) {
console.error('Streaming error:', error);
showStatus(`Error streaming speech: ${error.message}`, 'error');
showToast('Error streaming speech', 'error');
}
} else {
// Non-streaming uses the original endpoint
const response = await fetch('/v1/voice-cloning/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
voice_id: voiceId,
text: text,
temperature: parseFloat(temperature)
})
});
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
audioPreview.src = url;
audioPreview.style.display = 'block';
audioPreview.play();
showStatus('Speech generated successfully', 'success');
showToast('Speech generated successfully', 'success');
} else {
const error = await response.json();
showStatus(`Failed to generate speech: ${error.detail}`, 'error');
showToast('Failed to generate speech', 'error');
}
}
} catch (error) {
console.error('Error generating speech:', error);
showStatus('Error generating speech', 'error');
showToast('Error generating speech', 'error');
} finally {
submitButton.disabled = false;
submitButton.textContent = originalText;
}
});
// Load voices on page load
loadVoices();
</script>
</body>
</html>