tts-voice-creator-clone-and.../static/index.html
mARTin-B78 4f795e820c feat: noise gate, PDF search, sidebar active highlight, stats collapse, tooltip + dialogue fixes
- Conversation: adjustable noise gate slider (RMS threshold + 300 ms
  minimum burst duration) prevents short noise spikes from triggering
  STT; level meter shows gate position as a blue marker
- Conversation stats panel now collapsible (chevron button) to free
  chat width; floating expand button restores it; state persists
- Read Aloud: text search input in PDF toolbar (Enter = next hit,
  Shift+Enter = previous, Esc = clear)
- Sidebar: tooltip now works for all item types including sub-items
  that had no .nav-label span (text extracted by stripping icon/badge)
- Sidebar active section indicator added (.nav-tree-item.active was
  previously unstyled — active section now has bg + right accent bar)
- Casting audiobook: prompt instructs LLM to handle ?« / !« endings
  and unclosed » at passage end as dialogue; deterministic fallback
  also handles unclosed opening quote

Bumps to v1.12.2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 16:48:48 +02:00

343 lines
26 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">
<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.2">
<!-- ── 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-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-item" data-nav-section="s-reader" onclick="navTo('s-reader')"><span class="mdi mdi-book-open-page-variant-outline"></span> Read Aloud</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="true">Loading…</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>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.12.2"></script>
</body>
</html>