|
|
|
|
|
|
|
|
const API_BASE = 'https://instant-translat-production.up.railway.app'; |
|
|
|
|
|
let mediaRecorder; |
|
|
let audioChunks = []; |
|
|
let isRecording = false; |
|
|
let audioContext; |
|
|
let analyser; |
|
|
let micSource; |
|
|
let animationId; |
|
|
let recognition; |
|
|
let streamTimeout; |
|
|
let globalStream = null; |
|
|
let isRestarting = false; |
|
|
let isProcessingAudio = false; |
|
|
let detectedLanguage = null; |
|
|
let isTTSPlaying = false; |
|
|
let textProcessingTriggered = false; |
|
|
let silenceDetectionActive = true; |
|
|
let currentRecognitionLang = 'fr-FR'; |
|
|
|
|
|
|
|
|
window.continuousMode = false; |
|
|
window.lastBotAudio = null; |
|
|
|
|
|
|
|
|
let currentCycleId = 0; |
|
|
|
|
|
|
|
|
|
|
|
const VOLUME_THRESHOLD = 8; |
|
|
const SILENCE_LIMIT_MS = 5000; |
|
|
const SILENCE_THRESHOLD = 8; |
|
|
const MIN_RECORDING_TIME = 500; |
|
|
const MIN_SPEECH_VOLUME = 5; |
|
|
const TYPING_SPEED_MS = 25; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let recentMessages = new Set(); |
|
|
|
|
|
|
|
|
const HALLUCINATION_PHRASES = [ |
|
|
'thanks for watching', |
|
|
'thank you for watching', |
|
|
'subscribe', |
|
|
'like and subscribe', |
|
|
'see you next time', |
|
|
'bye bye', |
|
|
'goodbye', |
|
|
'merci d\'avoir regardรฉ', |
|
|
'merci de votre attention', |
|
|
'ร bientรดt', |
|
|
'sous-titres', |
|
|
'sous-titrage', |
|
|
'subtitles by', |
|
|
'transcribed by', |
|
|
'music', |
|
|
'applause', |
|
|
'[music]', |
|
|
'[applause]', |
|
|
'...', |
|
|
'you', |
|
|
'the', |
|
|
'i', |
|
|
'a' |
|
|
]; |
|
|
|
|
|
function isHallucination(text) { |
|
|
if (!text) return true; |
|
|
const cleaned = text.toLowerCase().trim(); |
|
|
|
|
|
|
|
|
if (cleaned.length < 3) return true; |
|
|
|
|
|
|
|
|
for (const phrase of HALLUCINATION_PHRASES) { |
|
|
if (cleaned === phrase || cleaned.startsWith(phrase + '.') || cleaned.startsWith(phrase + '!')) { |
|
|
console.log(`๐ซ HALLUCINATION BLOCKED: "${text}"`); |
|
|
return true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (/^(.)\1*$/.test(cleaned) || /^(\w+\s*)\1+$/.test(cleaned)) { |
|
|
console.log(`๐ซ REPEATED PATTERN BLOCKED: "${text}"`); |
|
|
return true; |
|
|
} |
|
|
|
|
|
return false; |
|
|
} |
|
|
|
|
|
function createChatMessage(role, text, audioSrc = null, info = null, lang = null) { |
|
|
const chatHistory = document.getElementById('chat-history'); |
|
|
if (!chatHistory) return; |
|
|
|
|
|
|
|
|
if (isHallucination(text)) { |
|
|
console.log(`๐ซ createChatMessage: Hallucination blocked: "${text}"`); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const normalizedText = text.trim().toLowerCase().substring(0, 100); |
|
|
const messageHash = `${role}-${normalizedText}`; |
|
|
if (recentMessages.has(messageHash)) { |
|
|
console.log(`๐ก๏ธ VISUAL SHIELD: Blocked duplicate message: "${text.substring(0, 30)}..."`); |
|
|
return; |
|
|
} |
|
|
recentMessages.add(messageHash); |
|
|
setTimeout(() => recentMessages.delete(messageHash), 5000); |
|
|
|
|
|
const msgDiv = document.createElement('div'); |
|
|
msgDiv.className = `message ${role}-message`; |
|
|
msgDiv.style.opacity = '0'; |
|
|
msgDiv.style.cssText = ` |
|
|
background: ${role === 'user' ? 'rgba(30, 30, 35, 0.8)' : 'rgba(45, 45, 52, 0.8)'}; |
|
|
border-radius: 16px; |
|
|
padding: 20px; |
|
|
margin-bottom: 16px; |
|
|
border: 1px solid ${role === 'user' ? 'rgba(60, 60, 70, 0.5)' : 'rgba(80, 80, 90, 0.5)'}; |
|
|
`; |
|
|
|
|
|
|
|
|
const langBadge = document.createElement('div'); |
|
|
langBadge.className = 'lang-badge'; |
|
|
langBadge.style.cssText = ` |
|
|
display: inline-block; |
|
|
background: ${role === 'user' ? 'rgba(60, 60, 70, 0.6)' : 'rgba(80, 80, 90, 0.6)'}; |
|
|
color: ${role === 'user' ? '#a0a0a8' : '#c0c0c8'}; |
|
|
padding: 6px 12px; |
|
|
border-radius: 8px; |
|
|
font-size: 0.75rem; |
|
|
font-weight: 600; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.05em; |
|
|
margin-bottom: 12px; |
|
|
`; |
|
|
|
|
|
|
|
|
let langDisplay = lang || (role === 'user' ? 'Input' : 'Translation'); |
|
|
langBadge.innerText = `Language: ${langDisplay}`; |
|
|
msgDiv.appendChild(langBadge); |
|
|
|
|
|
|
|
|
const textDiv = document.createElement('div'); |
|
|
textDiv.className = 'message-content'; |
|
|
textDiv.style.cssText = ` |
|
|
font-size: 1.25rem; |
|
|
line-height: 1.7; |
|
|
color: #ffffff; |
|
|
font-weight: 400; |
|
|
margin-top: 8px; |
|
|
`; |
|
|
textDiv.innerText = text; |
|
|
msgDiv.appendChild(textDiv); |
|
|
|
|
|
|
|
|
|
|
|
if (role === 'bot') { |
|
|
if (!audioSrc) { |
|
|
console.warn("โ ๏ธ No Audio from Server (API Limit/Error). Using Browser TTS Fallback."); |
|
|
|
|
|
const utterance = new SpeechSynthesisUtterance(text); |
|
|
|
|
|
|
|
|
window.speechSynthesis.speak(utterance); |
|
|
} else { |
|
|
|
|
|
const audioContainer = document.createElement('div'); |
|
|
audioContainer.className = 'audio-container'; |
|
|
audioContainer.style.marginTop = '12px'; |
|
|
audioContainer.style.background = 'rgba(0,0,0,0.1)'; |
|
|
audioContainer.style.borderRadius = '8px'; |
|
|
audioContainer.style.padding = '8px'; |
|
|
audioContainer.style.display = 'flex'; |
|
|
audioContainer.style.alignItems = 'center'; |
|
|
audioContainer.style.gap = '10px'; |
|
|
|
|
|
const playBtn = document.createElement('button'); |
|
|
playBtn.innerHTML = '<i class="fa-solid fa-play"></i>'; |
|
|
playBtn.className = 'icon-btn'; |
|
|
playBtn.style.width = '32px'; |
|
|
playBtn.style.height = '32px'; |
|
|
playBtn.style.background = '#fff'; |
|
|
playBtn.style.color = '#333'; |
|
|
|
|
|
|
|
|
const waveDiv = document.createElement('div'); |
|
|
waveDiv.style.flex = '1'; |
|
|
waveDiv.style.height = '4px'; |
|
|
waveDiv.style.background = 'rgba(255,255,255,0.3)'; |
|
|
waveDiv.style.borderRadius = '2px'; |
|
|
waveDiv.style.position = 'relative'; |
|
|
|
|
|
const progressDiv = document.createElement('div'); |
|
|
progressDiv.style.width = '0%'; |
|
|
progressDiv.style.height = '100%'; |
|
|
progressDiv.style.background = '#fff'; |
|
|
progressDiv.style.borderRadius = '2px'; |
|
|
progressDiv.style.transition = 'width 0.1s linear'; |
|
|
waveDiv.appendChild(progressDiv); |
|
|
|
|
|
|
|
|
const audio = new Audio(audioSrc); |
|
|
audio.preload = 'auto'; |
|
|
|
|
|
|
|
|
window.lastBotAudio = audio; |
|
|
|
|
|
playBtn.onclick = () => { |
|
|
if (audio.paused) { |
|
|
audio.play(); |
|
|
playBtn.innerHTML = '<i class="fa-solid fa-pause"></i>'; |
|
|
} else { |
|
|
audio.pause(); |
|
|
playBtn.innerHTML = '<i class="fa-solid fa-play"></i>'; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
audio.onplay = () => { |
|
|
isTTSPlaying = true; |
|
|
console.log('๐ TTS Started - Pausing speech recognition to prevent feedback'); |
|
|
|
|
|
|
|
|
if (recognition) { |
|
|
try { |
|
|
recognition.stop(); |
|
|
console.log('โธ๏ธ Paused speech recognition during TTS'); |
|
|
} catch (e) { } |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.log('๐๏ธ MediaRecorder continues running during TTS'); |
|
|
}; |
|
|
|
|
|
audio.onended = () => { |
|
|
playBtn.innerHTML = '<i class="fa-solid fa-play"></i>'; |
|
|
progressDiv.style.width = '0%'; |
|
|
|
|
|
|
|
|
isTTSPlaying = false; |
|
|
console.log('โ
TTS ended - Ready for next conversation'); |
|
|
|
|
|
|
|
|
if (window.continuousMode) { |
|
|
statusText.innerText = '๐ค Prรชt pour la suite...'; |
|
|
statusText.style.color = '#4a9b87'; |
|
|
console.log('๐ Continuous mode active - system will listen automatically'); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
audio.onerror = (e) => { |
|
|
console.error('โ TTS playback error:', e); |
|
|
isTTSPlaying = false; |
|
|
playBtn.innerHTML = '<i class="fa-solid fa-play"></i>'; |
|
|
|
|
|
if (window.continuousMode) { |
|
|
statusText.innerText = 'โ ๏ธ Erreur TTS - Prรชt'; |
|
|
statusText.style.color = '#ff6b6b'; |
|
|
} |
|
|
}; |
|
|
|
|
|
audio.ontimeupdate = () => { |
|
|
const percent = (audio.currentTime / audio.duration) * 100; |
|
|
progressDiv.style.width = `${percent}%`; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
audio.oncanplay = () => { |
|
|
|
|
|
}; |
|
|
|
|
|
audio.oncanplaythrough = () => { |
|
|
|
|
|
}; |
|
|
|
|
|
audioContainer.appendChild(playBtn); |
|
|
audioContainer.appendChild(waveDiv); |
|
|
msgDiv.appendChild(audioContainer); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const playPromise = audio.play(); |
|
|
if (playPromise !== undefined) { |
|
|
playPromise.then(_ => { |
|
|
playBtn.innerHTML = '<i class="fa-solid fa-pause"></i>'; |
|
|
}).catch(error => { |
|
|
console.log("Auto-play blocked by browser policy:", error); |
|
|
if (isTTSPlaying) { |
|
|
|
|
|
console.warn("โ ๏ธ Autoplay blocked. Resetting state."); |
|
|
isTTSPlaying = false; |
|
|
playBtn.innerHTML = '<i class="fa-solid fa-play"></i>'; |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
chatHistory.appendChild(msgDiv); |
|
|
|
|
|
|
|
|
const scrollToBottom = () => { |
|
|
|
|
|
chatHistory.scrollTo({ |
|
|
top: chatHistory.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
|
|
|
|
|
|
window.scrollTo({ |
|
|
top: document.body.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
}; |
|
|
|
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
|
|
|
setTimeout(scrollToBottom, 300); |
|
|
setTimeout(scrollToBottom, 600); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
msgDiv.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; |
|
|
msgDiv.style.transform = 'translateY(10px)'; |
|
|
requestAnimationFrame(() => { |
|
|
msgDiv.style.opacity = '1'; |
|
|
msgDiv.style.transform = 'translateY(0)'; |
|
|
}); |
|
|
}, 50); |
|
|
} |
|
|
|
|
|
|
|
|
let recordBtn, statusText, settingsBtn, settingsModal, audioPlayer; |
|
|
let originalTextField, translatedTextField, quickLangSelector, sourceLangSelector, aiModelSelector; |
|
|
|
|
|
|
|
|
function unlockAudioContext() { |
|
|
try { |
|
|
const ctx = new (window.AudioContext || window.webkitAudioContext)(); |
|
|
const osc = ctx.createOscillator(); |
|
|
const gain = ctx.createGain(); |
|
|
gain.gain.value = 0.001; |
|
|
osc.connect(gain); |
|
|
gain.connect(ctx.destination); |
|
|
osc.start(0); |
|
|
setTimeout(() => { osc.stop(); ctx.close(); }, 100); |
|
|
console.log("๐ Audio Autoplay Unlocked"); |
|
|
} catch (e) { |
|
|
console.log("Audio unlock not needed"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function initializeApp() { |
|
|
console.log('๐ฏ initializeApp() called'); |
|
|
|
|
|
|
|
|
if (!localStorage.getItem('googleKey')) { |
|
|
console.log('๐ FULL AUTO: Injecting Google API Key...'); |
|
|
localStorage.setItem('googleKey', 'AIzaSyDB9wiqXsy1dG9OLU9r4Tar8oDdeVy4NOQ'); |
|
|
} |
|
|
|
|
|
|
|
|
recordBtn = document.getElementById('record-btn'); |
|
|
statusText = document.getElementById('status-placeholder'); |
|
|
settingsBtn = document.getElementById('settings-trigger'); |
|
|
settingsModal = document.getElementById('settings-modal'); |
|
|
audioPlayer = document.getElementById('audio-player'); |
|
|
originalTextField = document.getElementById('original-text'); |
|
|
translatedTextField = document.getElementById('translated-text'); |
|
|
quickLangSelector = document.getElementById('target-lang-quick'); |
|
|
sourceLangSelector = document.getElementById('source-lang-selector'); |
|
|
aiModelSelector = document.getElementById('ai-model'); |
|
|
|
|
|
console.log('๐ฆ DOM Elements loaded:'); |
|
|
console.log(' - recordBtn:', recordBtn ? 'โ
FOUND' : 'โ NOT FOUND'); |
|
|
console.log(' - statusText:', statusText ? 'โ
FOUND' : 'โ NOT FOUND'); |
|
|
|
|
|
if (!recordBtn) { |
|
|
console.error('โโโ CRITICAL: record-btn NOT FOUND IN DOM! โโโ'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
console.log('๐ง Attaching click handler...'); |
|
|
|
|
|
recordBtn.onclick = async function (e) { |
|
|
console.log('๐๐๐ BUTTON CLICKED! ๐๐๐'); |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
|
|
|
|
|
|
unlockAudioContext(); |
|
|
|
|
|
if (!window.continuousMode) { |
|
|
|
|
|
console.log('โถ๏ธ Starting continuous mode...'); |
|
|
window.continuousMode = true; |
|
|
this.classList.add('active'); |
|
|
|
|
|
if (statusText) { |
|
|
statusText.innerText = 'รcoute en continu...'; |
|
|
statusText.style.color = '#4a9b87'; |
|
|
} |
|
|
|
|
|
try { |
|
|
await listenContinuously(); |
|
|
} catch (error) { |
|
|
console.error('โ Error:', error); |
|
|
window.continuousMode = false; |
|
|
this.classList.remove('active'); |
|
|
if (statusText) { |
|
|
statusText.innerText = 'Erreur: ' + error.message; |
|
|
statusText.style.color = '#ff6b6b'; |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
console.log('โน๏ธ Stopping continuous mode...'); |
|
|
window.continuousMode = false; |
|
|
this.classList.remove('active'); |
|
|
this.classList.remove('active-speech'); |
|
|
this.classList.remove('processing'); |
|
|
|
|
|
|
|
|
try { |
|
|
if (mediaRecorder && mediaRecorder.state !== 'inactive') { |
|
|
mediaRecorder.stop(); |
|
|
} |
|
|
if (recognition) { |
|
|
recognition.stop(); |
|
|
recognition = null; |
|
|
} |
|
|
if (audioContext && audioContext.state !== 'closed') { |
|
|
audioContext.close(); |
|
|
} |
|
|
} catch (e) { |
|
|
console.warn('Cleanup warning:', e); |
|
|
} |
|
|
|
|
|
|
|
|
console.log('๐งน Cleaning up memory and cache...'); |
|
|
|
|
|
audioContext = null; |
|
|
analyser = null; |
|
|
micSource = null; |
|
|
mediaRecorder = null; |
|
|
audioChunks = []; |
|
|
isRecording = false; |
|
|
isProcessingAudio = false; |
|
|
speechDetected = false; |
|
|
textProcessingTriggered = false; |
|
|
|
|
|
|
|
|
if (animationId) { |
|
|
cancelAnimationFrame(animationId); |
|
|
animationId = null; |
|
|
} |
|
|
|
|
|
|
|
|
fetch('/clear_cache', { method: 'POST' }) |
|
|
.then(res => res.json()) |
|
|
.then(data => console.log(`โ
Backend cache cleared: ${data.cleared} entries`)) |
|
|
.catch(e => console.warn('Cache clear failed:', e)); |
|
|
|
|
|
if (statusText) { |
|
|
statusText.innerText = 'Arrรชtรฉ'; |
|
|
statusText.style.color = '#888'; |
|
|
} |
|
|
console.log('โ
Stopped'); |
|
|
|
|
|
|
|
|
if (globalStream) { |
|
|
try { |
|
|
globalStream.getTracks().forEach(track => track.stop()); |
|
|
} catch (e) { } |
|
|
globalStream = null; |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let touchHandled = false; |
|
|
|
|
|
recordBtn.addEventListener('touchstart', (e) => { |
|
|
e.preventDefault(); |
|
|
touchHandled = true; |
|
|
recordBtn.onclick(e); |
|
|
}, { passive: false }); |
|
|
|
|
|
recordBtn.addEventListener('touchend', (e) => { |
|
|
e.preventDefault(); |
|
|
}, { passive: false }); |
|
|
|
|
|
|
|
|
recordBtn.addEventListener('click', (e) => { |
|
|
if (touchHandled) { |
|
|
touchHandled = false; |
|
|
return; |
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
recordBtn.oncontextmenu = (e) => e.preventDefault(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sourceLangQuick = document.getElementById('source-lang-quick'); |
|
|
const targetLangQuick = document.getElementById('target-lang-quick'); |
|
|
const swapLangsBtn = document.getElementById('swap-langs'); |
|
|
|
|
|
|
|
|
if (sourceLangQuick) { |
|
|
sourceLangQuick.addEventListener('change', function () { |
|
|
const newLang = this.value; |
|
|
console.log(`๐ Source language changed to: ${newLang}`); |
|
|
|
|
|
|
|
|
localStorage.setItem('sourceLangQuick', newLang); |
|
|
|
|
|
|
|
|
if (statusText) { |
|
|
statusText.innerText = `๐ Source: ${this.options[this.selectedIndex].text}`; |
|
|
statusText.style.color = '#4a9b87'; |
|
|
setTimeout(() => { |
|
|
statusText.innerText = 'Prรชt'; |
|
|
statusText.style.color = '#888'; |
|
|
}, 2000); |
|
|
} |
|
|
|
|
|
|
|
|
if (window.continuousMode && recognition) { |
|
|
console.log('๐ Restarting recognition with new source language...'); |
|
|
try { recognition.stop(); } catch (e) { } |
|
|
|
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const savedSource = localStorage.getItem('sourceLangQuick'); |
|
|
if (savedSource) { |
|
|
sourceLangQuick.value = savedSource; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (targetLangQuick) { |
|
|
targetLangQuick.addEventListener('change', function () { |
|
|
const newLang = this.value; |
|
|
console.log(`๐ฏ Target language changed to: ${newLang}`); |
|
|
|
|
|
|
|
|
localStorage.setItem('targetLangQuick', newLang); |
|
|
|
|
|
|
|
|
if (quickLangSelector) { |
|
|
quickLangSelector.value = newLang; |
|
|
} |
|
|
|
|
|
|
|
|
if (statusText) { |
|
|
statusText.innerText = `๐ฏ Cible: ${this.options[this.selectedIndex].text}`; |
|
|
statusText.style.color = '#4a9b87'; |
|
|
setTimeout(() => { |
|
|
statusText.innerText = 'Prรชt'; |
|
|
statusText.style.color = '#888'; |
|
|
}, 2000); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const savedTarget = localStorage.getItem('targetLangQuick'); |
|
|
if (savedTarget) { |
|
|
targetLangQuick.value = savedTarget; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (swapLangsBtn) { |
|
|
swapLangsBtn.addEventListener('click', function () { |
|
|
console.log('๐ Swapping languages...'); |
|
|
|
|
|
const sourceSelect = document.getElementById('source-lang-quick'); |
|
|
const targetSelect = document.getElementById('target-lang-quick'); |
|
|
|
|
|
if (!sourceSelect || !targetSelect) { |
|
|
console.warn('Language selectors not found'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const currentSource = sourceSelect.value; |
|
|
const currentTarget = targetSelect.value; |
|
|
|
|
|
|
|
|
const targetToSourceMap = { |
|
|
'French': 'fr-FR', |
|
|
'English': 'en-US', |
|
|
'Arabic': 'ar-SA', |
|
|
'Moroccan Darija': 'ar-SA', |
|
|
'Spanish': 'es-ES', |
|
|
'German': 'de-DE' |
|
|
}; |
|
|
|
|
|
|
|
|
const sourceToTargetMap = { |
|
|
'fr-FR': 'French', |
|
|
'en-US': 'English', |
|
|
'ar-SA': 'Arabic', |
|
|
'es-ES': 'Spanish', |
|
|
'de-DE': 'German', |
|
|
'auto': 'French' |
|
|
}; |
|
|
|
|
|
|
|
|
const newSourceCode = targetToSourceMap[currentTarget] || 'auto'; |
|
|
const newTargetName = sourceToTargetMap[currentSource] || 'French'; |
|
|
|
|
|
|
|
|
sourceSelect.value = newSourceCode; |
|
|
targetSelect.value = newTargetName; |
|
|
|
|
|
|
|
|
localStorage.setItem('sourceLangQuick', newSourceCode); |
|
|
localStorage.setItem('targetLangQuick', newTargetName); |
|
|
|
|
|
|
|
|
this.style.transform = 'rotate(180deg)'; |
|
|
setTimeout(() => { |
|
|
this.style.transform = 'rotate(0deg)'; |
|
|
}, 300); |
|
|
|
|
|
|
|
|
if (statusText) { |
|
|
statusText.innerText = `๐ ${sourceSelect.options[sourceSelect.selectedIndex].text} โ ${targetSelect.options[targetSelect.selectedIndex].text}`; |
|
|
statusText.style.color = '#60a5fa'; |
|
|
setTimeout(() => { |
|
|
statusText.innerText = 'Prรชt'; |
|
|
statusText.style.color = '#888'; |
|
|
}, 2500); |
|
|
} |
|
|
|
|
|
console.log(`โ
Swapped: ${currentSource} โ ${newTargetName}, ${currentTarget} โ ${newSourceCode}`); |
|
|
|
|
|
|
|
|
if (window.continuousMode && recognition) { |
|
|
try { recognition.stop(); } catch (e) { } |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
console.log('๐ Language quick selectors initialized'); |
|
|
console.log('โ
โ
โ
BUTTON HANDLER ATTACHED! โ
โ
โ
'); |
|
|
} |
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') { |
|
|
console.log('๐ DOM not ready, waiting for DOMContentLoaded...'); |
|
|
document.addEventListener('DOMContentLoaded', initializeApp); |
|
|
} else { |
|
|
console.log('๐ DOM already ready, initializing now...'); |
|
|
initializeApp(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function listenContinuously() { |
|
|
if (!window.continuousMode) { |
|
|
console.log("โ listenContinuously called but window.continuousMode is false"); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (isRecording) { |
|
|
console.log("โ ๏ธ Already recording, skipping duplicate start request"); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("๐๏ธ Starting NEW listening cycle..."); |
|
|
|
|
|
try { |
|
|
|
|
|
currentCycleId++; |
|
|
const thisCycleId = currentCycleId; |
|
|
console.log(`๐ Cycle ID: ${thisCycleId}`); |
|
|
|
|
|
|
|
|
if (mediaRecorder && mediaRecorder.state !== 'inactive') { |
|
|
try { |
|
|
console.log("๐งน Cleaning up old mediaRecorder"); |
|
|
|
|
|
|
|
|
mediaRecorder.ondataavailable = null; |
|
|
mediaRecorder.onstop = null; |
|
|
mediaRecorder = null; |
|
|
} catch (e) { console.warn("Cleanup warning:", e); } |
|
|
} |
|
|
|
|
|
isRecording = true; |
|
|
audioChunks = []; |
|
|
let speechDetected = false; |
|
|
textProcessingTriggered = false; |
|
|
silenceDetectionActive = true; |
|
|
|
|
|
let stream; |
|
|
|
|
|
|
|
|
if (globalStream && globalStream.active) { |
|
|
console.log("โป๏ธ Reusing existing microphone stream"); |
|
|
stream = globalStream; |
|
|
} else { |
|
|
console.log("๐ค Requesting NEW microphone access..."); |
|
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
|
globalStream = stream; |
|
|
console.log("โ
Microphone access granted"); |
|
|
} |
|
|
|
|
|
|
|
|
startRealTimeTranscription(); |
|
|
|
|
|
|
|
|
|
|
|
if (!audioContext || audioContext.state === 'closed') { |
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
|
} |
|
|
|
|
|
analyser = audioContext.createAnalyser(); |
|
|
micSource = audioContext.createMediaStreamSource(stream); |
|
|
micSource.connect(analyser); |
|
|
analyser.fftSize = 256; |
|
|
const bufferLength = analyser.frequencyBinCount; |
|
|
const dataArray = new Uint8Array(bufferLength); |
|
|
|
|
|
let silenceStart = Date.now(); |
|
|
|
|
|
|
|
|
mediaRecorder = new MediaRecorder(stream); |
|
|
|
|
|
mediaRecorder.ondataavailable = e => { |
|
|
|
|
|
if (thisCycleId === currentCycleId) { |
|
|
audioChunks.push(e.data); |
|
|
} else { |
|
|
console.warn(`โ ๏ธ Ignoring data from old cycle ${thisCycleId} (current: ${currentCycleId})`); |
|
|
} |
|
|
}; |
|
|
|
|
|
mediaRecorder.onstop = async () => { |
|
|
|
|
|
if (thisCycleId !== currentCycleId) { |
|
|
console.warn(`โ ๏ธ Ignoring onstop from old cycle ${thisCycleId} (current: ${currentCycleId})`); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log(`๐ Chunk finalized (Cycle ${thisCycleId}).`); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (audioChunks.length > 0 && speechDetected) { |
|
|
const blob = new Blob(audioChunks, { type: 'audio/wav' }); |
|
|
|
|
|
if (blob.size > 2000) { |
|
|
|
|
|
|
|
|
statusText.innerText = 'Traitement...'; |
|
|
statusText.style.color = '#4a9b87'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processAudio(blob, true).catch(e => console.error("Processing error:", e)); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (window.continuousMode) { |
|
|
|
|
|
speechDetected = false; |
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
if (window.continuousMode) { |
|
|
console.log("๐ Instant Restart Triggered (Parallel)"); |
|
|
listenContinuously(); |
|
|
} |
|
|
}, 0); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
if (micSource) micSource.disconnect(); |
|
|
if (analyser) analyser.disconnect(); |
|
|
if (animationId) cancelAnimationFrame(animationId); |
|
|
} catch (e) { } |
|
|
}; |
|
|
|
|
|
mediaRecorder.start(); |
|
|
|
|
|
if (animationId) cancelAnimationFrame(animationId); |
|
|
|
|
|
|
|
|
let consecutiveSpeechFrames = 0; |
|
|
let consecutiveSilenceFrames = 0; |
|
|
const SPEECH_FRAMES_THRESHOLD = 3; |
|
|
const SILENCE_FRAMES_THRESHOLD = 1200; |
|
|
|
|
|
function monitorAudio() { |
|
|
if (!window.continuousMode || !isRecording) { |
|
|
console.log("๐ Audio monitoring stopped"); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (isTTSPlaying) { |
|
|
requestAnimationFrame(monitorAudio); |
|
|
return; |
|
|
} |
|
|
|
|
|
analyser.getByteFrequencyData(dataArray); |
|
|
let sum = 0; |
|
|
for (let i = 0; i < bufferLength; i++) sum += dataArray[i]; |
|
|
const average = sum / bufferLength; |
|
|
|
|
|
|
|
|
|
|
|
if (average > 4) { |
|
|
consecutiveSpeechFrames++; |
|
|
consecutiveSilenceFrames = 0; |
|
|
|
|
|
|
|
|
if (consecutiveSpeechFrames >= SPEECH_FRAMES_THRESHOLD && !speechDetected) { |
|
|
speechDetected = true; |
|
|
silenceStart = Date.now(); |
|
|
console.log("๐ฃ๏ธ Speech confirmed (filtered noise)"); |
|
|
statusText.innerText = '๐ค Enregistrement...'; |
|
|
statusText.style.color = '#ff4444'; |
|
|
recordBtn.classList.add('active-speech'); |
|
|
} |
|
|
} else { |
|
|
consecutiveSpeechFrames = 0; |
|
|
consecutiveSilenceFrames++; |
|
|
|
|
|
if (!speechDetected) { |
|
|
|
|
|
if (!statusText.innerText.includes('Traitement')) { |
|
|
statusText.innerText = '๐ค En attente de parole...'; |
|
|
statusText.style.color = '#888'; |
|
|
} |
|
|
} else { |
|
|
|
|
|
if (consecutiveSilenceFrames >= SILENCE_FRAMES_THRESHOLD) { |
|
|
console.log('๐คซ Silence confirmed - ending speech'); |
|
|
consecutiveSpeechFrames = 0; |
|
|
consecutiveSilenceFrames = 0; |
|
|
|
|
|
|
|
|
isRecording = false; |
|
|
recordBtn.classList.remove('active-speech'); |
|
|
|
|
|
|
|
|
if (mediaRecorder && mediaRecorder.state === 'recording') { |
|
|
mediaRecorder.stop(); |
|
|
} |
|
|
return; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
animationId = requestAnimationFrame(monitorAudio); |
|
|
} |
|
|
|
|
|
monitorAudio(); |
|
|
|
|
|
} catch (err) { |
|
|
console.error('Erreur listenContinuously:', err); |
|
|
window.continuousMode = false; |
|
|
recordBtn.classList.remove('active'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let arabicModeActive = false; |
|
|
|
|
|
function startRealTimeTranscription() { |
|
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; |
|
|
|
|
|
|
|
|
if (!SpeechRecognition) { |
|
|
console.warn("โ ๏ธ Browser Speech Recognition not supported."); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (recognition) { |
|
|
try { recognition.stop(); } catch (e) { } |
|
|
recognition = null; |
|
|
} |
|
|
|
|
|
|
|
|
window.currentTranscript = ""; |
|
|
|
|
|
try { |
|
|
|
|
|
const sourceLangQuick = document.getElementById('source-lang-quick'); |
|
|
const targetLangQuick = document.getElementById('target-lang-quick'); |
|
|
|
|
|
const targetLang = targetLangQuick?.value || quickLangSelector?.value || 'French'; |
|
|
let sourceLang = sourceLangQuick?.value || localStorage.getItem('sourceLangQuick') || 'auto'; |
|
|
|
|
|
console.log(`๐ฏ Quick Selectors: Source=${sourceLang}, Target=${targetLang}`); |
|
|
|
|
|
|
|
|
if (sourceLang === 'auto') { |
|
|
|
|
|
if (targetLang === 'French') { |
|
|
sourceLang = 'ar-SA'; |
|
|
arabicModeActive = true; |
|
|
console.log('๐ฒ๐ฆ AUTO MODE: Target=French โ Assuming Arabic/Darija'); |
|
|
} else if (targetLang === 'Moroccan Darija' || targetLang === 'Arabic') { |
|
|
sourceLang = 'fr-FR'; |
|
|
arabicModeActive = false; |
|
|
console.log('๐ซ๐ท AUTO MODE: Target=Arabic โ Assuming French'); |
|
|
} else if (targetLang === 'English') { |
|
|
|
|
|
sourceLang = detectedLanguage === 'French' ? 'fr-FR' : 'ar-SA'; |
|
|
arabicModeActive = sourceLang === 'ar-SA'; |
|
|
console.log(`๐ AUTO MODE: Target=English โ Assuming ${sourceLang}`); |
|
|
} else { |
|
|
sourceLang = 'fr-FR'; |
|
|
arabicModeActive = false; |
|
|
} |
|
|
} else { |
|
|
|
|
|
arabicModeActive = sourceLang === 'ar-SA'; |
|
|
console.log(`๐ MANUAL MODE: Source=${sourceLang} (Arabic mode: ${arabicModeActive})`); |
|
|
} |
|
|
|
|
|
|
|
|
recognition = new SpeechRecognition(); |
|
|
recognition.continuous = true; |
|
|
recognition.interimResults = true; |
|
|
recognition.lang = sourceLang; |
|
|
currentRecognitionLang = sourceLang; |
|
|
|
|
|
console.log(`๐ค Browser Recognition: ${sourceLang} (Arabic mode: ${arabicModeActive})`); |
|
|
|
|
|
|
|
|
recognition.onstart = () => { |
|
|
console.log("โ
Real-time transcription active"); |
|
|
if (navigator.vibrate) navigator.vibrate(50); |
|
|
}; |
|
|
|
|
|
recognition.onerror = (event) => { |
|
|
console.warn("โ Recognition error:", event.error); |
|
|
if (event.error === 'not-allowed') { |
|
|
statusText.innerText = "โ ๏ธ Accรจs micro refusรฉ"; |
|
|
statusText.style.color = "yellow"; |
|
|
} else if (event.error !== 'aborted') { |
|
|
|
|
|
if (window.continuousMode && isRecording) { |
|
|
console.log("๐ Restarting recognition after error..."); |
|
|
setTimeout(() => { |
|
|
if (window.continuousMode && isRecording) { |
|
|
try { recognition.start(); } catch (e) { } |
|
|
} |
|
|
}, 500); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
recognition.onend = () => { |
|
|
console.log("๐ Recognition ended"); |
|
|
|
|
|
|
|
|
if (window.continuousMode && isRecording) { |
|
|
if (isTTSPlaying) { |
|
|
console.log("โธ๏ธ TTS is playing - recognition will restart when TTS ends"); |
|
|
|
|
|
} else { |
|
|
console.log("๐ Auto-restarting recognition for continuous mode..."); |
|
|
setTimeout(() => { |
|
|
if (window.continuousMode && isRecording && !isTTSPlaying) { |
|
|
try { |
|
|
recognition.start(); |
|
|
console.log("โ
Recognition restarted successfully"); |
|
|
} catch (e) { |
|
|
console.warn("Could not restart recognition:", e); |
|
|
} |
|
|
} |
|
|
}, 300); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
recognition.onresult = (event) => { |
|
|
let interimTranscript = ''; |
|
|
let finalTranscript = ''; |
|
|
|
|
|
for (let i = event.resultIndex; i < event.results.length; ++i) { |
|
|
const transcript = event.results[i][0].transcript; |
|
|
if (event.results[i].isFinal) { |
|
|
finalTranscript += transcript; |
|
|
} else { |
|
|
interimTranscript += transcript; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const fullText = finalTranscript || interimTranscript; |
|
|
|
|
|
|
|
|
if (finalTranscript.trim().length > 0) { |
|
|
window.currentTranscript = finalTranscript; |
|
|
} else if (interimTranscript.trim().length > 0) { |
|
|
window.currentTranscript = interimTranscript; |
|
|
} |
|
|
|
|
|
if (fullText.trim().length > 0) { |
|
|
|
|
|
|
|
|
if (arabicModeActive) { |
|
|
|
|
|
const hasArabicChars = /[\u0600-\u06FF]/.test(fullText); |
|
|
if (hasArabicChars && originalTextField) { |
|
|
originalTextField.innerText = fullText; |
|
|
originalTextField.hidden = false; |
|
|
originalTextField.style.opacity = '1'; |
|
|
originalTextField.style.direction = 'rtl'; |
|
|
originalTextField.style.textAlign = 'right'; |
|
|
originalTextField.style.fontSize = '1.3rem'; |
|
|
originalTextField.style.fontWeight = '500'; |
|
|
} else { |
|
|
|
|
|
if (originalTextField) { |
|
|
originalTextField.innerText = '๐ค ุฌุงุฑู ุงูุงุณุชู
ุงุน...'; |
|
|
originalTextField.style.direction = 'rtl'; |
|
|
originalTextField.style.textAlign = 'right'; |
|
|
originalTextField.style.opacity = '0.7'; |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
if (originalTextField) { |
|
|
originalTextField.innerText = fullText; |
|
|
originalTextField.hidden = false; |
|
|
originalTextField.style.opacity = '1'; |
|
|
originalTextField.style.direction = 'ltr'; |
|
|
originalTextField.style.textAlign = 'left'; |
|
|
originalTextField.style.fontSize = '1.2rem'; |
|
|
originalTextField.style.fontWeight = '500'; |
|
|
originalTextField.style.lineHeight = '1.6'; |
|
|
originalTextField.style.fontStyle = 'normal'; |
|
|
originalTextField.style.animation = 'fadeIn 0.3s ease'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const chatHistory = document.getElementById('chat-history'); |
|
|
if (chatHistory) chatHistory.scrollTop = chatHistory.scrollHeight; |
|
|
|
|
|
|
|
|
if (finalTranscript.trim().length > 2) { |
|
|
console.log("โ
Sentence transcribed (visual feedback only)"); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
recognition.start(); |
|
|
|
|
|
} catch (e) { |
|
|
console.error("โ Fatal Error starting recognition:", e); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function sendTextForProcessing(text) { |
|
|
if (isProcessingAudio) { |
|
|
console.log("โ ๏ธ Already processing, skipping duplicate..."); |
|
|
return; |
|
|
} |
|
|
isProcessingAudio = true; |
|
|
|
|
|
const targetLang = quickLangSelector?.value || 'French'; |
|
|
|
|
|
|
|
|
console.log(`๐ค Sending text for processing: "${text}"`); |
|
|
statusText.innerText = 'Traduction en cours...'; |
|
|
statusText.style.color = '#4a9b87'; |
|
|
|
|
|
const payload = { |
|
|
text_input: text, |
|
|
source_language: 'auto', |
|
|
target_language: targetLang, |
|
|
model: localStorage.getItem('selectedModel') || 'Gemini', |
|
|
tts_engine: localStorage.getItem('ttsEngine') || 'openai', |
|
|
stt_engine: localStorage.getItem('sttEngine') || 'seamless-m4t', |
|
|
ai_correction: localStorage.getItem('aiCorrectionEnabled') !== 'false', |
|
|
voice_cloning: false, |
|
|
use_grammar_correction: localStorage.getItem('grammarCorrectionEnabled') !== 'false', |
|
|
voice_gender_preference: localStorage.getItem('voiceGenderPreference') || 'auto' |
|
|
}; |
|
|
|
|
|
try { |
|
|
const response = await fetch(API_BASE + '/process_audio', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(payload) |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.error) { |
|
|
console.error("โ Processing error:", data.error); |
|
|
statusText.innerText = 'Erreur'; |
|
|
} else { |
|
|
|
|
|
|
|
|
if (translatedTextField) { |
|
|
translatedTextField.innerText = data.translated_text; |
|
|
translatedTextField.style.opacity = '1'; |
|
|
} |
|
|
|
|
|
|
|
|
if (data.source_language_full) { |
|
|
const newLang = data.source_language_full; |
|
|
console.log(`๐ง SMART MODE: Latching onto detected language: ${newLang}`); |
|
|
|
|
|
|
|
|
|
|
|
const langToCode = { |
|
|
'French': 'fr-FR', |
|
|
'English': 'en-US', |
|
|
'Arabic': 'ar-SA', |
|
|
'Moroccan Darija': 'ar-MA', |
|
|
'Spanish': 'es-ES', |
|
|
'German': 'de-DE', |
|
|
'Italian': 'it-IT', |
|
|
'Portuguese': 'pt-PT', |
|
|
'Russian': 'ru-RU', |
|
|
'Japanese': 'ja-JP', |
|
|
'Korean': 'ko-KR', |
|
|
'Chinese': 'zh-CN', |
|
|
'Hindi': 'hi-IN' |
|
|
}; |
|
|
|
|
|
const code = langToCode[newLang]; |
|
|
if (code) { |
|
|
currentRecognitionLang = code; |
|
|
detectedLanguage = newLang; |
|
|
|
|
|
|
|
|
if (document.getElementById('source-lang-selector').value === 'auto') { |
|
|
console.log(`๐ UPDATING RECOGNITION to ${code} for next turn`); |
|
|
|
|
|
|
|
|
if (recognition) { |
|
|
try { recognition.stop(); } catch (e) { } |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
console.log("โ
Text processing complete - TTS will play automatically"); |
|
|
|
|
|
|
|
|
if (window.continuousMode) { |
|
|
statusText.innerText = '๐ Lecture TTS...'; |
|
|
statusText.style.color = '#4a9b87'; |
|
|
console.log('๐๏ธ Continuous mode active - will resume listening after TTS'); |
|
|
} else { |
|
|
statusText.innerText = 'Prรชt'; |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
console.error("โ Text processing error:", e); |
|
|
statusText.innerText = 'Erreur rรฉseau'; |
|
|
} finally { |
|
|
isProcessingAudio = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function startSmartRecording() { |
|
|
try { |
|
|
console.log('๐ค STARTING RECORDING...'); |
|
|
isRecording = true; |
|
|
recordBtn.classList.add('active'); |
|
|
statusText.innerText = 'รcoute...'; |
|
|
statusText.style.color = 'white'; |
|
|
|
|
|
document.dispatchEvent(new Event('reset-ui')); |
|
|
originalTextField.innerText = '...'; |
|
|
translatedTextField.innerText = '...'; |
|
|
|
|
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ |
|
|
audio: { |
|
|
echoCancellation: true, |
|
|
noiseSuppression: true, |
|
|
autoGainControl: true, |
|
|
channelCount: 1, |
|
|
sampleRate: 48000 |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
|
|
|
|
|
|
|
if (audioContext.state === 'suspended') { |
|
|
await audioContext.resume(); |
|
|
console.log('โก AudioContext Force-Resumed'); |
|
|
} |
|
|
analyser = audioContext.createAnalyser(); |
|
|
micSource = audioContext.createMediaStreamSource(stream); |
|
|
micSource.connect(analyser); |
|
|
analyser.fftSize = 256; |
|
|
const bufferLength = analyser.frequencyBinCount; |
|
|
const dataArray = new Uint8Array(bufferLength); |
|
|
|
|
|
let silenceStart = Date.now(); |
|
|
|
|
|
|
|
|
let smartSpeechDetected = false; |
|
|
|
|
|
function detectSilence() { |
|
|
if (!isRecording) return; |
|
|
|
|
|
analyser.getByteFrequencyData(dataArray); |
|
|
|
|
|
|
|
|
let sum = 0; |
|
|
for (let i = 0; i < bufferLength; i++) sum += dataArray[i]; |
|
|
const average = sum / bufferLength; |
|
|
|
|
|
|
|
|
const scale = 1 + (average / 100); |
|
|
recordBtn.style.transform = `scale(${Math.min(scale, 1.2)})`; |
|
|
|
|
|
|
|
|
if (average < VOLUME_THRESHOLD && !smartSpeechDetected) { |
|
|
statusText.innerText = '๐ค En attente de parole...'; |
|
|
statusText.style.color = 'rgba(255,255,255,0.7)'; |
|
|
} |
|
|
|
|
|
if (average < VOLUME_THRESHOLD) { |
|
|
|
|
|
if (Date.now() - silenceStart > SILENCE_LIMIT_MS) { |
|
|
|
|
|
console.log("๐คซ Silence limit reached."); |
|
|
stopSmartRecording(); |
|
|
return; |
|
|
} |
|
|
} else { |
|
|
|
|
|
silenceStart = Date.now(); |
|
|
if (!smartSpeechDetected) { |
|
|
smartSpeechDetected = true; |
|
|
console.log("๐ฃ๏ธ Speech detected!"); |
|
|
statusText.innerText = '๐ค Je vous รฉcoute...'; |
|
|
statusText.style.color = '#fff'; |
|
|
recordBtn.classList.add('active-speech'); |
|
|
} |
|
|
} |
|
|
|
|
|
animationId = requestAnimationFrame(detectSilence); |
|
|
} |
|
|
|
|
|
detectSilence(); |
|
|
|
|
|
|
|
|
try { startRealTimeTranscription(); } catch (e) { } |
|
|
|
|
|
|
|
|
mediaRecorder = new MediaRecorder(stream); |
|
|
audioChunks = []; |
|
|
mediaRecorder.ondataavailable = e => audioChunks.push(e.data); |
|
|
mediaRecorder.onstop = async () => { |
|
|
console.log("๐ Recorder stopped. Processing audio..."); |
|
|
|
|
|
|
|
|
if (recognition) { try { recognition.stop(); } catch (e) { } } |
|
|
if (animationId) cancelAnimationFrame(animationId); |
|
|
if (micSource) micSource.disconnect(); |
|
|
if (audioContext) audioContext.close(); |
|
|
|
|
|
if (audioChunks.length > 0) { |
|
|
const blob = new Blob(audioChunks, { type: 'audio/wav' }); |
|
|
console.log(`๐ฆ Audio Data: ${blob.size} bytes`); |
|
|
|
|
|
|
|
|
statusText.innerText = 'Traitement...'; |
|
|
statusText.style.color = '#4a9b87'; |
|
|
|
|
|
try { |
|
|
await processAudio(blob); |
|
|
} catch (e) { |
|
|
console.error("Error in processAudio", e); |
|
|
statusText.innerText = 'Erreur'; |
|
|
} |
|
|
} else { |
|
|
console.error("โ Audio was empty!"); |
|
|
statusText.innerText = 'Audio Vide'; |
|
|
} |
|
|
|
|
|
|
|
|
if (window.continuousMode) { |
|
|
|
|
|
if (isTTSPlaying && window.lastBotAudio) { |
|
|
console.log("โธ๏ธ TTS Playing - Waiting for audio to finish before restarting..."); |
|
|
|
|
|
|
|
|
const originalEnded = window.lastBotAudio.onended; |
|
|
window.lastBotAudio.onended = () => { |
|
|
if (originalEnded) originalEnded(); |
|
|
console.log("โ
TTS Finished - Restarting conversation loop"); |
|
|
|
|
|
setTimeout(() => { |
|
|
if (window.continuousMode) listenContinuously(); |
|
|
}, 100); |
|
|
}; |
|
|
return; |
|
|
} else { |
|
|
|
|
|
console.log("๐ Auto-restarting conversation loop (No TTS active)..."); |
|
|
setTimeout(() => { |
|
|
if (window.continuousMode) listenContinuously(); |
|
|
}, 100); |
|
|
} |
|
|
} else { |
|
|
statusText.innerText = 'Prรชt'; |
|
|
} |
|
|
}; |
|
|
mediaRecorder.start(); |
|
|
console.log("๐ค Recording started (with Auto-Stop)..."); |
|
|
|
|
|
} catch (err) { |
|
|
console.error(err); |
|
|
statusText.innerText = "Erreur Micro"; |
|
|
isRecording = false; |
|
|
recordBtn.classList.remove('active'); |
|
|
} |
|
|
} |
|
|
|
|
|
function stopSmartRecording() { |
|
|
if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop(); |
|
|
if (recognition) { try { recognition.stop(); } catch (e) { } } |
|
|
isRecording = false; |
|
|
recordBtn.classList.remove('active'); |
|
|
statusText.innerText = 'Rรฉflexion...'; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function debouncedStreamTranslation(text) { |
|
|
if (streamTimeout) clearTimeout(streamTimeout); |
|
|
streamTimeout = setTimeout(() => performStreamTranslation(text), 200); |
|
|
} |
|
|
|
|
|
async function performStreamTranslation(text) { |
|
|
try { |
|
|
const res = await axios.post('/stream_text', { |
|
|
text: text, |
|
|
target_lang: quickLangSelector?.value || 'English' |
|
|
}); |
|
|
if (res.data.translation) { |
|
|
translatedTextField.innerText = res.data.translation; |
|
|
|
|
|
|
|
|
if (res.data.translation.trim().length > 0) { |
|
|
translatedTextField.style.opacity = '1'; |
|
|
console.log('๐ Real-time translation:', res.data.translation); |
|
|
} |
|
|
} |
|
|
} catch (e) { console.error("Stream Error", e); } |
|
|
} |
|
|
|
|
|
|
|
|
function analyzeAudioEnergy(blob) { |
|
|
return new Promise((resolve) => { |
|
|
const reader = new FileReader(); |
|
|
reader.readAsArrayBuffer(blob); |
|
|
reader.onloadend = async () => { |
|
|
try { |
|
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
|
const audioBuffer = await audioContext.decodeAudioData(reader.result); |
|
|
|
|
|
|
|
|
const channelData = audioBuffer.getChannelData(0); |
|
|
|
|
|
|
|
|
let sum = 0; |
|
|
for (let i = 0; i < channelData.length; i++) { |
|
|
sum += channelData[i] * channelData[i]; |
|
|
} |
|
|
const rms = Math.sqrt(sum / channelData.length); |
|
|
|
|
|
|
|
|
let peak = 0; |
|
|
for (let i = 0; i < channelData.length; i++) { |
|
|
const abs = Math.abs(channelData[i]); |
|
|
if (abs > peak) peak = abs; |
|
|
} |
|
|
|
|
|
|
|
|
const duration = audioBuffer.duration; |
|
|
|
|
|
console.log(`๐ Audio Analysis: RMS=${rms.toFixed(4)}, Peak=${peak.toFixed(4)}, Duration=${duration.toFixed(2)}s`); |
|
|
|
|
|
|
|
|
|
|
|
resolve({ rms, peak, duration, isSilent: rms < 0.002 && peak < 0.01 }); |
|
|
} catch (e) { |
|
|
console.error('โ ๏ธ Audio analysis failed:', e); |
|
|
resolve({ rms: 0, peak: 0, duration: 0, isSilent: true }); |
|
|
} |
|
|
}; |
|
|
}); |
|
|
} |
|
|
|
|
|
async function processAudio(blob, bypassSilenceCheck = false) { |
|
|
|
|
|
if (isProcessingAudio) { |
|
|
console.log('โ ๏ธ Audio already being processed, skipping...'); |
|
|
return; |
|
|
} |
|
|
|
|
|
isProcessingAudio = true; |
|
|
recordBtn.classList.add('processing'); |
|
|
recordBtn.classList.remove('active'); |
|
|
recordBtn.classList.remove('active-speech'); |
|
|
|
|
|
|
|
|
|
|
|
if (!bypassSilenceCheck) { |
|
|
const audioAnalysis = await analyzeAudioEnergy(blob); |
|
|
|
|
|
|
|
|
if (audioAnalysis.isSilent) { |
|
|
console.warn('๐ SILENCE DETECTED (Threshold check failed) - Skipping processing'); |
|
|
console.log(`๐ Analysis: RMS=${audioAnalysis.rms}, Peak=${audioAnalysis.peak}`); |
|
|
statusText.innerText = 'Trop silencieux'; |
|
|
isProcessingAudio = false; |
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
statusText.innerText = 'Prรชt'; |
|
|
|
|
|
if (window.continuousMode) listenContinuously(); |
|
|
}, 100); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (audioAnalysis.duration < 0.5) { |
|
|
console.log(`โฑ๏ธ Audio too short (${audioAnalysis.duration.toFixed(2)}s) - Skipping`); |
|
|
statusText.innerText = 'Audio trop court'; |
|
|
isProcessingAudio = false; |
|
|
|
|
|
setTimeout(() => { |
|
|
statusText.innerText = 'Prรชt'; |
|
|
if (window.continuousMode) listenContinuously(); |
|
|
}, 800); |
|
|
return; |
|
|
} |
|
|
} else { |
|
|
console.log("โก SPEED: Bypassing secondary silence check (Speech already confirmed)"); |
|
|
} |
|
|
|
|
|
console.log('โ
Audio validation passed - Processing...'); |
|
|
|
|
|
const startTime = Date.now(); |
|
|
const reader = new FileReader(); |
|
|
reader.readAsDataURL(blob); |
|
|
reader.onloadend = async () => { |
|
|
const base64 = reader.result.split(',')[1]; |
|
|
try { |
|
|
|
|
|
|
|
|
|
|
|
let textInput = (window.currentTranscript || originalTextField.innerText || "").trim(); |
|
|
|
|
|
|
|
|
textInput = textInput.replace('...', '').replace('๐ค', '').trim(); |
|
|
|
|
|
|
|
|
if (textInput.includes('รcoute') || textInput.length < 2) { |
|
|
textInput = ''; |
|
|
console.log('๐ฏ Using backend STT only (no client text available)'); |
|
|
} else { |
|
|
console.log(`๐ค Client-Side STT Injected: "${textInput}" (Skipping Server STT)`); |
|
|
} |
|
|
|
|
|
|
|
|
const targetLangQuick = document.getElementById('target-lang-quick'); |
|
|
const sourceLangQuick = document.getElementById('source-lang-quick'); |
|
|
const selectedTarget = targetLangQuick?.value || quickLangSelector?.value || 'French'; |
|
|
const selectedSource = sourceLangQuick?.value || 'auto'; |
|
|
|
|
|
const settings = { |
|
|
audio: base64, |
|
|
text_input: textInput, |
|
|
target_language: selectedTarget, |
|
|
source_language: selectedSource === 'auto' ? 'auto' : selectedSource, |
|
|
stt_engine: localStorage.getItem('sttEngine') || 'openai-whisper', |
|
|
model: localStorage.getItem('aiModel') || 'gpt-4o-mini', |
|
|
tts_engine: localStorage.getItem('ttsEngine') || 'seamless', |
|
|
openai_api_key: localStorage.getItem('openaiKey'), |
|
|
google_api_key: localStorage.getItem('googleKey'), |
|
|
openai_voice: localStorage.getItem('openaiVoice') || 'nova', |
|
|
elevenlabs_key: localStorage.getItem('elevenlabsKey'), |
|
|
use_grammar_correction: localStorage.getItem('grammarCorrectionEnabled') !== 'false', |
|
|
voice_gender_preference: localStorage.getItem('voiceGenderPreference') || 'auto' |
|
|
}; |
|
|
|
|
|
console.log(`๐ Grammar Correction: ${settings.use_grammar_correction ? 'ENABLED (GPT)' : 'DISABLED (Direct Translation)'}`); |
|
|
console.log(`๐๏ธ Voice Gender Preference: ${settings.voice_gender_preference.toUpperCase()}`); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const voiceCloneEnabled = localStorage.getItem('voiceCloneEnabled') === 'true'; |
|
|
const ttsEngine = settings.tts_engine; |
|
|
|
|
|
console.log(`๐ญ Voice Cloning Status: ${voiceCloneEnabled ? 'ENABLED' : 'DISABLED'}`); |
|
|
|
|
|
|
|
|
if (voiceCloneEnabled) { |
|
|
console.log('๐ค Voice Cloning ENABLED โ Sending audio sample to server'); |
|
|
settings.voice_audio = `data:audio/wav;base64,${base64}`; |
|
|
settings.voice_cloning = true; |
|
|
} else { |
|
|
console.log('๐ Voice Cloning DISABLED โ Using gender-matched fallback voices'); |
|
|
settings.voice_cloning = false; |
|
|
} |
|
|
|
|
|
const res = await axios.post('/process_audio', settings); |
|
|
|
|
|
if (res.data.translated_text) { |
|
|
const translation = res.data.translated_text; |
|
|
const userText = settings.text_input; |
|
|
|
|
|
console.log('โ
Response received:', { |
|
|
original: userText?.substring(0, 50), |
|
|
translation: translation?.substring(0, 50), |
|
|
hasAudio: !!res.data.tts_audio |
|
|
}); |
|
|
|
|
|
|
|
|
const resultDisplay = document.getElementById('result-display'); |
|
|
const originalDisplay = document.getElementById('original-display'); |
|
|
const translationDisplay = document.getElementById('translation-display'); |
|
|
const pronunciationDisplay = document.getElementById('pronunciation-display'); |
|
|
const greeting = document.getElementById('greeting'); |
|
|
|
|
|
if (resultDisplay && translationDisplay) { |
|
|
if (greeting) greeting.style.display = 'none'; |
|
|
resultDisplay.style.display = 'block'; |
|
|
if (originalDisplay) originalDisplay.innerText = userText || 'Audio input'; |
|
|
|
|
|
|
|
|
if (pronunciationDisplay) { |
|
|
const pronunciation = res.data.pronunciation; |
|
|
if (pronunciation && pronunciation !== translation) { |
|
|
pronunciationDisplay.innerText = pronunciation; |
|
|
pronunciationDisplay.style.display = 'block'; |
|
|
} else { |
|
|
pronunciationDisplay.style.display = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
translationDisplay.innerText = translation; |
|
|
console.log('๐บ Result displayed on screen'); |
|
|
} |
|
|
|
|
|
|
|
|
if (res.data.tts_audio) { |
|
|
const audioSrc = `data:audio/mp3;base64,${res.data.tts_audio}`; |
|
|
const audio = new Audio(audioSrc); |
|
|
audio.play().then(() => { |
|
|
console.log('๐ Audio playing!'); |
|
|
}).catch(err => { |
|
|
console.log('โ Auto-play blocked:', err); |
|
|
|
|
|
if (translationDisplay) { |
|
|
translationDisplay.innerHTML += ' <button onclick="this.previousSibling.click()" style="background:#4CAF50;color:white;border:none;padding:5px 10px;border-radius:5px;cursor:pointer;">โถ๏ธ Play</button>'; |
|
|
} |
|
|
}); |
|
|
window.lastAudio = audio; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (isHallucination(userText) || isHallucination(translation)) { |
|
|
console.log(`๐ซ HALLUCINATION DETECTED - Skipping message creation`); |
|
|
console.log(` User: "${userText}" | Translation: "${translation}"`); |
|
|
statusText.innerText = 'Prรชt'; |
|
|
isProcessingAudio = false; |
|
|
recordBtn.classList.remove('processing'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (res.data.source_language_full && sourceLangSelector) { |
|
|
const detectedLang = res.data.source_language_full; |
|
|
|
|
|
|
|
|
detectedLanguage = detectedLang; |
|
|
console.log(`๐ Language auto-detected: ${detectedLang}`); |
|
|
|
|
|
|
|
|
if (recognition) { |
|
|
const langMap = { |
|
|
'English': 'en-US', 'French': 'fr-FR', 'Spanish': 'es-ES', |
|
|
'German': 'de-DE', 'Italian': 'it-IT', 'Portuguese': 'pt-PT', |
|
|
'Russian': 'ru-RU', 'Japanese': 'ja-JP', 'Korean': 'ko-KR', |
|
|
'Chinese': 'zh-CN', 'Arabic': 'ar-SA', 'Hindi': 'hi-IN', |
|
|
'Dutch': 'nl-NL', 'Polish': 'pl-PL', 'Turkish': 'tr-TR', |
|
|
'Indonesian': 'id-ID', 'Malay': 'ms-MY', 'Thai': 'th-TH', |
|
|
'Vietnamese': 'vi-VN', 'Bengali': 'bn-IN', 'Urdu': 'ur-PK', |
|
|
'Swahili': 'sw-KE', 'Hebrew': 'he-IL', 'Persian': 'fa-IR', |
|
|
'Ukrainian': 'uk-UA', 'Swedish': 'sv-SE', 'Greek': 'el-GR', |
|
|
'Czech': 'cs-CZ', 'Romanian': 'ro-RO', 'Hungarian': 'hu-HU', |
|
|
'Danish': 'da-DK', 'Finnish': 'fi-FI', 'Norwegian': 'no-NO', |
|
|
'Slovak': 'sk-SK', 'Filipino': 'fil-PH', 'Amharic': 'am-ET' |
|
|
}; |
|
|
|
|
|
const speechLang = langMap[detectedLang] || navigator.language || 'en-US'; |
|
|
console.log(`๐ค Speech recognition updated to: ${speechLang}`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const greetingEl = document.getElementById('greeting'); |
|
|
if (greetingEl) greetingEl.style.display = 'none'; |
|
|
|
|
|
|
|
|
|
|
|
const sourceLang = res.data.source_language_full || 'Auto'; |
|
|
const targetLang = res.data.target_language || 'Translation'; |
|
|
createChatMessage('user', userText, null, null, sourceLang); |
|
|
|
|
|
|
|
|
let messageAudioSrc = null; |
|
|
if (res.data.tts_audio) { |
|
|
messageAudioSrc = `data:audio/mp3;base64,${res.data.tts_audio}`; |
|
|
|
|
|
audioPlayer.src = messageAudioSrc; |
|
|
audioPlayer.play().catch(err => { |
|
|
console.log('Auto-play blocked:', err); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const info = { |
|
|
latency: ((Date.now() - startTime) / 1000).toFixed(2), |
|
|
stt: res.data.stt_engine, |
|
|
translation: res.data.translation_engine, |
|
|
tts: res.data.tts_engine |
|
|
}; |
|
|
createChatMessage('bot', translation, messageAudioSrc, info, targetLang); |
|
|
|
|
|
|
|
|
if (window.continuousMode) { |
|
|
|
|
|
statusText.innerText = 'รcoute en continu...'; |
|
|
console.log('โ
TTS gรฉnรฉrรฉ - En attente de la prochaine phrase'); |
|
|
} else { |
|
|
|
|
|
isRecording = false; |
|
|
recordBtn.classList.remove('active'); |
|
|
recordBtn.disabled = false; |
|
|
statusText.innerText = 'Prรชt'; |
|
|
console.log('โ
TTS gรฉnรฉrรฉ - Bouton prรชt'); |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
console.error("Erreur de traitement:", e); |
|
|
statusText.innerText = "Erreur de connexion"; |
|
|
|
|
|
|
|
|
isRecording = false; |
|
|
recordBtn.classList.remove('active'); |
|
|
recordBtn.disabled = false; |
|
|
} |
|
|
finally { |
|
|
|
|
|
recordBtn.disabled = false; |
|
|
recordBtn.classList.remove('processing'); |
|
|
isProcessingAudio = false; |
|
|
|
|
|
|
|
|
if (!window.continuousMode) { |
|
|
statusText.innerText = 'Prรชt'; |
|
|
} |
|
|
} |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
window.loadModalSettings = () => { |
|
|
document.getElementById('stt-engine').value = localStorage.getItem('sttEngine') || 'openai-whisper'; |
|
|
document.getElementById('openai-key').value = localStorage.getItem('openaiKey') || ''; |
|
|
if (localStorage.getItem('sourceLang')) document.getElementById('source-lang-selector').value = localStorage.getItem('sourceLang'); |
|
|
|
|
|
|
|
|
const savedTargetLang = localStorage.getItem('targetLang'); |
|
|
if (savedTargetLang && quickLangSelector) { |
|
|
quickLangSelector.value = savedTargetLang; |
|
|
} else if (quickLangSelector) { |
|
|
|
|
|
quickLangSelector.value = 'French'; |
|
|
console.log('๐ Default target language set to French for bidirectional translation'); |
|
|
} |
|
|
}; |
|
|
|
|
|
window.saveModalSettings = () => { |
|
|
localStorage.setItem('sttEngine', document.getElementById('stt-engine').value); |
|
|
localStorage.setItem('openaiKey', document.getElementById('openai-key').value); |
|
|
localStorage.setItem('targetLang', quickLangSelector.value); |
|
|
localStorage.setItem('sourceLang', document.getElementById('source-lang-selector').value); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function setupToggle(id, storageKey, defaultValue, onToggle) { |
|
|
const btn = document.getElementById(id); |
|
|
if (!btn) return; |
|
|
|
|
|
|
|
|
const saved = localStorage.getItem(storageKey); |
|
|
const isActive = saved === null ? defaultValue : saved === 'true'; |
|
|
|
|
|
if (isActive) btn.classList.add('active'); |
|
|
else btn.classList.remove('active'); |
|
|
|
|
|
btn.addEventListener('click', (e) => { |
|
|
e.stopPropagation(); |
|
|
const currentlyActive = btn.classList.contains('active'); |
|
|
const newState = !currentlyActive; |
|
|
|
|
|
|
|
|
if (newState) btn.classList.add('active'); |
|
|
else btn.classList.remove('active'); |
|
|
|
|
|
|
|
|
localStorage.setItem(storageKey, newState); |
|
|
|
|
|
|
|
|
if (onToggle) onToggle(newState); |
|
|
|
|
|
console.log(`๐ Toggle ${id}: ${newState ? 'ON' : 'OFF'}`); |
|
|
}); |
|
|
} |
|
|
|
|
|
function setupCycle(id, storageKey, values, onCycle) { |
|
|
const btn = document.getElementById(id); |
|
|
if (!btn) return; |
|
|
|
|
|
|
|
|
let currentVal = localStorage.getItem(storageKey) || values[0]; |
|
|
if (!values.includes(currentVal)) currentVal = values[0]; |
|
|
|
|
|
const updateVisual = (val) => { |
|
|
|
|
|
|
|
|
|
|
|
if (val !== values[0]) btn.classList.add('active'); |
|
|
else btn.classList.remove('active'); |
|
|
|
|
|
|
|
|
btn.title = `Mode: ${val.toUpperCase()}`; |
|
|
}; |
|
|
|
|
|
updateVisual(currentVal); |
|
|
|
|
|
btn.addEventListener('click', (e) => { |
|
|
e.stopPropagation(); |
|
|
const currentIndex = values.indexOf(currentVal); |
|
|
const nextIndex = (currentIndex + 1) % values.length; |
|
|
currentVal = values[nextIndex]; |
|
|
|
|
|
localStorage.setItem(storageKey, currentVal); |
|
|
updateVisual(currentVal); |
|
|
|
|
|
if (onCycle) onCycle(currentVal); |
|
|
console.log(`๐ Cycle ${id}: ${currentVal}`); |
|
|
|
|
|
|
|
|
statusText.innerText = `Mode: ${currentVal.toUpperCase()}`; |
|
|
setTimeout(() => statusText.innerText = 'Prรชt', 1500); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
setupToggle('grammar-toggle', 'grammarCorrectionEnabled', true, (state) => { |
|
|
statusText.innerText = state ? 'โจ Correction: ON' : '๐ Correction: OFF'; |
|
|
setTimeout(() => statusText.innerText = 'Prรชt', 1500); |
|
|
}); |
|
|
|
|
|
|
|
|
setupCycle('voice-gender-toggle', 'voiceGenderPreference', ['auto', 'male', 'female']); |
|
|
|
|
|
|
|
|
setupToggle('smart-mode-toggle', 'smartModeEnabled', true, (state) => { |
|
|
statusText.innerText = state ? '๐ง Mode Smart: ON' : '๐ง Mode Smart: OFF'; |
|
|
setTimeout(() => statusText.innerText = 'Prรชt', 1500); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
const settingsBtn = document.getElementById('settings-trigger'); |
|
|
const closeSettingsBtn = document.getElementById('close-settings'); |
|
|
const settingsModal = document.getElementById('settings-modal'); |
|
|
|
|
|
|
|
|
if (settingsBtn && settingsModal) { |
|
|
settingsBtn.addEventListener('click', () => { |
|
|
settingsModal.style.display = 'flex'; |
|
|
|
|
|
console.log('โ๏ธ Settings Opened'); |
|
|
}); |
|
|
} else { |
|
|
console.error('โ Settings Trigger or Modal NOT FOUND'); |
|
|
} |
|
|
|
|
|
|
|
|
if (closeSettingsBtn && settingsModal) { |
|
|
closeSettingsBtn.addEventListener('click', () => { |
|
|
settingsModal.style.display = 'none'; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
window.addEventListener('click', (e) => { |
|
|
if (e.target === settingsModal) { |
|
|
settingsModal.style.display = 'none'; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const saveBtn = document.getElementById('save-settings'); |
|
|
const aiSelector = document.getElementById('ai-model-selector'); |
|
|
const ttsSelector = document.getElementById('tts-selector'); |
|
|
|
|
|
|
|
|
if (aiSelector) aiSelector.value = localStorage.getItem('aiModel') || 'gpt-4o-mini'; |
|
|
if (ttsSelector) ttsSelector.value = localStorage.getItem('ttsEngine') || 'openai'; |
|
|
|
|
|
|
|
|
if (saveBtn) { |
|
|
saveBtn.addEventListener('click', () => { |
|
|
if (aiSelector) { |
|
|
localStorage.setItem('aiModel', aiSelector.value); |
|
|
console.log(`๐ง AI Model set to: ${aiSelector.value}`); |
|
|
} |
|
|
if (ttsSelector) { |
|
|
localStorage.setItem('ttsEngine', ttsSelector.value); |
|
|
console.log(`๐ฃ๏ธ TTS Engine set to: ${ttsSelector.value}`); |
|
|
} |
|
|
|
|
|
|
|
|
if (settingsModal) settingsModal.style.display = 'none'; |
|
|
|
|
|
|
|
|
if (statusText) { |
|
|
statusText.innerText = 'โ
Sauvegardรฉ!'; |
|
|
setTimeout(() => statusText.innerText = 'Prรชt', 2000); |
|
|
} |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
if (quickLangSelector) { |
|
|
const savedTargetLang = localStorage.getItem('targetLang'); |
|
|
if (savedTargetLang) { |
|
|
quickLangSelector.value = savedTargetLang; |
|
|
console.log(`๐ Loaded saved target language: ${savedTargetLang}`); |
|
|
} else { |
|
|
quickLangSelector.value = 'French'; |
|
|
console.log('๐ Default target language set to French for Arabic โ French bidirectional translation'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const sourceLangSelector = document.getElementById('source-lang-selector'); |
|
|
if (sourceLangSelector) { |
|
|
sourceLangSelector.value = 'auto'; |
|
|
console.log('๐ฏ Source language set to AUTO for automatic detection'); |
|
|
|
|
|
|
|
|
sourceLangSelector.addEventListener('change', function () { |
|
|
console.log('๐ Source Language Changed -> Clearing Smart History...'); |
|
|
localStorage.setItem('sourceLang', this.value); |
|
|
fetch(API_BASE + '/clear_cache', { method: 'POST' }); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (quickLangSelector) { |
|
|
quickLangSelector.addEventListener('change', function () { |
|
|
console.log('๐ Target Language Changed -> Clearing Smart History...'); |
|
|
localStorage.setItem('targetLang', this.value); |
|
|
fetch(API_BASE + '/clear_cache', { method: 'POST' }); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let voiceCloneEnabled = localStorage.getItem('voiceCloneEnabled') !== 'false'; |
|
|
|
|
|
const voiceCloneToggle = document.getElementById('voice-clone-toggle'); |
|
|
if (voiceCloneToggle) { |
|
|
|
|
|
if (voiceCloneEnabled) { |
|
|
voiceCloneToggle.classList.add('active'); |
|
|
} else { |
|
|
voiceCloneToggle.classList.remove('active'); |
|
|
} |
|
|
|
|
|
|
|
|
voiceCloneToggle.addEventListener('click', function () { |
|
|
voiceCloneEnabled = !voiceCloneEnabled; |
|
|
localStorage.setItem('voiceCloneEnabled', voiceCloneEnabled); |
|
|
|
|
|
if (voiceCloneEnabled) { |
|
|
this.classList.add('active'); |
|
|
console.log('๐ญ Voice Cloning: ON'); |
|
|
} else { |
|
|
this.classList.remove('active'); |
|
|
console.log('๐ญ Voice Cloning: OFF'); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
}); |