Perceived startup speed: - index.html: animated shimmer skeleton (header + toolbar + 8 voice cards) visible immediately; fades out when loader.js finishes - WaveSurfer (57KB) and MDI icon font (394KB woff2) now served from static/vendor/ — removes 3 render-blocking external requests from <head> - Flag-icons CSS loaded async (rel=preload onload trick) — non-blocking loader.js: - Fade out skeleton + remove from DOM (300ms transition) - Reveal page-sections after JS finishes loading Conversation playground: - Qwen3 thinking mode fix: fall back to delta.reasoning_content when delta.content is empty so think-only LLM turns produce visible output - Better error message with /no-think hint when LLM returns empty Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
296 lines
17 KiB
HTML
296 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>TTS Voice Creator</title>
|
|
|
|
<!-- ── Preconnect to CDN (flag SVGs loaded lazily by JS) ──────────────── -->
|
|
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
|
|
|
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
|
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
|
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
|
<link rel="preload" as="style"
|
|
href="https://cdn.jsdelivr.net/npm/flag-icons@7.3.2/css/flag-icons.min.css"
|
|
onload="this.onload=null;this.rel='stylesheet'">
|
|
<noscript>
|
|
<link rel="stylesheet"
|
|
href="https://cdn.jsdelivr.net/npm/flag-icons@7.3.2/css/flag-icons.min.css">
|
|
</noscript>
|
|
|
|
<!-- ── Skeleton animation (inline — zero extra request) ──────────────── -->
|
|
<style>
|
|
@keyframes sk-shimmer {
|
|
0% { background-position: -400% 0; }
|
|
100% { background-position: 400% 0; }
|
|
}
|
|
.sk {
|
|
background: linear-gradient(
|
|
90deg,
|
|
var(--border, #e5e7eb) 25%,
|
|
var(--panel, #f3f4f6) 50%,
|
|
var(--border, #e5e7eb) 75%
|
|
);
|
|
background-size: 400% 100%;
|
|
animation: sk-shimmer 1.6s ease-in-out infinite;
|
|
border-radius: 6px;
|
|
}
|
|
#app-skeleton {
|
|
padding: 28px 32px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
opacity: 1;
|
|
transition: opacity .25s ease;
|
|
}
|
|
#app-skeleton.sk-hidden { opacity: 0; pointer-events: none; }
|
|
.sk-head { display: flex; align-items: center; gap: 12px; }
|
|
.sk-icon { width: 32px; height: 32px; border-radius: 8px; flex-shrink: 0; }
|
|
.sk-title { display: flex; flex-direction: column; gap: 7px; }
|
|
.sk-toolbar { display: flex; gap: 10px; align-items: center; }
|
|
.sk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
|
|
.sk-card { border-radius: 10px; overflow: hidden; }
|
|
.sk-card-img { height: 56px; border-radius: 0; }
|
|
.sk-card-body { padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
|
|
.sk-row { display: flex; gap: 10px; margin-top: 4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Hidden tabs nav — required for JS backend availability detection -->
|
|
<nav class="tabs" style="display:none" aria-hidden="true">
|
|
<div class="tab active" data-tab="library" data-backend-required="voice_clone" tabindex="-1"><span class="tab-title">Voice Clone</span></div>
|
|
<div class="tab" data-tab="design" data-backend-required="voice_design" tabindex="-1"><span class="tab-title">Voice Design</span></div>
|
|
<div class="tab" data-tab="custom" data-backend-required="customvoice" tabindex="-1"><span class="tab-title">Voice Custom</span></div>
|
|
<div class="tab" data-tab="generation" data-backend-required="any_tts" tabindex="-1"><span class="tab-title">TTS Generation</span></div>
|
|
<div class="tab" data-tab="stt-tts" data-backend-required="any_tts" tabindex="-1"><span class="tab-title">STT-TTS</span></div>
|
|
<div class="tab" data-tab="performance" data-backend-required="any_tts" tabindex="-1"><span class="tab-title">Performance</span></div>
|
|
<div class="tab" data-tab="routing" tabindex="-1"><span class="tab-title">Routing</span></div>
|
|
<div class="tab" data-tab="integrations" tabindex="-1"><span class="tab-title">How to</span></div>
|
|
<div class="tab" data-tab="getvoices" tabindex="-1"><span class="tab-title">Get Voices</span></div>
|
|
<div class="tab" data-tab="settings" tabindex="-1"><span class="tab-title">Settings</span></div>
|
|
</nav>
|
|
|
|
<!-- App shell -->
|
|
<div id="app-shell">
|
|
|
|
<!-- Sidebar (renders immediately — no JS needed) -->
|
|
<aside id="sidebar">
|
|
<div class="sidebar-brand">
|
|
<h1><span class="mdi mdi-microphone-variant"></span> Voice Creator</h1>
|
|
<p>Clone · Design · Deploy</p>
|
|
</div>
|
|
|
|
<nav class="sidebar-nav">
|
|
<div class="nav-group-label">Voices</div>
|
|
<div class="nav-item nav-tree-head active" data-nav-section="s-voices" id="nav-voices-head" onclick="navTo('s-voices')">
|
|
<span class="nav-icon"><span class="mdi mdi-account-voice"></span></span>
|
|
<span class="nav-label">My Voices</span>
|
|
<span class="nav-badge" id="nav-voices-count"></span>
|
|
<span class="nav-chevron" id="nav-voices-chevron"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree open" id="nav-voices-tree">
|
|
<div class="nav-tree-item is-active" data-voice-cat="all" onclick="navVoicesCat('all')">All voices <span class="ntc" id="ntc-all"></span></div>
|
|
<div class="nav-tree-item" data-voice-cat="cloned" onclick="navVoicesCat('cloned')">Cloned <span class="ntc" id="ntc-cloned"></span></div>
|
|
<div class="nav-tree-item" data-voice-cat="designed" onclick="navVoicesCat('designed')">Designed <span class="ntc" id="ntc-designed"></span></div>
|
|
<div class="nav-tree-item" data-voice-cat="favorites" onclick="navVoicesCat('favorites')">Favorites <span class="ntc" id="ntc-favorites"></span></div>
|
|
<div class="nav-tree-item" data-voice-cat="hidden" onclick="navVoicesCat('hidden')">Hidden <span class="ntc" id="ntc-hidden"></span></div>
|
|
<div class="nav-tree-item" data-voice-cat="tools" onclick="navVoicesCat('tools')">Library tools</div>
|
|
</div>
|
|
<div class="nav-item" data-nav-section="s-clone" onclick="navTo('s-clone')"> <span class="nav-icon"><span class="mdi mdi-microphone-variant"></span></span> Clone a Voice</div>
|
|
<div class="nav-item" data-nav-section="s-design" onclick="navTo('s-design')"> <span class="nav-icon"><span class="mdi mdi-auto-fix"></span></span> Design a Voice</div>
|
|
<div class="nav-item" data-nav-section="s-studio" onclick="navTo('s-studio')"> <span class="nav-icon"><span class="mdi mdi-earth"></span></span> Get Voices Online</div>
|
|
<div class="nav-item" data-nav-section="s-tryout" onclick="navTo('s-tryout')"> <span class="nav-icon"><span class="mdi mdi-play"></span></span> Try It Out</div>
|
|
<div class="nav-item" data-nav-section="s-conversation" onclick="navTo('s-conversation')"> <span class="nav-icon"><span class="mdi mdi-forum-outline"></span></span> Conversation</div>
|
|
<div class="nav-item" data-nav-section="s-performance" onclick="navTo('s-performance')"> <span class="nav-icon"><span class="mdi mdi-speedometer"></span></span> Benchmark</div>
|
|
|
|
<div class="nav-group-label" style="margin-top:6px">Setup</div>
|
|
<div class="nav-item nav-tree-head" data-nav-section="s-llms" id="nav-engines-head" onclick="navTo('s-llms')">
|
|
<span class="nav-icon"><span class="mdi mdi-brain"></span></span>
|
|
<span class="nav-label">Engines</span>
|
|
<span class="nav-chevron" id="nav-engines-chevron"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree" id="nav-engines-tree">
|
|
<div class="nav-tree-item is-active" data-engines-cat="llm" onclick="navEnginesCat('llm')">Language Models</div>
|
|
<div class="nav-tree-item" data-engines-cat="stt" onclick="navEnginesCat('stt')">Speech to Text</div>
|
|
<div class="nav-tree-item" data-engines-cat="tts" onclick="navEnginesCat('tts')">Text to Speech</div>
|
|
</div>
|
|
<div class="nav-item" data-nav-section="s-routing" onclick="navTo('s-routing')"> <span class="nav-icon"><span class="mdi mdi-swap-horizontal"></span></span> App Routing</div>
|
|
<div class="nav-item" data-nav-section="s-connect" onclick="navTo('s-connect')"> <span class="nav-icon"><span class="mdi mdi-api"></span></span> Connect Apps</div>
|
|
<div class="nav-item nav-tree-head" data-nav-section="s-settings" id="nav-settings-head" onclick="navTo('s-settings')">
|
|
<span class="nav-icon"><span class="mdi mdi-cog-outline"></span></span>
|
|
<span class="nav-label">Settings</span>
|
|
<span class="nav-chevron" id="nav-settings-chevron"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree" id="nav-settings-tree">
|
|
<div class="nav-tree-item" data-settings-cat="general" onclick="navSettingsCat('general')">General</div>
|
|
<div class="nav-tree-item" data-settings-cat="connections" onclick="navSettingsCat('connections')">Connections</div>
|
|
<div class="nav-tree-item" data-settings-cat="playback" onclick="navSettingsCat('playback')">Playback</div>
|
|
<div class="nav-tree-item" data-settings-cat="captures" onclick="navSettingsCat('captures')">Captures</div>
|
|
<div class="nav-tree-item" data-settings-cat="payloads" onclick="navSettingsCat('payloads')">Payloads</div>
|
|
<div class="nav-tree-item" data-settings-cat="storage" onclick="navSettingsCat('storage')">Storage</div>
|
|
<div class="nav-tree-item" data-settings-cat="apikeys" onclick="navSettingsCat('apikeys')">API Keys</div>
|
|
<div class="nav-tree-item" data-settings-cat="backup" onclick="navSettingsCat('backup')">Backup</div>
|
|
<div class="nav-tree-item" data-settings-cat="logs" onclick="navSettingsCat('logs')">Logs</div>
|
|
<div class="nav-tree-item" data-settings-cat="about" onclick="navSettingsCat('about')">About</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="sidebar-footer">
|
|
<button id="theme-btn" title="Switch theme" style="display:none"><span class="mdi mdi-weather-sunny"></span></button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main content — sections injected by loader.js -->
|
|
<main id="main-content">
|
|
|
|
<!-- ── Skeleton loading view ─────────────────────────────────────────── -->
|
|
<!-- Visible immediately; loader.js fades it out when real content is ready -->
|
|
<div id="app-skeleton">
|
|
|
|
<!-- Page head -->
|
|
<div class="sk-head">
|
|
<div class="sk sk-icon"></div>
|
|
<div class="sk-title">
|
|
<div class="sk" style="width:160px;height:22px"></div>
|
|
<div class="sk" style="width:280px;height:13px"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toolbar row -->
|
|
<div class="sk-toolbar">
|
|
<div class="sk" style="width:48px;height:24px;border-radius:12px"></div>
|
|
<div class="sk" style="width:200px;height:30px;border-radius:6px"></div>
|
|
<div class="sk" style="width:80px;height:30px;border-radius:6px;margin-left:auto"></div>
|
|
<div class="sk" style="width:80px;height:30px;border-radius:6px"></div>
|
|
</div>
|
|
|
|
<!-- Voice card grid (matches the voice library layout) -->
|
|
<div class="sk-grid">
|
|
<!-- 8 placeholder voice cards -->
|
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
|
<div class="sk sk-card-img"></div>
|
|
<div class="sk-card-body">
|
|
<div class="sk" style="width:70%;height:14px"></div>
|
|
<div class="sk" style="width:45%;height:12px"></div>
|
|
<div class="sk-row">
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
|
<div class="sk sk-card-img"></div>
|
|
<div class="sk-card-body">
|
|
<div class="sk" style="width:80%;height:14px"></div>
|
|
<div class="sk" style="width:55%;height:12px"></div>
|
|
<div class="sk-row">
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
|
<div class="sk sk-card-img"></div>
|
|
<div class="sk-card-body">
|
|
<div class="sk" style="width:60%;height:14px"></div>
|
|
<div class="sk" style="width:40%;height:12px"></div>
|
|
<div class="sk-row">
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
|
<div class="sk sk-card-img"></div>
|
|
<div class="sk-card-body">
|
|
<div class="sk" style="width:75%;height:14px"></div>
|
|
<div class="sk" style="width:50%;height:12px"></div>
|
|
<div class="sk-row">
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
|
<div class="sk sk-card-img"></div>
|
|
<div class="sk-card-body">
|
|
<div class="sk" style="width:65%;height:14px"></div>
|
|
<div class="sk" style="width:42%;height:12px"></div>
|
|
<div class="sk-row">
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
|
<div class="sk sk-card-img"></div>
|
|
<div class="sk-card-body">
|
|
<div class="sk" style="width:85%;height:14px"></div>
|
|
<div class="sk" style="width:48%;height:12px"></div>
|
|
<div class="sk-row">
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
|
<div class="sk sk-card-img"></div>
|
|
<div class="sk-card-body">
|
|
<div class="sk" style="width:72%;height:14px"></div>
|
|
<div class="sk" style="width:38%;height:12px"></div>
|
|
<div class="sk-row">
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sk-card" style="border:1px solid var(--border,#e5e7eb)">
|
|
<div class="sk sk-card-img"></div>
|
|
<div class="sk-card-body">
|
|
<div class="sk" style="width:58%;height:14px"></div>
|
|
<div class="sk" style="width:44%;height:12px"></div>
|
|
<div class="sk-row">
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
<div class="sk" style="width:28px;height:28px;border-radius:50%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- /app-skeleton -->
|
|
|
|
<section class="page-section" id="s-voices" style="display:none"></section>
|
|
<section class="page-section" id="s-clone" style="display:none"></section>
|
|
<section class="page-section" id="s-design" style="display:none"></section>
|
|
<section class="page-section" id="s-studio" style="display:none"></section>
|
|
<section class="page-section" id="s-tryout" style="display:none"></section>
|
|
<section class="page-section" id="s-performance" style="display:none"></section>
|
|
<section class="page-section" id="s-routing" style="display:none"></section>
|
|
<section class="page-section" id="s-connect" style="display:none"></section>
|
|
<section class="page-section" id="s-settings" style="display:none"></section>
|
|
<section class="page-section" id="s-llms" style="display:none"></section>
|
|
<section class="page-section" id="s-conversation" style="display:none"></section>
|
|
</main>
|
|
|
|
</div><!-- /app-shell -->
|
|
|
|
<div id="toast"></div>
|
|
<div id="status-bar">Loading…</div>
|
|
|
|
<!-- WaveSurfer — self-hosted, loaded synchronously (voice waveform requires it) -->
|
|
<script src="/static/vendor/wavesurfer.min.js"></script>
|
|
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
|
|
|
|
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
|
<script src="/static/loader.js"></script>
|
|
|
|
</body>
|
|
</html>
|