535 lines
22 KiB
HTML
535 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Text-to-Speech Testing</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
max-width: 800px;
|
|
margin: 50px auto;
|
|
padding: 20px;
|
|
background: #2c3e50;
|
|
color: white;
|
|
}
|
|
|
|
.test-section {
|
|
background: #34495e;
|
|
padding: 20px;
|
|
margin: 20px 0;
|
|
border-radius: 10px;
|
|
border: 2px solid #673ab7;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin: 15px 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
button {
|
|
background: linear-gradient(135deg, #673ab7, #5e35b1);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
}
|
|
|
|
button:hover {
|
|
background: linear-gradient(135deg, #5e35b1, #512da8);
|
|
}
|
|
|
|
button:disabled {
|
|
background: #666;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
select, input[type="range"] {
|
|
padding: 8px;
|
|
border-radius: 5px;
|
|
border: 1px solid #ccc;
|
|
background: white;
|
|
color: black;
|
|
}
|
|
|
|
.scenario-text {
|
|
background: #2c3e50;
|
|
padding: 15px;
|
|
margin: 10px 0;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #673ab7;
|
|
font-style: italic;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.voice-info {
|
|
font-size: 12px;
|
|
color: #bdc3c7;
|
|
margin: 5px 0;
|
|
}
|
|
|
|
.quality-indicator {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 12px;
|
|
font-size: 11px;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
.quality-high { background: #27ae60; }
|
|
.quality-medium { background: #f39c12; }
|
|
.quality-low { background: #e74c3c; }
|
|
|
|
label {
|
|
margin-right: 15px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🎙️ Electron Female Voice TTS Testing Lab</h1>
|
|
<p>Test female voices available in your Electron app. Shows system voices and Chromium built-ins that will work in your desktop application.</p>
|
|
|
|
<div class="test-section">
|
|
<h2>Electron Female Voice Selection & Settings</h2>
|
|
<p><strong>🚺 Female voices only</strong> - Filtered to show female voices available in Electron applications.</p>
|
|
<p style="font-size: 14px; color: #95a5a6;">
|
|
<strong>⚡ Electron Environment:</strong> Shows system voices (Windows SAPI, macOS System) and Chromium built-ins. These are the actual voices your users will hear.
|
|
</p>
|
|
|
|
<div class="controls">
|
|
<label>
|
|
Voice:
|
|
<select id="voice-select" style="min-width: 300px;">
|
|
<option>Loading voices...</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="voice-info" id="voice-info">Select a voice to see details...</div>
|
|
|
|
<div class="controls">
|
|
<label>
|
|
Speed:
|
|
<input type="range" id="rate-slider" min="0.3" max="2" step="0.1" value="0.8">
|
|
<span id="rate-display">0.8x</span>
|
|
</label>
|
|
|
|
<label>
|
|
Pitch:
|
|
<input type="range" id="pitch-slider" min="0.5" max="2" step="0.1" value="1">
|
|
<span id="pitch-display">1.0</span>
|
|
</label>
|
|
|
|
<label>
|
|
Volume:
|
|
<input type="range" id="volume-slider" min="0" max="1" step="0.1" value="0.7">
|
|
<span id="volume-display">70%</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Sample Scenario Content</h2>
|
|
|
|
<div class="controls">
|
|
<button onclick="speakText(sampleTexts.intro)">🎭 Scenario Introduction</button>
|
|
<button onclick="speakText(sampleTexts.instruction)">📋 Task Instruction</button>
|
|
<button onclick="speakText(sampleTexts.feedback)">💬 Feedback Message</button>
|
|
<button onclick="speakText(sampleTexts.ending)">🏁 Scenario Ending</button>
|
|
</div>
|
|
|
|
<h3>Custom Text Testing</h3>
|
|
<textarea id="custom-text" rows="4" style="width: 100%; padding: 10px; border-radius: 5px; background: #34495e; color: white; border: 1px solid #673ab7;">
|
|
Enter your own text here to test how it sounds with TTS...
|
|
</textarea>
|
|
|
|
<div class="controls">
|
|
<button onclick="speakCustomText()">🗣️ Speak Custom Text</button>
|
|
<button onclick="stopSpeaking()">⏹️ Stop</button>
|
|
<button onclick="pauseSpeaking()">⏸️ Pause</button>
|
|
<button onclick="resumeSpeaking()">▶️ Resume</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Sample Texts Being Tested</h2>
|
|
|
|
<div class="scenario-text">
|
|
<h4>Scenario Introduction:</h4>
|
|
<div id="intro-text"></div>
|
|
</div>
|
|
|
|
<div class="scenario-text">
|
|
<h4>Task Instruction:</h4>
|
|
<div id="instruction-text"></div>
|
|
</div>
|
|
|
|
<div class="scenario-text">
|
|
<h4>Feedback Message:</h4>
|
|
<div id="feedback-text"></div>
|
|
</div>
|
|
|
|
<div class="scenario-text">
|
|
<h4>Scenario Ending:</h4>
|
|
<div id="ending-text"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Electron Female Voices by Platform</h2>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 15px 0;">
|
|
<div>
|
|
<h4>🖥️ Windows in Electron:</h4>
|
|
<ul style="font-size: 14px;">
|
|
<li><strong>Microsoft Zira Desktop</strong> ⭐ (Default)</li>
|
|
<li><strong>Microsoft Hazel Desktop</strong></li>
|
|
<li>Microsoft Eva Desktop</li>
|
|
<li>Microsoft Aria (if updated)</li>
|
|
<li>Microsoft Jenny (if updated)</li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4>🍎 macOS in Electron:</h4>
|
|
<ul style="font-size: 14px;">
|
|
<li><strong>Samantha</strong> ⭐ (Default female)</li>
|
|
<li><strong>Alex</strong> (Can be female-sounding)</li>
|
|
<li>Victoria</li>
|
|
<li>Karen</li>
|
|
<li>Fiona</li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4>⚡ Electron Notes:</h4>
|
|
<ul style="font-size: 14px;">
|
|
<li><strong>System voices only</strong> - No Google/Amazon voices</li>
|
|
<li><strong>Quality varies</strong> - Depends on OS updates</li>
|
|
<li><strong>Local processing</strong> - No internet required</li>
|
|
<li><strong>Consistent</strong> - Same voice for all users on same OS</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="background: #3498db; padding: 15px; border-radius: 8px; margin: 15px 0;">
|
|
<h4 style="margin: 0 0 10px 0;">💡 Electron TTS Best Practices:</h4>
|
|
<ul style="margin: 5px 0; font-size: 14px;">
|
|
<li><strong>Windows:</strong> Zira is most reliable, Hazel for variety</li>
|
|
<li><strong>macOS:</strong> Samantha is the go-to female voice</li>
|
|
<li><strong>Fallback:</strong> Always provide text backup for accessibility</li>
|
|
<li><strong>Testing:</strong> Test on actual target OS, not just browser</li>
|
|
<li><strong>Chrome users:</strong> Online voices often sound better than local ones</li>
|
|
<li><strong>Speed matters:</strong> 0.7-0.8x often sounds more natural and intimate</li>
|
|
<li><strong>Pitch adjustment:</strong> Slightly lower pitch (0.9) can sound more mature</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// TTS Manager Class
|
|
class TTSTestManager {
|
|
constructor() {
|
|
this.synth = window.speechSynthesis;
|
|
this.currentUtterance = null;
|
|
this.selectedVoice = null;
|
|
this.settings = {
|
|
rate: 0.8,
|
|
pitch: 1.0,
|
|
volume: 0.7
|
|
};
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.loadVoices();
|
|
this.setupEventListeners();
|
|
this.displaySampleTexts();
|
|
|
|
// Voices might load asynchronously
|
|
if (speechSynthesis.onvoiceschanged !== undefined) {
|
|
speechSynthesis.onvoiceschanged = () => this.loadVoices();
|
|
}
|
|
}
|
|
|
|
loadVoices() {
|
|
const voices = this.synth.getVoices();
|
|
const select = document.getElementById('voice-select');
|
|
|
|
// Clear existing options
|
|
select.innerHTML = '';
|
|
|
|
if (voices.length === 0) {
|
|
select.innerHTML = '<option>No voices available</option>';
|
|
return;
|
|
}
|
|
|
|
// Filter for English female voices only
|
|
const femaleVoices = voices.filter(voice => {
|
|
return voice.lang.startsWith('en') && this.isFemaleVoice(voice);
|
|
});
|
|
|
|
if (femaleVoices.length === 0) {
|
|
select.innerHTML = '<option>No female voices found</option>';
|
|
console.log('All available voices:', voices.map(v => ({ name: v.name, lang: v.lang })));
|
|
return;
|
|
}
|
|
|
|
// Sort female voices by quality
|
|
const sortedVoices = femaleVoices.sort((a, b) => {
|
|
const aQuality = this.getVoiceQuality(a);
|
|
const bQuality = this.getVoiceQuality(b);
|
|
return bQuality - aQuality;
|
|
});
|
|
|
|
sortedVoices.forEach((voice, index) => {
|
|
const option = document.createElement('option');
|
|
option.value = index;
|
|
option.textContent = voice.name;
|
|
|
|
// Add quality indicator
|
|
const quality = this.getVoiceQuality(voice);
|
|
const qualityText = quality === 3 ? '⭐ High' : quality === 2 ? '✓ Medium' : '○ Basic';
|
|
option.textContent += ` (${qualityText})`;
|
|
|
|
select.appendChild(option);
|
|
|
|
// Select first high-quality voice as default
|
|
if (!this.selectedVoice && quality >= 2) {
|
|
this.selectedVoice = voice;
|
|
option.selected = true;
|
|
this.updateVoiceInfo(voice);
|
|
}
|
|
});
|
|
|
|
// Fallback to first female voice if no high-quality voice found
|
|
if (!this.selectedVoice && femaleVoices.length > 0) {
|
|
this.selectedVoice = femaleVoices[0];
|
|
this.updateVoiceInfo(femaleVoices[0]);
|
|
}
|
|
|
|
// Log all detected female voices for debugging
|
|
console.log('🎙️ Female voices found:', femaleVoices.map(v => ({
|
|
name: v.name,
|
|
quality: this.getVoiceQuality(v),
|
|
local: v.localService
|
|
})));
|
|
}
|
|
|
|
isFemaleVoice(voice) {
|
|
const name = voice.name.toLowerCase();
|
|
|
|
// Electron-specific female voices (system voices + Chromium built-ins)
|
|
const electronFemaleVoices = [
|
|
// Windows SAPI voices (available in Electron on Windows)
|
|
'zira', 'hazel', 'eva', 'aria', 'jenny',
|
|
|
|
// macOS system voices (available in Electron on Mac)
|
|
'samantha', 'alex', 'victoria', 'karen', 'fiona', 'moira', 'tessa',
|
|
'amelie', 'kyoko', 'mei-jia', 'sin-ji', 'ting-ting', 'yu-shu',
|
|
|
|
// Microsoft voices (may be available through system)
|
|
'cortana', 'eva', 'hedda', 'helle', 'herena',
|
|
|
|
// Chromium built-in voices (limited but cross-platform)
|
|
'female', 'woman',
|
|
|
|
// Common female names that appear in system voices
|
|
'anna', 'emma', 'mary', 'susan', 'kate', 'sara', 'laura', 'helena'
|
|
];
|
|
|
|
// Female voice indicators
|
|
const femaleIndicators = ['female', 'woman', 'girl', 'lady'];
|
|
|
|
// Check for Electron-compatible female voices
|
|
const isElectronFemale = electronFemaleVoices.some(femaleName => name.includes(femaleName));
|
|
const hasIndicator = femaleIndicators.some(indicator => name.includes(indicator));
|
|
|
|
return isElectronFemale || hasIndicator;
|
|
}
|
|
|
|
getVoiceQuality(voice) {
|
|
const name = voice.name.toLowerCase();
|
|
|
|
// Highest quality: System voices (best in Electron)
|
|
if (name.includes('zira') || name.includes('samantha') || name.includes('aria') ||
|
|
name.includes('eva') || name.includes('hazel')) {
|
|
return 3;
|
|
}
|
|
|
|
// High quality: Good system voices
|
|
if (name.includes('alex') || name.includes('victoria') || name.includes('karen') ||
|
|
name.includes('jenny') || name.includes('cortana')) {
|
|
return 3;
|
|
}
|
|
|
|
// Medium quality: Standard system voices
|
|
if (name.includes('fiona') || name.includes('moira') || name.includes('tessa') ||
|
|
voice.localService === true) {
|
|
return 2;
|
|
}
|
|
|
|
// Basic quality: Fallback voices
|
|
return 1;
|
|
}
|
|
|
|
updateVoiceInfo(voice) {
|
|
const info = document.getElementById('voice-info');
|
|
const quality = this.getVoiceQuality(voice);
|
|
const qualityClass = quality === 3 ? 'quality-high' : quality === 2 ? 'quality-medium' : 'quality-low';
|
|
const qualityText = quality === 3 ? 'High Quality' : quality === 2 ? 'Medium Quality' : 'Basic Quality';
|
|
|
|
info.innerHTML = `
|
|
<strong>${voice.name}</strong> - ${voice.lang}
|
|
<span class="quality-indicator ${qualityClass}">${qualityText}</span><br>
|
|
Local: ${voice.localService ? 'Yes' : 'No'} |
|
|
Default: ${voice.default ? 'Yes' : 'No'}
|
|
`;
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Voice selection
|
|
document.getElementById('voice-select').addEventListener('change', (e) => {
|
|
const voices = this.synth.getVoices().filter(v => v.lang.startsWith('en') && this.isFemaleVoice(v));
|
|
this.selectedVoice = voices[e.target.value];
|
|
this.updateVoiceInfo(this.selectedVoice);
|
|
});
|
|
|
|
// Rate slider
|
|
const rateSlider = document.getElementById('rate-slider');
|
|
const rateDisplay = document.getElementById('rate-display');
|
|
rateSlider.addEventListener('input', (e) => {
|
|
this.settings.rate = parseFloat(e.target.value);
|
|
rateDisplay.textContent = `${this.settings.rate}x`;
|
|
});
|
|
|
|
// Pitch slider
|
|
const pitchSlider = document.getElementById('pitch-slider');
|
|
const pitchDisplay = document.getElementById('pitch-display');
|
|
pitchSlider.addEventListener('input', (e) => {
|
|
this.settings.pitch = parseFloat(e.target.value);
|
|
pitchDisplay.textContent = this.settings.pitch.toFixed(1);
|
|
});
|
|
|
|
// Volume slider
|
|
const volumeSlider = document.getElementById('volume-slider');
|
|
const volumeDisplay = document.getElementById('volume-display');
|
|
volumeSlider.addEventListener('input', (e) => {
|
|
this.settings.volume = parseFloat(e.target.value);
|
|
volumeDisplay.textContent = `${Math.round(this.settings.volume * 100)}%`;
|
|
});
|
|
}
|
|
|
|
speak(text) {
|
|
// Stop any current speech
|
|
this.stop();
|
|
|
|
if (!text.trim()) return;
|
|
|
|
// Create utterance
|
|
this.currentUtterance = new SpeechSynthesisUtterance(text);
|
|
|
|
// Apply settings
|
|
if (this.selectedVoice) {
|
|
this.currentUtterance.voice = this.selectedVoice;
|
|
}
|
|
this.currentUtterance.rate = this.settings.rate;
|
|
this.currentUtterance.pitch = this.settings.pitch;
|
|
this.currentUtterance.volume = this.settings.volume;
|
|
|
|
// Event handlers
|
|
this.currentUtterance.onstart = () => {
|
|
console.log('🎙️ Speech started');
|
|
this.updateButtonStates(true);
|
|
};
|
|
|
|
this.currentUtterance.onend = () => {
|
|
console.log('🎙️ Speech ended');
|
|
this.updateButtonStates(false);
|
|
};
|
|
|
|
this.currentUtterance.onerror = (event) => {
|
|
console.error('🎙️ Speech error:', event.error);
|
|
this.updateButtonStates(false);
|
|
};
|
|
|
|
// Start speaking
|
|
this.synth.speak(this.currentUtterance);
|
|
}
|
|
|
|
stop() {
|
|
this.synth.cancel();
|
|
this.updateButtonStates(false);
|
|
}
|
|
|
|
pause() {
|
|
this.synth.pause();
|
|
}
|
|
|
|
resume() {
|
|
this.synth.resume();
|
|
}
|
|
|
|
updateButtonStates(speaking) {
|
|
const buttons = document.querySelectorAll('button');
|
|
// You could disable/enable buttons based on speaking state
|
|
// For now, just log the state
|
|
console.log('Speaking state:', speaking);
|
|
}
|
|
|
|
displaySampleTexts() {
|
|
document.getElementById('intro-text').textContent = sampleTexts.intro;
|
|
document.getElementById('instruction-text').textContent = sampleTexts.instruction;
|
|
document.getElementById('feedback-text').textContent = sampleTexts.feedback;
|
|
document.getElementById('ending-text').textContent = sampleTexts.ending;
|
|
}
|
|
}
|
|
|
|
// Sample texts for testing (similar to your game content)
|
|
const sampleTexts = {
|
|
intro: "Welcome to your training session. Today you will learn the importance of focus and control. Your arousal will be carefully managed as you progress through each challenge. Remember, obedience and attention to detail are essential for success.",
|
|
|
|
instruction: "Position yourself comfortably and maintain perfect stillness. Focus your attention on the screen while controlling your breathing. You must hold this position for exactly sixty seconds without any movement. Your discipline will be tested.",
|
|
|
|
feedback: "Excellent work. Your control is improving with each session. You maintained focus for the full duration and demonstrated proper obedience. Your arousal level is now at moderate intensity, exactly where it should be.",
|
|
|
|
ending: "Training session completed successfully. Final state: Arousal High, Control Excellent. You have demonstrated exceptional focus and discipline throughout this session. Your progress has been documented and you may now proceed to the next level."
|
|
};
|
|
|
|
// Initialize TTS manager
|
|
let ttsManager;
|
|
|
|
// Wait for page load
|
|
window.addEventListener('load', () => {
|
|
ttsManager = new TTSTestManager();
|
|
});
|
|
|
|
// Global functions for buttons
|
|
function speakText(text) {
|
|
ttsManager.speak(text);
|
|
}
|
|
|
|
function speakCustomText() {
|
|
const text = document.getElementById('custom-text').value;
|
|
ttsManager.speak(text);
|
|
}
|
|
|
|
function stopSpeaking() {
|
|
ttsManager.stop();
|
|
}
|
|
|
|
function pauseSpeaking() {
|
|
ttsManager.pause();
|
|
}
|
|
|
|
function resumeSpeaking() {
|
|
ttsManager.resume();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |