orpheus-fastapi/templates/tts.html
Lex-au 5ead1e4b89 Orpheus-FASTAPI v1.3.0: Multilingual TTS with 24 Voices Across 8 Languages
This major update brings comprehensive multilingual support to Orpheus-FASTAPI:

• Added 16 new voice actors across 7 languages (French, German, Korean, Hindi, Mandarin, Spanish, Italian)
• Released 6 language-specific optimized models for superior pronunciation and fluency
• Enhanced UI with dynamic language selector and voice filtering
• Updated Docker Compose workflow to simplify language-specific model deployment
• Reorganized code architecture with language-aware voice mappings

All voices support the same emotion tags and audio quality features as the original English voices, maintaining compatibility with existing applications while expanding international usability.
2025-04-18 11:42:49 +10:00

841 lines
38 KiB
HTML

<!DOCTYPE html>
<html lang="en" class="h-full bg-[#0f1729]">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Orpheus FASTAPI | Advanced Text-to-Speech</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
},
purple: {
50: '#faf5ff',
100: '#f3e8ff',
200: '#e9d5ff',
300: '#d8b4fe',
400: '#c084fc',
500: '#a855f7',
600: '#9333ea',
700: '#7e22ce',
800: '#6b21a8',
900: '#581c87',
},
dark: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712',
1000: '#0f1729'
}
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer components {
.btn-primary {
@apply bg-primary-600 text-white px-4 py-2 rounded-md shadow-sm hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-dark-800 focus:ring-offset-2 transition-colors;
}
.voice-card {
@apply border border-dark-700 rounded-lg p-4 cursor-pointer transition-all hover:border-primary-400 hover:shadow-md bg-dark-800 text-dark-200;
}
.voice-card.active {
@apply border-primary-500 ring-2 ring-primary-500 bg-dark-700;
}
.audio-progress {
@apply h-2 w-full bg-dark-700 rounded-full overflow-hidden;
}
.audio-progress-bar {
@apply h-full bg-primary-500 transition-all duration-300;
}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/wavesurfer.js@6/dist/wavesurfer.min.js"></script>
</head>
<body class="h-full">
<div class="min-h-full">
<!-- Navigation -->
<nav class="bg-gradient-to-r from-dark-900 to-purple-900 border-b border-purple-800 shadow-lg">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0">
<span class="text-white text-xl font-bold">Orpheus FASTAPI</span>
</div>
</div>
<div class="flex items-center space-x-4">
<a href="/docs" class="text-primary-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium">API Docs</a>
<a href="https://github.com/lex-au" target="_blank" class="text-primary-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium">GitHub</a>
</div>
</div>
</div>
</nav>
<!-- Main content -->
<main>
<div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<!-- Notification area -->
{% if error %}
<div class="mb-6 bg-red-50 border-l-4 border-red-400 p-4 rounded-md shadow-sm">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-red-700">{{ error }}</p>
</div>
</div>
</div>
{% endif %}
{% if success %}
<div class="mb-6 bg-green-50 border-l-4 border-green-400 p-4 rounded-md shadow-sm">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-green-700">Audio generated successfully in {{ generation_time }}s!</p>
</div>
</div>
</div>
{% endif %}
<!-- TTS form -->
<div class="bg-dark-800 shadow-lg rounded-lg overflow-hidden border border-dark-700">
<form id="tts-form" class="flex flex-col">
<div class="p-6">
<h2 class="text-lg font-medium text-white mb-4">Generate Speech</h2>
<!-- Text input -->
<div class="mb-6">
<label for="text" class="block text-sm font-medium text-white mb-1">Text to speak</label>
<div class="relative">
<textarea
name="text"
id="text"
rows="4"
maxlength="8192"
class="block w-full rounded-md border-dark-600 bg-dark-700 text-white shadow-sm focus:border-primary-500 focus:ring-primary-500 focus:ring-offset-dark-800 sm:text-sm px-3 py-2"
placeholder="Enter text to convert to speech..."
required
>{{ text if text else "" }}</textarea>
<div class="absolute bottom-2 right-2 text-xs text-purple-300">
<span id="char-count">0</span> / 8192 characters
</div>
</div>
</div>
<!-- Language selection -->
<div class="mb-6">
<label class="block text-sm font-medium text-white mb-2">Language</label>
<div class="flex flex-wrap gap-3">
{% for language in AVAILABLE_LANGUAGES %}
<div class="language-option {% if language == 'english' %}active{% endif %}" data-language="{{ language }}">
<input type="radio" name="language" value="{{ language }}" class="hidden" {% if language == 'english' %}checked{% endif %}>
<span class="px-3 py-1 rounded-full bg-dark-700 hover:bg-dark-600 text-white text-sm cursor-pointer">{{ language|capitalize }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- Voice selection -->
<div class="mb-6">
<label class="block text-sm font-medium text-white mb-2">Voice</label>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{% for voice_option in voices %}
<div class="voice-card {% if voice_option == DEFAULT_VOICE %}active{% endif %}"
data-voice="{{ voice_option }}"
data-language="{{ VOICE_TO_LANGUAGE[voice_option] }}"
style="display: {% if VOICE_TO_LANGUAGE[voice_option] != 'english' %}none{% endif %}">
<input type="radio" name="voice" value="{{ voice_option }}" class="hidden" {% if voice_option == DEFAULT_VOICE %}checked{% endif %}>
<div class="flex items-center mb-2">
<span class="font-medium text-white">{{ voice_option|capitalize }}</span>
</div>
<div class="text-xs text-dark-300">
{% if voice_option == "tara" %}Female, English, conversational, clear
{% elif voice_option == "leah" %}Female, English, warm, gentle
{% elif voice_option == "jess" %}Female, English, energetic, youthful
{% elif voice_option == "leo" %}Male, English, authoritative, deep
{% elif voice_option == "dan" %}Male, English, friendly, casual
{% elif voice_option == "mia" %}Female, English, professional, articulate
{% elif voice_option == "zac" %}Male, English, enthusiastic, dynamic
{% elif voice_option == "zoe" %}Female, English, calm, soothing
<!-- French voices -->
{% elif voice_option == "pierre" %}Male, French, sophisticated
{% elif voice_option == "amelie" %}Female, French, elegant
{% elif voice_option == "marie" %}Female, French, spirited
<!-- German voices -->
{% elif voice_option == "jana" %}Female, German, clear
{% elif voice_option == "thomas" %}Male, German, authoritative
{% elif voice_option == "max" %}Male, German, energetic
<!-- Korean voices -->
{% elif voice_option == "유나" %}Female, Korean, melodic
{% elif voice_option == "준서" %}Male, Korean, confident
<!-- Hindi voice -->
{% elif voice_option == "ऋतिका" %}Female, Hindi, expressive
<!-- Mandarin voices -->
{% elif voice_option == "长乐" %}Female, Mandarin, gentle
{% elif voice_option == "白芷" %}Female, Mandarin, clear
<!-- Spanish voices -->
{% elif voice_option == "javi" %}Male, Spanish, warm
{% elif voice_option == "sergio" %}Male, Spanish, professional
{% elif voice_option == "maria" %}Female, Spanish, friendly
<!-- Italian voices -->
{% elif voice_option == "pietro" %}Male, Italian, passionate
{% elif voice_option == "giulia" %}Female, Italian, expressive
{% elif voice_option == "carlo" %}Male, Italian, refined
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Advanced options (can be expanded) -->
<div class="mb-6">
<details class="group">
<summary class="list-none flex cursor-pointer">
<span class="text-sm font-medium text-white">Advanced options</span>
<span class="ml-2 text-purple-300">
<svg class="group-open:rotate-180 h-5 w-5 transition-transform" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</span>
</summary>
<!-- Audio generation options -->
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="model" class="block text-sm font-medium text-white mb-1">Model</label>
<div class="relative">
<select id="model" name="model" class="block w-full rounded-md bg-dark-700 border-dark-600 text-white shadow-sm focus:border-primary-500 focus:ring-primary-500 focus:ring-offset-dark-800 focus:outline-none outline-none sm:text-sm pl-3 pr-10 py-2 appearance-none">
<option value="orpheus" selected>Orpheus 3B (0.1)</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-purple-300">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</div>
</div>
<div>
<label for="speed" class="block text-sm font-medium text-white mb-1">Speed</label>
<input type="range" id="speed" name="speed" min="0.5" max="1.5" step="0.1" value="1.0"
class="mt-1 w-full h-2 bg-dark-600 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-purple-300 mt-1">
<span>Slower</span>
<span id="speed-value">1.0</span>
<span>Faster</span>
</div>
</div>
</div>
<!-- Server configuration section -->
<div class="mt-6 border-t border-dark-700 pt-4">
<h3 class="text-sm font-medium text-white mb-3">Server Configuration</h3>
<p class="text-xs text-purple-300 mb-3">These settings will be saved to a <code class="bg-dark-700 px-1 rounded">.env</code> file. Restart the server to apply changes.</p>
<!-- Form fields for all .env parameters -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Connection settings -->
<div>
<label for="api_url" class="block text-xs font-medium text-white mb-1">API URL</label>
<input type="text" id="api_url" name="ORPHEUS_API_URL"
placeholder="http://127.0.0.1:1234/v1/completions"
class="block w-full rounded-md bg-dark-700 border-dark-600 text-white text-sm focus:border-primary-500 focus:ring-primary-500 focus:ring-offset-dark-800 px-3 py-2">
</div>
<div>
<label for="api_timeout" class="block text-xs font-medium text-white mb-1">API Timeout (seconds)</label>
<input type="number" id="api_timeout" name="ORPHEUS_API_TIMEOUT" min="10" max="1800" step="1"
class="block w-full rounded-md bg-dark-700 border-dark-600 text-white text-sm focus:border-primary-500 focus:ring-primary-500 focus:ring-offset-dark-800 px-3 py-2">
</div>
<!-- Generation parameters -->
<div>
<label for="max_tokens" class="block text-xs font-medium text-white mb-1">Max Tokens</label>
<input type="number" id="max_tokens" name="ORPHEUS_MAX_TOKENS" min="100" max="200000" step="1"
class="block w-full rounded-md bg-dark-700 border-dark-600 text-white text-sm focus:border-primary-500 focus:ring-primary-500 focus:ring-offset-dark-800 px-3 py-2">
</div>
<div>
<label for="temperature" class="block text-xs font-medium text-white mb-1">Temperature</label>
<input type="number" id="temperature" name="ORPHEUS_TEMPERATURE" min="0.1" max="1.5" step="0.1"
class="block w-full rounded-md bg-dark-700 border-dark-600 text-white text-sm focus:border-primary-500 focus:ring-primary-500 focus:ring-offset-dark-800 px-3 py-2">
</div>
<div>
<label for="top_p" class="block text-xs font-medium text-white mb-1">Top P</label>
<input type="number" id="top_p" name="ORPHEUS_TOP_P" min="0.1" max="1" step="0.05"
class="block w-full rounded-md bg-dark-700 border-dark-600 text-white text-sm focus:border-primary-500 focus:ring-primary-500 focus:ring-offset-dark-800 px-3 py-2">
</div>
<div>
<div class="flex justify-between">
<label class="block text-xs font-medium text-white mb-1">Repetition Penalty</label>
<span class="text-xs text-primary-400">Fixed at 1.1</span>
</div>
<div class="bg-dark-700 text-gray-500 border-dark-600 rounded-md px-3 py-2 text-sm">
Value hardcoded to 1.1 for optimal generation quality
</div>
</div>
<!-- Server settings -->
<div>
<label for="server_host" class="block text-xs font-medium text-white mb-1">Server Host</label>
<input type="text" id="server_host" name="ORPHEUS_HOST" placeholder="0.0.0.0"
class="block w-full rounded-md bg-dark-700 border-dark-600 text-white text-sm focus:border-primary-500 focus:ring-primary-500 focus:ring-offset-dark-800 px-3 py-2">
</div>
<div>
<label for="server_port" class="block text-xs font-medium text-white mb-1">Server Port</label>
<input type="number" id="server_port" name="ORPHEUS_PORT" min="1024" max="65535" step="1"
class="block w-full rounded-md bg-dark-700 border-dark-600 text-white text-sm focus:border-primary-500 focus:ring-primary-500 focus:ring-offset-dark-800 px-3 py-2">
</div>
<!-- Save button and restart button -->
<div class="col-span-1 md:col-span-2 mt-4 flex flex-col md:flex-row gap-4">
<button id="save-config-btn" type="button"
class="btn-primary bg-purple-600 hover:bg-purple-700 active:bg-purple-800 w-full md:w-auto">
Save Configuration
</button>
<button id="restart-server-btn" type="button"
class="btn-primary bg-red-600 hover:bg-red-700 active:bg-red-800 w-full md:w-auto hidden">
Restart Server
</button>
<p class="text-xs text-purple-300 mt-2 flex-grow">
<span id="config-status" class="hidden"></span>
</p>
</div>
</div>
</div>
</details>
</div>
</div>
<div class="bg-dark-900 px-6 py-4 flex items-center justify-between">
<div class="text-sm text-purple-300">
<p>Supports emotion tags: <span class="font-mono text-xs">&lt;laugh&gt;</span>, <span class="font-mono text-xs">&lt;sigh&gt;</span>, etc.</p>
</div>
<button type="submit" id="generate-btn" class="btn-primary hover:bg-primary-600 active:bg-primary-800">
Generate Speech
</button>
</div>
</form>
</div>
<!-- Audio player container - will be populated by JavaScript -->
<div id="audio-player-container"></div>
<!-- Recent generations (could be expanded) -->
<div class="mt-8">
<h2 class="text-lg font-medium text-white mb-4">Tips & Tricks</h2>
<div class="bg-dark-800 shadow-lg rounded-lg overflow-hidden border border-dark-700">
<div class="p-6">
<ul class="list-disc pl-5 text-sm text-purple-300 space-y-2">
<li>Use <span class="font-mono text-xs">&lt;laugh&gt;</span> to add laughter to the speech</li>
<li>Use <span class="font-mono text-xs">&lt;sigh&gt;</span> for a sighing sound</li>
<li>Other supported tags: <span class="font-mono text-xs">&lt;chuckle&gt;</span>, <span class="font-mono text-xs">&lt;cough&gt;</span>, <span class="font-mono text-xs">&lt;sniffle&gt;</span>, <span class="font-mono text-xs">&lt;groan&gt;</span>, <span class="font-mono text-xs">&lt;yawn&gt;</span>, <span class="font-mono text-xs">&lt;gasp&gt;</span></li>
<li>For longer audio, the system can generate up to 2 minutes of speech in a single request</li>
<li>For API access, use the <code class="font-mono text-xs bg-dark-600 text-primary-300 p-1 rounded">/v1/audio/speech</code> endpoint (OpenAI compatible)</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-dark-900 border-t border-dark-700 py-6">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex justify-center">
<span class="text-purple-300 text-sm">Powered by <a href="https://fastapi.tiangolo.com/" target="_blank" class="text-primary-400 hover:text-primary-300">FASTAPI</a></span>
</div>
</div>
</footer>
</div>
<!-- Loading spinner template (hidden by default) -->
<div id="loading-overlay" class="hidden fixed inset-0 bg-dark-900 bg-opacity-75 flex items-center justify-center z-50">
<div class="bg-dark-800 p-6 rounded-lg shadow-lg flex flex-col items-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mb-4"></div>
<p class="text-white text-lg">Generating audio...</p>
</div>
</div>
<!-- Audio player template (used for dynamic insertion) -->
<template id="audio-player-template">
<div class="mt-8 bg-dark-800 shadow-lg rounded-lg overflow-hidden border border-dark-700">
<div class="p-6">
<h2 class="text-lg font-medium text-white mb-4">Generated Audio</h2>
<div class="mb-6">
<div id="waveform" class="w-full h-24"></div>
</div>
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center space-x-4">
<button id="play-btn" class="inline-flex items-center px-4 py-2 border border-primary-700 rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-dark-800 focus:ring-offset-2">
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
</svg>
Play
</button>
<a id="download-link" href="#" download class="inline-flex items-center px-4 py-2 border border-dark-600 rounded-md shadow-sm text-sm font-medium text-white bg-dark-700 hover:bg-dark-600 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-dark-800 focus:ring-offset-2">
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
Download
</a>
</div>
<div class="text-sm text-purple-300">
Voice: <span id="voice-name" class="font-medium"></span>
Duration: <span id="audio-duration" class="font-medium">--:--</span>
Generated in: <span id="generation-time" class="font-medium"></span>s
</div>
</div>
</div>
</div>
</template>
<!-- JavaScript for interactivity -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Global variables
let wavesurfer;
// Character counter
const textArea = document.getElementById('text');
const charCount = document.getElementById('char-count');
textArea.addEventListener('input', function() {
charCount.textContent = textArea.value.length;
});
// Initialize char count
charCount.textContent = textArea.value.length;
// Language selection
const languageOptions = document.querySelectorAll('.language-option');
const voiceCards = document.querySelectorAll('.voice-card');
languageOptions.forEach(option => {
option.addEventListener('click', function() {
// Unselect all language options
languageOptions.forEach(o => o.classList.remove('active'));
// Select this language option
this.classList.add('active');
// Check the radio button
const radio = this.querySelector('input[type="radio"]');
radio.checked = true;
// Get the selected language
const selectedLanguage = this.getAttribute('data-language');
// Show/hide voice cards based on language
let firstVisibleCard = null;
voiceCards.forEach(card => {
const cardLanguage = card.getAttribute('data-language');
if (cardLanguage === selectedLanguage) {
card.style.display = '';
if (!firstVisibleCard) {
firstVisibleCard = card;
}
} else {
card.style.display = 'none';
// If this hidden card was selected, unselect it
if (card.classList.contains('active')) {
card.classList.remove('active');
const cardRadio = card.querySelector('input[type="radio"]');
if (cardRadio) {
cardRadio.checked = false;
}
}
}
});
// If no voice card is selected for this language, select the first one
if (firstVisibleCard && document.querySelectorAll(`.voice-card[data-language="${selectedLanguage}"].active:not([style*="display: none"])`).length === 0) {
firstVisibleCard.classList.add('active');
const firstVisibleRadio = firstVisibleCard.querySelector('input[type="radio"]');
if (firstVisibleRadio) {
firstVisibleRadio.checked = true;
}
}
});
});
// Voice card selection
voiceCards.forEach(card => {
card.addEventListener('click', function() {
// Only allow selection of cards that are visible (not display:none)
if (this.style.display !== 'none') {
// Unselect all cards with the same language
const cardLanguage = this.getAttribute('data-language');
document.querySelectorAll(`.voice-card[data-language="${cardLanguage}"]`).forEach(c => {
c.classList.remove('active');
const radio = c.querySelector('input[type="radio"]');
if (radio) {
radio.checked = false;
}
});
// Select this card
this.classList.add('active');
// Check the radio button
const radio = this.querySelector('input[type="radio"]');
if (radio) {
radio.checked = true;
}
}
});
});
// Speed slider
const speedSlider = document.getElementById('speed');
const speedValue = document.getElementById('speed-value');
speedSlider.addEventListener('input', function() {
speedValue.textContent = speedSlider.value;
});
// No preview buttons in this version
// Function to initialize WaveSurfer
function initWaveSurfer(audioPath) {
// If wavesurfer already exists, destroy it to prevent memory leaks
if (wavesurfer) {
wavesurfer.destroy();
}
// Create new wavesurfer instance
wavesurfer = WaveSurfer.create({
container: '#waveform',
waveColor: '#38bdf8',
progressColor: '#0284c7',
cursorColor: '#0ea5e9',
barWidth: 3,
barRadius: 3,
cursorWidth: 1,
height: 80,
barGap: 2,
responsive: true
});
wavesurfer.load('/' + audioPath);
wavesurfer.on('ready', function() {
const duration = wavesurfer.getDuration();
const minutes = Math.floor(duration / 60);
const seconds = Math.floor(duration % 60);
document.getElementById('audio-duration').textContent =
`${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
});
// Play button
const playBtn = document.getElementById('play-btn');
playBtn.addEventListener('click', function() {
wavesurfer.playPause();
if (wavesurfer.isPlaying()) {
playBtn.innerHTML = `
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 00-1 1v2a1 1 0 001 1h6a1 1 0 001-1V9a1 1 0 00-1-1H7z" clip-rule="evenodd" />
</svg>
Pause
`;
} else {
playBtn.innerHTML = `
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
</svg>
Play
`;
}
});
}
// Function to create and add the audio player to the DOM
function createAudioPlayer(response) {
const container = document.getElementById('audio-player-container');
const template = document.getElementById('audio-player-template');
// Clone the template
const audioPlayer = template.content.cloneNode(true);
// Clear previous player if exists
container.innerHTML = '';
// Add the new player
container.appendChild(audioPlayer);
// Set values
document.getElementById('voice-name').textContent = response.voice.charAt(0).toUpperCase() + response.voice.slice(1);
document.getElementById('generation-time').textContent = response.generation_time;
document.getElementById('download-link').href = '/' + response.output_file;
// Initialize waveform
initWaveSurfer(response.output_file);
}
// Form submission handler
const form = document.getElementById('tts-form');
form.addEventListener('submit', async function(event) {
// Prevent default form submission
event.preventDefault();
// Get form data
const text = document.getElementById('text').value;
const voice = document.querySelector('input[name="voice"]:checked').value;
if (!text.trim()) {
alert('Please enter some text to generate speech');
return;
}
// Show loading overlay
document.getElementById('loading-overlay').classList.remove('hidden');
try {
// Make API request to /speak endpoint
const response = await fetch('/speak', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: text,
voice: voice
})
});
if (!response.ok) {
throw new Error('Failed to generate speech');
}
const data = await response.json();
// Create audio player with response
createAudioPlayer(data);
} catch (error) {
console.error('Error generating speech:', error);
alert('An error occurred while generating speech. Please try again.');
} finally {
// Hide loading overlay
document.getElementById('loading-overlay').classList.add('hidden');
}
});
// Initialize audio player if output file exists from server-side rendering
{% if output_file %}
createAudioPlayer({
voice: "{{ voice if voice else DEFAULT_VOICE }}",
output_file: "{{ output_file }}",
generation_time: "{{ generation_time }}"
});
{% endif %}
// Configuration management
async function loadConfigValues() {
try {
const response = await fetch('/get_config');
if (!response.ok) {
throw new Error('Failed to load configuration');
}
const config = await response.json();
// Populate form fields with config values
document.querySelectorAll('input[name^="ORPHEUS_"]').forEach(input => {
const name = input.name;
if (config[name] !== undefined) {
input.value = config[name];
}
});
console.log('Configuration loaded successfully');
} catch (error) {
console.error('Error loading configuration:', error);
}
}
// Save configuration
async function saveConfiguration() {
const configData = {};
// Collect values from all configuration inputs
document.querySelectorAll('input[name^="ORPHEUS_"]').forEach(input => {
configData[input.name] = input.value;
});
try {
const response = await fetch('/save_config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(configData)
});
if (!response.ok) {
throw new Error('Failed to save configuration');
}
const result = await response.json();
// Show success message
const statusElem = document.getElementById('config-status');
statusElem.textContent = result.message;
statusElem.classList.remove('hidden');
statusElem.classList.add('text-green-400');
// Hide message after 5 seconds
setTimeout(() => {
statusElem.classList.add('hidden');
}, 5000);
} catch (error) {
console.error('Error saving configuration:', error);
// Show error message
const statusElem = document.getElementById('config-status');
statusElem.textContent = 'Error saving configuration. Please try again.';
statusElem.classList.remove('hidden');
statusElem.classList.add('text-red-400');
// Hide message after 5 seconds
setTimeout(() => {
statusElem.classList.add('hidden');
}, 5000);
}
}
// Function to restart the server
async function restartServer() {
const restartBtn = document.getElementById('restart-server-btn');
restartBtn.disabled = true;
restartBtn.textContent = 'Restarting...';
try {
const response = await fetch('/restart_server', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to restart server');
}
const result = await response.json();
// Show server restarting message and overlay
const statusElem = document.getElementById('config-status');
statusElem.textContent = result.message;
statusElem.classList.remove('hidden');
statusElem.classList.add('text-yellow-400');
// Show loading overlay with different message
const loadingOverlay = document.getElementById('loading-overlay');
loadingOverlay.querySelector('p').textContent = 'Server is restarting...';
loadingOverlay.classList.remove('hidden');
// Poll for server readiness, then do a complete page reload with cache busting
function checkServerReady() {
console.log("Checking if server is ready...");
fetch('/get_config?cache=' + Date.now(), {
cache: 'no-store',
headers: { 'pragma': 'no-cache' }
})
.then(response => {
if (response.ok) {
console.log("Server is up and running, forcing complete page reload");
// Force a complete reload with cache busting
window.location = window.location.pathname + '?t=' + Date.now();
} else {
console.log("Server not ready yet, status: " + response.status);
setTimeout(checkServerReady, 1000);
}
})
.catch(error => {
console.log("Server not responding yet, waiting...");
setTimeout(checkServerReady, 1000);
});
}
// Start checking after initial delay
setTimeout(checkServerReady, 2000);
} catch (error) {
console.error('Error restarting server:', error);
// Show error message
const statusElem = document.getElementById('config-status');
statusElem.textContent = 'Error restarting server. Please try again.';
statusElem.classList.remove('hidden');
statusElem.classList.add('text-red-400');
// Reset button
restartBtn.disabled = false;
restartBtn.textContent = 'Restart Server';
}
}
// Add event listeners
document.getElementById('save-config-btn').addEventListener('click', function() {
saveConfiguration();
// Show restart button after saving
const restartBtn = document.getElementById('restart-server-btn');
restartBtn.classList.remove('hidden');
});
document.getElementById('restart-server-btn').addEventListener('click', restartServer);
// Load configuration values when page loads
loadConfigValues();
});
</script>
</body>
</html>