tts-voice-creator-clone-and.../static/index.html
mARTin-B78 d1969b7609 Fix quality-run roster display, blank API errors, resolver-first (v1.12.94)
The recast/quality view's sidebar rebuilt its roster from only the
lines being checked, appearing to wipe every named character (the cast
itself was safe: named lines are never recast targets and increases in
Unknowns already roll back). The sidebar now seeds from the full cast
and refreshes during the run. Error notes fall back to the HTTP status
(statusText is empty on HTTP/2). The deterministic grammar resolver
runs before any LLM call in quality runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:17:59 +02:00

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.94">
<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.94">
<!-- ── 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 &middot; Design &middot; 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.94"></script>
</body>
</html>