Spaces:
Runtime error
Runtime error
| (function () { | |
| function init() { | |
| /* ---- refs ---- */ | |
| const micBtn = document.getElementById('lumi-mic-btn'); | |
| if (!micBtn) return false; // DOM chưa sẵn sàng, thử lại sau | |
| const recLabel = document.getElementById('lumi-rec-label'); | |
| const canvas = document.getElementById('lumi-canvas'); | |
| const ctx2d = canvas.getContext('2d'); | |
| const procBar = document.getElementById('lumi-proc-bar'); | |
| const playWrap = document.getElementById('lumi-audio-playback'); | |
| const audioEl = document.getElementById('lumi-audio-el'); | |
| /* ---- state ---- */ | |
| let isRecording = false; | |
| let mediaRecorder = null; | |
| let audioChunks = []; | |
| let audioCtx = null; | |
| let analyser = null; | |
| let micStream = null; | |
| let animId = null; | |
| /* ---- mic button ---- */ | |
| micBtn.addEventListener('click', () => { | |
| isRecording ? stopRecording() : startRecording(); | |
| }); | |
| /* ======================== | |
| RECORDING | |
| ======================== */ | |
| async function startRecording() { | |
| try { | |
| micStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); | |
| } catch (e) { | |
| recLabel.textContent = '❌ Không có quyền truy cập micro!'; | |
| return; | |
| } | |
| // Web Audio → visualizer | |
| audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioCtx.createAnalyser(); | |
| analyser.fftSize = 128; | |
| audioCtx.createMediaStreamSource(micStream).connect(analyser); | |
| // MediaRecorder → lưu blob | |
| audioChunks = []; | |
| const mime = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg'] | |
| .find(t => MediaRecorder.isTypeSupported(t)) || ''; | |
| mediaRecorder = new MediaRecorder(micStream, mime ? { mimeType: mime } : {}); | |
| mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); }; | |
| mediaRecorder.onstop = onRecordingDone; | |
| mediaRecorder.start(100); | |
| isRecording = true; | |
| micBtn.classList.add('recording'); | |
| micBtn.textContent = '⏹️'; | |
| recLabel.textContent = '🔴 Đang ghi... Nhấn để dừng'; | |
| recLabel.classList.add('recording'); | |
| playWrap.style.display = 'none'; | |
| procBar.style.display = 'none'; | |
| drawWave(); | |
| } | |
| function stopRecording() { | |
| if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop(); | |
| if (micStream) micStream.getTracks().forEach(t => t.stop()); | |
| if (audioCtx) audioCtx.close(); | |
| if (animId) cancelAnimationFrame(animId); | |
| micStream = audioCtx = analyser = null; | |
| isRecording = false; | |
| micBtn.classList.remove('recording'); | |
| micBtn.textContent = '🎙️'; | |
| recLabel.textContent = 'Nhấn để ghi âm lại'; | |
| recLabel.classList.remove('recording'); | |
| procBar.style.display = 'block'; | |
| drawFlatLine(); | |
| } | |
| function onRecordingDone() { | |
| const mime = audioChunks[0]?.type || 'audio/webm'; | |
| const blob = new Blob(audioChunks, { type: mime }); | |
| // Playback | |
| const url = URL.createObjectURL(blob); | |
| audioEl.src = url; | |
| audioEl.oncanplay = () => { | |
| procBar.style.display = 'none'; | |
| playWrap.style.display = 'flex'; | |
| }; | |
| injectFileToGradio(blob, mime); | |
| } | |
| /* ======================== | |
| INJECT VÀO GRADIO | |
| ======================== */ | |
| function injectFileToGradio(blob, mime) { | |
| const ext = mime.includes('ogg') ? 'ogg' : 'webm'; | |
| const file = new File([blob], 'recording.' + ext, { type: mime }); | |
| function attempt(tries) { | |
| const inp = document.querySelector('.hidden-audio-wrap input[type="file"]'); | |
| if (inp) { | |
| const dt = new DataTransfer(); | |
| dt.items.add(file); | |
| inp.files = dt.files; | |
| inp.dispatchEvent(new Event('change', { bubbles: true })); | |
| } else if (tries > 0) { | |
| setTimeout(() => attempt(tries - 1), 300); | |
| } | |
| } | |
| attempt(10); | |
| } | |
| /* ======================== | |
| WAVEFORM VISUALIZER | |
| ======================== */ | |
| function drawWave() { | |
| if (!analyser) return; | |
| animId = requestAnimationFrame(drawWave); | |
| const buf = analyser.frequencyBinCount; | |
| const data = new Uint8Array(buf); | |
| analyser.getByteTimeDomainData(data); | |
| const W = canvas.width, H = canvas.height; | |
| ctx2d.clearRect(0, 0, W, H); | |
| const rms = Math.sqrt(data.reduce((s, v) => s + (v - 128) ** 2, 0) / buf); | |
| const intensity = Math.min(rms / 28, 1); | |
| // Glow rings | |
| if (intensity > 0.04) { | |
| const cx = W / 2, cy = H / 2; | |
| [0, 1, 2].forEach(r => { | |
| ctx2d.beginPath(); | |
| ctx2d.arc(cx, cy, 16 + r * 13 + intensity * 18, 0, Math.PI * 2); | |
| ctx2d.strokeStyle = `rgba(167,139,250,${(0.20 - r * 0.05) * intensity})`; | |
| ctx2d.lineWidth = 9 - r * 2.5; | |
| ctx2d.stroke(); | |
| }); | |
| } | |
| // Waveform | |
| const g = ctx2d.createLinearGradient(0, 0, W, 0); | |
| g.addColorStop(0, 'rgba(124,111,247,0)'); | |
| g.addColorStop(0.2, `rgba(124,111,247,${0.45 + intensity * 0.55})`); | |
| g.addColorStop(0.5, `rgba(167,139,250,${0.75 + intensity * 0.25})`); | |
| g.addColorStop(0.8, `rgba(56,189,248,${0.45 + intensity * 0.55})`); | |
| g.addColorStop(1, 'rgba(56,189,248,0)'); | |
| ctx2d.beginPath(); | |
| ctx2d.lineWidth = 2.5; | |
| ctx2d.strokeStyle = g; | |
| ctx2d.shadowColor = '#7c6ff7'; | |
| ctx2d.shadowBlur = 10 * intensity; | |
| const sw = W / buf; | |
| for (let i = 0; i < buf; i++) { | |
| const y = ((data[i] / 128) * H) / 2; | |
| i === 0 ? ctx2d.moveTo(0, y) : ctx2d.lineTo(i * sw, y); | |
| } | |
| ctx2d.stroke(); | |
| ctx2d.shadowBlur = 0; | |
| } | |
| function drawFlatLine() { | |
| ctx2d.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx2d.beginPath(); | |
| ctx2d.moveTo(0, canvas.height / 2); | |
| ctx2d.lineTo(canvas.width, canvas.height / 2); | |
| ctx2d.strokeStyle = 'rgba(124,111,247,0.18)'; | |
| ctx2d.lineWidth = 1.5; | |
| ctx2d.stroke(); | |
| } | |
| drawFlatLine(); // init | |
| return true; | |
| } | |
| /* ======================== | |
| ĐỢI DOM SẴN SÀNG | |
| Thử init ngay, nếu chưa được thì | |
| MutationObserver sẽ thử lại mỗi khi DOM thay đổi | |
| ======================== */ | |
| if (!init()) { | |
| const observer = new MutationObserver(() => { | |
| if (init()) observer.disconnect(); | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| })(); |