## Script Rehearser (new feature)
- New section s-rehearser.html + rehearser.js + nav/loader wiring
- Phase 1: paste/upload script (.txt), auto-detect characters from
'CHARACTER: dialog' or ALL-CAPS screenplay format
- Phase 2: assign a TTS voice per character, or mark 'I play this'
- Phase 3: step-through rehearsal — synthesizes other characters via TTS,
shows level-meter + oscilloscope for your own lines, records them from mic
- Phase 4: session summary with per-line audio playback + download
## Connect Apps
- Removed duplicate standalone MCP/speak/hotkey full-width cards
- Kept the integration-grid cards (they use the real server URL from JS)
- Added Global Hotkey Daemon as a proper integration card with snippet-hotkey
populated by integrations.js (uses proxyBase URL dynamically)
## About page
- GET /api/changelog endpoint reads CHANGELOG.md and returns it as text
- Collapsible 'Changelog' <details> card fetches and displays it lazily
## Try It Out
- Reorganised into three cards: Voice & backend / Text to synthesize / Generate
- Backend help panel moved below the voice row (not in the same flex row)
- Style instruction field gains a dynamic badge ('style-aware ✓' / 'weak style')
and a yellow warning when a non-style-aware backend is selected while the
field is filled — wired to both backend-select change and input events
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
215 lines
9.5 KiB
JavaScript
215 lines
9.5 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
const TAB_SECTION_MAP = {
|
|
library: 's-voices',
|
|
source: 's-clone',
|
|
save: 's-clone',
|
|
design: 's-design',
|
|
custom: 's-design',
|
|
getvoices: 's-studio',
|
|
generation: 's-tryout',
|
|
'stt-tts': 's-tryout',
|
|
performance: 's-performance',
|
|
routing: 's-routing',
|
|
integrations: 's-connect',
|
|
howto: 's-connect',
|
|
settings: 's-settings',
|
|
llms: 's-llms'
|
|
};
|
|
|
|
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'];
|
|
|
|
function runSideEffects(name) {
|
|
if ((name === 'source' || name === 'save') && typeof initCloneSampleText === 'function') initCloneSampleText();
|
|
if (name === 'library' && typeof loadVoiceLibrary === 'function') loadVoiceLibrary();
|
|
if ((name === 'integrations' || name === 'howto') && typeof renderIntegrationSnippets === 'function') {
|
|
if (typeof loadVoiceLibrary === 'function' && !(window._voices && window._voices.length)) loadVoiceLibrary();
|
|
renderIntegrationSnippets();
|
|
}
|
|
if (name === 'routing' && typeof loadRoutingTab === 'function') loadRoutingTab();
|
|
if (name === 'getvoices' && typeof loadGetVoices === 'function') loadGetVoices();
|
|
}
|
|
|
|
function applySettingsPage(cat) {
|
|
document.querySelectorAll('.s-settings-page').forEach(function (p) {
|
|
p.classList.toggle('is-active', p.dataset.page === cat);
|
|
});
|
|
document.querySelectorAll('[data-settings-cat]').forEach(function (el) {
|
|
el.classList.toggle('is-active', el.dataset.settingsCat === cat);
|
|
});
|
|
}
|
|
|
|
function showSection(sectionId) {
|
|
localStorage.setItem('ttsvc_section', sectionId);
|
|
SECTIONS.forEach(function (id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.classList.toggle('is-active', id === sectionId);
|
|
});
|
|
document.querySelectorAll('[data-nav-section]').forEach(function (item) {
|
|
item.classList.toggle('active', item.dataset.navSection === sectionId);
|
|
});
|
|
|
|
// Voices nav tree
|
|
var voicesTree = document.getElementById('nav-voices-tree');
|
|
var voicesChevron = document.getElementById('nav-voices-chevron');
|
|
if (voicesTree) voicesTree.classList.toggle('open', sectionId === 's-voices');
|
|
if (voicesChevron) voicesChevron.classList.toggle('open', sectionId === 's-voices');
|
|
|
|
// Settings nav tree
|
|
var settingsTree = document.getElementById('nav-settings-tree');
|
|
var settingsChevron = document.getElementById('nav-settings-chevron');
|
|
if (settingsTree) settingsTree.classList.toggle('open', sectionId === 's-settings');
|
|
if (settingsChevron) settingsChevron.classList.toggle('open', sectionId === 's-settings');
|
|
|
|
// Engines nav tree
|
|
var enginesTree = document.getElementById('nav-engines-tree');
|
|
var enginesChevron = document.getElementById('nav-engines-chevron');
|
|
if (enginesTree) enginesTree.classList.toggle('open', sectionId === 's-llms');
|
|
if (enginesChevron) enginesChevron.classList.toggle('open', sectionId === 's-llms');
|
|
|
|
// When entering settings, show the active sub-page (default: connections)
|
|
if (sectionId === 's-settings') {
|
|
applySettingsPage(window._settingsSidebarCat || 'connections');
|
|
}
|
|
|
|
// When entering engines, show the active sub-page (default: llm)
|
|
if (sectionId === 's-llms') {
|
|
applyEnginesPage(window._enginesSidebarCat || 'llm');
|
|
}
|
|
|
|
var main = document.getElementById('main-content');
|
|
if (main) main.scrollTop = 0;
|
|
}
|
|
|
|
window.navTo = function (sectionId) {
|
|
showSection(sectionId);
|
|
var tabName = Object.keys(TAB_SECTION_MAP).find(function (k) {
|
|
return TAB_SECTION_MAP[k] === sectionId;
|
|
});
|
|
if (tabName) runSideEffects(tabName);
|
|
};
|
|
|
|
window.switchTab = function (name) {
|
|
var sectionId = TAB_SECTION_MAP[name];
|
|
if (sectionId) showSection(sectionId);
|
|
runSideEffects(name);
|
|
return true;
|
|
};
|
|
|
|
// ── Voice tree sub-navigation ─────────────────────────────────────────────
|
|
window._voiceSidebarCat = 'all';
|
|
|
|
window.navVoicesCat = function (cat) {
|
|
navTo('s-voices');
|
|
window._voiceSidebarCat = cat;
|
|
document.querySelectorAll('[data-voice-cat]').forEach(function (el) {
|
|
el.classList.toggle('is-active', el.dataset.voiceCat === cat);
|
|
});
|
|
if (typeof renderVoiceList === 'function') renderVoiceList();
|
|
};
|
|
|
|
window.updateVoiceTree = function (voices) {
|
|
if (!voices) return;
|
|
function n(fn) { return voices.filter(fn).length; }
|
|
function set(id, v) { var el = document.getElementById(id); if (el) el.textContent = v || ''; }
|
|
set('ntc-all', n(function () { return true; }));
|
|
set('ntc-cloned', n(function (v) { return v.has_ref; }));
|
|
set('ntc-designed', n(function (v) { return !v.has_ref; }));
|
|
set('ntc-favorites',n(function (v) { return (v.rating || 0) >= 4; }));
|
|
set('ntc-hidden', n(function (v) { return v.enabled === false; }));
|
|
set('nav-voices-count', voices.length);
|
|
};
|
|
|
|
// ── Settings tree sub-navigation ──────────────────────────────────────────
|
|
window._settingsSidebarCat = 'connections';
|
|
|
|
window.navSettingsCat = function (cat) {
|
|
window._settingsSidebarCat = cat;
|
|
localStorage.setItem('ttsvc_settings_cat', cat);
|
|
navTo('s-settings'); // showSection will call applySettingsPage(cat)
|
|
if (cat === 'logs' && typeof loadSettingsLogs === 'function') loadSettingsLogs();
|
|
if (cat === 'about' && typeof renderSettingsAbout === 'function') renderSettingsAbout();
|
|
};
|
|
|
|
// ── Engines tree sub-navigation ───────────────────────────────────────────
|
|
function applyEnginesPage(cat) {
|
|
document.querySelectorAll('.s-engines-page').forEach(function (p) {
|
|
p.classList.toggle('is-active', p.dataset.page === cat);
|
|
});
|
|
document.querySelectorAll('[data-engines-cat]').forEach(function (el) {
|
|
el.classList.toggle('is-active', el.dataset.enginesCat === cat);
|
|
});
|
|
}
|
|
|
|
window._enginesSidebarCat = 'llm';
|
|
|
|
window.navEnginesCat = function (cat) {
|
|
window._enginesSidebarCat = cat;
|
|
localStorage.setItem('ttsvc_engines_cat', cat);
|
|
navTo('s-llms'); // showSection will call applyEnginesPage(cat)
|
|
if ((cat === 'tts' || cat === 'stt') && typeof loadLocalContainers === 'function') loadLocalContainers();
|
|
};
|
|
|
|
// Restore last-used section from localStorage (fallback: My Voices)
|
|
var _savedSection = localStorage.getItem('ttsvc_section');
|
|
window._settingsSidebarCat = localStorage.getItem('ttsvc_settings_cat') || 'connections';
|
|
window._enginesSidebarCat = localStorage.getItem('ttsvc_engines_cat') || 'llm';
|
|
var _startSection = (_savedSection && SECTIONS.includes(_savedSection)) ? _savedSection : 's-voices';
|
|
showSection(_startSection);
|
|
var _startTab = Object.keys(TAB_SECTION_MAP).find(function (k) { return TAB_SECTION_MAP[k] === _startSection; }) || 'library';
|
|
runSideEffects(_startTab);
|
|
|
|
// ── Mobile sidebar drawer ────────────────────────────────────────────────
|
|
var sidebar = document.getElementById('sidebar');
|
|
var backdrop = document.getElementById('sidebar-backdrop');
|
|
var toggleBtn = document.getElementById('sidebar-toggle');
|
|
|
|
function openSidebar() {
|
|
if (!sidebar) return;
|
|
sidebar.classList.add('open');
|
|
if (backdrop) backdrop.classList.add('open');
|
|
document.body.style.overflow = 'hidden'; // prevent scroll behind drawer
|
|
}
|
|
function closeSidebar() {
|
|
if (!sidebar) return;
|
|
sidebar.classList.remove('open');
|
|
if (backdrop) backdrop.classList.remove('open');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
if (toggleBtn) toggleBtn.addEventListener('click', openSidebar);
|
|
if (backdrop) backdrop.addEventListener('click', closeSidebar);
|
|
|
|
// Close drawer when a nav item is tapped on mobile
|
|
document.querySelectorAll('#sidebar .nav-item, #sidebar .nav-tree-item').forEach(function (el) {
|
|
el.addEventListener('click', function () {
|
|
if (window.innerWidth <= 767) closeSidebar();
|
|
});
|
|
});
|
|
|
|
// ── Mobile voice inspector: back button ──────────────────────────────────
|
|
// When a voice row is selected on mobile, show inspector full-height.
|
|
// The inspector JS will call window.onMobileInspectorOpen/Close as hooks.
|
|
window.onMobileInspectorOpen = function () {
|
|
var wb = document.querySelector('.voices-workbench');
|
|
if (!wb || window.innerWidth > 767) return;
|
|
wb.classList.add('mobile-inspector-open');
|
|
// Inject back button if not already there
|
|
var insp = document.getElementById('voices-inspector');
|
|
if (insp && !insp.querySelector('.insp-mobile-back')) {
|
|
var back = document.createElement('div');
|
|
back.className = 'insp-mobile-back';
|
|
back.innerHTML = '<span class="mdi mdi-arrow-left"></span> All voices';
|
|
back.addEventListener('click', function () { window.onMobileInspectorClose && window.onMobileInspectorClose(); });
|
|
insp.insertBefore(back, insp.firstChild);
|
|
}
|
|
};
|
|
window.onMobileInspectorClose = function () {
|
|
var wb = document.querySelector('.voices-workbench');
|
|
if (wb) wb.classList.remove('mobile-inspector-open');
|
|
var back = document.querySelector('.insp-mobile-back');
|
|
if (back) back.remove();
|
|
};
|
|
})();
|