- Add TTSService.js for text-to-speech functionality - Add comprehensive deployment documentation (guides, checklists, diagnostics) - Add new SBS content (chapters 8 & 9) - Refactor 14 game modules for better maintainability (-947 lines) - Enhance SettingsDebug.js with improved debugging capabilities - Update configuration files and startup scripts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1022 lines
37 KiB
JavaScript
1022 lines
37 KiB
JavaScript
import Module from '../core/Module.js';
|
|
import ttsService from '../services/TTSService.js';
|
|
|
|
class SettingsDebug extends Module {
|
|
constructor(name, dependencies, config) {
|
|
super(name, ['eventBus', 'router']);
|
|
|
|
if (!dependencies.eventBus || !dependencies.router) {
|
|
throw new Error('SettingsDebug requires EventBus and Router dependencies');
|
|
}
|
|
|
|
this._eventBus = dependencies.eventBus;
|
|
this._router = dependencies.router;
|
|
this._config = config || {};
|
|
|
|
// Internal state
|
|
this._container = null;
|
|
this._debugMessages = [];
|
|
this._availableVoices = [];
|
|
this._ttsSettings = {
|
|
rate: 0.85,
|
|
pitch: 1.0,
|
|
volume: 1.0,
|
|
voicesByLanguage: {} // e.g., { 'en-US': 'Google US English', 'zh-CN': 'Google 普通话' }
|
|
};
|
|
|
|
Object.seal(this);
|
|
}
|
|
|
|
async init() {
|
|
this._validateNotDestroyed();
|
|
|
|
this._loadTTSSettings();
|
|
this._injectCSS();
|
|
this._setupEventListeners();
|
|
this._exposePublicAPI();
|
|
|
|
this._setInitialized();
|
|
}
|
|
|
|
async destroy() {
|
|
this._validateNotDestroyed();
|
|
|
|
if (this._container) {
|
|
this._container.innerHTML = '';
|
|
}
|
|
|
|
this._removeInjectedCSS();
|
|
this._eventBus.off('settings:show', this._handleShowSettings.bind(this), this.name);
|
|
|
|
this._setDestroyed();
|
|
}
|
|
|
|
// Public API
|
|
show(container) {
|
|
this._validateInitialized();
|
|
|
|
this._container = container;
|
|
this._render();
|
|
this._loadVoices();
|
|
this._updateBrowserInfo();
|
|
this._addDebugMessage('Settings/Debug panel opened', 'info');
|
|
}
|
|
|
|
hide() {
|
|
this._validateInitialized();
|
|
|
|
if (this._container) {
|
|
this._container.innerHTML = '';
|
|
this._container = null;
|
|
}
|
|
}
|
|
|
|
// Private methods
|
|
_setupEventListeners() {
|
|
this._eventBus.on('settings:show', this._handleShowSettings.bind(this), this.name);
|
|
this._eventBus.on('router:navigate', this._handleNavigation.bind(this), this.name);
|
|
this._eventBus.on('navigation:settings', this._handleNavigationSettings.bind(this), this.name);
|
|
}
|
|
|
|
_handleShowSettings(event) {
|
|
if (event.data && event.data.container) {
|
|
this.show(event.data.container);
|
|
}
|
|
}
|
|
|
|
_handleNavigation(event) {
|
|
if (event.data && event.data.path !== '/settings') {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
_handleNavigationSettings(event) {
|
|
// Find the main container or create one
|
|
let container = document.getElementById('main-content');
|
|
if (!container) {
|
|
container = document.querySelector('main') || document.body;
|
|
}
|
|
|
|
this.show(container);
|
|
}
|
|
|
|
_loadTTSSettings() {
|
|
try {
|
|
// Load settings from TTSService
|
|
const defaults = ttsService.getDefaults();
|
|
this._ttsSettings.rate = defaults.rate;
|
|
this._ttsSettings.volume = defaults.volume;
|
|
this._ttsSettings.pitch = defaults.pitch;
|
|
|
|
// Also try to load from localStorage for voicesByLanguage
|
|
const saved = localStorage.getItem('tts-settings');
|
|
if (saved) {
|
|
const savedSettings = JSON.parse(saved);
|
|
if (savedSettings.voicesByLanguage) {
|
|
this._ttsSettings.voicesByLanguage = savedSettings.voicesByLanguage;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this._addDebugMessage(`Failed to load TTS settings: ${e.message}`, 'warning');
|
|
}
|
|
}
|
|
|
|
_saveTTSSettings() {
|
|
try {
|
|
// Apply settings to TTSService
|
|
ttsService.setDefaults({
|
|
rate: this._ttsSettings.rate,
|
|
volume: this._ttsSettings.volume,
|
|
pitch: this._ttsSettings.pitch
|
|
});
|
|
|
|
// Apply preferred voices to TTSService
|
|
ttsService.setPreferredVoices(this._ttsSettings.voicesByLanguage);
|
|
|
|
// Save to localStorage
|
|
localStorage.setItem('tts-settings', JSON.stringify(this._ttsSettings));
|
|
this._addDebugMessage('TTS settings saved and applied globally', 'success');
|
|
} catch (e) {
|
|
this._addDebugMessage(`Failed to save TTS settings: ${e.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
_injectCSS() {
|
|
if (document.getElementById('settings-debug-styles')) return;
|
|
|
|
const styleSheet = document.createElement('style');
|
|
styleSheet.id = 'settings-debug-styles';
|
|
styleSheet.textContent = `
|
|
.settings-container {
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 30px;
|
|
}
|
|
|
|
.settings-section {
|
|
background: var(--card-background, #fff);
|
|
border-radius: 12px;
|
|
padding: 25px;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
border: 1px solid var(--border-color, #e5e7eb);
|
|
}
|
|
|
|
.settings-section h3 {
|
|
font-size: 1.4em;
|
|
margin-bottom: 20px;
|
|
color: var(--text-primary, #111827);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.setting-group {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 15px;
|
|
padding: 12px 0;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.setting-group:last-child {
|
|
border-bottom: none;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.setting-group label {
|
|
font-weight: 500;
|
|
color: var(--text-primary, #111827);
|
|
min-width: 140px;
|
|
}
|
|
|
|
.setting-group input[type="range"] {
|
|
flex: 1;
|
|
margin: 0 15px;
|
|
accent-color: var(--primary-color, #3b82f6);
|
|
}
|
|
|
|
.setting-group select {
|
|
min-width: 200px;
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--border-color, #e5e7eb);
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
background: white;
|
|
}
|
|
|
|
.setting-group span {
|
|
min-width: 40px;
|
|
text-align: center;
|
|
font-weight: 600;
|
|
color: var(--primary-color, #3b82f6);
|
|
}
|
|
|
|
.debug-info {
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.info-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
|
|
.info-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.info-item .label {
|
|
font-weight: 500;
|
|
color: var(--text-secondary, #6b7280);
|
|
}
|
|
|
|
.info-item .value {
|
|
font-weight: 600;
|
|
color: var(--text-primary, #111827);
|
|
}
|
|
|
|
.info-item .value.small {
|
|
font-size: 0.85em;
|
|
max-width: 400px;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.debug-controls {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.debug-btn {
|
|
padding: 12px 16px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: var(--primary-color, #3b82f6);
|
|
color: white;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.debug-btn:hover {
|
|
background: #2563eb;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.debug-btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.debug-output {
|
|
background: #1a1a1a;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.debug-output h4 {
|
|
color: #ffffff;
|
|
margin-bottom: 10px;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.debug-log {
|
|
min-height: 120px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
background: #000000;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.debug-log:empty::before {
|
|
content: "No debug output yet. Click test buttons above.";
|
|
color: #888;
|
|
font-style: italic;
|
|
}
|
|
|
|
.clear-btn {
|
|
padding: 6px 12px;
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
background: #333;
|
|
color: #fff;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.clear-btn:hover {
|
|
background: #444;
|
|
border-color: #666;
|
|
}
|
|
|
|
.voice-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 12px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.voice-item {
|
|
background: #f8f9fa;
|
|
border: 1px solid var(--border-color, #e5e7eb);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
transition: all 0.3s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.voice-item:hover {
|
|
background: #e9ecef;
|
|
border-color: var(--primary-color, #3b82f6);
|
|
}
|
|
|
|
.voice-item.selected {
|
|
background: var(--primary-light, #dbeafe);
|
|
border-color: var(--primary-color, #3b82f6);
|
|
}
|
|
|
|
.voice-name {
|
|
font-weight: 600;
|
|
color: var(--text-primary, #111827);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.voice-lang {
|
|
font-size: 0.9em;
|
|
color: var(--text-secondary, #6b7280);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.voice-type {
|
|
font-size: 0.8em;
|
|
color: var(--accent-color, #f59e0b);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.debug-log .success { color: #4ade80; }
|
|
.debug-log .error { color: #f87171; }
|
|
.debug-log .warning { color: #fbbf24; }
|
|
.debug-log .info { color: #60a5fa; }
|
|
|
|
@media (max-width: 768px) {
|
|
.settings-container {
|
|
padding: 15px;
|
|
gap: 20px;
|
|
}
|
|
|
|
.setting-group {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
}
|
|
|
|
.setting-group input[type="range"] {
|
|
width: 100%;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.debug-controls {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.voice-list {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
`;
|
|
document.head.appendChild(styleSheet);
|
|
}
|
|
|
|
_removeInjectedCSS() {
|
|
const styleSheet = document.getElementById('settings-debug-styles');
|
|
if (styleSheet) {
|
|
styleSheet.remove();
|
|
}
|
|
}
|
|
|
|
_render() {
|
|
if (!this._container) return;
|
|
|
|
this._container.innerHTML = `
|
|
<div class="settings-container">
|
|
<!-- Navigation Button -->
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
|
<button class="debug-btn" style="background: #6b7280; width: auto; padding: 10px 20px;" onclick="window.app.getCore().router.navigate('/')">
|
|
🏠 Return to Home
|
|
</button>
|
|
<h2 style="margin: 0; color: var(--text-primary, #111827);">Settings & Debug</h2>
|
|
</div>
|
|
|
|
<!-- System Information -->
|
|
<div class="settings-section">
|
|
<h3>🔧 System Information</h3>
|
|
<div class="debug-info">
|
|
<div class="info-item">
|
|
<span class="label">Application Status:</span>
|
|
<span class="value" id="app-status">Running</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="label">Modules Loaded:</span>
|
|
<span class="value" id="modules-count">0</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="label">EventBus Status:</span>
|
|
<span class="value" id="eventbus-status">Active</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="label">Current Route:</span>
|
|
<span class="value" id="current-route">/settings</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="label">Browser Support:</span>
|
|
<span class="value" id="browser-support">Checking...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TTS Settings -->
|
|
<div class="settings-section">
|
|
<h3>🔊 Text-to-Speech Settings <small style="font-size: 0.7em; color: #888; font-weight: normal;">(Applied globally to all games)</small></h3>
|
|
<div class="setting-group">
|
|
<label>Speech Rate:</label>
|
|
<input type="range" id="tts-rate" min="0.1" max="2" step="0.1" value="${this._ttsSettings.rate}">
|
|
<span id="tts-rate-value">${this._ttsSettings.rate}</span>
|
|
</div>
|
|
<div class="setting-group">
|
|
<label>Pitch:</label>
|
|
<input type="range" id="tts-pitch" min="0" max="2" step="0.1" value="${this._ttsSettings.pitch}">
|
|
<span id="tts-pitch-value">${this._ttsSettings.pitch}</span>
|
|
</div>
|
|
<div class="setting-group">
|
|
<label>Volume:</label>
|
|
<input type="range" id="tts-volume" min="0" max="1" step="0.1" value="${this._ttsSettings.volume}">
|
|
<span id="tts-volume-value">${this._ttsSettings.volume}</span>
|
|
</div>
|
|
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
|
<button class="debug-btn" style="flex: 1;" onclick="window.settingsDebug.testCurrentSettings()">
|
|
🔊 Test Current Settings
|
|
</button>
|
|
<button class="debug-btn" style="flex: 1; background: #6b7280;" onclick="window.settingsDebug.resetToDefaults()">
|
|
🔄 Reset to Defaults
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Voice Selection by Language -->
|
|
<div class="settings-section">
|
|
<h3>🎤 Voice Selection by Language</h3>
|
|
<div id="voice-language-selectors"></div>
|
|
</div>
|
|
|
|
<!-- Voice Information -->
|
|
<div class="settings-section">
|
|
<h3>🎤 Voice Information</h3>
|
|
<div class="debug-info">
|
|
<div class="info-item">
|
|
<span class="label">Total Voices:</span>
|
|
<span class="value" id="voice-count">0</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="label">English Voices:</span>
|
|
<span class="value" id="english-voice-count">0</span>
|
|
</div>
|
|
</div>
|
|
<div class="voice-list" id="voice-list"></div>
|
|
</div>
|
|
|
|
<!-- Debug Controls -->
|
|
<div class="settings-section">
|
|
<h3>🧪 Debug Controls</h3>
|
|
<div class="debug-controls">
|
|
<button class="debug-btn" onclick="window.settingsDebug.testBasicTTS()">
|
|
🔊 Test Basic TTS
|
|
</button>
|
|
<button class="debug-btn" onclick="window.settingsDebug.testGameWords()">
|
|
📝 Test Game Words
|
|
</button>
|
|
<button class="debug-btn" onclick="window.settingsDebug.refreshVoices()">
|
|
🔄 Refresh Voices
|
|
</button>
|
|
<button class="debug-btn" onclick="window.settingsDebug.testSystem()">
|
|
⚙️ Test System
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Debug Output -->
|
|
<div class="settings-section">
|
|
<div class="debug-output">
|
|
<h4>Debug Log</h4>
|
|
<div class="debug-log" id="debug-log"></div>
|
|
<button class="clear-btn" onclick="window.settingsDebug.clearDebugLog()">Clear Log</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Browser Information -->
|
|
<div class="settings-section">
|
|
<h3>🌐 Browser Information</h3>
|
|
<div class="debug-info">
|
|
<div class="info-item">
|
|
<span class="label">User Agent:</span>
|
|
<span class="value small" id="user-agent"></span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="label">Platform:</span>
|
|
<span class="value" id="platform"></span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="label">Language:</span>
|
|
<span class="value" id="browser-language"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this._setupControlListeners();
|
|
this._updateSystemInfo();
|
|
}
|
|
|
|
_setupControlListeners() {
|
|
// TTS Rate slider
|
|
const rateSlider = document.getElementById('tts-rate');
|
|
if (rateSlider) {
|
|
rateSlider.addEventListener('input', (e) => {
|
|
this._ttsSettings.rate = parseFloat(e.target.value);
|
|
document.getElementById('tts-rate-value').textContent = this._ttsSettings.rate;
|
|
this._saveTTSSettings();
|
|
});
|
|
}
|
|
|
|
// Pitch slider
|
|
const pitchSlider = document.getElementById('tts-pitch');
|
|
if (pitchSlider) {
|
|
pitchSlider.addEventListener('input', (e) => {
|
|
this._ttsSettings.pitch = parseFloat(e.target.value);
|
|
document.getElementById('tts-pitch-value').textContent = this._ttsSettings.pitch;
|
|
this._saveTTSSettings();
|
|
});
|
|
}
|
|
|
|
// Volume slider
|
|
const volumeSlider = document.getElementById('tts-volume');
|
|
if (volumeSlider) {
|
|
volumeSlider.addEventListener('input', (e) => {
|
|
this._ttsSettings.volume = parseFloat(e.target.value);
|
|
document.getElementById('tts-volume-value').textContent = this._ttsSettings.volume;
|
|
this._saveTTSSettings();
|
|
});
|
|
}
|
|
|
|
// Voice selection
|
|
const voiceSelect = document.getElementById('tts-voice');
|
|
if (voiceSelect) {
|
|
voiceSelect.addEventListener('change', (e) => {
|
|
this._ttsSettings.selectedVoice = e.target.value;
|
|
this._saveTTSSettings();
|
|
});
|
|
}
|
|
}
|
|
|
|
_updateSystemInfo() {
|
|
// Get application instance from global
|
|
if (window.app) {
|
|
const status = window.app.getStatus();
|
|
const moduleLoader = window.app.getCore()?.moduleLoader;
|
|
|
|
if (status) {
|
|
document.getElementById('app-status').textContent = status.status;
|
|
}
|
|
|
|
if (moduleLoader) {
|
|
const moduleStatus = moduleLoader.getStatus();
|
|
document.getElementById('modules-count').textContent = moduleStatus?.loaded?.length || 0;
|
|
}
|
|
}
|
|
|
|
// Current route
|
|
document.getElementById('current-route').textContent = window.location.pathname || '/';
|
|
|
|
// EventBus status
|
|
const eventBusStatus = this._eventBus ? 'Active' : 'Inactive';
|
|
document.getElementById('eventbus-status').textContent = eventBusStatus;
|
|
}
|
|
|
|
_updateBrowserInfo() {
|
|
const elements = {
|
|
'user-agent': navigator.userAgent,
|
|
'platform': navigator.platform,
|
|
'browser-language': navigator.language
|
|
};
|
|
|
|
Object.entries(elements).forEach(([id, value]) => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.textContent = value;
|
|
}
|
|
});
|
|
|
|
this._checkBrowserSupport();
|
|
}
|
|
|
|
_checkBrowserSupport() {
|
|
const checks = [
|
|
{ name: 'speechSynthesis', available: 'speechSynthesis' in window },
|
|
{ name: 'SpeechSynthesisUtterance', available: 'SpeechSynthesisUtterance' in window },
|
|
{ name: 'getVoices', available: speechSynthesis && typeof speechSynthesis.getVoices === 'function' },
|
|
{ name: 'speak', available: speechSynthesis && typeof speechSynthesis.speak === 'function' }
|
|
];
|
|
|
|
const support = checks.every(check => check.available);
|
|
const supportElement = document.getElementById('browser-support');
|
|
if (supportElement) {
|
|
supportElement.textContent = support ? '✅ Full Support' : '❌ Limited Support';
|
|
supportElement.style.color = support ? '#22C55E' : '#EF4444';
|
|
}
|
|
|
|
this._addDebugMessage(`Browser TTS Support: ${support ? 'Full' : 'Limited'}`, support ? 'success' : 'warning');
|
|
return support;
|
|
}
|
|
|
|
async _loadVoices() {
|
|
try {
|
|
// Use TTSService to get voices
|
|
this._availableVoices = await ttsService.getVoices();
|
|
this._updateVoiceInfo();
|
|
this._populateVoiceByLanguageSelectors();
|
|
this._displayVoiceList();
|
|
} catch (error) {
|
|
this._addDebugMessage(`Failed to load voices: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
_updateVoiceInfo() {
|
|
const voiceCountElement = document.getElementById('voice-count');
|
|
const englishVoiceCountElement = document.getElementById('english-voice-count');
|
|
|
|
if (voiceCountElement) {
|
|
voiceCountElement.textContent = this._availableVoices.length;
|
|
}
|
|
|
|
const englishVoices = this._availableVoices.filter(voice => voice.lang.startsWith('en'));
|
|
if (englishVoiceCountElement) {
|
|
englishVoiceCountElement.textContent = englishVoices.length;
|
|
}
|
|
}
|
|
|
|
_populateVoiceByLanguageSelectors() {
|
|
const container = document.getElementById('voice-language-selectors');
|
|
if (!container) return;
|
|
|
|
// Group voices by language prefix
|
|
const voicesByLang = {};
|
|
this._availableVoices.forEach(voice => {
|
|
const langPrefix = voice.lang.split('-')[0];
|
|
|
|
// Group by prefix (e.g., 'en' for all English variants: en-US, en-GB, etc.)
|
|
if (!voicesByLang[langPrefix]) {
|
|
voicesByLang[langPrefix] = [];
|
|
}
|
|
voicesByLang[langPrefix].push(voice);
|
|
});
|
|
|
|
// Only show these main languages
|
|
const supportedLanguages = ['en', 'zh', 'fr', 'ja'];
|
|
|
|
// Language names mapping
|
|
const langNames = {
|
|
'en': 'English',
|
|
'zh': 'Chinese (中文)',
|
|
'fr': 'French (Français)',
|
|
'ja': 'Japanese (日本語)'
|
|
};
|
|
|
|
// Create selectors for each supported language
|
|
container.innerHTML = '';
|
|
const availableLangs = supportedLanguages.filter(lang => voicesByLang[lang] && voicesByLang[lang].length > 0);
|
|
|
|
availableLangs.forEach(langPrefix => {
|
|
const voices = voicesByLang[langPrefix];
|
|
const langName = langNames[langPrefix];
|
|
|
|
const selectorGroup = document.createElement('div');
|
|
selectorGroup.className = 'setting-group';
|
|
selectorGroup.innerHTML = `
|
|
<label>${langName}:</label>
|
|
<select class="voice-lang-select" data-lang="${langPrefix}" style="flex: 1; margin: 0 15px;">
|
|
<option value="">Auto (Best Available)</option>
|
|
</select>
|
|
<button class="debug-btn" style="padding: 6px 12px; font-size: 0.85em;" onclick="window.settingsDebug.testVoiceForLanguage('${langPrefix}')">
|
|
🔊 Test
|
|
</button>
|
|
`;
|
|
|
|
const select = selectorGroup.querySelector('select');
|
|
|
|
// Populate voice options for this language
|
|
voices.forEach(voice => {
|
|
const option = document.createElement('option');
|
|
option.value = voice.name;
|
|
const quality = voice.localService ? '🟢' : '🔵';
|
|
option.textContent = `${quality} ${voice.name} (${voice.lang})`;
|
|
|
|
// Check if this voice is selected for any variant of this language
|
|
const savedVoice = this._ttsSettings.voicesByLanguage[voice.lang];
|
|
if (savedVoice === voice.name) {
|
|
option.selected = true;
|
|
}
|
|
|
|
select.appendChild(option);
|
|
});
|
|
|
|
// Add change listener
|
|
select.addEventListener('change', (e) => {
|
|
const selectedVoiceName = e.target.value;
|
|
|
|
// Store the voice for all variants of this language
|
|
voices.forEach(voice => {
|
|
if (selectedVoiceName === '') {
|
|
delete this._ttsSettings.voicesByLanguage[voice.lang];
|
|
} else {
|
|
// Set the selected voice for ALL variants of this language
|
|
this._ttsSettings.voicesByLanguage[voice.lang] = selectedVoiceName;
|
|
}
|
|
});
|
|
|
|
this._saveTTSSettings();
|
|
this._addDebugMessage(`Voice for ${langName} set to: ${selectedVoiceName || 'Auto'}`, 'success');
|
|
});
|
|
|
|
container.appendChild(selectorGroup);
|
|
});
|
|
}
|
|
|
|
_displayVoiceList() {
|
|
const voiceListElement = document.getElementById('voice-list');
|
|
if (!voiceListElement) return;
|
|
|
|
if (this._availableVoices.length === 0) {
|
|
voiceListElement.innerHTML = '<div style="text-align: center; color: #666;">No voices available</div>';
|
|
return;
|
|
}
|
|
|
|
voiceListElement.innerHTML = '';
|
|
this._availableVoices.forEach(voice => {
|
|
const voiceItem = document.createElement('div');
|
|
voiceItem.className = 'voice-item';
|
|
voiceItem.innerHTML = `
|
|
<div class="voice-name">${voice.name}</div>
|
|
<div class="voice-lang">${voice.lang}</div>
|
|
<div class="voice-type">${voice.localService ? 'Local' : 'Remote'}</div>
|
|
`;
|
|
|
|
voiceItem.addEventListener('click', () => {
|
|
this._testVoice(voice);
|
|
document.querySelectorAll('.voice-item').forEach(item => item.classList.remove('selected'));
|
|
voiceItem.classList.add('selected');
|
|
});
|
|
|
|
voiceListElement.appendChild(voiceItem);
|
|
});
|
|
}
|
|
|
|
_testVoice(voice) {
|
|
try {
|
|
const utterance = new SpeechSynthesisUtterance('Hello, this is a voice test');
|
|
utterance.voice = voice;
|
|
utterance.rate = this._ttsSettings.rate;
|
|
utterance.volume = this._ttsSettings.volume;
|
|
|
|
utterance.onstart = () => {
|
|
this._addDebugMessage(`Testing voice: ${voice.name}`, 'info');
|
|
};
|
|
|
|
utterance.onerror = (event) => {
|
|
this._addDebugMessage(`Voice test error: ${event.error}`, 'error');
|
|
};
|
|
|
|
speechSynthesis.speak(utterance);
|
|
} catch (error) {
|
|
this._addDebugMessage(`Voice test failed: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
_addDebugMessage(message, type = 'info') {
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
const logEntry = `[${timestamp}] ${message}`;
|
|
|
|
this._debugMessages.push({ message: logEntry, type });
|
|
this._updateDebugDisplay();
|
|
|
|
console.log(`[Settings] ${logEntry}`);
|
|
}
|
|
|
|
_updateDebugDisplay() {
|
|
const debugLogElement = document.getElementById('debug-log');
|
|
if (!debugLogElement) return;
|
|
|
|
const lastEntries = this._debugMessages.slice(-50);
|
|
debugLogElement.innerHTML = lastEntries
|
|
.map(entry => `<span class="${entry.type}">${entry.message}</span>`)
|
|
.join('\n');
|
|
|
|
debugLogElement.scrollTop = debugLogElement.scrollHeight;
|
|
}
|
|
|
|
// Public test methods (exposed via window.settingsDebug)
|
|
testBasicTTS() {
|
|
this._addDebugMessage('Testing basic TTS...', 'info');
|
|
this._speak('Hello world, this is a basic test')
|
|
.then(() => this._addDebugMessage('✅ Basic TTS test completed', 'success'))
|
|
.catch(error => this._addDebugMessage(`❌ Basic TTS test failed: ${error.message}`, 'error'));
|
|
}
|
|
|
|
testGameWords() {
|
|
this._addDebugMessage('Testing game vocabulary words...', 'info');
|
|
const words = ['apple', 'cat', 'house', 'car', 'tree', 'book', 'sun', 'dog'];
|
|
|
|
let index = 0;
|
|
const speakNext = () => {
|
|
if (index >= words.length) {
|
|
this._addDebugMessage('✅ Game words test completed', 'success');
|
|
return;
|
|
}
|
|
|
|
const word = words[index++];
|
|
this._speak(word)
|
|
.then(() => {
|
|
this._addDebugMessage(`✅ Spoke: ${word}`, 'info');
|
|
setTimeout(speakNext, 500);
|
|
})
|
|
.catch(error => {
|
|
this._addDebugMessage(`❌ Failed to speak ${word}: ${error.message}`, 'error');
|
|
setTimeout(speakNext, 500);
|
|
});
|
|
};
|
|
|
|
speakNext();
|
|
}
|
|
|
|
refreshVoices() {
|
|
this._addDebugMessage('Refreshing voice list...', 'info');
|
|
this._loadVoices();
|
|
this._addDebugMessage('✅ Voice list refreshed', 'success');
|
|
}
|
|
|
|
testSystem() {
|
|
this._addDebugMessage('Testing system components...', 'info');
|
|
|
|
// Test EventBus
|
|
if (this._eventBus) {
|
|
this._addDebugMessage('✅ EventBus: Active', 'success');
|
|
} else {
|
|
this._addDebugMessage('❌ EventBus: Not found', 'error');
|
|
}
|
|
|
|
// Test Router
|
|
if (this._router) {
|
|
this._addDebugMessage('✅ Router: Active', 'success');
|
|
} else {
|
|
this._addDebugMessage('❌ Router: Not found', 'error');
|
|
}
|
|
|
|
// Test Application
|
|
if (window.app) {
|
|
this._addDebugMessage('✅ Application: Running', 'success');
|
|
} else {
|
|
this._addDebugMessage('❌ Application: Not found', 'error');
|
|
}
|
|
|
|
this._addDebugMessage('System test completed', 'info');
|
|
this._updateSystemInfo();
|
|
}
|
|
|
|
clearDebugLog() {
|
|
this._debugMessages = [];
|
|
this._updateDebugDisplay();
|
|
this._addDebugMessage('Debug log cleared', 'info');
|
|
}
|
|
|
|
testCurrentSettings() {
|
|
this._addDebugMessage(`Testing TTS with current settings (rate: ${this._ttsSettings.rate}, pitch: ${this._ttsSettings.pitch}, volume: ${this._ttsSettings.volume})...`, 'info');
|
|
|
|
const testText = 'Hello! This is a test of your current text-to-speech settings. You can adjust the rate, pitch, and volume to your preference.';
|
|
|
|
this._speak(testText)
|
|
.then(() => this._addDebugMessage('✅ TTS test completed successfully', 'success'))
|
|
.catch(error => this._addDebugMessage(`❌ TTS test failed: ${error.message}`, 'error'));
|
|
}
|
|
|
|
testVoiceForLanguage(langPrefix) {
|
|
this._addDebugMessage(`Testing voice for language: ${langPrefix}...`, 'info');
|
|
|
|
// Get test phrases for supported languages
|
|
const testPhrases = {
|
|
'en': 'Hello! This is a test of the English voice.',
|
|
'zh': '你好!这是中文语音的测试。',
|
|
'fr': 'Bonjour ! Ceci est un test de la voix française.',
|
|
'ja': 'こんにちは!これは日本語音声のテストです。'
|
|
};
|
|
|
|
const testPhrase = testPhrases[langPrefix] || 'Hello, this is a voice test.';
|
|
const language = langPrefix + '-' + langPrefix.toUpperCase(); // e.g., 'en-EN'
|
|
|
|
this._speak(testPhrase, { lang: language })
|
|
.then(() => this._addDebugMessage(`✅ ${langPrefix} voice test completed`, 'success'))
|
|
.catch(error => this._addDebugMessage(`❌ ${langPrefix} voice test failed: ${error.message}`, 'error'));
|
|
}
|
|
|
|
resetToDefaults() {
|
|
this._addDebugMessage('Resetting TTS settings to defaults...', 'info');
|
|
|
|
// Reset to default values
|
|
this._ttsSettings.rate = 0.85;
|
|
this._ttsSettings.pitch = 1.0;
|
|
this._ttsSettings.volume = 1.0;
|
|
this._ttsSettings.voicesByLanguage = {};
|
|
|
|
// Update UI
|
|
const rateSlider = document.getElementById('tts-rate');
|
|
const pitchSlider = document.getElementById('tts-pitch');
|
|
const volumeSlider = document.getElementById('tts-volume');
|
|
|
|
if (rateSlider) {
|
|
rateSlider.value = this._ttsSettings.rate;
|
|
document.getElementById('tts-rate-value').textContent = this._ttsSettings.rate;
|
|
}
|
|
|
|
if (pitchSlider) {
|
|
pitchSlider.value = this._ttsSettings.pitch;
|
|
document.getElementById('tts-pitch-value').textContent = this._ttsSettings.pitch;
|
|
}
|
|
|
|
if (volumeSlider) {
|
|
volumeSlider.value = this._ttsSettings.volume;
|
|
document.getElementById('tts-volume-value').textContent = this._ttsSettings.volume;
|
|
}
|
|
|
|
// Reset all language voice selectors
|
|
document.querySelectorAll('.voice-lang-select').forEach(select => {
|
|
select.value = '';
|
|
});
|
|
|
|
// Save settings
|
|
this._saveTTSSettings();
|
|
this._addDebugMessage('✅ Settings reset to defaults (rate: 0.85, pitch: 1.0, volume: 1.0, all voices: Auto)', 'success');
|
|
}
|
|
|
|
_exposePublicAPI() {
|
|
// Expose API for debug buttons to use
|
|
window.settingsDebug = {
|
|
testBasicTTS: () => this.testBasicTTS(),
|
|
testGameWords: () => this.testGameWords(),
|
|
refreshVoices: () => this.refreshVoices(),
|
|
testSystem: () => this.testSystem(),
|
|
clearDebugLog: () => this.clearDebugLog(),
|
|
testCurrentSettings: () => this.testCurrentSettings(),
|
|
resetToDefaults: () => this.resetToDefaults(),
|
|
testVoiceForLanguage: (langPrefix) => this.testVoiceForLanguage(langPrefix)
|
|
};
|
|
}
|
|
|
|
async _speak(text, options = {}) {
|
|
try {
|
|
const language = options.lang || 'en-US';
|
|
const ttsOptions = {
|
|
rate: options.rate || this._ttsSettings.rate,
|
|
volume: options.volume || this._ttsSettings.volume,
|
|
pitch: this._ttsSettings.pitch
|
|
};
|
|
|
|
await ttsService.speak(text, language, ttsOptions);
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
export default SettingsDebug; |