Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Voice Command</title> | |
<style> | |
.chat-container { | |
max-width: 400px; | |
margin: 20px auto; | |
padding: 10px; | |
border: 1px solid #ccc; | |
border-radius: 5px; | |
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); | |
font-family: Arial, sans-serif; | |
} | |
.user-message { | |
background-color: #f0f0f0; | |
border-radius: 5px; | |
padding: 5px 10px; | |
margin: 5px 0; | |
text-align: right; | |
} | |
.bot-message { | |
background-color: #d3e9ff; | |
border-radius: 5px; | |
padding: 5px 10px; | |
margin: 5px 0; | |
} | |
#languageSelector { | |
width: 100%; | |
margin-top: 10px; | |
padding: 5px; | |
border-radius: 5px; | |
border: 1px solid #ccc; | |
} | |
#status { | |
color: grey; | |
font-weight: 600; | |
margin-top: 10px; | |
text-align: center; | |
} | |
#permissionModal { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: rgba(0, 0, 0, 0.5); | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
z-index: 1000; | |
} | |
#permissionModal div { | |
background: white; | |
padding: 20px; | |
border-radius: 5px; | |
text-align: center; | |
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); | |
} | |
#permissionModal button { | |
margin: 10px; | |
padding: 10px 20px; | |
border: none; | |
border-radius: 5px; | |
background: #007bff; | |
color: white; | |
cursor: pointer; | |
} | |
#permissionModal button:hover { | |
background: #0056b3; | |
} | |
#testSpeakerButton { | |
display: block; | |
margin: 10px auto; | |
padding: 10px 20px; | |
border: none; | |
border-radius: 5px; | |
background: #28a745; | |
color: white; | |
cursor: pointer; | |
font-family: Arial, sans-serif; | |
font-weight: 600; | |
} | |
#testSpeakerButton:hover { | |
background: #218838; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="permissionModal" style="display: none;"> | |
<div> | |
<p>This site requires microphone and speaker permissions to enable voice input and output.</p> | |
<button id="grantPermissions">Grant Permissions</button> | |
</div> | |
</div> | |
<button id="testSpeakerButton">Speaker Test</button> | |
<div class="chat-container"> | |
<div id="chat-box"></div> | |
<select id="languageSelector"> | |
<option value="English (US)">English (US)</option> | |
<option value="Hindi (India)">Hindi (India)</option> | |
<option value="Spanish (Spain)">Spanish (Spain)</option> | |
<option value="French (France)">French (France)</option> | |
<option value="German (Germany)">German (Germany)</option> | |
<option value="Arabic (Saudi Arabia)">Arabic (Saudi Arabia)</option> | |
</select> | |
<div class="speaker" style="display: flex; justify-content: space-between; width: 100%; box-shadow: 0 0 13px #0000003d; border-radius: 5px; margin-top: 10px;"> | |
<p id="action" style="color: grey; font-weight: 800; padding: 0; padding-left: 2rem;"></p> | |
<button id="speech" style="border: transparent; padding: 0 0.5rem;"> | |
Tap to Speak | |
</button> | |
</div> | |
<p id="status"></p> | |
</div> | |
<script> | |
// Browser detection | |
function detectBrowser() { | |
const ua = navigator.userAgent.toLowerCase(); | |
if (ua.includes('safari') && !ua.includes('chrome')) return 'Safari'; | |
if (ua.includes('firefox')) return 'Firefox'; | |
if (ua.includes('edg')) return 'Edge'; | |
if (ua.includes('chrome')) return 'Chrome'; | |
return 'Unknown'; | |
} | |
const browser = detectBrowser(); | |
const statusBar = document.getElementById('status'); | |
const permissionModal = document.getElementById('permissionModal'); | |
const grantPermissionsButton = document.getElementById('grantPermissions'); | |
const testSpeakerButton = document.getElementById('testSpeakerButton'); | |
// Language mapping | |
const speechLangMap = { | |
'English (US)': 'en-US', | |
'Hindi (India)': 'hi-IN', | |
'Spanish (Spain)': 'es-ES', | |
'French (France)': 'fr-FR', | |
'German (Germany)': 'de-DE', | |
'Arabic (Saudi Arabia)': 'ar-SA' | |
}; | |
// Initialize speech synthesis | |
const synth = window.speechSynthesis || null; | |
// const synth = 'en-US'; | |
let voices = []; | |
// Load voices asynchronously | |
function loadVoices() { | |
return new Promise((resolve) => { | |
if (!synth) { | |
statusBar.textContent = 'Text-to-speech not supported in this browser.'; | |
resolve([]); | |
return; | |
} | |
voices = synth.getVoices(); | |
if (voices.length > 0) { | |
resolve(voices); | |
} else { | |
synth.addEventListener('voiceschanged', () => { | |
voices = synth.getVoices(); | |
resolve(voices); | |
}, { once: true }); | |
} | |
}); | |
} | |
// Speak text with fallback | |
async function speakResponse(text, language) { | |
if (!synth) { | |
statusBar.textContent = 'Text-to-speech is unavailable. Displaying text only.'; | |
showBotMessage(text); | |
return; | |
} | |
// const langCode = speechLangMap[language] || 'en-US'; | |
const langCode = 'en-US'; | |
await loadVoices(); | |
const utterance = new SpeechSynthesisUtterance(text); | |
let selectedVoice = voices.find(voice => voice.lang === langCode); | |
if (!selectedVoice) { | |
console.warn(`No voice for ${langCode}. Falling back to English.`); | |
selectedVoice = voices.find(voice => voice.lang.startsWith('en')) || voices[0]; | |
statusBar.textContent = `Voice for ${language} unavailable. Using English voice.`; | |
} | |
if (selectedVoice) { | |
utterance.voice = selectedVoice; | |
utterance.lang = selectedVoice.lang; | |
} else { | |
statusBar.textContent = 'No voices available for text-to-speech.'; | |
showBotMessage(text); | |
return; | |
} | |
utterance.volume = 1.0; | |
utterance.rate = 1.0; | |
utterance.pitch = 1.0; | |
utterance.onerror = (event) => { | |
console.error('TTS error:', event.error); | |
statusBar.textContent = 'Error in text-to-speech. Displaying text only.'; | |
showBotMessage(text); | |
}; | |
utterance.onend = () => { | |
console.log('TTS finished.'); | |
statusBar.textContent = ''; | |
}; | |
if (synth.speaking || synth.paused) { | |
synth.cancel(); | |
} | |
try { | |
synth.speak(utterance); | |
} catch (error) { | |
console.error('TTS failed:', error); | |
statusBar.textContent = 'Failed to play speech. Displaying text only.'; | |
showBotMessage(text); | |
} | |
document.getElementById('speech').addEventListener('click', () => { | |
if (synth.speaking) synth.cancel(); | |
}, { once: true }); | |
} | |
// Initialize TTS and test speaker | |
async function testSpeaker() { | |
if (!synth) { | |
statusBar.textContent = 'Text-to-speech not supported in this browser.'; | |
return false; | |
} | |
try { | |
await loadVoices(); | |
// Silent utterance to unlock audio in Safari | |
const silentUtterance = new SpeechSynthesisUtterance(''); | |
silentUtterance.volume = 0; | |
silentUtterance.onend = () => synth.cancel(); | |
silentUtterance.onerror = (event) => { | |
console.error('Silent TTS error:', event.error); | |
synth.cancel(); | |
}; | |
synth.speak(silentUtterance); | |
await new Promise(resolve => setTimeout(resolve, 100)); | |
synth.cancel(); | |
// Test utterance | |
const selectedLang = document.getElementById('languageSelector').value; | |
// const langCode = speechLangMap[selectedLang] || 'en-US'; | |
const langCode = 'en-US'; | |
const utterance = new SpeechSynthesisUtterance('Speaker works fine'); | |
let selectedVoice = voices.find(voice => voice.lang === langCode); | |
if (!selectedVoice) { | |
selectedVoice = voices.find(voice => voice.lang.startsWith('en')) || voices[0]; | |
statusBar.textContent = `Voice for ${selectedLang} unavailable. Using English voice.`; | |
} | |
if (selectedVoice) { | |
utterance.voice = selectedVoice; | |
utterance.lang = selectedVoice.lang; | |
} else { | |
statusBar.textContent = 'No voices available for text-to-speech.'; | |
return false; | |
} | |
utterance.volume = 1.0; | |
utterance.rate = 1.0; | |
utterance.pitch = 1.0; | |
utterance.onerror = (event) => { | |
console.error('Test TTS error:', event.error); | |
statusBar.textContent = 'Error testing speaker.'; | |
}; | |
utterance.onend = () => { | |
console.log('Test TTS finished.'); | |
statusBar.textContent = 'Speaker test successful.'; | |
}; | |
synth.speak(utterance); | |
return true; | |
} catch (error) { | |
console.error('TTS test failed:', error); | |
statusBar.textContent = 'Failed to test speaker.'; | |
return false; | |
} | |
} | |
// Request microphone permission | |
async function requestMicPermission() { | |
try { | |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
stream.getTracks().forEach(track => track.stop()); | |
return true; | |
} catch (error) { | |
console.error('Microphone permission error:', error); | |
statusBar.textContent = 'Microphone access denied. Voice input unavailable.'; | |
return false; | |
} | |
} | |
// Check and request all permissions | |
async function checkAndRequestPermissions() { | |
if (browser !== 'Safari') return true; | |
try { | |
const permissionStatus = await navigator.permissions.query({ name: 'microphone' }); | |
if (permissionStatus.state === 'granted') { | |
return await testSpeaker(); // Test speaker if mic is granted | |
} | |
} catch (error) { | |
console.warn('Permission query not supported:', error); | |
} | |
permissionModal.style.display = 'flex'; | |
return new Promise((resolve) => { | |
grantPermissionsButton.onclick = async () => { | |
permissionModal.style.display = 'none'; | |
const micGranted = await requestMicPermission(); | |
const ttsReady = micGranted ? await testSpeaker() : false; | |
if (!micGranted || !ttsReady) { | |
statusBar.textContent = 'Some permissions were not granted. Features may be limited.'; | |
} | |
resolve(micGranted && ttsReady); | |
}; | |
}); | |
} | |
// Speech recognition | |
function runSpeechRecog() { | |
const selectedLang = document.getElementById('languageSelector').value; | |
const action = document.getElementById('action'); | |
if (!window.SpeechRecognition && !window.webkitSpeechRecognition) { | |
statusBar.textContent = 'Speech recognition not supported in this browser.'; | |
return; | |
} | |
const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)(); | |
// recognition.lang = speechLangMap[selectedLang] || 'en-US'; | |
recognition.lang = 'en-US'; | |
recognition.interimResults = false; | |
recognition.continuous = false; | |
recognition.onstart = () => { | |
action.textContent = 'Listening...'; | |
statusBar.textContent = ''; | |
}; | |
recognition.onresult = (event) => { | |
const transcript = event.results[0][0].transcript; | |
action.textContent = ''; | |
sendMessage(transcript, selectedLang); | |
}; | |
recognition.onerror = (event) => { | |
action.textContent = ''; | |
statusBar.textContent = `Speech recognition error: ${event.error}`; | |
}; | |
recognition.onend = () => { | |
action.textContent = ''; | |
}; | |
try { | |
recognition.start(); | |
} catch (error) { | |
statusBar.textContent = 'Failed to start speech recognition.'; | |
console.error('STT error:', error); | |
} | |
} | |
// Send message to Flask API | |
async function sendMessage(message, language) { | |
showUserMessage(message); | |
try { | |
const response = await fetch('/api/process_text', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ text: message, language }) | |
}); | |
const data = await response.json(); | |
console.log('Response from Flask API:', data); | |
handleResponse(data); | |
} catch (error) { | |
console.error('Error sending data to Flask API:', error); | |
statusBar.textContent = 'Error: Unable to process request'; | |
showBotMessage('Error: Unable to process request'); | |
} | |
} | |
// Handle API response | |
function handleResponse(data) { | |
if (data.error) { | |
statusBar.textContent = data.error; | |
showBotMessage(data.error); | |
return; | |
} | |
showBotMessage(data.response); | |
speakResponse(data.response, data.language); | |
} | |
// Show user message | |
function showUserMessage(message) { | |
const chatBox = document.getElementById('chat-box'); | |
chatBox.innerHTML += `<div class="user-message">${message}</div>`; | |
chatBox.scrollTop = chatBox.scrollHeight; | |
} | |
// Show bot message | |
function showBotMessage(message) { | |
const chatBox = document.getElementById('chat-box'); | |
chatBox.innerHTML += `<div class="bot-message">${message}</div>`; | |
chatBox.scrollTop = chatBox.scrollHeight; | |
} | |
// Initialize | |
window.addEventListener('load', async () => { | |
await loadVoices(); | |
if (browser === 'Safari') { | |
statusBar.textContent = 'Safari detected. Please grant microphone and speaker permissions.'; | |
const permissionsGranted = await checkAndRequestPermissions(); | |
if (!permissionsGranted) { | |
statusBar.textContent = 'Permissions denied. Some features may not work.'; | |
} | |
} | |
document.getElementById('speech').addEventListener('click', runSpeechRecog); | |
testSpeakerButton.addEventListener('click', testSpeaker); | |
}); | |
// Clean up on unload | |
window.addEventListener('beforeunload', () => { | |
if (synth && synth.speaking) synth.cancel(); | |
}); | |
</script> | |
</body> | |
</html> |