const clientId = Math.random().toString(36).substring(7); const uploadState = document.getElementById('upload-state'); const loadingState = document.getElementById('loading-state'); const resultState = document.getElementById('result-state'); const fileInput = document.getElementById('file-input'); const dropZone = document.getElementById('drop-zone'); let socket; let currentStep = 1; const ICON_SPINNER = ''; const ICON_CHECK = ''; function updateStepUI(stepNum, status, percent = 0) { const stepEl = document.getElementById(`step-${stepNum}`); if (!stepEl) return; const iconContainer = stepEl.querySelector('.status-icon'); const percentLabel = stepEl.querySelector('.step-percent'); stepEl.classList.remove('step-pending', 'step-active', 'step-done', 'opacity-30'); if (status === 'active') { stepEl.classList.add('step-active'); iconContainer.innerHTML = ICON_SPINNER; percentLabel.innerText = `${Math.round(percent)}%`; percentLabel.classList.add('text-brand'); } else if (status === 'done') { stepEl.classList.add('step-done'); iconContainer.innerHTML = ICON_CHECK; percentLabel.innerText = '100%'; percentLabel.classList.remove('text-brand'); } else { stepEl.classList.add('step-pending', 'opacity-30'); percentLabel.innerText = '0%'; } } function connectWebSocket(callback) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; socket = new WebSocket(`${protocol}//${window.location.host}/ws/${clientId}`); socket.onopen = () => { console.log("WebSocket connecté"); if (callback) callback(); }; socket.onmessage = (event) => { const data = JSON.parse(event.data); const progress = data.progress; console.log("Progrès reçu:", progress, data.status); // Logique de transition automatique des étapes if (progress > 0) { // Si on reçoit du progrès, l'upload (Etape 1) est forcément fini if (currentStep < 2) { updateStepUI(1, 'done'); currentStep = 2; } if (progress <= 65) { // Phase IA (Etape 2) const iaPercent = ((progress - 20) / 45) * 100; updateStepUI(2, 'active', Math.max(0, iaPercent)); updateStepUI(3, 'pending'); } else if (progress < 100) { // Phase Rendu (Etape 3) if (currentStep < 3) { updateStepUI(2, 'done'); currentStep = 3; } const renderPercent = ((progress - 65) / 35) * 100; updateStepUI(3, 'active', Math.max(0, renderPercent)); } } if (data.result_url || data.stream_url) { updateStepUI(3, 'done'); const finalUrl = data.stream_url || data.result_url; setTimeout(() => showResult(finalUrl, data.result_url), 800); } }; } // Helper pour encoder en WAV (format simple pour le serveur) function bufferToWave(abuffer, len) { let numOfChan = abuffer.numberOfChannels, length = len * numOfChan * 2 + 44, buffer = new ArrayBuffer(length), view = new DataView(buffer), channels = [], i, sample, offset = 0, pos = 0; setUint32(0x46464952); // "RIFF" setUint32(length - 8); // file length - 8 setUint32(0x45564157); // "WAVE" setUint32(0x20746d66); // "fmt " chunk setUint32(16); // length = 16 setUint16(1); // PCM (uncompressed) setUint16(numOfChan); setUint32(abuffer.sampleRate); setUint32(abuffer.sampleRate * 2 * numOfChan); // avg. bytes/sec setUint16(numOfChan * 2); // block-align setUint16(16); // 16-bit setUint32(0x61746164); // "data" - chunk setUint32(length - pos - 4); // chunk length for (i = 0; i < abuffer.numberOfChannels; i++) channels.push(abuffer.getChannelData(i)); while (pos < length) { for (i = 0; i < numOfChan; i++) { sample = Math.max(-1, Math.min(1, channels[i][offset])); sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; view.setInt16(pos, sample, true); pos += 2; } offset++; } return new Blob([buffer], { type: "audio/wav" }); function setUint16(data) { view.setUint16(pos, data, true); pos += 2; } function setUint32(data) { view.setUint32(pos, data, true); pos += 4; } } async function extractAudio(file) { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const arrayBuffer = await file.arrayBuffer(); const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer); return bufferToWave(audioBuffer, audioBuffer.length); } async function handleUpload(file) { if (!file) return; // Pré-connexion au WebSocket connectWebSocket(async () => { uploadState.classList.add('hidden'); loadingState.classList.remove('hidden'); updateStepUI(1, 'active', 0); let serverFileId = null; try { // 1. EXTRACTION ET UPLOAD AUDIO (Priorité haute) console.log("Extraction audio..."); const audioBlob = await extractAudio(file); const audioData = new FormData(); audioData.append('file', audioBlob, 'audio.wav'); const audioXhr = new XMLHttpRequest(); audioXhr.open('POST', `/upload?client_id=${clientId}&type=audio`); audioXhr.onload = () => { const res = JSON.parse(audioXhr.responseText); serverFileId = res.file_id; console.log("Audio uploadé, ID:", serverFileId); // 2. UPLOAD VIDÉO (En arrière-plan) const videoData = new FormData(); videoData.append('file', file); const videoXhr = new XMLHttpRequest(); videoXhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = (e.loaded / e.total) * 100; updateStepUI(1, 'active', percent); } }); videoXhr.open('POST', `/upload?client_id=${clientId}&file_id=${serverFileId}&type=video`); videoXhr.send(videoData); }; audioXhr.send(audioData); } catch (err) { console.error("Erreur Turbo-Pipeline:", err); // Fallback sur upload classique si l'extraction échoue const formData = new FormData(); formData.append('file', file); const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = (e.loaded / e.total) * 100; updateStepUI(1, 'active', percent); } }); xhr.open('POST', `/upload?client_id=${clientId}`); xhr.send(formData); } }); } // Gestion des usages pour le feedback let videoUsageCount = parseInt(localStorage.getItem('video_usage_count') || '0'); let feedbackShown = localStorage.getItem('feedback_shown') === 'true'; let selectedRating = null; function showResult(url, downloadUrl) { loadingState.classList.add('hidden'); resultState.classList.remove('hidden'); const video = document.getElementById('final-video'); video.src = url; document.getElementById('download-btn').href = downloadUrl || url; video.load(); // Important pour forcer le rechargement avec le stream video.play().catch(() => {}); // Incrémenter le compteur d'usage videoUsageCount++; localStorage.setItem('video_usage_count', videoUsageCount); // Afficher le modal tous les 3 usages (3, 6, 9...) if (videoUsageCount > 0 && videoUsageCount % 3 === 0) { setTimeout(openFeedback, 2000); } } // Fonctions Feedback function openFeedback() { document.getElementById('feedback-modal').classList.remove('hidden'); } function closeFeedback() { document.getElementById('feedback-modal').classList.add('hidden'); } function setRating(e, rating) { selectedRating = rating; document.querySelectorAll('.rating-btn').forEach(btn => { btn.classList.remove('border-brand', 'bg-brand/10'); }); const target = e.currentTarget || e.target; target.classList.add('border-brand', 'bg-brand/10'); } async function sendFeedback() { const commentInput = document.getElementById('feedback-text'); const comment = commentInput ? commentInput.value : ""; if (!selectedRating) { const errorMsg = document.createElement('p'); errorMsg.id = 'feedback-error'; errorMsg.className = 'text-red-500 text-xs text-center mt-2 animate-pulse'; errorMsg.innerText = "Veuillez sélectionner 'Oui' ou 'Non' pour continuer."; const existingError = document.getElementById('feedback-error'); if (!existingError) { document.querySelector('.rating-btn').parentNode.parentNode.appendChild(errorMsg); } return; } const existingError = document.getElementById('feedback-error'); if (existingError) existingError.remove(); const btn = document.getElementById('submit-feedback'); if (btn) { btn.disabled = true; btn.innerText = "Envoi..."; } try { const response = await fetch('/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: clientId, rating: selectedRating, comment: comment }) }); if (response.ok) { localStorage.setItem('feedback_shown', 'true'); feedbackShown = true; // Remplacer le contenu du modal par un message de succès const modalContent = document.querySelector('#feedback-modal .glass'); const originalHTML = modalContent.innerHTML; modalContent.innerHTML = `

Merci beaucoup !

Votre retour a bien été envoyé. Shadrak apprécie votre aide pour améliorer cet outil.

`; // Réinitialiser le modal après la fermeture (pour une prochaine ouverture éventuelle) setTimeout(() => { if (commentInput) commentInput.value = ""; selectedRating = null; }, 500); } } catch (err) { console.error("Erreur feedback:", err); } finally { if (btn) { btn.disabled = false; btn.innerText = "Envoyer le feedback"; } } } if (fileInput) { fileInput.addEventListener('change', () => handleUpload(fileInput.files[0])); } if (dropZone) { ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(n => { dropZone.addEventListener(n, e => { e.preventDefault(); e.stopPropagation(); }); }); dropZone.addEventListener('dragenter', () => uploadState && uploadState.classList.add('border-brand', 'bg-brand/5')); dropZone.addEventListener('dragleave', () => uploadState && uploadState.classList.remove('border-brand', 'bg-brand/5')); dropZone.addEventListener('drop', (e) => { uploadState && uploadState.classList.remove('border-brand', 'bg-brand/5'); handleUpload(e.dataTransfer.files[0]); }); }