358 lines
14 KiB
HTML
358 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Electron TTS Integration Test</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
padding: 20px;
|
|
background: #2c3e50;
|
|
color: white;
|
|
}
|
|
.test-section {
|
|
background: #34495e;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
margin: 20px 0;
|
|
}
|
|
button {
|
|
padding: 10px 20px;
|
|
margin: 5px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
}
|
|
.enabled {
|
|
background: #27ae60;
|
|
color: white;
|
|
}
|
|
.disabled {
|
|
background: #95a5a6;
|
|
color: white;
|
|
}
|
|
#voice-info {
|
|
background: #2c3e50;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin: 10px 0;
|
|
border-left: 3px solid #3498db;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🎙️ Electron TTS Integration Test</h1>
|
|
|
|
<div class="test-section">
|
|
<h2>Voice Manager Test</h2>
|
|
<p>This tests the cross-platform voice selection system.</p>
|
|
|
|
<button id="init-btn" onclick="initializeVoiceManager()">Initialize Voice Manager</button>
|
|
<button id="test-speak" onclick="testSpeak()" disabled>Test Female Voice</button>
|
|
<button id="stop-btn" onclick="stopSpeech()">Stop Speech</button>
|
|
|
|
<div id="voice-info">
|
|
<strong>Voice Manager Status:</strong> Not initialized
|
|
</div>
|
|
|
|
<div id="platform-info"></div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Scenario Text Test</h2>
|
|
<p>Test TTS with scenario-style text including placeholders.</p>
|
|
|
|
<textarea id="scenario-text" rows="4" style="width: 100%; padding: 10px; background: #2c3e50; color: white; border: 1px solid #556983; border-radius: 5px;">
|
|
You feel your {arousal} building as you continue to focus. Your {control} wavers slightly, but you maintain your composure. The {intensity} of the moment increases.
|
|
</textarea>
|
|
|
|
<div style="margin: 10px 0;">
|
|
<label>Arousal Level:
|
|
<select id="arousal-level">
|
|
<option value="low">Low</option>
|
|
<option value="moderate" selected>Moderate</option>
|
|
<option value="high">High</option>
|
|
<option value="extreme">Extreme</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label style="margin-left: 20px;">Control Level:
|
|
<select id="control-level">
|
|
<option value="weak">Weak</option>
|
|
<option value="moderate" selected>Moderate</option>
|
|
<option value="strong">Strong</option>
|
|
<option value="iron">Iron</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label style="margin-left: 20px;">Intensity:
|
|
<select id="intensity-level">
|
|
<option value="low">Low</option>
|
|
<option value="moderate" selected>Moderate</option>
|
|
<option value="high">High</option>
|
|
<option value="extreme">Extreme</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<button onclick="testScenarioSpeak()">Speak Processed Text</button>
|
|
</div>
|
|
|
|
<script type="module">
|
|
// Import the voice manager (simulated for testing)
|
|
class VoiceManager {
|
|
constructor() {
|
|
this.synth = window.speechSynthesis;
|
|
this.selectedVoice = null;
|
|
this.fallbackVoices = this.getPlatformFallbacks();
|
|
}
|
|
|
|
getPlatformFallbacks() {
|
|
const platform = this.detectPlatform();
|
|
|
|
switch (platform) {
|
|
case 'windows':
|
|
return [
|
|
'Microsoft Zira Desktop', 'Microsoft Zira',
|
|
'Microsoft Hazel Desktop', 'Microsoft Hazel',
|
|
'Microsoft Eva Desktop', 'Microsoft Eva',
|
|
'Microsoft Aria', 'Microsoft Jenny',
|
|
'Zira', 'Hazel', 'Eva', 'Aria', 'Jenny'
|
|
];
|
|
|
|
case 'mac':
|
|
return [
|
|
'Samantha', 'Victoria', 'Karen', 'Fiona', 'Moira', 'Tessa', 'Alex'
|
|
];
|
|
|
|
default:
|
|
return ['Samantha', 'Zira', 'Victoria', 'Hazel', 'Karen', 'Eva', 'female', 'woman'];
|
|
}
|
|
}
|
|
|
|
detectPlatform() {
|
|
const userAgent = navigator.userAgent.toLowerCase();
|
|
if (userAgent.includes('win')) return 'windows';
|
|
if (userAgent.includes('mac')) return 'mac';
|
|
if (userAgent.includes('linux')) return 'linux';
|
|
return 'unknown';
|
|
}
|
|
|
|
isFemaleVoice(voice) {
|
|
const name = voice.name.toLowerCase();
|
|
const femaleVoiceNames = [
|
|
'zira', 'hazel', 'eva', 'aria', 'jenny',
|
|
'samantha', 'victoria', 'karen', 'fiona', 'moira', 'tessa',
|
|
'female', 'woman', 'girl', 'lady'
|
|
];
|
|
return femaleVoiceNames.some(femaleName => name.includes(femaleName));
|
|
}
|
|
|
|
getAvailableFemaleVoices() {
|
|
const voices = this.synth.getVoices();
|
|
return voices.filter(voice =>
|
|
voice.lang.startsWith('en') && this.isFemaleVoice(voice)
|
|
);
|
|
}
|
|
|
|
selectBestVoice() {
|
|
const availableVoices = this.getAvailableFemaleVoices();
|
|
|
|
if (availableVoices.length === 0) {
|
|
console.warn('No female voices available');
|
|
return null;
|
|
}
|
|
|
|
for (const preferredName of this.fallbackVoices) {
|
|
const voice = availableVoices.find(v =>
|
|
v.name.toLowerCase().includes(preferredName.toLowerCase())
|
|
);
|
|
|
|
if (voice) {
|
|
console.log(`Selected voice: ${voice.name} (${voice.lang})`);
|
|
this.selectedVoice = voice;
|
|
return voice;
|
|
}
|
|
}
|
|
|
|
this.selectedVoice = availableVoices[0];
|
|
console.log(`Fallback to first available: ${this.selectedVoice.name}`);
|
|
return this.selectedVoice;
|
|
}
|
|
|
|
async initialize() {
|
|
return new Promise((resolve) => {
|
|
if (this.synth.getVoices().length > 0) {
|
|
this.selectBestVoice();
|
|
resolve(this.selectedVoice);
|
|
} else {
|
|
this.synth.addEventListener('voiceschanged', () => {
|
|
this.selectBestVoice();
|
|
resolve(this.selectedVoice);
|
|
}, { once: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
speak(text, options = {}) {
|
|
if (!this.selectedVoice) {
|
|
console.warn('No voice selected, attempting to initialize...');
|
|
this.selectBestVoice();
|
|
}
|
|
|
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
|
|
if (this.selectedVoice) {
|
|
utterance.voice = this.selectedVoice;
|
|
}
|
|
|
|
utterance.rate = options.rate || 0.9;
|
|
utterance.pitch = options.pitch || 1.1;
|
|
utterance.volume = options.volume || 0.8;
|
|
|
|
// Platform-specific pitch adjustments
|
|
if (this.selectedVoice) {
|
|
const voiceName = this.selectedVoice.name.toLowerCase();
|
|
if (voiceName.includes('alex')) {
|
|
utterance.pitch = Math.max(utterance.pitch * 1.2, 1.5);
|
|
}
|
|
}
|
|
|
|
if (options.onStart) utterance.onstart = options.onStart;
|
|
if (options.onEnd) utterance.onend = options.onEnd;
|
|
if (options.onError) utterance.onerror = options.onError;
|
|
|
|
this.synth.speak(utterance);
|
|
return utterance;
|
|
}
|
|
|
|
getVoiceInfo() {
|
|
if (!this.selectedVoice) {
|
|
return {
|
|
name: 'No voice selected',
|
|
platform: this.detectPlatform(),
|
|
available: false
|
|
};
|
|
}
|
|
|
|
return {
|
|
name: this.selectedVoice.name,
|
|
lang: this.selectedVoice.lang,
|
|
platform: this.detectPlatform(),
|
|
available: true,
|
|
localService: this.selectedVoice.localService,
|
|
voiceURI: this.selectedVoice.voiceURI
|
|
};
|
|
}
|
|
|
|
stop() {
|
|
this.synth.cancel();
|
|
}
|
|
|
|
isSupported() {
|
|
return 'speechSynthesis' in window;
|
|
}
|
|
}
|
|
|
|
// Global variables
|
|
let voiceManager = null;
|
|
|
|
// Global functions
|
|
window.initializeVoiceManager = async function() {
|
|
const button = document.getElementById('init-btn');
|
|
const voiceInfo = document.getElementById('voice-info');
|
|
const platformInfo = document.getElementById('platform-info');
|
|
const testButton = document.getElementById('test-speak');
|
|
|
|
button.textContent = 'Initializing...';
|
|
button.disabled = true;
|
|
|
|
try {
|
|
voiceManager = new VoiceManager();
|
|
await voiceManager.initialize();
|
|
|
|
const info = voiceManager.getVoiceInfo();
|
|
|
|
voiceInfo.innerHTML = `
|
|
<strong>Voice Manager Status:</strong> ✅ Initialized<br>
|
|
<strong>Selected Voice:</strong> ${info.name}<br>
|
|
<strong>Language:</strong> ${info.lang}<br>
|
|
<strong>Platform:</strong> ${info.platform}<br>
|
|
<strong>Local Service:</strong> ${info.localService ? 'Yes' : 'No'}
|
|
`;
|
|
|
|
// Show available female voices
|
|
const femaleVoices = voiceManager.getAvailableFemaleVoices();
|
|
platformInfo.innerHTML = `
|
|
<h4>Available Female Voices (${femaleVoices.length}):</h4>
|
|
<ul>
|
|
${femaleVoices.map(voice =>
|
|
`<li>${voice.name} (${voice.lang}) ${voice === voiceManager.selectedVoice ? '⭐ Selected' : ''}</li>`
|
|
).join('')}
|
|
</ul>
|
|
`;
|
|
|
|
button.textContent = '✅ Voice Manager Ready';
|
|
button.className = 'enabled';
|
|
testButton.disabled = false;
|
|
|
|
} catch (error) {
|
|
voiceInfo.innerHTML = `<strong>Error:</strong> Failed to initialize voice manager - ${error.message}`;
|
|
button.textContent = '❌ Initialization Failed';
|
|
button.className = 'disabled';
|
|
}
|
|
};
|
|
|
|
window.testSpeak = function() {
|
|
if (!voiceManager) {
|
|
alert('Please initialize voice manager first');
|
|
return;
|
|
}
|
|
|
|
const testText = "Hello! I am your female voice assistant for the interactive scenarios. This voice will be used on both Windows and Mac platforms.";
|
|
|
|
voiceManager.speak(testText, {
|
|
onStart: () => console.log('🎤 TTS started'),
|
|
onEnd: () => console.log('🎤 TTS finished'),
|
|
onError: (error) => console.error('🎤 TTS error:', error)
|
|
});
|
|
};
|
|
|
|
window.stopSpeech = function() {
|
|
if (voiceManager) {
|
|
voiceManager.stop();
|
|
}
|
|
};
|
|
|
|
window.testScenarioSpeak = function() {
|
|
if (!voiceManager) {
|
|
alert('Please initialize voice manager first');
|
|
return;
|
|
}
|
|
|
|
const text = document.getElementById('scenario-text').value;
|
|
const arousal = document.getElementById('arousal-level').value;
|
|
const control = document.getElementById('control-level').value;
|
|
const intensity = document.getElementById('intensity-level').value;
|
|
|
|
// Process the text like in the game
|
|
const processedText = text
|
|
.replace(/\{arousal\}/g, arousal)
|
|
.replace(/\{control\}/g, control)
|
|
.replace(/\{intensity\}/g, intensity);
|
|
|
|
console.log('Original text:', text);
|
|
console.log('Processed text:', processedText);
|
|
|
|
voiceManager.speak(processedText, {
|
|
rate: 0.9,
|
|
pitch: 1.1,
|
|
onStart: () => console.log('🎤 Scenario TTS started'),
|
|
onEnd: () => console.log('🎤 Scenario TTS finished')
|
|
});
|
|
};
|
|
</script>
|
|
</body>
|
|
</html> |