The LLM left ~44% of dialogue Unknown even with all deduction rules in its prompt, so the mechanical ones now run in code per passage: colon rule (with a non-agent-noun stoplist), post-quote inquit, and the "who had spoken" pattern. Fills only Unknowns, never overrides the LLM. Tested against the exact reported failure cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
372 lines
28 KiB
HTML
372 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<!-- viewport-fit=cover respects the iPhone notch/safe areas; allow zoom for accessibility -->
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="format-detection" content="telephone=no">
|
|
<meta name="color-scheme" content="light dark">
|
|
<meta name="theme-color" content="#2563EB">
|
|
<meta name="app-version" content="1.12.93">
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
|
<link rel="apple-touch-icon" href="/static/icon.svg">
|
|
<title>TTS Voice Creator</title>
|
|
<script>
|
|
/* PWA: register the network-first service worker (offline shell + installable) */
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(() => {}));
|
|
}
|
|
</script>
|
|
|
|
<!-- ── 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?v=1.12.93">
|
|
|
|
|
|
<!-- ── 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; display: 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-row { display: flex; gap: 10px; align-items: center; }
|
|
.sk-workbench { display: grid; grid-template-columns: 300px 1fr; gap: 0; border: 1px solid var(--border,#e5e7eb); border-radius: 10px; overflow: hidden; min-height: 480px; }
|
|
.sk-list { border-right: 1px solid var(--border,#e5e7eb); display: flex; flex-direction: column; }
|
|
.sk-list-row { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-bottom: 1px solid var(--border,#e5e7eb); }
|
|
.sk-avatar { width: 36px; height: 36px; border-radius: 8px; flex-shrink: 0; }
|
|
.sk-list-body { display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 0; }
|
|
.sk-insp { padding: 20px; display: flex; flex-direction: column; gap: 16px; }
|
|
.sk-insp-head { display: flex; flex-direction: column; gap: 8px; }
|
|
@media (max-width: 700px) { .sk-workbench { grid-template-columns: 1fr; } .sk-insp { display: none; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a class="skip-link" href="#main-content">Skip to main content</a>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Mobile top bar (hidden on desktop via CSS) -->
|
|
<div id="mobile-topbar">
|
|
<button id="sidebar-toggle" aria-label="Open navigation"><span class="mdi mdi-menu"></span></button>
|
|
<span class="mobile-brand"><span class="mdi mdi-microphone-variant"></span> Voice Creator</span>
|
|
</div>
|
|
|
|
<!-- Backdrop for sidebar drawer on mobile -->
|
|
<div id="sidebar-backdrop"></div>
|
|
|
|
<!-- App shell -->
|
|
<div id="app-shell">
|
|
<button id="sidebar-expand-btn" class="sidebar-expand-btn" type="button" title="Expand sidebar" aria-label="Expand sidebar" onclick="toggleSidebarCollapsed()"><span class="mdi mdi-menu"></span></button>
|
|
|
|
<!-- Sidebar (renders immediately — no JS needed) -->
|
|
<aside id="sidebar" tabindex="0" aria-label="Navigation">
|
|
<div class="sidebar-brand">
|
|
<div class="sidebar-brand-text">
|
|
<h1><span class="mdi mdi-microphone-variant"></span> Voice Creator</h1>
|
|
<p>Clone · Design · Deploy</p>
|
|
</div>
|
|
<button id="sidebar-collapse-btn" class="sidebar-collapse-btn" type="button" title="Collapse sidebar" aria-label="Collapse sidebar" onclick="toggleSidebarCollapsed()"><span class="mdi mdi-chevron-double-left"></span></button>
|
|
</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">Library</span>
|
|
<span class="nav-badge" id="nav-voices-count"></span>
|
|
<span class="nav-chevron" id="nav-voices-chevron" onclick="event.stopPropagation();toggleNavTree('nav-voices-tree','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')"><span class="mdi mdi-account-voice"></span> All voices <span class="ntc" id="ntc-all"></span></div>
|
|
<div class="nav-tree-item" data-voice-cat="favorites" onclick="navVoicesCat('favorites')"><span class="mdi mdi-star-outline"></span> Favourites <span class="ntc" id="ntc-favorites"></span></div>
|
|
<div class="nav-tree-item" data-voice-cat="hidden" onclick="navVoicesCat('hidden')"><span class="mdi mdi-eye-off-outline"></span> Hidden <span class="ntc" id="ntc-hidden"></span></div>
|
|
<div class="nav-tree-item" data-voice-cat="tools" onclick="navVoicesCat('tools')"><span class="mdi mdi-wrench-outline"></span> Tools</div>
|
|
<div class="nav-tree-head nav-subhead" id="nav-tags-head" onclick="toggleNavTree('nav-tags-tree','nav-tags-chevron')">
|
|
<span class="nav-icon"><span class="mdi mdi-tag-multiple-outline"></span></span>
|
|
<span class="nav-label">Tags</span>
|
|
<span class="nav-chevron" id="nav-tags-chevron"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree" id="nav-tags-tree"></div>
|
|
</div>
|
|
<div class="nav-group-head" id="nav-actions-head" onclick="toggleNavTree('nav-actions-tree','nav-actions-chevron')">
|
|
<span class="nav-label">Voice Actions</span>
|
|
<span class="nav-chevron" id="nav-actions-chevron"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree" id="nav-actions-tree">
|
|
<div class="nav-tree-item" data-nav-section="s-clone" onclick="navTo('s-clone')"><span class="mdi mdi-microphone-variant"></span> Clone a Voice</div>
|
|
<div class="nav-tree-item" data-nav-section="s-design" onclick="navTo('s-design')"><span class="mdi mdi-auto-fix"></span> Design a Voice</div>
|
|
<div class="nav-tree-item" data-nav-section="s-studio" onclick="navTo('s-studio')"><span class="mdi mdi-earth"></span> Get a Voice Online</div>
|
|
</div>
|
|
|
|
<div class="nav-group-head" id="nav-speak-head" onclick="toggleNavTree('nav-speak-tree','nav-speak-chevron')">
|
|
<span class="nav-label">Speak</span>
|
|
<span class="nav-chevron" id="nav-speak-chevron"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree" id="nav-speak-tree">
|
|
<div class="nav-tree-item" data-nav-section="s-tryout" onclick="navTo('s-tryout')"><span class="mdi mdi-play"></span> Quick Play</div>
|
|
<div class="nav-tree-item" data-nav-section="s-conversation" onclick="navTo('s-conversation')"><span class="mdi mdi-forum-outline"></span> Conversation</div>
|
|
<div class="nav-tree-head nav-subhead" data-nav-section="s-reader" id="nav-reader-head" onclick="navTo('s-reader')">
|
|
<span class="nav-icon"><span class="mdi mdi-book-open-page-variant-outline"></span></span>
|
|
<span class="nav-label">Read Aloud</span>
|
|
<span class="nav-chevron" id="nav-reader-chevron" onclick="event.stopPropagation();toggleNavTree('nav-reader-tree','nav-reader-chevron')"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree" id="nav-reader-tree">
|
|
<div class="nav-tree-item" onclick="navTo('s-reader');if(typeof showReaderView==='function')showReaderView('main')"><span class="mdi mdi-file-document-outline"></span> Reader</div>
|
|
<div class="nav-tree-item" id="nav-reader-cast-item" onclick="navTo('s-reader');if(typeof audiobookOpenCastView==='function')audiobookOpenCastView();else if(typeof showReaderView==='function')showReaderView('cast')"><span class="mdi mdi-drama-masks"></span> Casting</div>
|
|
</div>
|
|
<div class="nav-tree-head nav-subhead" data-nav-section="s-rehearser" id="nav-rehearser-head" onclick="navTo('s-rehearser')">
|
|
<span class="nav-icon"><span class="mdi mdi-theater"></span></span>
|
|
<span class="nav-label">Script Rehearsal</span>
|
|
<span class="nav-chevron" id="nav-rehearser-chevron" onclick="event.stopPropagation();toggleNavTree('nav-rehearser-tree','nav-rehearser-chevron')"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree" id="nav-rehearser-tree">
|
|
<div class="nav-tree-item is-active" data-rehearser-phase="3" onclick="navRehearserPhase(3)"><span class="mdi mdi-theater"></span> Stage</div>
|
|
<div class="nav-tree-item" data-rehearser-phase="4" onclick="navRehearserPhase(4)"><span class="mdi mdi-check-circle-outline"></span> Summary</div>
|
|
<div class="nav-tree-item" id="nav-reh-impex" onclick="navRehearserImpEx()"><span class="mdi mdi-swap-vertical"></span> Import / Export</div>
|
|
</div>
|
|
<div class="nav-tree-head nav-subhead" data-nav-section="s-library" id="nav-library-head" onclick="navTo('s-library')">
|
|
<span class="nav-icon"><span class="mdi mdi-bookshelf"></span></span>
|
|
<span class="nav-label">Library</span>
|
|
<span class="nav-chevron" id="nav-library-chevron" onclick="event.stopPropagation();toggleNavTree('nav-library-tree','nav-library-chevron')"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree" id="nav-library-tree">
|
|
<div class="nav-tree-item is-active" data-library-view="books" onclick="navLibraryView('books')"><span class="mdi mdi-book-open-page-variant-outline"></span> Books</div>
|
|
<div class="nav-tree-item" data-library-view="plays" onclick="navLibraryView('plays')"><span class="mdi mdi-theater"></span> Theater Plays</div>
|
|
<div class="nav-tree-item" data-library-view="characters" onclick="navLibraryView('characters')"><span class="mdi mdi-account-box-multiple-outline"></span> Characters / Cast</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nav-group-head" id="nav-setup-head" onclick="toggleNavTree('nav-setup-tree','nav-setup-chevron')">
|
|
<span class="nav-label">Setup</span>
|
|
<span class="nav-chevron" id="nav-setup-chevron"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree" id="nav-setup-tree">
|
|
<div class="nav-tree-item" data-nav-section="s-performance" onclick="navTo('s-performance')"><span class="mdi mdi-speedometer"></span> Benchmark</div>
|
|
<div class="nav-tree-head nav-subhead" 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" onclick="event.stopPropagation();toggleNavTree('nav-engines-tree','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')"><span class="mdi mdi-brain"></span> Language Models</div>
|
|
<div class="nav-tree-item" data-engines-cat="stt" onclick="navEnginesCat('stt')"><span class="mdi mdi-ear-hearing"></span> Speech to Text</div>
|
|
<div class="nav-tree-item" data-engines-cat="tts" onclick="navEnginesCat('tts')"><span class="mdi mdi-account-voice"></span> Text to Speech</div>
|
|
</div>
|
|
<div class="nav-tree-head nav-subhead" id="nav-integrations-head" onclick="toggleNavTree('nav-integrations-tree','nav-integrations-chevron')">
|
|
<span class="nav-icon"><span class="mdi mdi-transit-connection-variant"></span></span>
|
|
<span class="nav-label">Integrations</span>
|
|
<span class="nav-chevron" id="nav-integrations-chevron"><span class="mdi mdi-chevron-down"></span></span>
|
|
</div>
|
|
<div class="nav-tree" id="nav-integrations-tree">
|
|
<div class="nav-tree-item" data-nav-section="s-routing" onclick="navTo('s-routing')"><span class="mdi mdi-swap-horizontal"></span> App Routing</div>
|
|
<div class="nav-tree-item" data-nav-section="s-connect" onclick="navTo('s-connect')"><span class="mdi mdi-api"></span> Connect Apps</div>
|
|
</div>
|
|
<div class="nav-tree-head nav-subhead" 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" onclick="event.stopPropagation();toggleNavTree('nav-settings-tree','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')"><span class="mdi mdi-tune-vertical"></span> General</div>
|
|
<div class="nav-tree-item" data-settings-cat="payloads" onclick="navSettingsCat('payloads')"><span class="mdi mdi-code-json"></span> Payloads</div>
|
|
<div class="nav-tree-item" data-settings-cat="storage" onclick="navSettingsCat('storage')"><span class="mdi mdi-folder-outline"></span> Storage</div>
|
|
<div class="nav-tree-item" data-settings-cat="apikeys" onclick="navSettingsCat('apikeys')"><span class="mdi mdi-key-outline"></span> API Keys</div>
|
|
<div class="nav-tree-item" data-settings-cat="backup" onclick="navSettingsCat('backup')"><span class="mdi mdi-backup-restore"></span> Backup</div>
|
|
<div class="nav-tree-item" data-settings-cat="logs" onclick="navSettingsCat('logs')"><span class="mdi mdi-text-box-outline"></span> Logs</div>
|
|
<div class="nav-tree-item" data-settings-cat="about" onclick="navSettingsCat('about')"><span class="mdi mdi-information-outline"></span> About</div>
|
|
</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" tabindex="-1">
|
|
|
|
<!-- ── 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>
|
|
|
|
<!-- Voices workbench skeleton (list + inspector, matches real My Voices layout) -->
|
|
<div class="sk-workbench">
|
|
<div class="sk-list">
|
|
<div class="sk-list-row"><div class="sk sk-avatar"></div><div class="sk-list-body"><div class="sk" style="width:75%;height:13px"></div><div class="sk" style="width:50%;height:11px"></div></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div></div>
|
|
<div class="sk-list-row"><div class="sk sk-avatar"></div><div class="sk-list-body"><div class="sk" style="width:65%;height:13px"></div><div class="sk" style="width:40%;height:11px"></div></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div></div>
|
|
<div class="sk-list-row"><div class="sk sk-avatar"></div><div class="sk-list-body"><div class="sk" style="width:80%;height:13px"></div><div class="sk" style="width:45%;height:11px"></div></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div></div>
|
|
<div class="sk-list-row"><div class="sk sk-avatar"></div><div class="sk-list-body"><div class="sk" style="width:55%;height:13px"></div><div class="sk" style="width:35%;height:11px"></div></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div></div>
|
|
<div class="sk-list-row"><div class="sk sk-avatar"></div><div class="sk-list-body"><div class="sk" style="width:70%;height:13px"></div><div class="sk" style="width:48%;height:11px"></div></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div></div>
|
|
<div class="sk-list-row"><div class="sk sk-avatar"></div><div class="sk-list-body"><div class="sk" style="width:60%;height:13px"></div><div class="sk" style="width:42%;height:11px"></div></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div></div>
|
|
<div class="sk-list-row"><div class="sk sk-avatar"></div><div class="sk-list-body"><div class="sk" style="width:85%;height:13px"></div><div class="sk" style="width:38%;height:11px"></div></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div></div>
|
|
<div class="sk-list-row"><div class="sk sk-avatar"></div><div class="sk-list-body"><div class="sk" style="width:72%;height:13px"></div><div class="sk" style="width:44%;height:11px"></div></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div></div>
|
|
<div class="sk-list-row"><div class="sk sk-avatar"></div><div class="sk-list-body"><div class="sk" style="width:58%;height:13px"></div><div class="sk" style="width:36%;height:11px"></div></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div></div>
|
|
<div class="sk-list-row"><div class="sk sk-avatar"></div><div class="sk-list-body"><div class="sk" style="width:78%;height:13px"></div><div class="sk" style="width:52%;height:11px"></div></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div><div class="sk" style="width:28px;height:28px;border-radius:50%;flex-shrink:0"></div></div>
|
|
</div>
|
|
<div class="sk-insp">
|
|
<div class="sk-insp-head">
|
|
<div class="sk" style="width:55%;height:20px"></div>
|
|
<div class="sk" style="width:35%;height:13px"></div>
|
|
<div class="sk-row" style="margin-top:4px"><div class="sk" style="width:60px;height:22px;border-radius:4px"></div><div class="sk" style="width:50px;height:22px;border-radius:4px"></div><div class="sk" style="width:80px;height:22px;border-radius:4px;margin-left:auto"></div></div>
|
|
</div>
|
|
<div class="sk" style="height:1px;border-radius:0"></div>
|
|
<div class="sk-insp-head">
|
|
<div class="sk" style="width:40%;height:14px"></div>
|
|
<div class="sk" style="width:100%;height:56px;border-radius:6px"></div>
|
|
</div>
|
|
<div class="sk-insp-head">
|
|
<div class="sk" style="width:30%;height:14px"></div>
|
|
<div class="sk" style="width:100%;height:36px;border-radius:6px"></div>
|
|
</div>
|
|
<div class="sk-insp-head">
|
|
<div class="sk" style="width:45%;height:14px"></div>
|
|
<div class="sk" style="width:100%;height:36px;border-radius:6px"></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-rehearser" style="display:none"></section>
|
|
<section class="page-section" id="s-reader" style="display:none"></section>
|
|
<section class="page-section" id="s-conversation" style="display:none"></section>
|
|
<section class="page-section" id="s-library" style="display:none"></section>
|
|
</main>
|
|
|
|
</div><!-- /app-shell -->
|
|
|
|
<div id="toast" aria-live="polite" aria-atomic="true"></div>
|
|
<div id="status-bar" role="status" aria-live="polite" aria-atomic="false">
|
|
<span id="status-message">Loading…</span>
|
|
<span id="status-engines" aria-label="Active engine status"></span>
|
|
</div>
|
|
|
|
<!-- Global Directory Browser Modal -->
|
|
<div class="reh-modal-overlay" id="global-dir-browser-modal" hidden style="z-index:9999;">
|
|
<div class="reh-modal-box" style="width:500px; max-width:90vw; max-height:80vh;">
|
|
<div class="reh-modal-head">
|
|
<span>Browse Container Folders</span>
|
|
<button type="button" class="btn-icon" onclick="document.getElementById('global-dir-browser-modal').hidden = true;"><span class="mdi mdi-close"></span></button>
|
|
</div>
|
|
<div style="padding:16px; overflow-y:auto; flex:1;">
|
|
<div class="vef-dir-browser" style="margin-top:0; border:1px solid var(--border); border-radius:var(--radius); background:var(--bg-card); display:flex; flex-direction:column; min-height: 250px;">
|
|
<div class="vef-breadcrumb" id="global-dir-browser-crumb" style="padding:8px 12px; border-bottom:1px solid var(--border); background:var(--bg-sunken); overflow-x:auto; white-space:nowrap;"></div>
|
|
<div class="vef-dir-list" id="global-dir-browser-list" style="padding:8px; overflow-y:auto; flex:1; max-height:300px;"><span class="vef-loading" style="padding:8px; color:var(--text-muted);">Loading…</span></div>
|
|
</div>
|
|
<p class="note" style="margin-top:12px;">This browses the filesystem <strong>inside</strong> the Docker container, not your host computer. Ensure volumes are mapped correctly.</p>
|
|
</div>
|
|
<div style="padding:16px; border-top:1px solid var(--border); display:flex; gap:12px; align-items:center;">
|
|
<span class="note" id="global-dir-browser-path" style="flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:monospace;">/</span>
|
|
<button class="btn-secondary" type="button" onclick="document.getElementById('global-dir-browser-modal').hidden = true;">Cancel</button>
|
|
<button class="btn-primary" type="button" id="global-dir-browser-select">Select Folder</button>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
|
|
<!-- Early stub: nav tree toggles are wired inline in the HTML and can fire
|
|
before nav.js executes (slow / interrupted loads). This stub handles them
|
|
by reading/writing localStorage directly so nav.js picks up the state. -->
|
|
<script>
|
|
window.toggleNavTree = function(treeId, chevronId) {
|
|
try {
|
|
var state = JSON.parse(localStorage.getItem('ttsvc_trees') || '{}');
|
|
state[treeId] = !state[treeId];
|
|
localStorage.setItem('ttsvc_trees', JSON.stringify(state));
|
|
var tree = document.getElementById(treeId);
|
|
var chev = document.getElementById(chevronId);
|
|
if (tree) tree.classList.toggle('open', !!state[treeId]);
|
|
if (chev) chev.classList.toggle('open', !!state[treeId]);
|
|
} catch (_) {}
|
|
};
|
|
</script>
|
|
|
|
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
|
<script src="/static/loader.js?v=1.12.93"></script>
|
|
|
|
</body>
|
|
</html>
|