cdupland commited on
Commit
a47b1b6
·
verified ·
1 Parent(s): bc92670

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +351 -17
index.html CHANGED
@@ -1,19 +1,353 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Enregistreur Audio — Envoi API</title>
7
+ <!-- Tailwind CDN (utile pour preview rapide). Dans un projet Angular avec Tailwind installé, vous pouvez supprimer cette ligne. -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ </head>
10
+ <body class="bg-gray-50 min-h-screen flex items-center justify-center p-6">
11
+ <main class="w-full max-w-xl bg-white rounded-2xl shadow p-6">
12
+ <h1 class="text-2xl font-semibold mb-4">Enregistreur audio</h1>
13
+
14
+ <section id="controls" class="space-y-3">
15
+ <div class="flex gap-3">
16
+ <button id="startBtn" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50">Démarrer</button>
17
+ <button id="stopBtn" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50" disabled>Stopper</button>
18
+ <button id="downloadBtn" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50" disabled>Télécharger</button>
19
+ </div>
20
+
21
+ <div class="text-sm text-gray-600">Durée : <span id="timer">00:00</span></div>
22
+
23
+ <div class="mt-2">
24
+ <label class="block text-sm font-medium text-gray-700">API endpoint (changer par défaut si besoin)</label>
25
+ <input id="apiEndpoint" class="mt-1 block w-full rounded border-gray-200 shadow-sm" value="https://n8n.bziiit.com/webhook/transcript-audio" />
26
+ </div>
27
+
28
+ <div class="mt-2 text-xs text-gray-500">Note : l'audio sera automatiquement envoyé à l'API lors de l'arrêt de la captation.</div>
29
+ </section>
30
+
31
+ <hr class="my-4" />
32
+
33
+ <section id="upload" class="space-y-3">
34
+ <h2 class="text-lg font-medium text-gray-800">Upload d'un fichier audio</h2>
35
+ <div class="flex gap-3 items-center">
36
+ <input id="fileInput" type="file" accept="audio/*" class="block w-full text-sm text-gray-500 file:mr-3 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" />
37
+ <button id="uploadBtn" class="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 disabled:opacity-50" disabled>Transcrire</button>
38
+ </div>
39
+ <div id="fileInfo" class="text-xs text-gray-500"></div>
40
+ </section>
41
+
42
+ <hr class="my-4" />
43
+
44
+ <section id="player" class="space-y-3">
45
+ <audio id="playback" controls class="w-full" hidden></audio>
46
+ <div id="status" class="text-sm text-gray-700">Statut : <span id="statusText">Prêt</span></div>
47
+
48
+ <!-- Section pour la transcription -->
49
+ <div id="transcription-section" class="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-200" hidden>
50
+ <h3 class="text-lg font-medium text-blue-800 mb-2">Transcription</h3>
51
+ <div id="transcription-text" class="text-gray-800 bg-white p-3 rounded border whitespace-pre-wrap"></div>
52
+ <div id="transcription-info" class="text-xs text-blue-600 mt-2"></div>
53
+ </div>
54
+
55
+ <pre id="log" class="text-xs text-gray-500 overflow-auto max-h-28 bg-gray-50 p-2 rounded"></pre>
56
+ </section>
57
+ </main>
58
+
59
+ <script type="module">
60
+ /*
61
+ Clean, modular vanilla JS pour enregistrement audio et envoi à une API.
62
+ Principes appliqués : séparation des responsabilités, petites fonctions, erreurs gérées.
63
+ */
64
+
65
+ // ---- Elements DOM ----
66
+ const startBtn = document.getElementById('startBtn');
67
+ const stopBtn = document.getElementById('stopBtn');
68
+ const downloadBtn = document.getElementById('downloadBtn');
69
+ const playback = document.getElementById('playback');
70
+ const statusText = document.getElementById('statusText');
71
+ const logEl = document.getElementById('log');
72
+ const timerEl = document.getElementById('timer');
73
+ const apiEndpointInput = document.getElementById('apiEndpoint');
74
+ const transcriptionSection = document.getElementById('transcription-section');
75
+ const transcriptionText = document.getElementById('transcription-text');
76
+ const transcriptionInfo = document.getElementById('transcription-info');
77
+ const fileInput = document.getElementById('fileInput');
78
+ const uploadBtn = document.getElementById('uploadBtn');
79
+ const fileInfo = document.getElementById('fileInfo');
80
+
81
+ // ---- State ----
82
+ let mediaRecorder = null;
83
+ let recordedChunks = [];
84
+ let startTime = null;
85
+ let timerInterval = null;
86
+
87
+ // ---- Utilities (single responsibility functions) ----
88
+ function log(message) {
89
+ const time = new Date().toLocaleTimeString();
90
+ logEl.textContent = `[${time}] ${message}\n` + logEl.textContent;
91
+ }
92
+
93
+ function setStatus(text) {
94
+ statusText.textContent = text;
95
+ log(text);
96
+ }
97
+
98
+ function displayTranscription(text, usage = null) {
99
+ transcriptionText.textContent = text;
100
+ transcriptionSection.hidden = false;
101
+
102
+ if (usage && usage.type === 'duration' && usage.seconds) {
103
+ transcriptionInfo.textContent = `Durée de traitement : ${usage.seconds} secondes`;
104
+ } else {
105
+ transcriptionInfo.textContent = '';
106
+ }
107
+
108
+ log(`Transcription reçue : ${text}`);
109
+ }
110
+
111
+ function formatDuration(ms) {
112
+ const totalSeconds = Math.floor(ms / 1000);
113
+ const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
114
+ const seconds = String(totalSeconds % 60).padStart(2, '0');
115
+ return `${minutes}:${seconds}`;
116
+ }
117
+
118
+ function startTimer() {
119
+ startTime = Date.now();
120
+ timerEl.textContent = '00:00';
121
+ timerInterval = setInterval(() => {
122
+ timerEl.textContent = formatDuration(Date.now() - startTime);
123
+ }, 250);
124
+ }
125
+
126
+ function stopTimer() {
127
+ clearInterval(timerInterval);
128
+ timerInterval = null;
129
+ }
130
+
131
+ function formatFileSize(bytes) {
132
+ if (bytes === 0) return '0 Bytes';
133
+ const k = 1024;
134
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
135
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
136
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
137
+ }
138
+
139
+ function updateFileInfo(file) {
140
+ if (file) {
141
+ const duration = file.name.includes('.') ? 'Format: ' + file.name.split('.').pop().toUpperCase() : '';
142
+ fileInfo.textContent = `${file.name} (${formatFileSize(file.size)}) ${duration}`;
143
+ uploadBtn.disabled = false;
144
+ } else {
145
+ fileInfo.textContent = '';
146
+ uploadBtn.disabled = true;
147
+ }
148
+ }
149
+
150
+ // ---- Core logic ----
151
+ async function requestMicrophone() {
152
+ try {
153
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
154
+ return stream;
155
+ } catch (err) {
156
+ setStatus('Erreur : impossible d\'accéder au microphone');
157
+ console.error(err);
158
+ throw err;
159
+ }
160
+ }
161
+
162
+ function initMediaRecorder(stream) {
163
+ // Prefer opus/webm if available; fallback to default
164
+ const options = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
165
+ ? { mimeType: 'audio/webm;codecs=opus' }
166
+ : undefined;
167
+
168
+ const mr = new MediaRecorder(stream, options);
169
+
170
+ mr.addEventListener('dataavailable', (e) => {
171
+ if (e.data && e.data.size > 0) recordedChunks.push(e.data);
172
+ });
173
+
174
+ mr.addEventListener('start', () => setStatus('Enregistrement démarré'));
175
+ mr.addEventListener('stop', () => setStatus('Enregistrement arrêté'));
176
+ mr.addEventListener('error', (e) => setStatus('Erreur MediaRecorder: ' + e.message));
177
+
178
+ return mr;
179
+ }
180
+
181
+ async function startRecording() {
182
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
183
+ setStatus('API getUserMedia non supportée par ce navigateur.');
184
+ return;
185
+ }
186
+
187
+ try {
188
+ startBtn.disabled = true;
189
+ stopBtn.disabled = false;
190
+
191
+ const stream = await requestMicrophone();
192
+ mediaRecorder = initMediaRecorder(stream);
193
+ recordedChunks = [];
194
+ mediaRecorder.start();
195
+ startTimer();
196
+ } catch (err) {
197
+ startBtn.disabled = false;
198
+ stopBtn.disabled = true;
199
+ }
200
+ }
201
+
202
+ async function stopRecording() {
203
+ if (!mediaRecorder) return;
204
+
205
+ stopBtn.disabled = true;
206
+ startBtn.disabled = false;
207
+
208
+ mediaRecorder.stop();
209
+ stopTimer();
210
+
211
+ // Small delay to ensure 'dataavailable' has been pushed
212
+ await new Promise((resolve) => setTimeout(resolve, 200));
213
+
214
+ const blob = new Blob(recordedChunks, { type: recordedChunks[0]?.type || 'audio/webm' });
215
+
216
+ // enable playback
217
+ const url = URL.createObjectURL(blob);
218
+ playback.src = url;
219
+ playback.hidden = false;
220
+ downloadBtn.disabled = false;
221
+
222
+ // auto-send to API
223
+ try {
224
+ await sendAudioToApi(blob);
225
+ } catch (err) {
226
+ console.error('Envoi API échoué', err);
227
+ }
228
+ }
229
+
230
+ function downloadBlob(blob, filename = 'enregistrement.webm') {
231
+ const a = document.createElement('a');
232
+ a.href = URL.createObjectURL(blob);
233
+ a.download = filename;
234
+ document.body.appendChild(a);
235
+ a.click();
236
+ a.remove();
237
+ }
238
+
239
+ async function uploadFile() {
240
+ const file = fileInput.files[0];
241
+ if (!file) {
242
+ setStatus('Aucun fichier sélectionné.');
243
+ return;
244
+ }
245
+
246
+ // Vérifier que c'est bien un fichier audio ou vidéo
247
+ if (!file.type.startsWith('audio/') && !file.type.startsWith('video/')) {
248
+ setStatus('Veuillez sélectionner un fichier audio ou vidéo.');
249
+ return;
250
+ }
251
+
252
+ uploadBtn.disabled = true;
253
+
254
+ try {
255
+ await sendAudioToApi(file);
256
+ setStatus('Fichier uploadé avec succès');
257
+ } catch (err) {
258
+ setStatus('Erreur lors de l\'upload du fichier');
259
+ console.error(err);
260
+ } finally {
261
+ uploadBtn.disabled = false;
262
+ }
263
+ }
264
+
265
+ // ---- Networking ----
266
+ /**
267
+ * Envoie le Blob audio à l'API. Respecte le principe d'une seule responsabilité : cette fonction
268
+ * ne s'occupe que d'envoyer. Pour modifier l'endpoint ou les headers, modifier apiEndpointInput.value.
269
+ */
270
+ async function sendAudioToApi(blob) {
271
+ const endpoint = apiEndpointInput.value.trim();
272
+ if (!endpoint) {
273
+ setStatus('Aucun endpoint API configuré.');
274
+ return;
275
+ }
276
+
277
+ setStatus('Envoi de l\'audio à l\'API...');
278
+
279
+ const form = new FormData();
280
+ // Nom du fichier : timestamp + extension déduite
281
+ const extension = blob.type.includes('wav') ? 'wav' : (blob.type.includes('mpeg') ? 'mp3' : 'webm');
282
+ const filename = `recording-${Date.now()}.${extension}`;
283
+ form.append('file', blob, filename);
284
+
285
+ // Exemple : ajouter d'autres champs si nécessaire
286
+ // form.append('userId', '123');
287
+
288
+ try {
289
+ const response = await fetch(endpoint, {
290
+ method: 'POST',
291
+ body: form,
292
+ // headers: ne pas définir Content-Type (le navigateur s'en charge pour multipart/form-data)
293
+ });
294
+
295
+ if (!response.ok) {
296
+ const text = await response.text().catch(() => '');
297
+ setStatus(`Envoi échoué (status ${response.status})`);
298
+ log(`Réponse API : ${text}`);
299
+ throw new Error('Upload failed');
300
+ }
301
+
302
+ const result = await response.json().catch(() => null);
303
+ setStatus('Envoi réussi');
304
+ log('Réponse API JSON : ' + JSON.stringify(result));
305
+
306
+ // Extraire et afficher la transcription si présente
307
+ if (result && result.text) {
308
+ displayTranscription(result.text, result.usage);
309
+ } else {
310
+ log('Aucune transcription trouvée dans la réponse de l\'API');
311
+ }
312
+
313
+ return result;
314
+ } catch (err) {
315
+ setStatus('Erreur lors de l\'envoi à l\'API');
316
+ console.error(err);
317
+ throw err;
318
+ }
319
+ }
320
+
321
+ // ---- Event binding ----
322
+ startBtn.addEventListener('click', () => startRecording());
323
+ stopBtn.addEventListener('click', () => stopRecording());
324
+ downloadBtn.addEventListener('click', () => {
325
+ if (!recordedChunks.length) return;
326
+ const blob = new Blob(recordedChunks, { type: recordedChunks[0]?.type || 'audio/webm' });
327
+ downloadBlob(blob);
328
+ });
329
+
330
+ // File upload events
331
+ fileInput.addEventListener('change', (e) => {
332
+ const file = e.target.files[0];
333
+ updateFileInfo(file);
334
+ });
335
+
336
+ uploadBtn.addEventListener('click', () => uploadFile());
337
+
338
+ // ---- Initialization ----
339
+ (function init() {
340
+ if (!window.MediaRecorder) {
341
+ setStatus('MediaRecorder non supporté par ce navigateur.');
342
+ startBtn.disabled = true;
343
+ } else {
344
+ setStatus('Prêt');
345
+ }
346
+
347
+ // pour debug / développement
348
+ log('Interface prête.');
349
+ })();
350
+
351
+ </script>
352
+ </body>
353
  </html>