File size: 11,573 Bytes
d0b5038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
<!DOCTYPE html>
<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>