captation-audio / index.html
cdupland's picture
Update index.html
a47b1b6 verified
<!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>