WEB SPEECH-API - INTERACTIVE APP SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SpeechSynthesis TTS App</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}
:root {
color-scheme: light dark;
--bg-color: transparent;
--surface: #111111;
--surface-light: #222222;
--surface-lighter: #333333;
--text-color: #ffffff;
--text-secondary: #aaaaaa;
--border-color: #444444;
--select-bg: #000000;
--select-border: #444444;
--accent-white: #ffffff;
--accent-light: #f0f0f0;
--accent-dark: #cccccc;
}
html:not(.dark) {
--bg-color: transparent;
--surface: #f9f9f9;
--surface-light: #f0f0f0;
--surface-lighter: #e0e0e0;
--text-color: #111111;
--text-secondary: #555555;
--border-color: #cccccc;
--select-bg: #ffffff;
--select-border: #cccccc;
--accent-white: #111111;
--accent-light: #333333;
--accent-dark: #555555;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
gap: 24px;
overflow: hidden;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
flex: 1;
min-height: 0;
overflow: hidden;
}
@media (max-width: 1024px) {
.main-content {
grid-template-columns: 1fr;
}
}
.panel {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-title {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.panel-title i {
color: var(--accent-white);
font-size: 18px;
}
.panel-title h2 {
font-size: 20px;
font-weight: 600;
color: var(--accent-white);
}
.text-input-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
min-height: 0;
gap: 16px;
}
.warning-banner {
background-color: rgba(255, 255, 255, 0.05);
border-left: 3px solid var(--accent-white);
padding: 12px 16px;
border-radius: 6px;
font-size: 13px;
color: var(--text-color);
line-height: 1.5;
}
.warning-banner i {
color: var(--accent-white);
margin-right: 8px;
font-size: 14px;
}
#textInput {
flex: 1;
background-color: var(--surface-light);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
font-size: 15px;
line-height: 1.6;
resize: none;
transition: border-color 0.2s;
min-height: 120px;
}
#textInput:focus {
outline: none;
border-color: var(--accent-white);
}
.input-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.char-count {
color: var(--text-secondary);
font-size: 13px;
}
.language-match {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
padding: 6px 12px;
border-radius: 6px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
}
.language-match-icon {
font-size: 12px;
color: var(--accent-white);
}
.action-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
flex-shrink: 0;
}
.btn {
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
background-color: var(--surface-light);
color: var(--text-color);
user-select: none;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.btn:hover {
background-color: var(--surface-lighter);
transform: translateY(-1px);
}
.btn:active {
transform: translateY(0);
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.btn-primary {
background-color: var(--accent-white);
color: var(--surface);
border: none;
font-weight: 600;
}
.btn-primary:hover {
background-color: var(--accent-light);
}
.btn-danger {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
}
.btn-danger:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.language-examples {
display: flex;
flex-wrap: wrap;
gap: 8px;
flex-shrink: 0;
}
.lang-btn {
padding: 6px 12px;
background-color: var(--surface-light);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.lang-btn:hover {
background-color: var(--surface-lighter);
border-color: var(--accent-white);
}
/* VOICE CONTROLS PANEL STYLING */
.controls-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.voice-controls {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
gap: 24px;
overflow: hidden;
}
.control-group {
display: flex;
flex-direction: column;
gap: 12px;
flex-shrink: 0;
}
.control-label {
display: flex;
justify-content: space-between;
align-items: center;
}
.control-label span {
font-size: 14px;
font-weight: 500;
color: var(--accent-white);
}
.control-value {
color: var(--accent-white);
font-weight: 600;
font-size: 13px;
}
select {
background-color: var(--select-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px;
font-size: 14px;
cursor: pointer;
transition: border-color 0.2s;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
padding-right: 40px;
flex-shrink: 0;
}
select:focus {
outline: none;
border-color: var(--accent-white);
}
select option {
background-color: var(--select-bg);
color: var(--text-color);
padding: 12px;
}
.slider-container {
display: flex;
align-items: center;
gap: 12px;
}
.slider-icon {
color: var(--accent-white);
font-size: 14px;
width: 20px;
text-align: center;
flex-shrink: 0;
}
input[type="range"] {
flex-grow: 1;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: var(--surface-light);
border-radius: 2px;
outline: none;
flex-shrink: 1;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--accent-white);
cursor: pointer;
border: 2px solid var(--bg-color);
}
input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--accent-white);
cursor: pointer;
border: 2px solid var(--bg-color);
}
.voice-info {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 12px;
color: var(--text-secondary);
background-color: var(--surface-light);
padding: 16px;
border-radius: 8px;
border: 1px solid var(--border-color);
flex-shrink: 0;
}
.voice-detail {
display: flex;
justify-content: space-between;
}
.voice-detail-label {
font-weight: 500;
color: var(--text-secondary);
}
.voice-detail-value {
color: var(--accent-white);
}
.info-tooltip {
display: inline-block;
margin-left: 8px;
color: var(--accent-white);
cursor: help;
position: relative;
}
.info-tooltip .tooltip-text {
visibility: hidden;
width: 280px;
background-color: var(--surface);
color: var(--text-color);
text-align: left;
border-radius: 6px;
padding: 12px;
position: absolute;
z-index: 1000;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
border: 1px solid var(--border-color);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
line-height: 1.5;
}
.info-tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
.status-container {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: var(--surface);
border: 1px solid var(--border-color);
border-radius: 20px;
font-size: 13px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--accent-white);
opacity: 0.5;
}
.status-dot.active {
opacity: 1;
box-shadow: 0 0 8px var(--accent-white);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
/* RESPONSIVE DESIGN FIXES */
@media (max-width: 768px) {
.app-container {
gap: 16px;
}
.panel {
min-height: min-content;
}
.main-content {
gap: 16px;
}
.text-input-container {
overflow: visible;
min-height: min-content;
}
.action-buttons {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-top: 16px;
padding-top: 16px;
position: sticky;
bottom: 0;
background-color: var(--surface);
z-index: 10;
}
.btn {
padding: 14px;
font-size: 14px;
min-height: 50px;
}
#textInput {
max-height: 200px;
}
}
@media (max-width: 480px) {
.action-buttons {
grid-template-columns: repeat(2, 1fr);
position: fixed;
bottom: 16px;
left: 16px;
right: 16px;
background-color: var(--surface);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 12px;
margin: 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.btn {
height: 50px;
font-size: 14px;
}
.input-footer {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.language-examples {
justify-content: center;
}
.lang-btn {
font-size: 11px;
padding: 5px 10px;
}
.voice-controls {
gap: 20px;
}
.control-group {
gap: 10px;
}
}
@media (max-width: 360px) {
.action-buttons {
grid-template-columns: 1fr;
gap: 8px;
}
.btn {
height: 46px;
}
.panel-title h2 {
font-size: 18px;
}
}
</style>
</head>
<body>
<div class="app-container">
<div class="main-content">
<!-- TEXT INPUT PANEL -->
<div class="panel">
<div class="text-input-container">
<div class="warning-banner">
<i class="fas fa-exclamation-triangle"></i>
<strong>Important:</strong> This tool speaks text using different voices, but does NOT translate between languages.
</div>
<div class="language-examples">
<button class="lang-btn" data-lang="en">English</button>
<button class="lang-btn" data-lang="es">Spanish</button>
<button class="lang-btn" data-lang="fr">French</button>
<button class="lang-btn" data-lang="de">German</button>
<button class="lang-btn" data-lang="it">Italian</button>
<button class="lang-btn" data-lang="ja">Japanese</button>
<button class="lang-btn" data-lang="zh">Chinese</button>
</div>
<textarea id="textInput" placeholder="Enter text to convert to speech... For best results, match the language of your text with the selected voice's language.">Welcome to the Speech Synthesis demonstration. This tool converts text to speech using different voices and accents. Remember: this is NOT a translation tool. Each voice speaks in a specific language. For clear pronunciation, use text that matches your selected voice's language.</textarea>
<div class="input-footer">
<div class="char-count">
<span id="charCount">0</span> characters
</div>
<div id="languageMatch" class="language-match">
<i class="fas fa-exclamation-circle language-match-icon"></i>
<span>Select a voice to check language compatibility</span>
</div>
</div>
<div class="action-buttons">
<button class="btn btn-primary" id="speakBtn">
<i class="fas fa-play"></i> Play
</button>
<button class="btn" id="pauseBtn" disabled>
<i class="fas fa-pause"></i> Pause
</button>
<button class="btn" id="resumeBtn" disabled>
<i class="fas fa-play"></i> Resume
</button>
<button class="btn btn-danger" id="stopBtn" disabled>
<i class="fas fa-stop"></i> Stop
</button>
</div>
</div>
</div>
<!-- VOICE CONTROLS PANEL -->
<div class="panel controls-panel">
<div class="voice-controls">
<div class="control-group">
<div class="control-label">
<span>Voice Selection <small>(Language/Accent)</small></span>
</div>
<select id="voiceSelect">
<option value="">Loading voices...</option>
</select>
<div id="selectedVoiceInfo" class="voice-info" style="display: none;">
<div class="voice-detail">
<span class="voice-detail-label">Language:</span>
<span class="voice-detail-value" id="voiceLanguage">-</span>
</div>
<div class="voice-detail">
<span class="voice-detail-label">Accent/Voice Type:</span>
<span class="voice-detail-value" id="voiceType">-</span>
</div>
<div class="voice-detail">
<span class="voice-detail-label">Local Service:</span>
<span class="voice-detail-value" id="voiceLocal">-</span>
</div>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span>Rate (Speed)</span>
<span class="control-value" id="rateValue">1.0</span>
</div>
<div class="slider-container">
<div class="slider-icon"><i class="fas fa-turtle"></i></div>
<input type="range" id="rateSlider" min="0.1" max="4" step="0.1" value="1">
<div class="slider-icon"><i class="fas fa-rabbit"></i></div>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span>Pitch (Tone)</span>
<span class="control-value" id="pitchValue">1.0</span>
</div>
<div class="slider-container">
<div class="slider-icon"><i class="fas fa-volume-down"></i></div>
<input type="range" id="pitchSlider" min="0.1" max="2" step="0.1" value="1">
<div class="slider-icon"><i class="fas fa-volume-up"></i></div>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span>Volume</span>
<span class="control-value" id="volumeValue">1.0</span>
</div>
<div class="slider-container">
<div class="slider-icon"><i class="fas fa-volume-off"></i></div>
<input type="range" id="volumeSlider" min="0" max="1" step="0.1" value="1">
<div class="slider-icon"><i class="fas fa-volume-up"></i></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Initialize all DOM element references
function initDOMReferences() {
return {
textInput: document.getElementById('textInput'),
charCount: document.getElementById('charCount'),
voiceSelect: document.getElementById('voiceSelect'),
rateSlider: document.getElementById('rateSlider'),
rateValue: document.getElementById('rateValue'),
pitchSlider: document.getElementById('pitchSlider'),
pitchValue: document.getElementById('pitchValue'),
volumeSlider: document.getElementById('volumeSlider'),
volumeValue: document.getElementById('volumeValue'),
speakBtn: document.getElementById('speakBtn'),
pauseBtn: document.getElementById('pauseBtn'),
resumeBtn: document.getElementById('resumeBtn'),
stopBtn: document.getElementById('stopBtn'),
statusDot: document.getElementById('statusDot'),
statusText: document.getElementById('statusText'),
languageMatch: document.getElementById('languageMatch'),
selectedVoiceInfo: document.getElementById('selectedVoiceInfo'),
voiceLanguage: document.getElementById('voiceLanguage'),
voiceType: document.getElementById('voiceType'),
voiceLocal: document.getElementById('voiceLocal'),
langButtons: document.querySelectorAll('.lang-btn')
};
}
// SpeechSynthesis Controller
const synth = window.speechSynthesis;
// Get DOM references
const dom = initDOMReferences();
// State variables
let voices = [];
let currentUtterance = null;
let isSpeaking = false;
let isPaused = false;
let languageMap = {};
let selectedVoice = null;
// Initialize character count
if (dom.charCount) {
dom.charCount.textContent = dom.textInput.value.length;
}
// Language code to name mapping
function initLanguageMap() {
languageMap = {
'af': 'Afrikaans',
'am': 'Amharic',
'ar': 'Arabic',
'as': 'Assamese',
'az': 'Azerbaijani',
'be': 'Belarusian',
'bg': 'Bulgarian',
'bn': 'Bengali',
'bo': 'Tibetan',
'bs': 'Bosnian',
'ca': 'Catalan',
'cs': 'Czech',
'cy': 'Welsh',
'da': 'Danish',
'de': 'German',
'el': 'Greek',
'en': 'English',
'es': 'Spanish',
'et': 'Estonian',
'eu': 'Basque',
'fa': 'Persian',
'fi': 'Finnish',
'fil': 'Filipino',
'fr': 'French',
'ga': 'Irish',
'gl': 'Galician',
'gu': 'Gujarati',
'he': 'Hebrew',
'hi': 'Hindi',
'hr': 'Croatian',
'hu': 'Hungarian',
'hy': 'Armenian',
'id': 'Indonesian',
'is': 'Icelandic',
'it': 'Italian',
'ja': 'Japanese',
'jv': 'Javanese',
'ka': 'Georgian',
'kk': 'Kazakh',
'km': 'Khmer',
'kn': 'Kannada',
'ko': 'Korean',
'lo': 'Lao',
'lt': 'Lithuanian',
'lv': 'Latvian',
'mk': 'Macedonian',
'ml': 'Malayalam',
'mn': 'Mongolian',
'mr': 'Marathi',
'ms': 'Malay',
'mt': 'Maltese',
'my': 'Burmese',
'ne': 'Nepali',
'nl': 'Dutch',
'no': 'Norwegian',
'or': 'Odia',
'pa': 'Punjabi',
'pl': 'Polish',
'ps': 'Pashto',
'pt': 'Portuguese',
'ro': 'Romanian',
'ru': 'Russian',
'sa': 'Sanskrit',
'sd': 'Sindhi',
'si': 'Sinhala',
'sk': 'Slovak',
'sl': 'Slovenian',
'so': 'Somali',
'sq': 'Albanian',
'sr': 'Serbian',
'su': 'Sundanese',
'sv': 'Swedish',
'sw': 'Swahili',
'ta': 'Tamil',
'te': 'Telugu',
'th': 'Thai',
'ti': 'Tigrinya',
'tr': 'Turkish',
'uk': 'Ukrainian',
'ur': 'Urdu',
'uz': 'Uzbek',
'vi': 'Vietnamese',
'zh': 'Chinese',
'zu': 'Zulu'
};
}
// Get language name from code
function getLanguageName(langCode) {
const baseCode = langCode.split('-')[0].toLowerCase();
return languageMap[baseCode] || languageMap[langCode] || langCode;
}
// Simple language detection
function detectTextLanguage(text) {
const languagePatterns = {
'en': /\b(the|and|that|have|for|not|with|you|this|but)\b/i,
'es': /\b(el|la|que|y|en|los|se|del|las|un)\b/i,
'fr': /\b(le|la|et|les|des|un|une|est|dans|pour)\b/i,
'de': /\b(der|die|das|und|in|den|von|zu|mit|sich)\b/i,
'it': /\b(il|la|e|di|che|in|una|per|con|non)\b/i,
'pt': /\b(o|a|e|do|da|em|um|uma|para|com)\b/i,
'ru': /[а-яА-Я]/,
'zh': /[\u4e00-\u9fff]/,
'ja': /[\u3040-\u309f\u30a0-\u30ff]/,
'ko': /[\uac00-\ud7af]/,
'ar': /[\u0600-\u06ff]/
};
for (const [lang, pattern] of Object.entries(languagePatterns)) {
if (pattern.test(text)) {
return getLanguageName(lang);
}
}
return 'Unknown / Mixed';
}
// Check if text language matches voice language
function checkLanguageMatch(text, voiceLang) {
const textLang = detectTextLanguage(text);
const voiceBaseLang = voiceLang.split('-')[0].toLowerCase();
let textBaseLang = 'unknown';
for (const [code, name] of Object.entries(languageMap)) {
if (name === textLang) {
textBaseLang = code;
break;
}
}
if (textLang === 'Unknown / Mixed') {
return {
match: 'unknown',
message: 'Text language could not be detected',
textLang: textLang,
voiceLang: getLanguageName(voiceBaseLang)
};
}
if (textBaseLang === voiceBaseLang) {
return {
match: 'good',
message: `Text (${textLang}) matches voice language`,
textLang: textLang,
voiceLang: getLanguageName(voiceBaseLang)
};
} else {
return {
match: 'bad',
message: `Text (${textLang}) may not match voice language (${getLanguageName(voiceBaseLang)})`,
textLang: textLang,
voiceLang: getLanguageName(voiceBaseLang)
};
}
}
// Update language match display
function updateLanguageMatch() {
if (!selectedVoice) {
if (dom.languageMatch) {
dom.languageMatch.innerHTML = '<i class="fas fa-exclamation-circle language-match-icon"></i><span>Select a voice to check language compatibility</span>';
}
return;
}
const text = dom.textInput.value;
const match = checkLanguageMatch(text, selectedVoice.lang);
let icon = 'fa-exclamation-circle';
if (match.match === 'good') icon = 'fa-check-circle';
if (match.match === 'bad') icon = 'fa-times-circle';
if (dom.languageMatch) {
dom.languageMatch.innerHTML = `<i class="fas ${icon} language-match-icon"></i><span>${match.message}</span>`;
}
}
// Update character count and language match
if (dom.textInput) {
dom.textInput.addEventListener('input', () => {
if (dom.charCount) {
dom.charCount.textContent = dom.textInput.value.length;
}
updateLanguageMatch();
});
}
// Load available voices
function loadVoices() {
voices = synth.getVoices();
if (!dom.voiceSelect) return;
dom.voiceSelect.innerHTML = '';
const defaultOption = document.createElement('option');
defaultOption.textContent = 'Select a voice (language/accent)';
defaultOption.value = '';
dom.voiceSelect.appendChild(defaultOption);
const voicesByLanguage = {};
voices.forEach(voice => {
const lang = getLanguageName(voice.lang);
if (!voicesByLanguage[lang]) {
voicesByLanguage[lang] = [];
}
voicesByLanguage[lang].push(voice);
});
const sortedLanguages = Object.keys(voicesByLanguage).sort();
sortedLanguages.forEach(language => {
const groupHeader = document.createElement('optgroup');
groupHeader.label = `${language} Voices`;
voicesByLanguage[language].forEach(voice => {
const option = document.createElement('option');
let voiceName = voice.name;
voiceName = voiceName.replace(/Microsoft |Google |Apple /g, '');
voiceName = voiceName.replace(new RegExp(`\\(${language}\\)`, 'i'), '').trim();
voiceName = voiceName.replace(new RegExp(`- ${language}`, 'i'), '').trim();
option.textContent = voiceName;
option.value = voice.name;
if (voice.default) {
option.textContent += ' (Default)';
option.selected = true;
selectedVoice = voice;
updateSelectedVoiceInfo();
updateLanguageMatch();
}
option.setAttribute('data-lang', voice.lang);
option.setAttribute('data-name', voice.name);
option.setAttribute('data-voice', JSON.stringify({
name: voice.name,
lang: voice.lang,
localService: voice.localService,
default: voice.default
}));
groupHeader.appendChild(option);
});
dom.voiceSelect.appendChild(groupHeader);
});
if (!selectedVoice && voices.length > 0) {
selectedVoice = voices[0];
dom.voiceSelect.value = selectedVoice.name;
updateSelectedVoiceInfo();
updateLanguageMatch();
}
}
// Update selected voice info panel
function updateSelectedVoiceInfo() {
if (!selectedVoice) return;
if (dom.selectedVoiceInfo) {
dom.selectedVoiceInfo.style.display = 'flex';
}
if (dom.voiceLanguage) {
dom.voiceLanguage.textContent = getLanguageName(selectedVoice.lang);
}
let type = 'Standard';
const name = selectedVoice.name.toLowerCase();
if (name.includes('female') || name.includes('woman')) type = 'Female';
if (name.includes('male') || name.includes('man')) type = 'Male';
if (name.includes('natural') || name.includes('neural')) type = 'Neural/Enhanced';
if (dom.voiceType) {
dom.voiceType.textContent = type;
}
if (dom.voiceLocal) {
dom.voiceLocal.textContent = selectedVoice.localService ? 'Yes (Local)' : 'No (Cloud/Network)';
}
}
// Load voices when they become available
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = loadVoices;
}
// Initialize language map and voices
initLanguageMap();
loadVoices();
// Voice selection change handler
if (dom.voiceSelect) {
dom.voiceSelect.addEventListener('change', () => {
const selectedOption = dom.voiceSelect.options[dom.voiceSelect.selectedIndex];
if (selectedOption.value) {
const voiceData = JSON.parse(selectedOption.getAttribute('data-voice'));
selectedVoice = voices.find(v => v.name === voiceData.name);
updateSelectedVoiceInfo();
updateLanguageMatch();
if (currentUtterance && isSpeaking && !isPaused) {
synth.cancel();
speakText();
}
} else {
selectedVoice = null;
if (dom.selectedVoiceInfo) {
dom.selectedVoiceInfo.style.display = 'none';
}
updateLanguageMatch();
}
});
}
// Update slider value displays
if (dom.rateSlider && dom.rateValue) {
dom.rateSlider.addEventListener('input', () => {
const value = parseFloat(dom.rateSlider.value);
dom.rateValue.textContent = value.toFixed(1);
if (currentUtterance && isSpeaking) {
currentUtterance.rate = value;
}
});
}
if (dom.pitchSlider && dom.pitchValue) {
dom.pitchSlider.addEventListener('input', () => {
const value = parseFloat(dom.pitchSlider.value);
dom.pitchValue.textContent = value.toFixed(1);
if (currentUtterance && isSpeaking) {
currentUtterance.pitch = value;
}
});
}
if (dom.volumeSlider && dom.volumeValue) {
dom.volumeSlider.addEventListener('input', () => {
const value = parseFloat(dom.volumeSlider.value);
dom.volumeValue.textContent = value.toFixed(1);
if (currentUtterance && isSpeaking) {
currentUtterance.volume = value;
}
});
}
// Language example buttons
const languageExamples = {
'en': 'Welcome to the Speech Synthesis demonstration. This tool converts text to speech using different voices and accents. Remember: this is NOT a translation tool.',
'es': 'Bienvenido a la demostración de Síntesis de Voz. Esta herramienta convierte texto en voz usando diferentes voces y acentos. Recuerda: esto NO es una herramienta de traducción.',
'fr': 'Bienvenue à la démonstration de Synthèse Vocale. Cet outil convertit el texto en parole en utilisant différentes voix et accents. Rappelez-vous: ceci N\'EST PAS un outil de traduction.',
'de': 'Willkommen zur Sprachsynthese-Demonstration. Dieses Tool wandelt Text in Sprache um und verwendet dabei verschiedene Stimmen und Akzente. Denken Sie daran: Dies ist KEIN Übersetzungstool.',
'it': 'Benvenuto alla dimostrazione di Sintesi Vocale. Questo strumento converte il texto in voce utilizzando diverse voci e accenti. Ricorda: questo NON è uno strumento di traducción.',
'ja': '音声合成デモンストレーションへようこそ。このツールは、さまざまな声とアクセントを使用してテキストを音声に変換します。注意:これは翻訳ツールではありません。',
'zh': '欢迎使用语音合成演示。此工具使用不同的语音和口音将文本转换为语音。请记住:这不是翻译工具。'
};
if (dom.langButtons) {
dom.langButtons.forEach(btn => {
btn.addEventListener('click', () => {
const lang = btn.getAttribute('data-lang');
if (dom.textInput) {
dom.textInput.value = languageExamples[lang] || languageExamples['en'];
if (dom.charCount) {
dom.charCount.textContent = dom.textInput.value.length;
}
updateLanguageMatch();
}
});
});
}
// Update status indicator
function updateStatus(text) {
if (dom.statusText) {
dom.statusText.textContent = text;
}
}
// Update button states
function updateButtonStates(speakEnabled, pauseEnabled, resumeEnabled, stopEnabled) {
if (dom.speakBtn) dom.speakBtn.disabled = !speakEnabled;
if (dom.pauseBtn) dom.pauseBtn.disabled = !pauseEnabled;
if (dom.resumeBtn) dom.resumeBtn.disabled = !resumeEnabled;
if (dom.stopBtn) dom.stopBtn.disabled = !stopEnabled;
}
// Speak function - Play button action
function speakText() {
// Stop any current speech
if (synth.speaking) {
synth.cancel();
}
if (!dom.textInput) return;
const text = dom.textInput.value.trim();
if (text === '') {
updateStatus('Please enter text to play');
return;
}
if (!selectedVoice) {
updateStatus('Please select a voice first');
return;
}
currentUtterance = new SpeechSynthesisUtterance(text);
currentUtterance.rate = parseFloat(dom.rateSlider?.value || 1);
currentUtterance.pitch = parseFloat(dom.pitchSlider?.value || 1);
currentUtterance.volume = parseFloat(dom.volumeSlider?.value || 1);
currentUtterance.voice = selectedVoice;
// Event handlers
currentUtterance.onstart = () => {
isSpeaking = true;
isPaused = false;
updateStatus('Playing');
updateButtonStates(false, true, false, true);
if (dom.statusDot) {
dom.statusDot.classList.add('active');
}
};
currentUtterance.onend = () => {
isSpeaking = false;
isPaused = false;
updateStatus('Finished');
updateButtonStates(true, false, false, false);
if (dom.statusDot) {
dom.statusDot.classList.remove('active');
}
};
currentUtterance.onerror = (event) => {
isSpeaking = false;
isPaused = false;
updateStatus(`Error: ${event.error}`);
updateButtonStates(true, false, false, false);
if (dom.statusDot) {
dom.statusDot.classList.remove('active');
}
};
currentUtterance.onpause = () => {
isPaused = true;
updateStatus('Paused');
updateButtonStates(true, false, true, true);
};
currentUtterance.onresume = () => {
isPaused = false;
updateStatus('Playing');
updateButtonStates(false, true, false, true);
};
// Start speaking
synth.speak(currentUtterance);
updateStatus('Starting...');
}
// Pause speech
function pauseSpeech() {
if (synth.speaking && !synth.paused) {
synth.pause();
isPaused = true;
updateStatus('Paused');
updateButtonStates(true, false, true, true);
}
}
// Resume speech
function resumeSpeech() {
if (synth.speaking && synth.paused) {
synth.resume();
isPaused = false;
updateStatus('Playing');
updateButtonStates(false, true, false, true);
}
}
// Stop speech
function stopSpeech() {
if (synth.speaking || isSpeaking) {
synth.cancel();
isSpeaking = false;
isPaused = false;
updateStatus('Stopped');
updateButtonStates(true, false, false, false);
if (dom.statusDot) {
dom.statusDot.classList.remove('active');
}
currentUtterance = null;
}
}
// Event listeners for buttons
if (dom.speakBtn) {
dom.speakBtn.addEventListener('click', speakText);
dom.speakBtn.addEventListener('touchend', (e) => {
e.preventDefault();
if (!dom.speakBtn.disabled) speakText();
});
}
if (dom.pauseBtn) {
dom.pauseBtn.addEventListener('click', pauseSpeech);
dom.pauseBtn.addEventListener('touchend', (e) => {
e.preventDefault();
if (!dom.pauseBtn.disabled) pauseSpeech();
});
}
if (dom.resumeBtn) {
dom.resumeBtn.addEventListener('click', resumeSpeech);
dom.resumeBtn.addEventListener('touchend', (e) => {
e.preventDefault();
if (!dom.resumeBtn.disabled) resumeSpeech();
});
}
if (dom.stopBtn) {
dom.stopBtn.addEventListener('click', stopSpeech);
dom.stopBtn.addEventListener('touchend', (e) => {
e.preventDefault();
if (!dom.stopBtn.disabled) stopSpeech();
});
}
// Keyboard shortcuts
document.addEventListener('keydown', (event) => {
// Space bar to play/pause
if (event.key === ' ' && event.target.tagName !== 'TEXTAREA' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'SELECT') {
event.preventDefault();
if (synth.speaking && !synth.paused) {
pauseSpeech();
} else if (synth.speaking && synth.paused) {
resumeSpeech();
} else {
speakText();
}
}
// Ctrl/Cmd + Enter to play
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
event.preventDefault();
speakText();
}
// Escape to stop
if (event.key === 'Escape') {
stopSpeech();
}
});
// Initialize on load
document.addEventListener('DOMContentLoaded', () => {
updateStatus('Ready');
updateButtonStates(true, false, false, false);
if (dom.rateValue) {
dom.rateValue.textContent = dom.rateSlider?.value || '1.0';
}
if (dom.pitchValue) {
dom.pitchValue.textContent = dom.pitchSlider?.value || '1.0';
}
if (dom.volumeValue) {
dom.volumeValue.textContent = dom.volumeSlider?.value || '1.0';
}
updateLanguageMatch();
if (!('speechSynthesis' in window)) {
updateStatus('Speech Synthesis not supported');
alert('Your browser does not support the Speech Synthesis API. Please try Chrome, Edge, or Safari.');
}
});
// State monitoring function
function monitorSpeechState() {
if (synth.speaking) {
if (synth.paused) {
if (!isPaused) {
isPaused = true;
updateStatus('Paused');
updateButtonStates(true, false, true, true);
}
} else {
if (!isSpeaking || isPaused) {
isSpeaking = true;
isPaused = false;
updateStatus('Playing');
updateButtonStates(false, true, false, true);
}
}
} else {
if (isSpeaking) {
isSpeaking = false;
isPaused = false;
updateStatus('Ready');
updateButtonStates(true, false, false, false);
if (dom.statusDot) {
dom.statusDot.classList.remove('active');
}
}
}
}
// Monitor speech state regularly
setInterval(monitorSpeechState, 100);
</script>
<script>
(function syncTheme() {
try {
if (window.parent && window.parent !== window) {
const parentHtml = window.parent.document.documentElement;
const localHtml = document.documentElement;
const updateTheme = () => {
if (parentHtml.classList.contains('dark')) {
localHtml.classList.add('dark');
} else {
localHtml.classList.remove('dark');
}
};
updateTheme();
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
updateTheme();
}
});
});
observer.observe(parentHtml, { attributes: true });
return;
}
} catch (e) {
console.warn('Could not sync theme with parent window.', e);
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
})();
</script>
</body>
</html>