| 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 = '<i class="fas fa-circle-notch spinner text-brand text-xl"></i>'; |
| const ICON_CHECK = '<i class="fas fa-check-circle text-brand text-xl"></i>'; |
|
|
| 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); |
|
|
| |
| if (progress > 0) { |
| |
| if (currentStep < 2) { |
| updateStepUI(1, 'done'); |
| currentStep = 2; |
| } |
|
|
| if (progress <= 65) { |
| |
| const iaPercent = ((progress - 20) / 45) * 100; |
| updateStepUI(2, 'active', Math.max(0, iaPercent)); |
| updateStepUI(3, 'pending'); |
| } else if (progress < 100) { |
| |
| 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); |
| } |
| }; |
| } |
|
|
| |
| 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); |
| setUint32(length - 8); |
| setUint32(0x45564157); |
| setUint32(0x20746d66); |
| setUint32(16); |
| setUint16(1); |
| setUint16(numOfChan); |
| setUint32(abuffer.sampleRate); |
| setUint32(abuffer.sampleRate * 2 * numOfChan); |
| setUint16(numOfChan * 2); |
| setUint16(16); |
| setUint32(0x61746164); |
| setUint32(length - pos - 4); |
|
|
| 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; |
|
|
| |
| connectWebSocket(async () => { |
| uploadState.classList.add('hidden'); |
| loadingState.classList.remove('hidden'); |
| updateStepUI(1, 'active', 0); |
|
|
| let serverFileId = null; |
|
|
| try { |
| |
| 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); |
|
|
| |
| 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); |
| |
| 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); |
| } |
| }); |
| } |
|
|
| |
| 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(); |
| video.play().catch(() => {}); |
|
|
| |
| videoUsageCount++; |
| localStorage.setItem('video_usage_count', videoUsageCount); |
|
|
| |
| if (videoUsageCount > 0 && videoUsageCount % 3 === 0) { |
| setTimeout(openFeedback, 2000); |
| } |
| } |
|
|
| |
| 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; |
|
|
| |
| const modalContent = document.querySelector('#feedback-modal .glass'); |
| const originalHTML = modalContent.innerHTML; |
|
|
| modalContent.innerHTML = ` |
| <div class="text-center py-10 fade-in"> |
| <div class="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-6"> |
| <i class="fas fa-check text-3xl text-green-500"></i> |
| </div> |
| <h2 class="text-2xl font-bold mb-4">Merci beaucoup !</h2> |
| <p class="text-gray-400 mb-8">Votre retour a bien été envoyé. Shadrak apprécie votre aide pour améliorer cet outil.</p> |
| <button onclick="closeFeedback()" class="bg-brand text-black font-bold py-3 px-8 rounded-xl transition-all hover:scale-105"> |
| Fermer |
| </button> |
| </div> |
| `; |
|
|
| |
| 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]); |
| }); |
| } |
|
|