solo el indicador del microfono- b-Web Speech API

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grabador con Transcripción en Tiempo Real</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
#controls {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.button-group {
display: flex;
gap: 10px;
justify-content: center;
}
button {
padding: 12px 24px;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 6px;
transition: all 0.3s;
font-weight: 600;
}
#recordButton {
background-color: #e74c3c;
color: white;
}
#recordButton:hover {
background-color: #c0392b;
}
#pauseButton {
background-color: #f39c12;
color: white;
}
#pauseButton:hover {
background-color: #d35400;
}
#stopButton {
background-color: #2c3e50;
color: white;
}
#stopButton:hover {
background-color: #1a252f;
}
button:disabled {
background-color: #95a5a6 !important;
cursor: not-allowed;
}
.status {
padding: 12px;
border-radius: 6px;
text-align: center;
font-weight: 500;
}
.recording {
background-color: #ffebee;
color: #c62828;
}
.paused {
background-color: #fff8e1;
color: #ff8f00;
}
.transcribing {
background-color: #e8f5e9;
color: #2e7d32;
}
.error {
background-color: #ffebee;
color: #c62828;
}
#transcriptionContainer {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
height: 400px;
overflow-y: auto;
}
.segment {
margin-bottom: 15px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #3498db;
}
.timestamp {
font-size: 0.85em;
color: #7f8c8d;
margin-bottom: 5px;
font-weight: 500;
}
.transcript {
line-height: 1.6;
}
.audio-visualizer {
height: 60px;
width: 100%;
margin: 15px 0;
background-color: #ecf0f1;
border-radius: 4px;
overflow: hidden;
}
#audioIndicator {
height: 100%;
width: 0%;
background-color: #e74c3c;
transition: width 0.1s;
}
.settings {
background-color: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.settings label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.settings select, .settings input {
width: 100%;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
.queue-status {
font-size: 0.9em;
color: #7f8c8d;
text-align: center;
margin-top: 10px;
}
</style>
</head>
<body>
<h1>Grabador con Transcripción en Tiempo Real</h1>
<div class="settings">
<h2>Configuración</h2>
<label for="language">Idioma:</label>
<select id="language">
<option value="spanish">Español</option>
<option value="english">Inglés</option>
</select>
<label for="segmentDuration">Duración de segmentos (ms):</label>
<input type="number" id="segmentDuration" value="3000" min="1000" max="10000">
</div>
<div id="controls">
<div class="audio-visualizer">
<div id="audioIndicator"></div>
</div>
<div class="button-group">
<button id="recordButton">Iniciar Grabación</button>
<button id="pauseButton" disabled>Pausar</button>
<button id="stopButton" disabled>Detener</button>
</div>
<div id="recordingStatus" class="status">Preparado para grabar</div>
<div id="queueStatus" class="queue-status">Segmentos en cola: 0</div>
</div>
<h2>Transcripción en Tiempo Real</h2>
<div id="transcriptionStatus" class="status">Esperando para transcribir...</div>
<div id="transcriptionContainer">
<div id="transcriptionText"></div>
</div>
<script>
// Elementos del DOM
const recordButton = document.getElementById('recordButton');
const pauseButton = document.getElementById('pauseButton');
const stopButton = document.getElementById('stopButton');
const recordingStatus = document.getElementById('recordingStatus');
const transcriptionStatus = document.getElementById('transcriptionStatus');
const transcriptionText = document.getElementById('transcriptionText');
const audioIndicator = document.getElementById('audioIndicator');
const languageSelect = document.getElementById('language');
const segmentDurationInput = document.getElementById('segmentDuration');
const queueStatus = document.getElementById('queueStatus');
const transcriptionContainer = document.getElementById('transcriptionContainer');
// Variables de estado optimizadas
let mediaRecorder;
let audioContext;
let analyser;
let microphone;
let isRecording = false;
let isPaused = false;
let transcriptionSocket;
let currentSegmentId = 0;
let audioChunks = [];
let audioQueue = [];
let stream;
let animationId;
let mimeType;
let lastSegmentTime = 0;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_DELAY = 3000;
const MIN_SEGMENT_DURATION = 1500;
const MAX_QUEUE_SIZE = 5;
// Configuración
let WHISPER_SERVER_URL = 'ws://localhost:9000';
let SEGMENT_DURATION = 3000;
// Inicializar análisis de audio para el visualizador
function initAudioAnalyzer(audioStream) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
microphone = audioContext.createMediaStreamSource(audioStream);
microphone.connect(analyser);
analyser.fftSize = 256;
updateAudioIndicator();
}
// Actualizar visualizador de audio
function updateAudioIndicator() {
if (!isRecording || isPaused) {
audioIndicator.style.width = '0%';
animationId = requestAnimationFrame(updateAudioIndicator);
return;
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
let sum = 0;
for (let i = 0; i < bufferLength; i++) {
sum += dataArray[i];
}
const average = sum / bufferLength;
const level = Math.min(100, Math.max(0, average / 2));
audioIndicator.style.width = `${level}%`;
animationId = requestAnimationFrame(updateAudioIndicator);
}
// Obtener códec compatible
function getSupportedMimeType() {
const types = [
'audio/webm;codecs=opus',
'audio/ogg;codecs=opus',
'audio/mp4;codecs=mp4a',
'audio/webm',
'audio/ogg',
'audio/mp4'
];
for (let type of types) {
if (MediaRecorder.isTypeSupported(type)) {
console.log("Usando códec:", type);
return type;
}
}
return null;
}
// Conectar al servidor de transcripción con reconexión automática
function connectToTranscriptionServer() {
transcriptionSocket = new WebSocket(WHISPER_SERVER_URL);
transcriptionSocket.onopen = () => {
reconnectAttempts = 0;
transcriptionStatus.textContent = "Conectado al servidor de transcripción";
transcriptionStatus.className = "status transcribing";
// Procesar cualquier segmento en cola
processQueue();
};
transcriptionSocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
const now = new Date();
const segmentDiv = document.createElement('div');
segmentDiv.className = 'segment';
const timestampDiv = document.createElement('div');
timestampDiv.className = 'timestamp';
timestampDiv.textContent = `[${now.toLocaleTimeString()}] Segmento ${data.segmentId}`;
const transcriptDiv = document.createElement('div');
transcriptDiv.className = 'transcript';
transcriptDiv.textContent = data.text;
segmentDiv.appendChild(timestampDiv);
segmentDiv.appendChild(transcriptDiv);
transcriptionText.appendChild(segmentDiv);
// Auto-scroll
transcriptionContainer.scrollTop = transcriptionContainer.scrollHeight;
} catch (error) {
console.error("Error procesando mensaje:", error);
}
};
transcriptionSocket.onerror = (error) => {
console.error("Error en conexión de transcripción:", error);
transcriptionStatus.textContent = "Error en conexión de transcripción";
transcriptionStatus.className = "status error";
};
transcriptionSocket.onclose = () => {
transcriptionStatus.textContent = "Conexión de transcripción cerrada";
transcriptionStatus.className = "status";
if (isRecording && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
transcriptionStatus.textContent = `Reconectando (intento ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`;
setTimeout(connectToTranscriptionServer, RECONNECT_DELAY);
}
};
}
// Procesar segmento de audio optimizado
async function processAudioSegment(blob) {
const segmentId = ++currentSegmentId;
const now = new Date();
try {
const reader = new FileReader();
reader.onload = () => {
const base64Data = reader.result.split(',')[1];
if (transcriptionSocket && transcriptionSocket.readyState === WebSocket.OPEN) {
transcriptionSocket.send(JSON.stringify({
segmentId: segmentId,
timestamp: now.toISOString(),
audioData: base64Data,
language: languageSelect.value,
sampleRate: 16000,
sampleSize: 16,
channels: 1
}));
addStatusMessage(`Enviando segmento ${segmentId} (${Math.round(blob.size/1024)} KB)`);
lastSegmentTime = Date.now();
updateQueueStatus();
} else {
if (audioQueue.length < MAX_QUEUE_SIZE) {
audioQueue.push({
blob,
segmentId,
timestamp: now.toISOString(),
language: languageSelect.value
});
updateQueueStatus();
} else {
addStatusMessage("Cola llena - descartando segmento");
}
}
};
reader.readAsDataURL(blob);
} catch (error) {
console.error("Error procesando segmento:", error);
addStatusMessage(`Error al procesar segmento ${segmentId}`);
}
}
// Procesar cola de segmentos
function processQueue() {
if (audioQueue.length > 0 && transcriptionSocket && transcriptionSocket.readyState === WebSocket.OPEN) {
const queuedItem = audioQueue.shift();
const reader = new FileReader();
reader.onload = () => {
const base64Data = reader.result.split(',')[1];
transcriptionSocket.send(JSON.stringify({
segmentId: queuedItem.segmentId,
timestamp: queuedItem.timestamp,
audioData: base64Data,
language: queuedItem.language,
sampleRate: 16000,
sampleSize: 16,
channels: 1
}));
updateQueueStatus();
};
reader.readAsDataURL(queuedItem.blob);
}
}
// Actualizar estado de la cola
function updateQueueStatus() {
queueStatus.textContent = `Segmentos en cola: ${audioQueue.length}`;
}
// Agregar mensaje de estado
function addStatusMessage(message) {
const now = new Date();
const statusDiv = document.createElement('div');
statusDiv.className = 'segment';
const timestampDiv = document.createElement('div');
timestampDiv.className = 'timestamp';
timestampDiv.textContent = `[${now.toLocaleTimeString()}] ${message}`;
statusDiv.appendChild(timestampDiv);
transcriptionText.appendChild(statusDiv);
transcriptionContainer.scrollTop = transcriptionContainer.scrollHeight;
}
// Iniciar grabación optimizada
async function startRecording() {
try {
SEGMENT_DURATION = parseInt(segmentDurationInput.value) || 3000;
// Obtener stream de audio optimizado
stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1,
sampleRate: 16000,
sampleSize: 16,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
},
video: false
});
// Configurar visualizador de audio
initAudioAnalyzer(stream);
// Obtener códec compatible
mimeType = getSupportedMimeType();
if (!mimeType) {
throw new Error("No se encontró un códec de audio soportado");
}
// Configurar MediaRecorder optimizado
mediaRecorder = new MediaRecorder(stream, {
mimeType: mimeType,
audioBitsPerSecond: 64000 // Calidad suficiente para voz
});
// Manejar eventos optimizados
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
// Solo procesar si ha pasado el tiempo mínimo entre segmentos
const now = Date.now();
if (now - lastSegmentTime >= MIN_SEGMENT_DURATION || !isRecording) {
const audioBlob = new Blob(audioChunks, { type: mimeType });
processAudioSegment(audioBlob);
audioChunks = [];
}
}
};
mediaRecorder.onstart = () => {
recordingStatus.textContent = "Grabando...";
recordingStatus.className = "status recording";
recordButton.textContent = "Grabando...";
pauseButton.disabled = false;
stopButton.disabled = false;
isRecording = true;
audioQueue = [];
updateQueueStatus();
connectToTranscriptionServer();
addStatusMessage("Iniciando grabación...");
};
mediaRecorder.onpause = () => {
recordingStatus.textContent = "Grabación pausada";
recordingStatus.className = "status paused";
pauseButton.textContent = "Reanudar";
isPaused = true;
addStatusMessage("Grabación pausada");
};
mediaRecorder.onresume = () => {
recordingStatus.textContent = "Grabando...";
recordingStatus.className = "status recording";
pauseButton.textContent = "Pausar";
isPaused = false;
addStatusMessage("Grabación reanudada");
};
mediaRecorder.onstop = () => {
// Procesar cualquier audio restante
if (audioChunks.length > 0) {
const audioBlob = new Blob(audioChunks, { type: mimeType });
processAudioSegment(audioBlob);
audioChunks = [];
}
recordingStatus.textContent = "Grabación finalizada";
recordingStatus.className = "status";
recordButton.textContent = "Iniciar Grabación";
pauseButton.disabled = true;
stopButton.disabled = true;
isRecording = false;
isPaused = false;
if (transcriptionSocket) {
transcriptionSocket.close();
}
stream.getTracks().forEach(track => track.stop());
cancelAnimationFrame(animationId);
addStatusMessage("Grabación finalizada");
};
mediaRecorder.onerror = (event) => {
console.error("Error en MediaRecorder:", event.error);
recordingStatus.textContent = `Error: ${event.error.name}`;
recordingStatus.className = "status error";
stopRecording();
};
// Iniciar grabación con segmentación
mediaRecorder.start(SEGMENT_DURATION);
} catch (error) {
console.error("Error al iniciar grabación:", error);
recordingStatus.textContent = `Error: ${error.message}`;
recordingStatus.className = "status error";
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
}
}
// Detener grabación
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
}
// Event listeners
recordButton.addEventListener('click', () => {
if (!isRecording) {
startRecording();
} else {
stopRecording();
}
});
pauseButton.addEventListener('click', () => {
if (!isPaused) {
mediaRecorder.pause();
} else {
mediaRecorder.resume();
}
});
stopButton.addEventListener('click', stopRecording);
// Verificar compatibilidad al cargar la página
window.addEventListener('load', () => {
if (!navigator.mediaDevices || !window.MediaRecorder) {
recordingStatus.textContent = "Error: Tu navegador no soporta las APIs necesarias";
recordingStatus.className = "status error";
recordButton.disabled = true;
}
});
</script>
</body>
</html>
Comentarios
Publicar un comentario