| | |
| |
|
| | 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'); |
| | } |
| | }); |
| | } |
| |
|
| | }); |