Save current state: Fixed text readability in scenarios and interactive tasks

This commit is contained in:
dilgenfritz 2025-10-29 17:04:42 -05:00
parent f08bfbb5cc
commit c43737a71d
21 changed files with 2386 additions and 2324 deletions

View File

@ -1,4 +1,4 @@
# Edge & Punishment - Desktop Application
# Gooner Training Academy - Desktop Application
*How long can you last?*

View File

@ -1,4 +1,4 @@
# Edge & Punishment
# Gooner Training Academy
*How long can you last?*

117
ROADMAP.md Normal file
View File

@ -0,0 +1,117 @@
# Gooner Training Academy - Development Roadmap
## 🎮 Current Status
- ✅ Core game mechanics implemented
- ✅ Interactive task system with focus interruptions
- ✅ Video integration for focus sessions
- ✅ Audio and image management
- ✅ Scenario system with state management
- ✅ Loading overlay and initialization protection
- ✅ Desktop file management integration
## 🚧 Active Development
- Enhanced user experience improvements
- Bug fixes and stability enhancements
- Performance optimizations
## 📋 Feature Backlog
### 🎯 High Priority (Core Game Polish)
- [ ] More interactive task types and scenarios
- [ ] Improved user interface and visual design
- [ ] Better error handling and user feedback
- [ ] Performance optimizations for large media libraries
- [ ] Comprehensive testing and bug fixes
- [ ] User settings and customization options
- [ ] Save/load game state improvements
### 🎨 Medium Priority (User Experience)
- [ ] Advanced scenario editor
- [ ] Custom task creation tools
- [ ] Enhanced statistics and progress tracking
- [ ] Multiple game mode variations
- [ ] Improved audio/visual feedback systems
- [ ] Mobile-responsive design considerations
- [ ] Accessibility features
### 🎵 Content & Media
- [ ] Built-in content library (sample images/audio/video)
- [ ] Content recommendation system
- [ ] Media organization and tagging features
- [ ] Import/export functionality for user content
- [ ] Content validation and quality checks
### 🔧 Technical Improvements
- [ ] Code refactoring and optimization
- [ ] Automated testing suite
- [ ] Better documentation and code comments
- [ ] Cross-platform compatibility testing
- [ ] Security enhancements
- [ ] Backup and recovery features
### 🔮 Future Enhancements (Low Priority)
- [ ] Enhanced AI task generation features
- *Note: NSFW-friendly AI models are limited and expensive. Manual content creation is more reliable for now.*
- [ ] Advanced machine learning features
- [ ] Voice recognition/synthesis integration
- [ ] VR/AR compatibility exploration
## 💰 Future Monetization Considerations
*Added: October 29, 2025*
When the core game is feature-complete and polished, consider:
### Freemium Model Concepts
- **Free Tier**: Basic game modes, limited media (50 images, 20 audio files, 10 videos)
- **Premium Tier**: Unlimited media, all scenarios, advanced features
- **Subscription vs One-time**: Consider both models
### Distribution Platforms
- **Itch.io**: Indie-friendly, adult content allowed, easy setup
- **Steam**: Large audience, requires more polish and marketing
- **Direct Sales**: Own website with payment processing
- **Patreon/SubscribeStar**: Subscription-based community building
### Technical Requirements for Monetization
- License management system
- Content gating and feature restrictions
- Payment integration (Stripe, PayPal)
- User account system
- Analytics and usage tracking
- Anti-piracy measures (lightweight)
### Marketing Strategy
- Demo videos showcasing unique features
- Community building (Discord, Reddit)
- SEO-optimized website
- Content creator partnerships
- Social media presence
**Note**: Focus on creating an exceptional user experience first. A great free product will naturally generate demand for premium features.
## 🗓️ Version Planning
### v1.0 - Core Game Release
- All essential features working reliably
- Comprehensive testing completed
- Documentation and user guides
- Stable performance with large media libraries
### v1.5 - Enhanced Experience
- Advanced scenarios and customization
- Improved UI/UX
- Additional game modes
### v2.0 - Community Features
- User-generated content support
- Sharing and community features
- Advanced customization tools
### vFuture - Commercial Considerations
- Monetization system implementation
- Multi-platform distribution
- Advanced analytics and user management
---
*This roadmap is a living document and will be updated as development progresses.*

View File

@ -6,8 +6,8 @@ start /min ollama serve
REM --- Wait a few seconds for Ollama to initialize ---
timeout /t 3 /nobreak >nul
REM --- Start the Edge & Punishment webGame ---
echo Launching Edge & Punishment...
REM --- Start the Gooner Training Academy webGame ---
echo Launching Gooner Training Academy...
REM If you use npm:
npm start

View File

@ -1,4 +1,4 @@
# Audio Library for Edge & Punishment
# Audio Library for Gooner Training Academy
This directory contains all audio files used in the game. The AudioManager will automatically discover and categorize audio files placed in the appropriate subdirectories.

View File

@ -4,8 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: file: blob: http://localhost:* https:; connect-src 'self' http://localhost:* https: ws://localhost:*; img-src 'self' data: file: blob:; media-src 'self' data: file: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;">
<title>Edge & Punishment - How long can you last?</title>
<title>Gooner Training Academy - Master Your Dedication</title>
<link rel="stylesheet" href="src/styles/styles.css">
<link href="https://fonts.googleapis.com/css2?family=Audiowide&display=swap" rel="stylesheet">
<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js" crossorigin="anonymous"></script>
<script>
// Test JSZip availability when page loads
@ -37,8 +38,8 @@
<div class="game-container">
<!-- Game Header -->
<header class="game-header">
<h1>Edge & Punishment</h1>
<p class="tagline">How long can you last?</p>
<h1>Gooner Training Academy</h1>
<p class="tagline">Master Your Dedication</p>
<!-- Compact Timer (top-right corner) -->
<div class="timer-compact">
@ -161,14 +162,12 @@
<div class="option-item theme-selector">
<label for="theme-dropdown" class="option-label">Choose Your Vibe:</label>
<select id="theme-dropdown" class="theme-dropdown">
<option value="ocean">🌊 Ocean Blue</option>
<option value="sunset">🌅 Warm Sunset</option>
<option value="forest">🌲 Nature Green</option>
<option value="midnight">🌙 Midnight Dark</option>
<option value="pastel">🌸 Soft Pastel</option>
<option value="neon">⚡ Neon Cyber</option>
<option value="autumn">🍂 Autumn Vibes</option>
<option value="monochrome">⚫ Monochrome</option>
<option value="pink-orange"><EFBFBD> Pink/Orange Gaming</option>
<option value="blue-cyan">💙 Blue/Cyan Tech</option>
<option value="purple-violet">💜 Purple/Violet Luxe</option>
<option value="green-lime">💚 Green/Lime Matrix</option>
<option value="red-crimson" selected>❤️ Red/Crimson Power</option>
<option value="gold-amber"><EFBFBD> Gold/Amber Elite</option>
</select>
</div>
@ -1658,6 +1657,7 @@
<script src="src/features/images/image-discovery-fix.js"></script>
<script src="src/core/gameModeManager.js"></script>
<script src="src/features/webcam/webcamManager.js"></script>
<script src="src/features/tts/voiceManager.js"></script>
<script src="src/features/tasks/interactiveTaskManager.js"></script>
<script src="src/features/video/videoPlayerManager.js"></script>
<script src="src/utils/desktop-file-manager.js"></script>

View File

@ -1,7 +1,7 @@
{
"name": "edge-and-punishment",
"name": "gooner-training-academy",
"version": "1.0.0",
"description": "An edging challenge game where skipping tasks floods your screen with consequences - How long can you last?",
"description": "A comprehensive interactive training platform with guided scenarios, TTS narration, and advanced focus challenges - Master your dedication through structured academy training.",
"main": "src/core/main.js",
"scripts": {
"start": "electron .",
@ -11,15 +11,15 @@
"build-linux": "electron-builder --linux",
"dev": "electron . --dev"
},
"author": "Edge & Punishment Developer",
"author": "Gooner Training Academy Developer",
"license": "MIT",
"devDependencies": {
"electron": "^27.0.0",
"electron-builder": "^24.6.4"
},
"build": {
"appId": "com.edgeandpunishment.game",
"productName": "Edge & Punishment",
"appId": "com.goonertrainingacademy.app",
"productName": "Gooner Training Academy",
"directories": {
"output": "dist"
},
@ -47,12 +47,15 @@
"url": "."
},
"keywords": [
"gooner",
"training",
"academy",
"interactive",
"scenarios",
"tts",
"focus",
"edging",
"adult",
"game",
"challenge",
"punishment",
"consequences",
"desktop",
"electron",
"nsfw"

View File

@ -1,6 +1,6 @@
# Source Code Structure
This directory contains the organized source code for the Edge & Punishment game, structured by feature and responsibility.
This directory contains the organized source code for the Gooner Training Academy application, structured by feature and responsibility.
## 📁 Directory Organization

View File

@ -2656,8 +2656,8 @@ class TaskChallengeGame {
}
changeTheme(themeName) {
// Remove all existing theme classes
const themeClasses = ['theme-ocean', 'theme-sunset', 'theme-forest', 'theme-midnight', 'theme-pastel', 'theme-neon', 'theme-autumn', 'theme-monochrome'];
// Remove all existing theme classes (old theme names from CSS)
const themeClasses = ['theme-sunset', 'theme-forest', 'theme-midnight', 'theme-pastel', 'theme-neon', 'theme-autumn', 'theme-monochrome'];
document.body.classList.remove(...themeClasses);
// Add new theme class (ocean is default, no class needed)
@ -2667,28 +2667,26 @@ class TaskChallengeGame {
// Save theme preference
localStorage.setItem('gameTheme', themeName);
console.log(`Theme changed to: ${themeName}`);
}
loadSavedTheme() {
const savedTheme = localStorage.getItem('gameTheme') || 'ocean';
// Clear any problematic theme and force default
localStorage.removeItem('gameTheme');
const savedTheme = 'ocean'; // Force ocean theme for readability
document.getElementById('theme-dropdown').value = savedTheme;
this.changeTheme(savedTheme);
}
showScreen(screenId) {
console.log(`showScreen(${screenId}) called`);
// Hide all screens
document.querySelectorAll('.screen').forEach(screen => {
screen.classList.remove('active');
console.log(`Removed active class from screen:`, screen.id);
});
// Show target screen
const targetScreen = document.getElementById(screenId);
if (targetScreen) {
targetScreen.classList.add('active');
console.log(`Added active class to screen: ${screenId}`);
} else {
console.error(`Screen element not found: ${screenId}`);
}
@ -4233,7 +4231,6 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag
}
startGame() {
console.log('startGame() called');
if (!this.imageDiscoveryComplete) {
console.log('Image discovery not complete, retrying in 500ms...');
@ -4303,15 +4300,12 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag
return;
}
console.log('Starting game initialization...');
// Get selected game mode
const selectedMode = document.querySelector('input[name="gameMode"]:checked')?.value || 'standard';
if (window.gameModeManager) {
window.gameModeManager.currentMode = selectedMode;
// Map the new mode names to the original game engine modes
this.gameState.gameMode = window.gameModeManager.getGameModeForEngine();
console.log(`Starting game in ${window.gameModeManager.getCurrentMode().name} mode (engine: ${this.gameState.gameMode})`);
} else {
// Fallback if gameModeManager is not available
this.gameState.gameMode = selectedMode === 'timed' ? 'timed' :
@ -4328,29 +4322,20 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag
// Load focus interruption setting
const savedInterruptionChance = this.dataManager.get('focusInterruptionChance') || 0;
this.gameState.focusInterruptionChance = savedInterruptionChance;
console.log(`🧘 Focus interruption setting loaded at game start: ${savedInterruptionChance}%`);
console.log('Recording game start in data manager...');
// Record game start in data manager
this.dataManager.recordGameStart();
console.log('Starting timer...');
this.startTimer();
console.log('Loading next task...');
this.loadNextTask();
console.log('Showing game screen...');
this.showScreen('game-screen');
console.log('Starting flash message system...');
this.flashMessageManager.start();
this.flashMessageManager.triggerEventMessage('gameStart');
console.log('Starting periodic popup system...');
const periodicSettings = this.dataManager.get('periodicPopupSettings') || { enabled: true };
if (this.popupImageManager && this.popupImageManager.startPeriodicPopups && periodicSettings.enabled) {
this.popupImageManager.startPeriodicPopups();
}
console.log('Updating stats...');
this.updateStats();
console.log('startGame() completed successfully');
}
loadNextTask() {
@ -4371,22 +4356,14 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag
loadMainTask() {
// Get tasks from the game mode manager
let tasksToUse = gameData.mainTasks;
console.log(`🎮 Mode check - gameModeManager exists: ${!!window.gameModeManager}`);
if (window.gameModeManager) {
console.log(`🎮 Current mode: ${window.gameModeManager.currentMode}, isScenarioMode: ${window.gameModeManager.isScenarioMode()}`);
}
if (window.gameModeManager && window.gameModeManager.isScenarioMode()) {
tasksToUse = window.gameModeManager.getTasksForMode();
console.log(`Using ${window.gameModeManager.getCurrentMode().name} tasks:`, tasksToUse.length);
} else {
// For standard, timed, and scored modes, exclude interactive tasks
const currentMode = window.gameModeManager ? window.gameModeManager.currentMode : 'standard';
if (currentMode === 'standard' || currentMode === 'timed' || currentMode === 'scored') {
tasksToUse = gameData.mainTasks.filter(task => !task.interactiveType);
console.log(`Using ${currentMode} tasks (interactive tasks excluded): ${tasksToUse.length}`);
} else {
console.log(`Using standard tasks: ${tasksToUse.length}`);
}
}
@ -4399,9 +4376,6 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag
t.interactiveType === 'scenario-adventure' || t.interactiveType === 'mirror-task'
);
console.log(`Available tasks: ${availableTasks.length}, Interactive tasks available:`,
interactiveTasksForSelection.map(t => `${t.id}:${t.interactiveType}`));
if (interactiveTasksForSelection.length === 0 && window.gameModeManager?.isScenarioMode()) {
// All main tasks completed
if (this.gameState.gameMode === 'complete-all') {
@ -4449,9 +4423,6 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag
image: this.getRandomImage(false) // false = main task image
};
this.gameState.isConsequenceTask = false;
console.log(`Selected task ${selectedTask.id}: "${selectedTask.text}"`,
selectedTask.interactiveType ? `(Interactive: ${selectedTask.interactiveType})` : '(Regular)');
}
loadConsequenceTask() {
@ -4609,7 +4580,6 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag
// Check if this is an interactive task
if (this.interactiveTaskManager.isInteractiveTask(this.gameState.currentTask)) {
console.log('Displaying interactive task:', this.gameState.currentTask.interactiveType);
this.interactiveTaskManager.displayInteractiveTask(this.gameState.currentTask);
return; // Interactive task manager handles the rest
}
@ -4938,9 +4908,13 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag
this.flashMessageManager.pause();
// Stop all audio immediately when pausing
console.log('Game paused - stopping all audio');
this.audioManager.stopAllImmediate();
// Stop any ongoing TTS narration when pausing
if (this.interactiveTaskManager && this.interactiveTaskManager.stopTTS) {
this.interactiveTaskManager.stopTTS();
}
this.showScreen('paused-screen');
// Auto-save the paused game state
@ -4973,13 +4947,21 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag
}
// Stop all audio immediately when game ends - multiple attempts to ensure it stops
console.log('Game ended - stopping all audio immediately');
this.audioManager.stopAllImmediate();
// Stop any ongoing TTS narration
if (this.interactiveTaskManager && this.interactiveTaskManager.stopTTS) {
this.interactiveTaskManager.stopTTS();
}
// Additional safeguard - stop all audio again after a brief delay
setTimeout(() => {
console.log('Game ended - secondary audio stop to ensure silence');
this.audioManager.stopAllImmediate();
// Double-check TTS is stopped
if (this.interactiveTaskManager && this.interactiveTaskManager.stopTTS) {
this.interactiveTaskManager.stopTTS();
}
}, 100);
// Show photo gallery if any photos were taken during the game

View File

@ -222,7 +222,6 @@ class GameModeManager {
const mode = card.dataset.mode;
const radio = card.querySelector('input[type="radio"]');
if (radio && mode) {
console.log('🃏 Game mode card clicked:', mode);
radio.checked = true;
this.handleModeChange(mode);
}
@ -259,7 +258,6 @@ class GameModeManager {
* Handle mode selection change
*/
handleModeChange(mode) {
console.log(`🎮 Mode changed to: ${mode}`);
this.currentMode = mode;
// Show/hide mode-specific options
@ -272,7 +270,6 @@ class GameModeManager {
if (scenarioConfig) {
scenarioConfig.style.display = 'block';
}
console.log(`📋 Scenario mode activated - scenarios available: ${this.scenarioCollections[mode]?.length || 0}`);
}
// For timed and scored modes, show the original configuration UI
@ -343,16 +340,11 @@ class GameModeManager {
* Get tasks for scenario-based modes
*/
getScenarioModeTasks() {
console.log(`🎭 Getting tasks for scenario mode: ${this.currentMode}`);
// Use the new data manager to get mode-specific data
if (window.gameDataManager) {
console.log(`📋 Using GameDataManager for mode: ${this.currentMode}`);
const tasks = window.gameDataManager.getTasksForMode(this.currentMode);
const scenarios = window.gameDataManager.getScenariosForMode(this.currentMode);
console.log(`📋 Found ${tasks.length} tasks and ${scenarios.length} scenarios for ${this.currentMode}`);
// Combine tasks and scenarios into the format expected by the game
const allTasks = [];
@ -369,7 +361,6 @@ class GameModeManager {
image: this.getRandomScenarioImage()
};
allTasks.push(gameTask);
console.log(`✅ Added scenario: ${scenario.id} (${scenario.interactiveType})`);
});
// Add regular tasks AFTER scenarios
@ -387,15 +378,13 @@ class GameModeManager {
image: this.getRandomScenarioImage()
};
allTasks.push(gameTask);
console.log(`✅ Added task: ${task.id} (${task.interactiveType})`);
});
console.log(`🎯 Created ${allTasks.length} total tasks for ${this.currentMode}`);
return allTasks;
}
// Fallback to old system if data manager not available
console.log(`⚠️ GameDataManager not available, using fallback system`);
console.warn('GameDataManager not available, using fallback system');
return this.getScenarioModeTasksLegacy();
}
@ -497,17 +486,13 @@ class GameModeManager {
* Get random image from both task and consequence folders for scenario modes
*/
getRandomScenarioImage() {
console.log(`🖼️ Getting random scenario image from both folders`);
// Get images from both folders
const taskImages = window.gameData?.discoveredTaskImages || [];
const consequenceImages = window.gameData?.discoveredConsequenceImages || [];
const allImages = [...taskImages, ...consequenceImages];
console.log(`📁 Found ${taskImages.length} task images and ${consequenceImages.length} consequence images`);
if (allImages.length === 0) {
console.log(`⚠️ No images found, creating placeholder`);
console.warn('No images found, creating placeholder');
return this.createPlaceholderImage('Scenario Image');
}
@ -530,7 +515,6 @@ class GameModeManager {
// Convert to displayable format
const displayImage = this.getImageSrc(selectedImage);
console.log(`🎯 Selected scenario image: ${typeof selectedImage === 'string' ? selectedImage : selectedImage.originalName}`);
return displayImage;
}

View File

@ -406,6 +406,7 @@ const trainingGameData = {
}
}
},
/*
{
id: 'scenario-training-session',
text: "Quick Endurance Assessment",
@ -479,6 +480,7 @@ const trainingGameData = {
}
}
}
*/
],
// Configuration for gooner training mode

View File

@ -1,5 +1,5 @@
/**
* Audio Manager - Centralized Audio System for Edge & Punishment
* Audio Manager - Centralized Audio System for Gooner Training Academy
* Handles multiple audio categories with volume controls and fade effects
*/
class AudioManager {

View File

@ -1,5 +1,5 @@
/**
* AI Task Manager - Ollama Integration for Edge & Punishment
* AI Task Manager - Ollama Integration for Gooner Training Academy
* Generates NSFW edging tasks using local AI models
*/
class AITaskManager {

View File

@ -10,11 +10,39 @@ class InteractiveTaskManager {
this.taskContainer = null;
this.isInteractiveTaskActive = false;
// Initialize TTS voice manager (will be loaded when needed)
this.voiceManager = null;
this.ttsEnabled = false;
this.initializeTTS();
// Task type registry
this.taskTypes = new Map();
this.registerBuiltInTaskTypes();
}
/**
* Initialize TTS system
*/
async initializeTTS() {
try {
// Check if VoiceManager is available globally
if (window.VoiceManager) {
this.voiceManager = new window.VoiceManager();
if (this.voiceManager.isSupported()) {
await this.voiceManager.initialize();
this.ttsEnabled = true;
console.log('🎤 TTS initialized with voice:', this.voiceManager.getVoiceInfo().name);
} else {
console.warn('TTS not supported in this environment');
}
} else {
console.log('VoiceManager not available, TTS disabled');
}
} catch (error) {
console.error('Failed to initialize TTS:', error);
}
}
/**
* Register all built-in interactive task types
*/
@ -73,32 +101,24 @@ class InteractiveTaskManager {
* Display an interactive task
*/
async displayInteractiveTask(task) {
console.log(`🔍 InteractiveTaskManager received task:`, task);
console.log(`🔍 Task interactiveType:`, task.interactiveType);
console.log(`🔍 Available task types:`, Array.from(this.taskTypes.keys()));
if (!this.isInteractiveTask(task)) {
console.warn('Task is not interactive:', task);
return false;
}
console.log(`Displaying interactive task: ${task.interactiveType}`);
this.currentInteractiveTask = task;
this.isInteractiveTaskActive = true;
// Handle task image for interactive tasks
const taskImage = document.getElementById('task-image');
if (taskImage && task.image) {
console.log(`🖼️ Setting interactive task image: ${task.image}`);
taskImage.src = task.image;
taskImage.onerror = () => {
console.log('Interactive task image failed to load:', task.image);
console.warn('Interactive task image failed to load:', task.image);
// Create placeholder if image fails
taskImage.src = this.createPlaceholderImage('Interactive Task');
};
} else if (taskImage) {
console.log(`⚠️ No image provided for interactive task, creating placeholder`);
taskImage.src = this.createPlaceholderImage('Interactive Task');
}
@ -334,11 +354,15 @@ class InteractiveTaskManager {
cleanupInteractiveTask() {
// Stop any audio that might be playing before cleanup
if (this.game && this.game.audioManager) {
console.log('Interactive task cleanup - stopping audio');
this.game.audioManager.stopCategory('tasks', 0);
this.game.audioManager.stopCategory('punishments', 0);
}
// Stop any ongoing TTS narration
if (this.voiceManager) {
this.stopTTS();
}
// Remove interactive container
const container = document.getElementById('interactive-task-container');
if (container) {
@ -1585,6 +1609,15 @@ class InteractiveTaskManager {
<div class="scenario-progress">
Step <span id="scenario-step">1</span> of <span id="scenario-total">${this.getScenarioStepCount(scenario)}</span>
</div>
<div class="scenario-controls">
<button id="tts-toggle" class="tts-control ${this.ttsEnabled ? 'enabled' : 'disabled'}"
title="Toggle text-to-speech narration">
🎤 ${this.ttsEnabled ? 'TTS On' : 'TTS Off'}
</button>
<button id="tts-stop" class="tts-control" title="Stop current narration" style="display: none;">
Stop
</button>
</div>
</div>
<div class="scenario-content">
@ -1598,6 +1631,7 @@ class InteractiveTaskManager {
<span class="stat">Control: <span id="scenario-control">Unknown</span></span>
<span class="stat">Intensity: <span id="scenario-intensity">Low</span></span>
</div>
<div class="tts-info" id="tts-info" style="font-size: 12px; color: #888; margin-top: 10px;"></div>
</div>
</div>
`;
@ -1620,8 +1654,6 @@ class InteractiveTaskManager {
return;
}
console.log('✅ Initializing scenario for task:', task.id, 'with scenario:', scenario);
// Scenario state tracking
task.scenarioState = {
currentStep: 'start',
@ -1637,6 +1669,54 @@ class InteractiveTaskManager {
// Start the scenario
this.displayScenarioStep(task, scenario, 'start');
// Setup TTS controls
this.setupTTSControls();
}
/**
* Setup TTS control event handlers
*/
setupTTSControls() {
const ttsToggle = document.getElementById('tts-toggle');
const ttsStop = document.getElementById('tts-stop');
const ttsInfo = document.getElementById('tts-info');
// TTS toggle button
if (ttsToggle) {
ttsToggle.addEventListener('click', () => {
const newState = this.toggleTTS();
ttsToggle.textContent = `🎤 ${newState ? 'TTS On' : 'TTS Off'}`;
ttsToggle.className = `tts-control ${newState ? 'enabled' : 'disabled'}`;
this.updateTTSInfo();
});
}
// TTS stop button
if (ttsStop) {
ttsStop.addEventListener('click', () => {
this.stopTTS();
});
}
// Display current TTS info
this.updateTTSInfo();
}
/**
* Update TTS information display
*/
updateTTSInfo() {
const ttsInfo = document.getElementById('tts-info');
if (!ttsInfo) return;
const info = this.getTTSInfo();
if (info.available) {
const voiceInfo = info.voiceInfo;
ttsInfo.innerHTML = `🎤 Voice: ${voiceInfo.name} (${voiceInfo.platform})`;
} else {
ttsInfo.innerHTML = '🎤 TTS not available';
}
}
displayScenarioStep(task, scenario, stepId) {
@ -1646,6 +1726,11 @@ class InteractiveTaskManager {
return;
}
// Stop any previous TTS narration before displaying new step
if (this.voiceManager) {
this.stopTTS();
}
// Check if scenario interface elements exist (safety check)
const storyEl = document.getElementById('scenario-story');
const choicesEl = document.getElementById('scenario-choices');
@ -1677,6 +1762,13 @@ class InteractiveTaskManager {
</div>
`;
// Speak the story text using TTS
this.speakScenarioText(step.story, task.scenarioState, {
onComplete: () => {
// Story narration complete - could add visual feedback here if needed
}
});
// Clear previous choices
choicesEl.innerHTML = '';
@ -1773,6 +1865,11 @@ class InteractiveTaskManager {
task.scenarioState.completed = true;
task.scenarioState.outcome = step.outcome;
// Stop any ongoing TTS narration when scenario ends
if (this.voiceManager) {
this.stopTTS();
}
const endingDiv = document.createElement('div');
endingDiv.className = 'scenario-ending';
endingDiv.innerHTML = `
@ -2240,6 +2337,85 @@ class InteractiveTaskManager {
.replace(/\{intensity\}/g, this.getIntensityText(state.intensity));
}
/**
* Speak scenario text using TTS with cross-platform voice
*/
speakScenarioText(text, state, options = {}) {
if (!this.ttsEnabled || !text) {
return;
}
// Process the text to replace placeholders
const processedText = this.processScenarioText(text, state);
// Clean up HTML tags and excessive whitespace for better speech
const cleanText = processedText
.replace(/<[^>]*>/g, ' ') // Remove HTML tags
.replace(/\s+/g, ' ') // Collapse whitespace
.trim();
if (!cleanText) {
return;
}
// Default TTS settings optimized for scenario narration
const ttsOptions = {
rate: options.rate || 0.9, // Slightly slower for dramatic effect
pitch: options.pitch || 1.1, // Slightly higher for feminine voice
volume: options.volume || 0.8, // Not too loud
onStart: () => {
// Optional: could add subtle visual indicator
},
onEnd: () => {
if (options.onComplete) options.onComplete();
},
onError: (error) => {
console.warn('TTS error:', error.message || error);
if (options.onError) options.onError(error);
}
};
// Speak the text
this.voiceManager.speak(cleanText, ttsOptions);
}
/**
* Stop any current TTS playback
*/
stopTTS() {
if (this.voiceManager) {
this.voiceManager.stop();
}
}
/**
* Toggle TTS on/off
*/
toggleTTS() {
this.ttsEnabled = !this.ttsEnabled;
if (!this.ttsEnabled) {
this.stopTTS();
}
return this.ttsEnabled;
}
/**
* Get current TTS voice information
*/
getTTSInfo() {
if (!this.voiceManager) {
return { available: false, reason: 'Voice manager not initialized' };
}
return {
available: this.ttsEnabled,
voiceInfo: this.voiceManager.getVoiceInfo(),
supported: this.voiceManager.isSupported()
};
}
getScenarioStepCount(scenario) {
return Object.keys(scenario.steps || {}).length;
}

View File

@ -0,0 +1,215 @@
/**
* Cross-platform TTS Voice Manager for Electron
* Handles voice selection with platform-specific fallbacks
*/
class VoiceManager {
constructor() {
this.synth = window.speechSynthesis;
this.selectedVoice = null;
this.fallbackVoices = this.getPlatformFallbacks();
}
/**
* Get platform-specific fallback voice names in order of preference
*/
getPlatformFallbacks() {
const platform = this.detectPlatform();
switch (platform) {
case 'windows':
return [
'Microsoft Zira Desktop', 'Microsoft Zira', // Windows default female
'Microsoft Hazel Desktop', 'Microsoft Hazel', // Alternative
'Microsoft Eva Desktop', 'Microsoft Eva', // Another option
'Microsoft Aria', 'Microsoft Jenny', // Newer voices if available
'Zira', 'Hazel', 'Eva', 'Aria', 'Jenny' // Short names as fallback
];
case 'mac':
return [
'Samantha', // macOS default female voice
'Victoria', // Alternative female
'Karen', // Another option
'Fiona', // Additional choice
'Moira', // Backup
'Tessa', // Last resort
'Alex' // Can work as female with pitch adjustment
];
default:
return [
'Samantha', 'Zira', // Try both platform defaults
'Victoria', 'Hazel', // Alternatives
'Karen', 'Eva', // More options
'female', 'woman' // Generic indicators
];
}
}
/**
* Detect current platform
*/
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';
}
/**
* Check if a voice is female
*/
isFemaleVoice(voice) {
const name = voice.name.toLowerCase();
// Platform-specific female voice names
const femaleVoiceNames = [
// Windows voices
'zira', 'hazel', 'eva', 'aria', 'jenny',
// macOS voices
'samantha', 'victoria', 'karen', 'fiona', 'moira', 'tessa',
// Generic indicators
'female', 'woman', 'girl', 'lady'
];
return femaleVoiceNames.some(femaleName => name.includes(femaleName));
}
/**
* Get all available female voices
*/
getAvailableFemaleVoices() {
const voices = this.synth.getVoices();
return voices.filter(voice =>
voice.lang.startsWith('en') && this.isFemaleVoice(voice)
);
}
/**
* Select the best available female voice for this platform
*/
selectBestVoice() {
const availableVoices = this.getAvailableFemaleVoices();
if (availableVoices.length === 0) {
console.warn('No female voices available');
return null;
}
// Try to find voices in order of preference
for (const preferredName of this.fallbackVoices) {
const voice = availableVoices.find(v =>
v.name.toLowerCase().includes(preferredName.toLowerCase())
);
if (voice) {
this.selectedVoice = voice;
return voice;
}
}
// If no preferred voice found, use the first available female voice
this.selectedVoice = availableVoices[0];
return this.selectedVoice;
}
/**
* Initialize voice selection (call after voices are loaded)
*/
async initialize() {
return new Promise((resolve) => {
// Wait for voices to be loaded
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 with the selected voice
*/
speak(text, options = {}) {
if (!this.selectedVoice) {
this.selectBestVoice();
}
const utterance = new SpeechSynthesisUtterance(text);
// Apply voice
if (this.selectedVoice) {
utterance.voice = this.selectedVoice;
}
// Apply settings with defaults
utterance.rate = options.rate || 1.0;
utterance.pitch = options.pitch || 1.0;
utterance.volume = options.volume || 1.0;
// Platform-specific pitch adjustments for better female sound
if (this.selectedVoice) {
const voiceName = this.selectedVoice.name.toLowerCase();
// Adjust pitch for Alex on Mac to sound more feminine
if (voiceName.includes('alex')) {
utterance.pitch = Math.max(utterance.pitch * 1.2, 1.5);
}
}
// Add event listeners if provided
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;
}
/**
* Get current voice information
*/
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 current speech
*/
stop() {
this.synth.cancel();
}
/**
* Check if speech synthesis is supported
*/
isSupported() {
return 'speechSynthesis' in window;
}
}
// Export for use in other modules
window.VoiceManager = VoiceManager;

View File

@ -154,7 +154,6 @@ class FlashMessageManager {
this.isActive = true;
this.isPaused = false;
this.scheduleNext();
console.log('Flash message system started');
}
stop() {
@ -247,8 +246,6 @@ class FlashMessageManager {
setTimeout(() => {
this.hideCurrentMessage(tempConfig.animation);
}, tempConfig.displayDuration);
console.log(`Showed flash message: "${messageObj.text || messageObj}"`);
}
applyShowAnimation(animationType = 'fade') {

File diff suppressed because it is too large Load Diff

509
title-color-variations.html Normal file
View File

@ -0,0 +1,509 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Option 4 - Color Variations & Game Palettes</title>
<link href="https://fonts.googleapis.com/css2?family=Audiowide&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%);
font-family: Arial, sans-serif;
min-height: 100vh;
padding: 20px;
color: #fff;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.intro {
text-align: center;
margin-bottom: 40px;
}
.intro h1 {
font-size: 2.5rem;
margin-bottom: 10px;
color: #fff;
}
.color-option {
background: rgba(0, 0, 0, 0.4);
border: 1px solid #333;
border-radius: 15px;
padding: 30px;
margin: 30px 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
align-items: center;
}
.title-section {
text-align: center;
}
.option-number {
display: inline-block;
background: #e74c3c;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 18px;
margin-bottom: 20px;
margin: 0 auto 20px;
}
.title {
font-family: 'Audiowide', cursive;
font-size: 2.8rem;
letter-spacing: 1px;
margin-bottom: 10px;
}
.tagline {
font-family: 'Audiowide', cursive;
font-size: 14px;
margin-bottom: 15px;
}
.palette-section h3 {
margin-bottom: 20px;
font-size: 1.4rem;
text-align: center;
}
.color-palette {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.color-swatch {
text-align: center;
padding: 15px 10px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.2);
}
.color-swatch .color-name {
font-size: 12px;
font-weight: bold;
margin-bottom: 5px;
}
.color-swatch .color-code {
font-size: 10px;
opacity: 0.8;
font-family: monospace;
}
.usage-info {
font-size: 13px;
line-height: 1.6;
background: rgba(0,0,0,0.3);
padding: 15px;
border-radius: 8px;
border-left: 3px solid;
}
/* Color Scheme 1 - Original Pink/Orange */
.scheme-1 .title {
background: linear-gradient(45deg, #ff0080, #ff8000);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(255, 0, 128, 0.7);
animation: glow1 2s ease-in-out infinite alternate;
}
.scheme-1 .tagline { color: #ff4d4d; }
.scheme-1 .usage-info { border-left-color: #ff0080; }
@keyframes glow1 {
from { filter: drop-shadow(0 0 5px rgba(255, 0, 128, 0.5)); }
to { filter: drop-shadow(0 0 15px rgba(255, 0, 128, 0.9)); }
}
/* Color Scheme 2 - Electric Blue/Cyan */
.scheme-2 .title {
background: linear-gradient(45deg, #00bfff, #1e90ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(0, 191, 255, 0.7);
animation: glow2 2s ease-in-out infinite alternate;
}
.scheme-2 .tagline { color: #4db8ff; }
.scheme-2 .usage-info { border-left-color: #00bfff; }
@keyframes glow2 {
from { filter: drop-shadow(0 0 5px rgba(0, 191, 255, 0.5)); }
to { filter: drop-shadow(0 0 15px rgba(0, 191, 255, 0.9)); }
}
/* Color Scheme 3 - Purple/Violet */
.scheme-3 .title {
background: linear-gradient(45deg, #8a2be2, #da70d6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(138, 43, 226, 0.7);
animation: glow3 2s ease-in-out infinite alternate;
}
.scheme-3 .tagline { color: #ba55d3; }
.scheme-3 .usage-info { border-left-color: #8a2be2; }
@keyframes glow3 {
from { filter: drop-shadow(0 0 5px rgba(138, 43, 226, 0.5)); }
to { filter: drop-shadow(0 0 15px rgba(138, 43, 226, 0.9)); }
}
/* Color Scheme 4 - Green/Lime */
.scheme-4 .title {
background: linear-gradient(45deg, #00ff00, #32cd32);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(0, 255, 0, 0.7);
animation: glow4 2s ease-in-out infinite alternate;
}
.scheme-4 .tagline { color: #7fff00; }
.scheme-4 .usage-info { border-left-color: #00ff00; }
@keyframes glow4 {
from { filter: drop-shadow(0 0 5px rgba(0, 255, 0, 0.5)); }
to { filter: drop-shadow(0 0 15px rgba(0, 255, 0, 0.9)); }
}
/* Color Scheme 5 - Red/Crimson */
.scheme-5 .title {
background: linear-gradient(45deg, #dc143c, #ff6347);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(220, 20, 60, 0.7);
animation: glow5 2s ease-in-out infinite alternate;
}
.scheme-5 .tagline { color: #ff4757; }
.scheme-5 .usage-info { border-left-color: #dc143c; }
@keyframes glow5 {
from { filter: drop-shadow(0 0 5px rgba(220, 20, 60, 0.5)); }
to { filter: drop-shadow(0 0 15px rgba(220, 20, 60, 0.9)); }
}
/* Color Scheme 6 - Gold/Amber */
.scheme-6 .title {
background: linear-gradient(45deg, #ffd700, #ffa500);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(255, 215, 0, 0.7);
animation: glow6 2s ease-in-out infinite alternate;
}
.scheme-6 .tagline { color: #ffb347; }
.scheme-6 .usage-info { border-left-color: #ffd700; }
@keyframes glow6 {
from { filter: drop-shadow(0 0 5px rgba(255, 215, 0, 0.5)); }
to { filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.9)); }
}
@media (max-width: 768px) {
.color-option {
grid-template-columns: 1fr;
gap: 20px;
}
.title {
font-size: 2rem !important;
}
.color-palette {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
}
}
</style>
</head>
<body>
<div class="container">
<div class="intro">
<h1>Option 4: Gaming Aesthetic - Color Variations</h1>
<p>Audiowide font with different color schemes and complete game palettes</p>
</div>
<!-- Color Scheme 1 - Original Pink/Orange -->
<div class="color-option scheme-1">
<div class="title-section">
<div class="option-number">A</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
</div>
<div class="palette-section">
<h3>🌈 Pink/Orange Gaming Palette</h3>
<div class="color-palette">
<div class="color-swatch" style="background: #ff0080; color: white;">
<div class="color-name">Primary</div>
<div class="color-code">#ff0080</div>
</div>
<div class="color-swatch" style="background: #ff8000; color: white;">
<div class="color-name">Secondary</div>
<div class="color-code">#ff8000</div>
</div>
<div class="color-swatch" style="background: #ff4d4d; color: white;">
<div class="color-name">Accent</div>
<div class="color-code">#ff4d4d</div>
</div>
<div class="color-swatch" style="background: #2d2d2d; color: white;">
<div class="color-name">Dark BG</div>
<div class="color-code">#2d2d2d</div>
</div>
<div class="color-swatch" style="background: #1a1a1a; color: white;">
<div class="color-name">Darker BG</div>
<div class="color-code">#1a1a1a</div>
</div>
<div class="color-swatch" style="background: #ff0080; opacity: 0.1; color: white;">
<div class="color-name">Glow</div>
<div class="color-code">rgba(255,0,128,0.1)</div>
</div>
</div>
<div class="usage-info">
<strong>Game Implementation:</strong> High-energy gaming vibe with pink primary for buttons/highlights, orange for progress bars/completion states, dark backgrounds for contrast. Perfect for an intense, exciting gaming experience.
</div>
</div>
</div>
<!-- Color Scheme 2 - Electric Blue/Cyan -->
<div class="color-option scheme-2">
<div class="title-section">
<div class="option-number">B</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
</div>
<div class="palette-section">
<h3>💙 Electric Blue/Cyan Tech Palette</h3>
<div class="color-palette">
<div class="color-swatch" style="background: #00bfff; color: white;">
<div class="color-name">Primary</div>
<div class="color-code">#00bfff</div>
</div>
<div class="color-swatch" style="background: #1e90ff; color: white;">
<div class="color-name">Secondary</div>
<div class="color-code">#1e90ff</div>
</div>
<div class="color-swatch" style="background: #4db8ff; color: white;">
<div class="color-name">Accent</div>
<div class="color-code">#4db8ff</div>
</div>
<div class="color-swatch" style="background: #0f2027; color: white;">
<div class="color-name">Dark BG</div>
<div class="color-code">#0f2027</div>
</div>
<div class="color-swatch" style="background: #203a43; color: white;">
<div class="color-name">Mid BG</div>
<div class="color-code">#203a43</div>
</div>
<div class="color-swatch" style="background: #00ffff; opacity: 0.15; color: white;">
<div class="color-name">Glow</div>
<div class="color-code">rgba(0,255,255,0.15)</div>
</div>
</div>
<div class="usage-info">
<strong>Game Implementation:</strong> Professional tech aesthetic with blue primary for navigation/buttons, cyan for active states/progress, dark blue-grey backgrounds. Creates a sophisticated, high-tech training environment feel.
</div>
</div>
</div>
<!-- Color Scheme 3 - Purple/Violet -->
<div class="color-option scheme-3">
<div class="title-section">
<div class="option-number">C</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
</div>
<div class="palette-section">
<h3>💜 Purple/Violet Luxe Palette</h3>
<div class="color-palette">
<div class="color-swatch" style="background: #8a2be2; color: white;">
<div class="color-name">Primary</div>
<div class="color-code">#8a2be2</div>
</div>
<div class="color-swatch" style="background: #da70d6; color: white;">
<div class="color-name">Secondary</div>
<div class="color-code">#da70d6</div>
</div>
<div class="color-swatch" style="background: #ba55d3; color: white;">
<div class="color-name">Accent</div>
<div class="color-code">#ba55d3</div>
</div>
<div class="color-swatch" style="background: #1a0d26; color: white;">
<div class="color-name">Dark BG</div>
<div class="color-code">#1a0d26</div>
</div>
<div class="color-swatch" style="background: #2d1b3d; color: white;">
<div class="color-name">Mid BG</div>
<div class="color-code">#2d1b3d</div>
</div>
<div class="color-swatch" style="background: #8a2be2; opacity: 0.12; color: white;">
<div class="color-name">Glow</div>
<div class="color-code">rgba(138,43,226,0.12)</div>
</div>
</div>
<div class="usage-info">
<strong>Game Implementation:</strong> Luxurious, premium feel with purple primary for main actions, orchid for highlights/hover states, deep purple backgrounds. Creates an exclusive, high-end academy atmosphere.
</div>
</div>
</div>
<!-- Color Scheme 4 - Green/Lime -->
<div class="color-option scheme-4">
<div class="title-section">
<div class="option-number">D</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
</div>
<div class="palette-section">
<h3>💚 Green/Lime Matrix Palette</h3>
<div class="color-palette">
<div class="color-swatch" style="background: #00ff00; color: black;">
<div class="color-name">Primary</div>
<div class="color-code">#00ff00</div>
</div>
<div class="color-swatch" style="background: #32cd32; color: white;">
<div class="color-name">Secondary</div>
<div class="color-code">#32cd32</div>
</div>
<div class="color-swatch" style="background: #7fff00; color: black;">
<div class="color-name">Accent</div>
<div class="color-code">#7fff00</div>
</div>
<div class="color-swatch" style="background: #0d1a0d; color: white;">
<div class="color-name">Dark BG</div>
<div class="color-code">#0d1a0d</div>
</div>
<div class="color-swatch" style="background: #1b2d1b; color: white;">
<div class="color-name">Mid BG</div>
<div class="color-code">#1b2d1b</div>
</div>
<div class="color-swatch" style="background: #00ff00; opacity: 0.08; color: white;">
<div class="color-name">Glow</div>
<div class="color-code">rgba(0,255,0,0.08)</div>
</div>
</div>
<div class="usage-info">
<strong>Game Implementation:</strong> Matrix/hacker aesthetic with bright green primary for success states/progress, lime for active elements, very dark green backgrounds. Perfect for a digital training simulation vibe.
</div>
</div>
</div>
<!-- Color Scheme 5 - Red/Crimson -->
<div class="color-option scheme-5">
<div class="title-section">
<div class="option-number">E</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
</div>
<div class="palette-section">
<h3>❤️ Red/Crimson Power Palette</h3>
<div class="color-palette">
<div class="color-swatch" style="background: #dc143c; color: white;">
<div class="color-name">Primary</div>
<div class="color-code">#dc143c</div>
</div>
<div class="color-swatch" style="background: #ff6347; color: white;">
<div class="color-name">Secondary</div>
<div class="color-code">#ff6347</div>
</div>
<div class="color-swatch" style="background: #ff4757; color: white;">
<div class="color-name">Accent</div>
<div class="color-code">#ff4757</div>
</div>
<div class="color-swatch" style="background: #1a0a0a; color: white;">
<div class="color-name">Dark BG</div>
<div class="color-code">#1a0a0a</div>
</div>
<div class="color-swatch" style="background: #2d1515; color: white;">
<div class="color-name">Mid BG</div>
<div class="color-code">#2d1515</div>
</div>
<div class="color-swatch" style="background: #dc143c; opacity: 0.1; color: white;">
<div class="color-name">Glow</div>
<div class="color-code">rgba(220,20,60,0.1)</div>
</div>
</div>
<div class="usage-info">
<strong>Game Implementation:</strong> Bold, intense atmosphere with crimson primary for warnings/important actions, coral for completion states, dark red backgrounds. Creates a powerful, commanding training environment.
</div>
</div>
</div>
<!-- Color Scheme 6 - Gold/Amber -->
<div class="color-option scheme-6">
<div class="title-section">
<div class="option-number">F</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
</div>
<div class="palette-section">
<h3>🏆 Gold/Amber Elite Palette</h3>
<div class="color-palette">
<div class="color-swatch" style="background: #ffd700; color: black;">
<div class="color-name">Primary</div>
<div class="color-code">#ffd700</div>
</div>
<div class="color-swatch" style="background: #ffa500; color: white;">
<div class="color-name">Secondary</div>
<div class="color-code">#ffa500</div>
</div>
<div class="color-swatch" style="background: #ffb347; color: black;">
<div class="color-name">Accent</div>
<div class="color-code">#ffb347</div>
</div>
<div class="color-swatch" style="background: #1a1a0d; color: white;">
<div class="color-name">Dark BG</div>
<div class="color-code">#1a1a0d</div>
</div>
<div class="color-swatch" style="background: #2d2d1b; color: white;">
<div class="color-name">Mid BG</div>
<div class="color-code">#2d2d1b</div>
</div>
<div class="color-swatch" style="background: #ffd700; opacity: 0.08; color: white;">
<div class="color-name">Glow</div>
<div class="color-code">rgba(255,215,0,0.08)</div>
</div>
</div>
<div class="usage-info">
<strong>Game Implementation:</strong> Premium, achievement-focused design with gold primary for rewards/completion, orange for progress indicators, warm dark backgrounds. Perfect for highlighting accomplishments and elite status.
</div>
</div>
</div>
<div style="background: rgba(0, 0, 0, 0.6); border: 2px solid #444; border-radius: 15px; padding: 30px; margin: 40px 0; text-align: center;">
<h2 style="margin-bottom: 15px; font-size: 24px;">🎨 Choose Your Academy's Identity!</h2>
<p style="font-size: 16px; line-height: 1.6; color: #ccc;">Each color scheme creates a completely different atmosphere for your training academy. Consider what mood and personality best fits your vision:</p>
<ul style="text-align: left; max-width: 600px; margin: 20px auto; color: #bbb;">
<li><strong>A - Pink/Orange:</strong> High-energy gaming excitement</li>
<li><strong>B - Blue/Cyan:</strong> Professional tech sophistication</li>
<li><strong>C - Purple/Violet:</strong> Luxurious premium experience</li>
<li><strong>D - Green/Lime:</strong> Matrix-style digital simulation</li>
<li><strong>E - Red/Crimson:</strong> Bold commanding intensity</li>
<li><strong>F - Gold/Amber:</strong> Elite achievement-focused</li>
</ul>
<p style="margin-top: 20px;"><strong>Just tell me which letter you prefer!</strong></p>
</div>
</div>
</body>
</html>

385
title-design-test.html Normal file
View File

@ -0,0 +1,385 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gooner Training Academy - Title Design Options</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Exo+2:wght@300;400;600;700;800&family=Rajdhani:wght@300;400;500;600;700&family=Russo+One&family=Bebas+Neue&family=Audiowide&family=Saira+Condensed:wght@300;400;500;600;700&family=Titillium+Web:wght@300;400;600;700;900&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%);
font-family: Arial, sans-serif;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.intro {
text-align: center;
color: #fff;
margin-bottom: 40px;
}
.design-option {
background: rgba(0, 0, 0, 0.3);
border: 1px solid #333;
border-radius: 10px;
padding: 30px;
margin: 20px 0;
text-align: center;
position: relative;
}
.option-number {
position: absolute;
top: 10px;
left: 15px;
background: #e74c3c;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
}
.tagline {
margin-top: 10px;
font-size: 16px;
opacity: 0.8;
}
/* Design Option 1 - Futuristic Tech */
.option-1 .title {
font-family: 'Orbitron', monospace;
font-size: 3.5rem;
font-weight: 900;
background: linear-gradient(45deg, #00ffff, #0080ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 20px rgba(0, 255, 255, 0.5);
letter-spacing: 3px;
}
.option-1 .tagline {
font-family: 'Orbitron', monospace;
color: #00bfff;
font-weight: 400;
letter-spacing: 2px;
}
/* Design Option 2 - Military Academy */
.option-2 .title {
font-family: 'Bebas Neue', cursive;
font-size: 4rem;
color: #ff6b35;
text-shadow:
2px 2px 0px #000,
4px 4px 0px #333,
6px 6px 10px rgba(0,0,0,0.8);
letter-spacing: 4px;
transform: perspective(500px) rotateX(10deg);
}
.option-2 .tagline {
font-family: 'Rajdhani', sans-serif;
color: #ffa500;
font-weight: 600;
font-size: 18px;
letter-spacing: 1px;
}
/* Design Option 3 - Sleek Modern */
.option-3 .title {
font-family: 'Exo 2', sans-serif;
font-size: 3.8rem;
font-weight: 800;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 2px;
}
.option-3 .tagline {
font-family: 'Exo 2', sans-serif;
color: #8e94f2;
font-weight: 300;
font-style: italic;
}
/* Design Option 4 - Gaming Aesthetic */
.option-4 .title {
font-family: 'Audiowide', cursive;
font-size: 3.2rem;
background: linear-gradient(45deg, #ff0080, #ff8000);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(255, 0, 128, 0.7);
letter-spacing: 1px;
animation: glow 2s ease-in-out infinite alternate;
}
.option-4 .tagline {
font-family: 'Audiowide', cursive;
color: #ff4d4d;
font-size: 15px;
}
@keyframes glow {
from { filter: drop-shadow(0 0 5px rgba(255, 0, 128, 0.5)); }
to { filter: drop-shadow(0 0 15px rgba(255, 0, 128, 0.9)); }
}
/* Design Option 5 - Professional Bold */
.option-5 .title {
font-family: 'Russo One', sans-serif;
font-size: 3.6rem;
color: #2ecc71;
text-shadow:
1px 1px 0px #000,
2px 2px 0px #1a5c3a,
3px 3px 5px rgba(0,0,0,0.5);
letter-spacing: 2px;
}
.option-5 .tagline {
font-family: 'Titillium Web', sans-serif;
color: #27ae60;
font-weight: 600;
}
/* Design Option 6 - Elegant Minimalist */
.option-6 .title {
font-family: 'Saira Condensed', sans-serif;
font-size: 4.2rem;
font-weight: 700;
color: #ecf0f1;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
letter-spacing: 1px;
}
.option-6 .tagline {
font-family: 'Saira Condensed', sans-serif;
color: #bdc3c7;
font-weight: 400;
font-size: 18px;
}
/* Design Option 7 - Neon Cyberpunk */
.option-7 .title {
font-family: 'Rajdhani', sans-serif;
font-size: 3.4rem;
font-weight: 700;
color: #ff073a;
text-shadow:
0 0 5px #ff073a,
0 0 10px #ff073a,
0 0 15px #ff073a,
0 0 20px #ff073a;
letter-spacing: 3px;
animation: neonFlicker 3s infinite;
}
.option-7 .tagline {
font-family: 'Rajdhani', sans-serif;
color: #ff1744;
font-weight: 500;
}
@keyframes neonFlicker {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
/* Design Option 8 - Gold Luxury */
.option-8 .title {
font-family: 'Titillium Web', sans-serif;
font-size: 3.7rem;
font-weight: 900;
background: linear-gradient(45deg, #ffd700, #ffed4e, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
letter-spacing: 2px;
}
.option-8 .tagline {
font-family: 'Titillium Web', sans-serif;
color: #f39c12;
font-weight: 600;
font-style: italic;
}
/* Design Option 9 - Purple Power */
.option-9 .title {
font-family: 'Exo 2', sans-serif;
font-size: 3.5rem;
font-weight: 800;
background: linear-gradient(135deg, #8e2de2, #4a00e0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 20px rgba(142, 45, 226, 0.6);
letter-spacing: 1px;
}
.option-9 .tagline {
font-family: 'Exo 2', sans-serif;
color: #9d4edd;
font-weight: 400;
}
/* Design Option 10 - Fire Theme */
.option-10 .title {
font-family: 'Bebas Neue', cursive;
font-size: 4rem;
background: linear-gradient(45deg, #ff4500, #ff6347, #ff0000);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 25px rgba(255, 69, 0, 0.8);
letter-spacing: 3px;
animation: fireGlow 2.5s ease-in-out infinite alternate;
}
.option-10 .tagline {
font-family: 'Rajdhani', sans-serif;
color: #ff6b47;
font-weight: 600;
}
@keyframes fireGlow {
from { filter: drop-shadow(0 0 5px rgba(255, 69, 0, 0.6)); }
to { filter: drop-shadow(0 0 20px rgba(255, 69, 0, 1)); }
}
.description {
margin-top: 15px;
font-size: 14px;
color: #999;
font-style: italic;
}
.vote-section {
background: rgba(0, 0, 0, 0.5);
border: 2px solid #444;
border-radius: 15px;
padding: 30px;
margin: 40px 0;
text-align: center;
}
.vote-section h2 {
color: #fff;
margin-bottom: 15px;
font-size: 24px;
}
.vote-section p {
color: #ccc;
font-size: 16px;
line-height: 1.6;
}
@media (max-width: 768px) {
.title {
font-size: 2.5rem !important;
}
.tagline {
font-size: 14px !important;
}
}
</style>
</head>
<body>
<div class="container">
<div class="intro">
<h1>Gooner Training Academy - Title Design Options</h1>
<p>Choose your favorite design for the main header!</p>
</div>
<div class="design-option option-1">
<div class="option-number">1</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
<div class="description">Futuristic Tech - Orbitron font with cyan gradients and glow effects</div>
</div>
<div class="design-option option-2">
<div class="option-number">2</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
<div class="description">Military Academy - Bebas Neue with orange theme and 3D perspective</div>
</div>
<div class="design-option option-3">
<div class="option-number">3</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
<div class="description">Sleek Modern - Exo 2 font with purple-blue gradients</div>
</div>
<div class="design-option option-4">
<div class="option-number">4</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
<div class="description">Gaming Aesthetic - Audiowide font with pink-orange gradients and glow animation</div>
</div>
<div class="design-option option-5">
<div class="option-number">5</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
<div class="description">Professional Bold - Russo One font with green theme and layered shadows</div>
</div>
<div class="design-option option-6">
<div class="option-number">6</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
<div class="description">Elegant Minimalist - Saira Condensed with clean white text and subtle shadows</div>
</div>
<div class="design-option option-7">
<div class="option-number">7</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
<div class="description">Neon Cyberpunk - Rajdhani font with red neon glow and flicker animation</div>
</div>
<div class="design-option option-8">
<div class="option-number">8</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
<div class="description">Gold Luxury - Titillium Web with gold gradients for premium feel</div>
</div>
<div class="design-option option-9">
<div class="option-number">9</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
<div class="description">Purple Power - Exo 2 font with purple gradients and ethereal glow</div>
</div>
<div class="design-option option-10">
<div class="option-number">10</div>
<h1 class="title">GOONER TRAINING ACADEMY</h1>
<p class="tagline">Master Your Dedication</p>
<div class="description">Fire Theme - Bebas Neue with red-orange fire gradients and pulse animation</div>
</div>
<div class="vote-section">
<h2>🎯 Your Choice Matters!</h2>
<p>Each design option has its own personality and vibe. Consider which one best represents the sophisticated training platform you've built with interactive scenarios, TTS narration, and advanced focus challenges.</p>
<p><strong>Just tell me which number(s) you prefer!</strong></p>
</div>
</div>
</body>
</html>

358
tts-integration-test.html Normal file
View File

@ -0,0 +1,358 @@
<!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>

535
tts-test.html Normal file
View File

@ -0,0 +1,535 @@
<!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>