Spaces:
Sleeping
Sleeping
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WebSocket Pipeline - Exemple</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| </head> | |
| <body class="bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 min-h-screen p-8"> | |
| <div class="max-w-4xl mx-auto"> | |
| <h1 class="text-4xl font-bold text-white mb-2">🔄 WebSocket Pipeline</h1> | |
| <p class="text-slate-300 mb-8">Pipeline temps réel : Audio → STT → Traduction</p> | |
| <div class="grid gap-6"> | |
| <!-- Controls --> | |
| <div class="bg-slate-800/50 backdrop-blur-sm rounded-xl p-6 border border-slate-700"> | |
| <h2 class="text-xl font-bold text-white mb-4">📡 Contrôles</h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="text-slate-300 mb-2 block">Langue cible :</label> | |
| <select id="targetLang" class="w-full bg-slate-900 text-white rounded-lg px-4 py-2 border border-slate-600"> | |
| <option value="fr">Français</option> | |
| <option value="dyu">Dioula</option> | |
| </select> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <input type="checkbox" id="includeTts" class="w-5 h-5"> | |
| <label for="includeTts" class="text-slate-300">Générer l'audio (TTS)</label> | |
| </div> | |
| <button id="recordBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-4 rounded-lg transition"> | |
| 🎙️ Enregistrer et envoyer | |
| </button> | |
| <button id="connectBtn" class="w-full bg-green-600 hover:bg-green-700 text-white font-semibold py-2 rounded-lg transition"> | |
| 🔌 Connecter WebSocket | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Status --> | |
| <div class="bg-slate-800/50 backdrop-blur-sm rounded-xl p-6 border border-slate-700"> | |
| <h2 class="text-xl font-bold text-white mb-4">📊 Statut</h2> | |
| <div id="status" class="space-y-2"> | |
| <div class="text-slate-400">WebSocket: <span id="wsStatus" class="text-red-400">Déconnecté</span></div> | |
| <div class="text-slate-400">Étape: <span id="currentStep" class="text-slate-300">-</span></div> | |
| </div> | |
| </div> | |
| <!-- Progress Log --> | |
| <div class="bg-slate-800/50 backdrop-blur-sm rounded-xl p-6 border border-slate-700"> | |
| <h2 class="text-xl font-bold text-white mb-4">📝 Journal</h2> | |
| <div id="log" class="bg-slate-900/50 rounded-lg p-4 h-48 overflow-y-auto font-mono text-sm text-green-400"></div> | |
| </div> | |
| <!-- Results --> | |
| <div class="bg-slate-800/50 backdrop-blur-sm rounded-xl p-6 border border-slate-700"> | |
| <h2 class="text-xl font-bold text-white mb-4">✨ Résultats</h2> | |
| <div class="space-y-4"> | |
| <div id="transcriptionDiv" class="hidden"> | |
| <div class="text-sm text-slate-400 mb-1">Transcription (Dioula) :</div> | |
| <div id="transcription" class="bg-slate-900/50 rounded-lg p-4 text-white"></div> | |
| </div> | |
| <div id="traductionDiv" class="hidden"> | |
| <div class="text-sm text-slate-400 mb-1">Traduction :</div> | |
| <div id="traduction" class="bg-slate-900/50 rounded-lg p-4 text-white"></div> | |
| </div> | |
| <div id="audioDiv" class="hidden"> | |
| <div class="text-sm text-slate-400 mb-2">Audio généré :</div> | |
| <audio id="audioResult" controls class="w-full"></audio> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let ws = null; | |
| let mediaRecorder = null; | |
| let audioChunks = []; | |
| const log = document.getElementById('log'); | |
| const wsStatus = document.getElementById('wsStatus'); | |
| const currentStep = document.getElementById('currentStep'); | |
| const recordBtn = document.getElementById('recordBtn'); | |
| const connectBtn = document.getElementById('connectBtn'); | |
| function addLog(message, type = 'info') { | |
| const time = new Date().toLocaleTimeString(); | |
| const color = type === 'error' ? 'text-red-400' : type === 'success' ? 'text-green-400' : 'text-blue-400'; | |
| log.innerHTML += `<div class="${color}">[${time}] ${message}</div>`; | |
| log.scrollTop = log.scrollHeight; | |
| } | |
| // Connect WebSocket | |
| connectBtn.addEventListener('click', () => { | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const wsUrl = `${protocol}//${window.location.host}/ws/pipeline`; | |
| ws = new WebSocket(wsUrl); | |
| ws.onopen = () => { | |
| wsStatus.textContent = 'Connecté'; | |
| wsStatus.className = 'text-green-400'; | |
| addLog('WebSocket connecté', 'success'); | |
| connectBtn.disabled = true; | |
| connectBtn.textContent = '✅ Connecté'; | |
| }; | |
| ws.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| handleMessage(data); | |
| }; | |
| ws.onerror = (error) => { | |
| addLog('Erreur WebSocket: ' + error, 'error'); | |
| }; | |
| ws.onclose = () => { | |
| wsStatus.textContent = 'Déconnecté'; | |
| wsStatus.className = 'text-red-400'; | |
| addLog('WebSocket déconnecté', 'error'); | |
| connectBtn.disabled = false; | |
| connectBtn.textContent = '🔌 Reconnecter'; | |
| }; | |
| }); | |
| function handleMessage(data) { | |
| if (data.error) { | |
| addLog('❌ Erreur: ' + data.error, 'error'); | |
| currentStep.textContent = 'Erreur'; | |
| return; | |
| } | |
| if (data.status === 'processing') { | |
| currentStep.textContent = data.step; | |
| if (data.message) { | |
| addLog(data.message); | |
| } | |
| if (data.transcription) { | |
| document.getElementById('transcription').textContent = data.transcription; | |
| document.getElementById('transcriptionDiv').classList.remove('hidden'); | |
| addLog('✅ Transcription: ' + data.transcription, 'success'); | |
| } | |
| if (data.traduction) { | |
| document.getElementById('traduction').textContent = data.traduction; | |
| document.getElementById('traductionDiv').classList.remove('hidden'); | |
| addLog('✅ Traduction: ' + data.traduction, 'success'); | |
| } | |
| } | |
| if (data.status === 'done') { | |
| currentStep.textContent = 'Terminé ✅'; | |
| addLog('✨ Pipeline terminé', 'success'); | |
| if (data.audio_base64) { | |
| const audioBlob = base64ToBlob(data.audio_base64, 'audio/wav'); | |
| const audioUrl = URL.createObjectURL(audioBlob); | |
| document.getElementById('audioResult').src = audioUrl; | |
| document.getElementById('audioDiv').classList.remove('hidden'); | |
| addLog('🔊 Audio généré', 'success'); | |
| } | |
| } | |
| } | |
| // Record and send | |
| recordBtn.addEventListener('click', async () => { | |
| if (!ws || ws.readyState !== WebSocket.OPEN) { | |
| alert('WebSocket non connecté. Cliquez sur "Connecter WebSocket" d\'abord.'); | |
| return; | |
| } | |
| if (!mediaRecorder || mediaRecorder.state === 'inactive') { | |
| // Start recording | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| mediaRecorder = new MediaRecorder(stream); | |
| audioChunks = []; | |
| mediaRecorder.ondataavailable = (e) => { | |
| if (e.data.size > 0) audioChunks.push(e.data); | |
| }; | |
| mediaRecorder.onstop = async () => { | |
| stream.getTracks().forEach(t => t.stop()); | |
| const blob = new Blob(audioChunks, { type: 'audio/webm' }); | |
| await sendAudioToWebSocket(blob); | |
| }; | |
| mediaRecorder.start(); | |
| recordBtn.textContent = '⏹️ Arrêter et envoyer'; | |
| recordBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); | |
| recordBtn.classList.add('bg-red-600', 'hover:bg-red-700', 'animate-pulse'); | |
| addLog('🎙️ Enregistrement en cours...'); | |
| } catch (err) { | |
| addLog('❌ Erreur micro: ' + err.message, 'error'); | |
| } | |
| } else { | |
| // Stop recording | |
| mediaRecorder.stop(); | |
| recordBtn.textContent = '🎙️ Enregistrer et envoyer'; | |
| recordBtn.classList.remove('bg-red-600', 'hover:bg-red-700', 'animate-pulse'); | |
| recordBtn.classList.add('bg-blue-600', 'hover:bg-blue-700'); | |
| } | |
| }); | |
| async function sendAudioToWebSocket(blob) { | |
| addLog('📤 Envoi de l\'audio...'); | |
| // Convert blob to base64 | |
| const base64 = await blobToBase64(blob); | |
| const message = { | |
| action: 'process', | |
| audio: base64.split(',')[1], // Remove data:audio/webm;base64, | |
| target_lang: document.getElementById('targetLang').value, | |
| include_tts: document.getElementById('includeTts').checked | |
| }; | |
| ws.send(JSON.stringify(message)); | |
| addLog('✅ Audio envoyé', 'success'); | |
| // Clear previous results | |
| document.getElementById('transcriptionDiv').classList.add('hidden'); | |
| document.getElementById('traductionDiv').classList.add('hidden'); | |
| document.getElementById('audioDiv').classList.add('hidden'); | |
| currentStep.textContent = 'En traitement...'; | |
| } | |
| function blobToBase64(blob) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onloadend = () => resolve(reader.result); | |
| reader.onerror = reject; | |
| reader.readAsDataURL(blob); | |
| }); | |
| } | |
| function base64ToBlob(base64, mimeType) { | |
| const byteCharacters = atob(base64); | |
| const byteNumbers = new Array(byteCharacters.length); | |
| for (let i = 0; i < byteCharacters.length; i++) { | |
| byteNumbers[i] = byteCharacters.charCodeAt(i); | |
| } | |
| const byteArray = new Uint8Array(byteNumbers); | |
| return new Blob([byteArray], { type: mimeType }); | |
| } | |
| // Auto-connect on load | |
| window.addEventListener('load', () => { | |
| addLog('Page chargée. Cliquez sur "Connecter WebSocket" pour commencer.'); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |