Spaces:
Running
Running
<html lang="es"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>AudioPro X - Ultimate Media Processor</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<!-- CDNs Premium --> | |
<script src="https://cdn.jsdelivr.net/npm/@xenova/transformers@2.4.0"></script> | |
<script src="https://cdn.jsdelivr.net/npm/uikit@3.16.22/dist/js/uikit.min.js"></script> | |
<link href="https://cdn.jsdelivr.net/npm/uikit@3.16.22/dist/css/uikit.min.css" rel="stylesheet"> | |
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.2.96/css/materialdesignicons.min.css" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> | |
<style> | |
:root { | |
--gradient-primary: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%); | |
} | |
body { | |
font-family: 'Inter', sans-serif; | |
background: #0f172a; | |
color: #f8fafc; | |
min-height: 100vh; | |
} | |
.glass-panel { | |
background: rgba(30, 41, 59, 0.7); | |
backdrop-filter: blur(16px); | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
border-radius: 16px; | |
} | |
.waveform { | |
background: rgba(148, 163, 184, 0.2); | |
height: 120px; | |
border-radius: 12px; | |
position: relative; | |
overflow: hidden; | |
} | |
.waveform canvas { | |
width: 100%; | |
height: 100%; | |
display: block; | |
} | |
</style> | |
</head> | |
<body class="uk-padding"> | |
<div class="uk-container uk-container-expand uk-margin-medium-top"> | |
<div class="glass-panel uk-padding-large"> | |
<!-- Header --> | |
<div class="uk-flex uk-flex-middle uk-margin-medium-bottom"> | |
<h1 class="uk-heading-medium uk-margin-remove">AudioPro X</h1> | |
<div class="uk-margin-left"> | |
<span class="uk-badge" style="background: var(--gradient-primary)">v2.1.0</span> | |
</div> | |
</div> | |
<!-- Model Loader --> | |
<div class="uk-margin-large"> | |
<button | |
id="loadModelBtn" | |
class="uk-button uk-button-large uk-border-pill" | |
style="background: var(--gradient-primary)" | |
onclick="loadModel()" | |
> | |
<span class="mdi mdi-brain uk-margin-small-right"></span> | |
Cargar Modelo AI | |
</button> | |
<div id="modelStatus" class="uk-text-meta uk-margin-small-top"></div> | |
</div> | |
<!-- Main Interface --> | |
<div class="uk-grid-large" uk-grid> | |
<!-- File Upload & Recorder --> | |
<div class="uk-width-1-3@m"> | |
<div class="glass-panel uk-padding"> | |
<div class="uk-text-center"> | |
<!-- Waveform Container --> | |
<div class="waveform uk-margin-bottom" id="waveform"> | |
<canvas id="waveformCanvas"></canvas> | |
</div> | |
<!-- File Input --> | |
<input | |
type="file" | |
id="mediaInput" | |
hidden | |
accept="video/*,audio/*" | |
/> | |
<button | |
class="uk-button uk-button-default uk-width-1-1 uk-border-pill" | |
onclick="document.getElementById('mediaInput').click()" | |
> | |
<span class="mdi mdi-upload uk-margin-small-right"></span> | |
Subir Multimedia | |
</button> | |
<!-- Record Button --> | |
<div class="uk-margin"> | |
<button | |
id="recordBtn" | |
class="uk-button uk-button-danger uk-border-pill" | |
onclick="toggleRecording()" | |
> | |
<span class="mdi mdi-microphone"></span> | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Processing --> | |
<div class="uk-width-2-3@m"> | |
<div class="glass-panel uk-padding"> | |
<div id="progressBar" class="uk-progress uk-margin" hidden> | |
<div | |
class="uk-progress-bar" | |
style="width: 0%; background: var(--gradient-primary)" | |
></div> | |
</div> | |
<div id="outputContainer" class="uk-margin"></div> | |
<div class="uk-grid-small" uk-grid> | |
<div class="uk-width-auto"> | |
<select id="formatSelect" class="uk-select uk-border-pill"> | |
<option value="wav">WAV</option> | |
<option value="opus" selected>WhatsApp Audio (OPUS)</option> | |
<option value="mp3">MP3</option> | |
</select> | |
</div> | |
<div class="uk-width-expand"> | |
<button | |
class="uk-button uk-button-primary uk-width-1-1 uk-border-pill" | |
onclick="processMedia()" | |
> | |
<span class="mdi mdi-autorenew uk-margin-small-right"></span> | |
Procesar Media | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
let mediaRecorder; | |
let audioChunks = []; | |
let pipeline = null; | |
// For Visualization | |
let audioCtx; | |
let analyser; | |
let dataArray; | |
let animationId; | |
/** | |
* Carga el modelo Whisper. | |
*/ | |
async function loadModel() { | |
try { | |
document.getElementById('modelStatus').textContent = '⏳ Cargando modelo...'; | |
document.getElementById('loadModelBtn').disabled = true; | |
// Carga de modelo con Transformers.js | |
pipeline = await transformers.pipeline( | |
'automatic-speech-recognition', | |
'Xenova/whisper-small' | |
); | |
document.getElementById('modelStatus').innerHTML = | |
'✅ Modelo cargado: <span class="uk-text-bold">Whisper v2</span>'; | |
} catch (error) { | |
showError('Error al cargar el modelo: ' + error.message); | |
document.getElementById('loadModelBtn').disabled = false; | |
} | |
} | |
/** | |
* Procesa un archivo multimedia (audio/video) ya cargado. | |
*/ | |
async function processMedia() { | |
const file = document.getElementById('mediaInput').files[0]; | |
if (!file) { | |
return showError('Selecciona un archivo primero'); | |
} | |
if (!pipeline) { | |
return showError('Carga el modelo primero'); | |
} | |
try { | |
toggleUI(false); | |
updateProgress(30); | |
const output = await pipeline(file, { | |
chunk_length_s: 30, | |
stride_length_s: 5, | |
return_timestamps: true | |
}); | |
updateProgress(100); | |
showResult(output.text); | |
} catch (error) { | |
showError('Error de procesamiento: ' + error.message); | |
} finally { | |
toggleUI(true); | |
} | |
} | |
/** | |
* Graba o detiene la grabación de audio a través del micrófono. | |
*/ | |
async function toggleRecording() { | |
// Si no existe mediaRecorder aún, inicializar | |
if (!mediaRecorder) { | |
try { | |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
// Configurar MediaRecorder | |
mediaRecorder = new MediaRecorder(stream); | |
mediaRecorder.ondataavailable = e => audioChunks.push(e.data); | |
mediaRecorder.onstop = async () => { | |
// Detener la visualización y cerrar el contexto de audio | |
stopVisualizer(); | |
// Crear Blob final | |
const audioBlob = new Blob(audioChunks, { type: 'audio/ogg; codecs=opus' }); | |
// Si el modelo está cargado, procesamos inmediatamente el audio grabado | |
if (pipeline) { | |
toggleUI(false); | |
updateProgress(30); | |
try { | |
const output = await pipeline(audioBlob, { | |
chunk_length_s: 30, | |
stride_length_s: 5, | |
return_timestamps: true | |
}); | |
updateProgress(100); | |
showResult(output.text); | |
} catch (err) { | |
showError('Error al procesar audio grabado: ' + err.message); | |
} finally { | |
toggleUI(true); | |
} | |
} else { | |
showError('El modelo no está cargado aún. Por favor, cárgalo antes de grabar.'); | |
} | |
}; | |
// Inicializar y comenzar la grabación | |
audioChunks = []; | |
mediaRecorder.start(); | |
document.getElementById('recordBtn').classList.add('uk-button-danger'); | |
// Iniciar visualización | |
startVisualizer(stream); | |
} catch (error) { | |
showError('No se pudo acceder al micrófono: ' + error.message); | |
} | |
} else { | |
// Si ya existe un recorder, alternar entre grabar y parar | |
if (mediaRecorder.state === 'recording') { | |
mediaRecorder.stop(); | |
document.getElementById('recordBtn').classList.remove('uk-button-danger'); | |
} else { | |
audioChunks = []; | |
mediaRecorder.start(); | |
document.getElementById('recordBtn').classList.add('uk-button-danger'); | |
} | |
} | |
} | |
/** | |
* Inicia el visualizador de forma de onda usando un AnalyserNode. | |
*/ | |
function startVisualizer(stream) { | |
audioCtx = new AudioContext(); | |
const source = audioCtx.createMediaStreamSource(stream); | |
analyser = audioCtx.createAnalyser(); | |
analyser.fftSize = 2048; | |
const bufferLength = analyser.fftSize; | |
dataArray = new Uint8Array(bufferLength); | |
source.connect(analyser); | |
drawWaveform(); | |
} | |
/** | |
* Dibujado del waveform en el canvas por cada frame. | |
*/ | |
function drawWaveform() { | |
const canvas = document.getElementById('waveformCanvas'); | |
const canvasCtx = canvas.getContext('2d'); | |
const width = canvas.width = canvas.offsetWidth; | |
const height = canvas.height = canvas.offsetHeight; | |
// Animación | |
function draw() { | |
animationId = requestAnimationFrame(draw); | |
analyser.getByteTimeDomainData(dataArray); | |
// Limpiar canvas | |
canvasCtx.clearRect(0, 0, width, height); | |
// Dibujar waveform | |
canvasCtx.lineWidth = 2; | |
canvasCtx.beginPath(); | |
let sliceWidth = width * 1.0 / dataArray.length; | |
let x = 0; | |
for (let i = 0; i < dataArray.length; i++) { | |
const v = dataArray[i] / 128.0; | |
const y = (v * height) / 2; | |
if (i === 0) { | |
canvasCtx.moveTo(x, y); | |
} else { | |
canvasCtx.lineTo(x, y); | |
} | |
x += sliceWidth; | |
} | |
canvasCtx.lineTo(width, height / 2); | |
canvasCtx.stroke(); | |
} | |
draw(); | |
} | |
/** | |
* Detiene la visualización de la forma de onda y cierra el audio context. | |
*/ | |
function stopVisualizer() { | |
if (animationId) { | |
cancelAnimationFrame(animationId); | |
animationId = null; | |
} | |
if (audioCtx) { | |
audioCtx.close(); | |
audioCtx = null; | |
} | |
} | |
// Funciones auxiliares | |
function toggleUI(enabled) { | |
document.querySelectorAll('button, select').forEach(el => el.disabled = !enabled); | |
document.getElementById('progressBar').hidden = enabled; | |
} | |
function updateProgress(percent) { | |
document.querySelector('.uk-progress-bar').style.width = percent + '%'; | |
} | |
function showResult(text) { | |
const output = ` | |
<div class="uk-alert-success" uk-alert> | |
<h3>Resultado del procesamiento:</h3> | |
<p>${text}</p> | |
</div>`; | |
document.getElementById('outputContainer').innerHTML = output; | |
} | |
function showError(message) { | |
UIkit.notification({ | |
message: `<span class="mdi mdi-alert-circle"></span> ${message}`, | |
status: 'danger', | |
pos: 'top-center' | |
}); | |
} | |
</script> | |
</body> | |
</html> |