Cast: card/list views, sort & filter, online voice picker, "Hear a line" sample button, AI character notes, import auto-save. Platform: WCAG 2.1 AA accessibility pass; German UI translation + language picker; installable PWA with offline shell; GZip + content-visibility virtualization + lazy images + Rehearser PCM memory cap (mobile stability); Playwright suite (desktop + iPhone); opt-in minified bundle build. Fixes: screenplay parser false characters; Fish-Speech inline-tag tones; narrator/voice pickers list full library; clone GUI rework; fish.audio import dedup; voice-ID rename; bulk-delete modal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
152 lines
6.2 KiB
JavaScript
152 lines
6.2 KiB
JavaScript
(async function () {
|
|
'use strict';
|
|
|
|
const SECTIONS = [
|
|
's-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-rehearser',
|
|
's-performance', 's-routing', 's-connect', 's-settings',
|
|
's-llms', 's-conversation',
|
|
];
|
|
|
|
// Determine which section to show first (same logic as nav.js)
|
|
var _savedSection = localStorage.getItem('ttsvc_section');
|
|
var _activeSection = (_savedSection && SECTIONS.indexOf(_savedSection) !== -1)
|
|
? _savedSection : 's-voices';
|
|
|
|
// ── Fetch app version once for JS cache-busting ──────────────────────────
|
|
var _appVersion = 'dev';
|
|
try {
|
|
var _vr = await fetch('/api/version');
|
|
var _vd = await _vr.json();
|
|
_appVersion = _vd.version || _appVersion;
|
|
} catch (_) {}
|
|
|
|
var _ts = Date.now(); // sections always fresh during a session
|
|
// Append session timestamp so that a version bump + container restart
|
|
// always invalidates previously cached JS, even on the same version string.
|
|
_appVersion = _appVersion + '-' + _ts;
|
|
|
|
function _inject(id, html) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.innerHTML = html;
|
|
}
|
|
|
|
// ── 1. Load ONLY the active section before anything else ─────────────────
|
|
// Feature modules set up DOM event listeners — the active section's elements
|
|
// must exist when JS runs. Other sections are loaded in step 2.
|
|
try {
|
|
var _ar = await fetch('/static/sections/' + _activeSection + '.html?v=' + _ts);
|
|
if (!_ar.ok) throw new Error(_ar.status + ' ' + _ar.statusText);
|
|
_inject(_activeSection, await _ar.text());
|
|
} catch (e) {
|
|
var _ael = document.getElementById(_activeSection);
|
|
if (_ael) _ael.innerHTML =
|
|
'<p style="color:var(--red);padding:24px">Failed to load section <b>' +
|
|
_activeSection + '</b>: ' + e.message + '</p>';
|
|
}
|
|
|
|
// ── 2. Fetch remaining sections in background (parallel with JS loading) ──
|
|
// These run concurrently with utils + settings below. Because section HTML
|
|
// files are much smaller than JS, they finish before the feature-module batch
|
|
// needs them.
|
|
var _bgSections = Promise.all(
|
|
SECTIONS.filter(function (id) { return id !== _activeSection; })
|
|
.map(async function (id) {
|
|
try {
|
|
var res = await fetch('/static/sections/' + id + '.html?v=' + _ts);
|
|
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
|
_inject(id, await res.text());
|
|
} catch (e) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.innerHTML =
|
|
'<p style="color:var(--red);padding:24px">Failed to load section <b>' +
|
|
id + '</b>: ' + e.message + '</p>';
|
|
}
|
|
})
|
|
);
|
|
|
|
// ── 3. Load JS in parallel batches ────────────────────────────────────────
|
|
// Scripts with async=false are FETCHED in parallel but EXECUTED in DOM
|
|
// insertion order — safe for modules that share a global scope.
|
|
function _load(src) {
|
|
return new Promise(function (resolve, reject) {
|
|
var s = document.createElement('script');
|
|
s.src = src + '?v=' + _appVersion;
|
|
s.async = false; // fetch in parallel, execute in DOM order
|
|
s.onload = resolve;
|
|
s.onerror = function () { reject(new Error('Failed to load ' + src)); };
|
|
document.body.appendChild(s);
|
|
});
|
|
}
|
|
|
|
function _loadBatch(srcs) {
|
|
return Promise.all(srcs.map(_load));
|
|
}
|
|
|
|
// A — foundation (sequential: each must finish before the next batch starts)
|
|
await _load('/static/js/utils.js');
|
|
await _load('/static/js/i18n.js'); // defines window.t / applyI18n before features run
|
|
await _load('/static/js/settings.js');
|
|
|
|
// Wait for background sections BEFORE feature modules run.
|
|
// Feature modules set up event listeners on DOM elements; all section HTML
|
|
// must already be injected. On a local server the sections finish well before
|
|
// settings.js completes, so this await is typically instant.
|
|
await _bgSections;
|
|
|
|
// C — feature modules (executed in order; share a global scope).
|
|
// Opt-in single bundle (npm run minify) keeps these as one minified request;
|
|
// default loads them individually so editing static/js/* is live without a build.
|
|
var _featureFiles = [
|
|
'/static/js/voice-picker.js',
|
|
'/static/js/voice-inspector.js',
|
|
'/static/js/voice-sources.js',
|
|
'/static/js/fishaudio-browser.js',
|
|
'/static/js/integrations.js',
|
|
'/static/js/routing.js',
|
|
'/static/js/voice-clone.js',
|
|
'/static/js/voice-library.js',
|
|
'/static/js/tts-preview.js',
|
|
'/static/js/benchmark.js',
|
|
'/static/js/stt.js',
|
|
'/static/js/rehearser-parse.js',
|
|
'/static/js/rehearser.js',
|
|
];
|
|
var _useBundle = window.APP_USE_BUNDLE === true || location.search.indexOf('bundle=1') !== -1;
|
|
var _bundleOk = false;
|
|
if (_useBundle) {
|
|
try { await _load('/static/dist/main.min.js'); _bundleOk = true; }
|
|
catch (_) { _bundleOk = false; } // not built / missing → fall back to per-file
|
|
}
|
|
if (!_bundleOk) await _loadBatch(_featureFiles);
|
|
|
|
// D — init (needs everything above to be defined)
|
|
await _load('/static/js/init.js');
|
|
|
|
// F — navigation (must run before revealing UI so the correct section shows)
|
|
await _load('/static/nav.js');
|
|
|
|
// ── Remove skeleton, reveal sections — as soon as nav is ready ──────────
|
|
var sk = document.getElementById('app-skeleton');
|
|
if (sk) {
|
|
sk.classList.add('sk-hidden');
|
|
setTimeout(function () { if (sk.parentNode) sk.parentNode.removeChild(sk); }, 300);
|
|
}
|
|
document.querySelectorAll('.page-section').forEach(function (s) {
|
|
s.style.display = '';
|
|
});
|
|
|
|
// ── i18n: add the language picker and translate the (now fully-rendered) UI ──
|
|
if (window.initLangPicker) window.initLangPicker();
|
|
if (window.applyI18n) window.applyI18n(document);
|
|
|
|
// E — post-init modules load in background AFTER the UI is already visible.
|
|
// Engines, AI backends, generation, and conversation sections load while
|
|
// the user browses Voices / Clone / Design, not before.
|
|
_loadBatch([
|
|
'/static/js/engines.js',
|
|
'/static/js/ai-backends.js',
|
|
'/static/js/generation.js',
|
|
'/static/js/conversation.js',
|
|
]);
|
|
})();
|