Spaces:
Running
Running
File size: 12,771 Bytes
bc92670 a47b1b6 bc92670 |
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 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Enregistreur Audio — Envoi API</title>
<!-- Tailwind CDN (utile pour preview rapide). Dans un projet Angular avec Tailwind installé, vous pouvez supprimer cette ligne. -->
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 min-h-screen flex items-center justify-center p-6">
<main class="w-full max-w-xl bg-white rounded-2xl shadow p-6">
<h1 class="text-2xl font-semibold mb-4">Enregistreur audio</h1>
<section id="controls" class="space-y-3">
<div class="flex gap-3">
<button id="startBtn" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50">Démarrer</button>
<button id="stopBtn" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50" disabled>Stopper</button>
<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>
</div>
<div class="text-sm text-gray-600">Durée : <span id="timer">00:00</span></div>
<div class="mt-2">
<label class="block text-sm font-medium text-gray-700">API endpoint (changer par défaut si besoin)</label>
<input id="apiEndpoint" class="mt-1 block w-full rounded border-gray-200 shadow-sm" value="https://n8n.bziiit.com/webhook/transcript-audio" />
</div>
<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>
</section>
<hr class="my-4" />
<section id="upload" class="space-y-3">
<h2 class="text-lg font-medium text-gray-800">Upload d'un fichier audio</h2>
<div class="flex gap-3 items-center">
<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" />
<button id="uploadBtn" class="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 disabled:opacity-50" disabled>Transcrire</button>
</div>
<div id="fileInfo" class="text-xs text-gray-500"></div>
</section>
<hr class="my-4" />
<section id="player" class="space-y-3">
<audio id="playback" controls class="w-full" hidden></audio>
<div id="status" class="text-sm text-gray-700">Statut : <span id="statusText">Prêt</span></div>
<!-- Section pour la transcription -->
<div id="transcription-section" class="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-200" hidden>
<h3 class="text-lg font-medium text-blue-800 mb-2">Transcription</h3>
<div id="transcription-text" class="text-gray-800 bg-white p-3 rounded border whitespace-pre-wrap"></div>
<div id="transcription-info" class="text-xs text-blue-600 mt-2"></div>
</div>
<pre id="log" class="text-xs text-gray-500 overflow-auto max-h-28 bg-gray-50 p-2 rounded"></pre>
</section>
</main>
<script type="module">
/*
Clean, modular vanilla JS pour enregistrement audio et envoi à une API.
Principes appliqués : séparation des responsabilités, petites fonctions, erreurs gérées.
*/
// ---- Elements DOM ----
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const downloadBtn = document.getElementById('downloadBtn');
const playback = document.getElementById('playback');
const statusText = document.getElementById('statusText');
const logEl = document.getElementById('log');
const timerEl = document.getElementById('timer');
const apiEndpointInput = document.getElementById('apiEndpoint');
const transcriptionSection = document.getElementById('transcription-section');
const transcriptionText = document.getElementById('transcription-text');
const transcriptionInfo = document.getElementById('transcription-info');
const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
const fileInfo = document.getElementById('fileInfo');
// ---- State ----
let mediaRecorder = null;
let recordedChunks = [];
let startTime = null;
let timerInterval = null;
// ---- Utilities (single responsibility functions) ----
function log(message) {
const time = new Date().toLocaleTimeString();
logEl.textContent = `[${time}] ${message}\n` + logEl.textContent;
}
function setStatus(text) {
statusText.textContent = text;
log(text);
}
function displayTranscription(text, usage = null) {
transcriptionText.textContent = text;
transcriptionSection.hidden = false;
if (usage && usage.type === 'duration' && usage.seconds) {
transcriptionInfo.textContent = `Durée de traitement : ${usage.seconds} secondes`;
} else {
transcriptionInfo.textContent = '';
}
log(`Transcription reçue : ${text}`);
}
function formatDuration(ms) {
const totalSeconds = Math.floor(ms / 1000);
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
const seconds = String(totalSeconds % 60).padStart(2, '0');
return `${minutes}:${seconds}`;
}
function startTimer() {
startTime = Date.now();
timerEl.textContent = '00:00';
timerInterval = setInterval(() => {
timerEl.textContent = formatDuration(Date.now() - startTime);
}, 250);
}
function stopTimer() {
clearInterval(timerInterval);
timerInterval = null;
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function updateFileInfo(file) {
if (file) {
const duration = file.name.includes('.') ? 'Format: ' + file.name.split('.').pop().toUpperCase() : '';
fileInfo.textContent = `${file.name} (${formatFileSize(file.size)}) ${duration}`;
uploadBtn.disabled = false;
} else {
fileInfo.textContent = '';
uploadBtn.disabled = true;
}
}
// ---- Core logic ----
async function requestMicrophone() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
return stream;
} catch (err) {
setStatus('Erreur : impossible d\'accéder au microphone');
console.error(err);
throw err;
}
}
function initMediaRecorder(stream) {
// Prefer opus/webm if available; fallback to default
const options = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
? { mimeType: 'audio/webm;codecs=opus' }
: undefined;
const mr = new MediaRecorder(stream, options);
mr.addEventListener('dataavailable', (e) => {
if (e.data && e.data.size > 0) recordedChunks.push(e.data);
});
mr.addEventListener('start', () => setStatus('Enregistrement démarré'));
mr.addEventListener('stop', () => setStatus('Enregistrement arrêté'));
mr.addEventListener('error', (e) => setStatus('Erreur MediaRecorder: ' + e.message));
return mr;
}
async function startRecording() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
setStatus('API getUserMedia non supportée par ce navigateur.');
return;
}
try {
startBtn.disabled = true;
stopBtn.disabled = false;
const stream = await requestMicrophone();
mediaRecorder = initMediaRecorder(stream);
recordedChunks = [];
mediaRecorder.start();
startTimer();
} catch (err) {
startBtn.disabled = false;
stopBtn.disabled = true;
}
}
async function stopRecording() {
if (!mediaRecorder) return;
stopBtn.disabled = true;
startBtn.disabled = false;
mediaRecorder.stop();
stopTimer();
// Small delay to ensure 'dataavailable' has been pushed
await new Promise((resolve) => setTimeout(resolve, 200));
const blob = new Blob(recordedChunks, { type: recordedChunks[0]?.type || 'audio/webm' });
// enable playback
const url = URL.createObjectURL(blob);
playback.src = url;
playback.hidden = false;
downloadBtn.disabled = false;
// auto-send to API
try {
await sendAudioToApi(blob);
} catch (err) {
console.error('Envoi API échoué', err);
}
}
function downloadBlob(blob, filename = 'enregistrement.webm') {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
}
async function uploadFile() {
const file = fileInput.files[0];
if (!file) {
setStatus('Aucun fichier sélectionné.');
return;
}
// Vérifier que c'est bien un fichier audio ou vidéo
if (!file.type.startsWith('audio/') && !file.type.startsWith('video/')) {
setStatus('Veuillez sélectionner un fichier audio ou vidéo.');
return;
}
uploadBtn.disabled = true;
try {
await sendAudioToApi(file);
setStatus('Fichier uploadé avec succès');
} catch (err) {
setStatus('Erreur lors de l\'upload du fichier');
console.error(err);
} finally {
uploadBtn.disabled = false;
}
}
// ---- Networking ----
/**
* Envoie le Blob audio à l'API. Respecte le principe d'une seule responsabilité : cette fonction
* ne s'occupe que d'envoyer. Pour modifier l'endpoint ou les headers, modifier apiEndpointInput.value.
*/
async function sendAudioToApi(blob) {
const endpoint = apiEndpointInput.value.trim();
if (!endpoint) {
setStatus('Aucun endpoint API configuré.');
return;
}
setStatus('Envoi de l\'audio à l\'API...');
const form = new FormData();
// Nom du fichier : timestamp + extension déduite
const extension = blob.type.includes('wav') ? 'wav' : (blob.type.includes('mpeg') ? 'mp3' : 'webm');
const filename = `recording-${Date.now()}.${extension}`;
form.append('file', blob, filename);
// Exemple : ajouter d'autres champs si nécessaire
// form.append('userId', '123');
try {
const response = await fetch(endpoint, {
method: 'POST',
body: form,
// headers: ne pas définir Content-Type (le navigateur s'en charge pour multipart/form-data)
});
if (!response.ok) {
const text = await response.text().catch(() => '');
setStatus(`Envoi échoué (status ${response.status})`);
log(`Réponse API : ${text}`);
throw new Error('Upload failed');
}
const result = await response.json().catch(() => null);
setStatus('Envoi réussi');
log('Réponse API JSON : ' + JSON.stringify(result));
// Extraire et afficher la transcription si présente
if (result && result.text) {
displayTranscription(result.text, result.usage);
} else {
log('Aucune transcription trouvée dans la réponse de l\'API');
}
return result;
} catch (err) {
setStatus('Erreur lors de l\'envoi à l\'API');
console.error(err);
throw err;
}
}
// ---- Event binding ----
startBtn.addEventListener('click', () => startRecording());
stopBtn.addEventListener('click', () => stopRecording());
downloadBtn.addEventListener('click', () => {
if (!recordedChunks.length) return;
const blob = new Blob(recordedChunks, { type: recordedChunks[0]?.type || 'audio/webm' });
downloadBlob(blob);
});
// File upload events
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
updateFileInfo(file);
});
uploadBtn.addEventListener('click', () => uploadFile());
// ---- Initialization ----
(function init() {
if (!window.MediaRecorder) {
setStatus('MediaRecorder non supporté par ce navigateur.');
startBtn.disabled = true;
} else {
setStatus('Prêt');
}
// pour debug / développement
log('Interface prête.');
})();
</script>
</body>
</html>
|