Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| <html lang="fr" class="h-full"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Wami - Demo Live</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| </head> | |
| <body class="h-full bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900"> | |
| <div class="min-h-full"> | |
| <!-- Header --> | |
| <header class="bg-slate-800/50 backdrop-blur-sm border-b border-slate-700"> | |
| <div class="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <h1 class="text-3xl font-bold text-white">🎙️ Wami - Demo Live</h1> | |
| <p class="text-slate-300 mt-1">API Dioula STT, TTS & Traduction</p> | |
| </div> | |
| <a href="/docs" target="_blank" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition"> | |
| 📖 API Docs | |
| </a> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8"> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <!-- STT Card --> | |
| <div class="bg-slate-800/50 backdrop-blur-sm rounded-xl shadow-xl p-6 border border-slate-700"> | |
| <h2 class="text-2xl font-bold text-white mb-4 flex items-center"> | |
| <span class="text-3xl mr-2">🎤</span> Speech-to-Text | |
| </h2> | |
| <p class="text-slate-300 mb-4">Enregistrez votre voix en Dioula</p> | |
| <div class="space-y-4"> | |
| <button id="sttBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-4 px-6 rounded-lg transition transform hover:scale-105 flex items-center justify-center gap-2"> | |
| <span id="sttBtnIcon">🎙️</span> | |
| <span id="sttBtnText">Cliquer pour enregistrer</span> | |
| </button> | |
| <div id="sttStatus" class="text-sm text-slate-400 text-center min-h-6"></div> | |
| <div id="sttResult" class="bg-slate-900/50 rounded-lg p-4 min-h-24 text-white hidden"> | |
| <div class="text-sm text-slate-400 mb-2">Transcription :</div> | |
| <div id="sttText" class="text-lg"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TTS Card --> | |
| <div class="bg-slate-800/50 backdrop-blur-sm rounded-xl shadow-xl p-6 border border-slate-700"> | |
| <h2 class="text-2xl font-bold text-white mb-4 flex items-center"> | |
| <span class="text-3xl mr-2">🔊</span> Text-to-Speech | |
| </h2> | |
| <p class="text-slate-300 mb-4">Générez un audio en Dioula</p> | |
| <div class="space-y-4"> | |
| <textarea id="ttsInput" class="w-full bg-slate-900/50 text-white rounded-lg p-4 border border-slate-600 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 transition" rows="3" placeholder="Tapez du texte en Dioula..."></textarea> | |
| <button id="ttsBtn" class="w-full bg-green-600 hover:bg-green-700 text-white font-semibold py-3 px-6 rounded-lg transition transform hover:scale-105"> | |
| 🔊 Générer la voix | |
| </button> | |
| <div id="ttsStatus" class="text-sm text-slate-400 text-center min-h-6"></div> | |
| <audio id="ttsAudio" controls class="w-full rounded-lg hidden"></audio> | |
| </div> | |
| </div> | |
| <!-- Translation DYU→FR Card --> | |
| <div class="bg-slate-800/50 backdrop-blur-sm rounded-xl shadow-xl p-6 border border-slate-700"> | |
| <h2 class="text-2xl font-bold text-white mb-4 flex items-center"> | |
| <span class="text-3xl mr-2">🇲🇱</span> Dioula → Français | |
| </h2> | |
| <p class="text-slate-300 mb-4">Traduisez du Dioula vers le Français</p> | |
| <div class="space-y-4"> | |
| <textarea id="dyuInput" class="w-full bg-slate-900/50 text-white rounded-lg p-4 border border-slate-600 focus:border-purple-500 focus:ring-2 focus:ring-purple-500 transition" rows="3" placeholder="Texte en Dioula..."></textarea> | |
| <button id="dyuBtn" class="w-full bg-purple-600 hover:bg-purple-700 text-white font-semibold py-3 px-6 rounded-lg transition transform hover:scale-105"> | |
| → Traduire en Français | |
| </button> | |
| <div id="dyuStatus" class="text-sm text-slate-400 text-center min-h-6"></div> | |
| <div id="dyuResult" class="bg-slate-900/50 rounded-lg p-4 min-h-20 text-white hidden"> | |
| <div class="text-sm text-slate-400 mb-2">🇫🇷 Traduction française :</div> | |
| <div id="dyuText" class="text-lg"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Translation FR→DYU Card --> | |
| <div class="bg-slate-800/50 backdrop-blur-sm rounded-xl shadow-xl p-6 border border-slate-700"> | |
| <h2 class="text-2xl font-bold text-white mb-4 flex items-center"> | |
| <span class="text-3xl mr-2">🇫🇷</span> Français → Dioula | |
| </h2> | |
| <p class="text-slate-300 mb-4">Traduisez du Français vers le Dioula</p> | |
| <div class="space-y-4"> | |
| <textarea id="frInput" class="w-full bg-slate-900/50 text-white rounded-lg p-4 border border-slate-600 focus:border-orange-500 focus:ring-2 focus:ring-orange-500 transition" rows="3" placeholder="Texte en Français..."></textarea> | |
| <button id="frBtn" class="w-full bg-orange-600 hover:bg-orange-700 text-white font-semibold py-3 px-6 rounded-lg transition transform hover:scale-105"> | |
| → Traduire en Dioula | |
| </button> | |
| <div id="frStatus" class="text-sm text-slate-400 text-center min-h-6"></div> | |
| <div id="frResult" class="bg-slate-900/50 rounded-lg p-4 min-h-20 text-white hidden"> | |
| <div class="text-sm text-slate-400 mb-2">🇲🇱 Traduction dioula :</div> | |
| <div id="frText" class="text-lg"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Examples Section --> | |
| <div class="mt-8 bg-slate-800/50 backdrop-blur-sm rounded-xl shadow-xl p-6 border border-slate-700"> | |
| <h3 class="text-xl font-bold text-white mb-4">💡 Exemples de phrases</h3> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="bg-slate-900/50 p-4 rounded-lg"> | |
| <div class="text-sm text-slate-400 mb-1">Dioula:</div> | |
| <div class="text-white">Sanji bɛna kɛ bi</div> | |
| <div class="text-sm text-green-400 mt-2">→ Il pleuvra demain</div> | |
| </div> | |
| <div class="bg-slate-900/50 p-4 rounded-lg"> | |
| <div class="text-sm text-slate-400 mb-1">Dioula:</div> | |
| <div class="text-white">Aw ka séné ninnge ma</div> | |
| <div class="text-sm text-green-400 mt-2">→ Protégez vos récoltes</div> | |
| </div> | |
| <div class="bg-slate-900/50 p-4 rounded-lg"> | |
| <div class="text-sm text-slate-400 mb-1">Français:</div> | |
| <div class="text-white">Bonjour, comment allez-vous?</div> | |
| <div class="text-sm text-green-400 mt-2">→ I ni sɔgɔma, i ka kɛnɛya?</div> | |
| </div> | |
| <div class="bg-slate-900/50 p-4 rounded-lg"> | |
| <div class="text-sm text-slate-400 mb-1">Français:</div> | |
| <div class="text-white">Le temps est beau aujourd'hui</div> | |
| <div class="text-sm text-green-400 mt-2">→ Tile ka di bi</div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| // ========== STT ========== | |
| const sttBtn = document.getElementById('sttBtn'); | |
| const sttBtnIcon = document.getElementById('sttBtnIcon'); | |
| const sttBtnText = document.getElementById('sttBtnText'); | |
| const sttStatus = document.getElementById('sttStatus'); | |
| const sttResult = document.getElementById('sttResult'); | |
| const sttText = document.getElementById('sttText'); | |
| let mediaRecorder = null; | |
| let audioChunks = []; | |
| let isRecording = false; | |
| sttBtn.addEventListener('click', async () => { | |
| if (!isRecording) { | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' }); | |
| 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 sendAudioToSTT(blob); | |
| }; | |
| mediaRecorder.start(); | |
| isRecording = true; | |
| sttBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); | |
| sttBtn.classList.add('bg-red-600', 'hover:bg-red-700', 'animate-pulse'); | |
| sttBtnIcon.textContent = '🔴'; | |
| sttBtnText.textContent = 'Enregistrement... Cliquez pour arrêter'; | |
| } catch (err) { | |
| sttStatus.textContent = '❌ Erreur : accès micro refusé'; | |
| sttStatus.classList.add('text-red-400'); | |
| } | |
| } else { | |
| mediaRecorder.stop(); | |
| isRecording = false; | |
| sttBtn.classList.remove('bg-red-600', 'hover:bg-red-700', 'animate-pulse'); | |
| sttBtn.classList.add('bg-blue-600', 'hover:bg-blue-700'); | |
| sttBtnIcon.textContent = '🎙️'; | |
| sttBtnText.textContent = 'Cliquer pour enregistrer'; | |
| } | |
| }); | |
| async function sendAudioToSTT(blob) { | |
| sttStatus.textContent = '⏳ Transcription en cours...'; | |
| sttStatus.classList.remove('text-red-400'); | |
| sttStatus.classList.add('text-blue-400'); | |
| sttResult.classList.add('hidden'); | |
| const formData = new FormData(); | |
| formData.append('audio', blob, 'recording.wav'); | |
| try { | |
| const res = await fetch('/api/stt', { method: 'POST', body: formData }); | |
| const data = await res.json(); | |
| sttText.textContent = data.transcription || 'Aucun résultat'; | |
| sttResult.classList.remove('hidden'); | |
| sttStatus.textContent = '✅ Transcription terminée'; | |
| sttStatus.classList.remove('text-blue-400'); | |
| sttStatus.classList.add('text-green-400'); | |
| } catch (err) { | |
| sttStatus.textContent = '❌ Erreur : ' + err.message; | |
| sttStatus.classList.remove('text-blue-400'); | |
| sttStatus.classList.add('text-red-400'); | |
| } | |
| } | |
| // ========== TTS ========== | |
| const ttsBtn = document.getElementById('ttsBtn'); | |
| const ttsInput = document.getElementById('ttsInput'); | |
| const ttsStatus = document.getElementById('ttsStatus'); | |
| const ttsAudio = document.getElementById('ttsAudio'); | |
| ttsBtn.addEventListener('click', async () => { | |
| const text = ttsInput.value.trim(); | |
| if (!text) { | |
| ttsStatus.textContent = '⚠️ Veuillez entrer du texte'; | |
| ttsStatus.classList.add('text-yellow-400'); | |
| return; | |
| } | |
| ttsBtn.disabled = true; | |
| ttsStatus.textContent = '⏳ Génération en cours...'; | |
| ttsStatus.classList.remove('text-yellow-400', 'text-red-400'); | |
| ttsStatus.classList.add('text-blue-400'); | |
| ttsAudio.classList.add('hidden'); | |
| const formData = new FormData(); | |
| formData.append('text', text); | |
| try { | |
| const res = await fetch('/api/tts', { method: 'POST', body: formData }); | |
| const blob = await res.blob(); | |
| const url = URL.createObjectURL(blob); | |
| ttsAudio.src = url; | |
| ttsAudio.classList.remove('hidden'); | |
| ttsAudio.play(); | |
| ttsStatus.textContent = '✅ Audio généré'; | |
| ttsStatus.classList.remove('text-blue-400'); | |
| ttsStatus.classList.add('text-green-400'); | |
| } catch (err) { | |
| ttsStatus.textContent = '❌ Erreur : ' + err.message; | |
| ttsStatus.classList.remove('text-blue-400'); | |
| ttsStatus.classList.add('text-red-400'); | |
| } finally { | |
| ttsBtn.disabled = false; | |
| } | |
| }); | |
| // ========== Translation DYU→FR ========== | |
| const dyuBtn = document.getElementById('dyuBtn'); | |
| const dyuInput = document.getElementById('dyuInput'); | |
| const dyuStatus = document.getElementById('dyuStatus'); | |
| const dyuResult = document.getElementById('dyuResult'); | |
| const dyuText = document.getElementById('dyuText'); | |
| dyuBtn.addEventListener('click', async () => { | |
| const text = dyuInput.value.trim(); | |
| if (!text) { | |
| dyuStatus.textContent = '⚠️ Veuillez entrer du texte'; | |
| dyuStatus.classList.add('text-yellow-400'); | |
| return; | |
| } | |
| dyuBtn.disabled = true; | |
| dyuStatus.textContent = '⏳ Traduction en cours...'; | |
| dyuStatus.classList.remove('text-yellow-400', 'text-red-400'); | |
| dyuStatus.classList.add('text-blue-400'); | |
| dyuResult.classList.add('hidden'); | |
| const formData = new FormData(); | |
| formData.append('text', text); | |
| try { | |
| const res = await fetch('/api/translate/dyu-fr', { method: 'POST', body: formData }); | |
| const data = await res.json(); | |
| dyuText.textContent = data.texte_traduit; | |
| dyuResult.classList.remove('hidden'); | |
| dyuStatus.textContent = '✅ Traduction terminée'; | |
| dyuStatus.classList.remove('text-blue-400'); | |
| dyuStatus.classList.add('text-green-400'); | |
| } catch (err) { | |
| dyuStatus.textContent = '❌ Erreur : ' + err.message; | |
| dyuStatus.classList.remove('text-blue-400'); | |
| dyuStatus.classList.add('text-red-400'); | |
| } finally { | |
| dyuBtn.disabled = false; | |
| } | |
| }); | |
| // ========== Translation FR→DYU ========== | |
| const frBtn = document.getElementById('frBtn'); | |
| const frInput = document.getElementById('frInput'); | |
| const frStatus = document.getElementById('frStatus'); | |
| const frResult = document.getElementById('frResult'); | |
| const frText = document.getElementById('frText'); | |
| frBtn.addEventListener('click', async () => { | |
| const text = frInput.value.trim(); | |
| if (!text) { | |
| frStatus.textContent = '⚠️ Veuillez entrer du texte'; | |
| frStatus.classList.add('text-yellow-400'); | |
| return; | |
| } | |
| frBtn.disabled = true; | |
| frStatus.textContent = '⏳ Traduction en cours...'; | |
| frStatus.classList.remove('text-yellow-400', 'text-red-400'); | |
| frStatus.classList.add('text-blue-400'); | |
| frResult.classList.add('hidden'); | |
| const formData = new FormData(); | |
| formData.append('text', text); | |
| try { | |
| const res = await fetch('/api/translate/fr-dyu', { method: 'POST', body: formData }); | |
| const data = await res.json(); | |
| frText.textContent = data.texte_traduit; | |
| frResult.classList.remove('hidden'); | |
| frStatus.textContent = '✅ Traduction terminée'; | |
| frStatus.classList.remove('text-blue-400'); | |
| frStatus.classList.add('text-green-400'); | |
| } catch (err) { | |
| frStatus.textContent = '❌ Erreur : ' + err.message; | |
| frStatus.classList.remove('text-blue-400'); | |
| frStatus.classList.add('text-red-400'); | |
| } finally { | |
| frBtn.disabled = false; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |