Download static/index.html from piclez/hal: direct link, hf CLI and curl.
- Browser
- Download file 7.65 kB
-
https://huggingface.co/spaces/piclez/hal/resolve/main/static/index.html
- Command line
-
hf download hf://spaces/piclez/hal/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/piclez/hal/resolve/main/static/index.html
7.65 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /> | |
| <title>HAL</title> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { | |
| height: 100%; | |
| background: #000; | |
| color: #888; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, monospace; | |
| overflow: hidden; | |
| -webkit-user-select: none; | |
| user-select: none; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| body { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 48px; | |
| padding: 24px; | |
| } | |
| .eye { | |
| width: 320px; | |
| height: 320px; | |
| border-radius: 50%; | |
| background: radial-gradient(circle at 50% 50%, | |
| #fff2ee 0%, | |
| #ff5a3a 10%, | |
| #ff2a1a 30%, | |
| #8a0a00 70%, | |
| #1a0000 100%); | |
| box-shadow: | |
| 0 0 60px 10px rgba(255, 42, 26, 0.55), | |
| 0 0 140px 40px rgba(255, 42, 26, 0.25), | |
| inset 0 0 40px rgba(0, 0, 0, 0.6); | |
| cursor: pointer; | |
| transition: filter 0.3s ease, box-shadow 0.3s ease; | |
| animation: breathe 4s ease-in-out infinite; | |
| touch-action: none; | |
| } | |
| @keyframes breathe { | |
| 0%, 100% { opacity: 0.75; transform: scale(0.98); } | |
| 50% { opacity: 1.0; transform: scale(1.02); } | |
| } | |
| @keyframes breathe-fast { | |
| 0%, 100% { opacity: 0.85; transform: scale(0.99); } | |
| 50% { opacity: 1.0; transform: scale(1.04); } | |
| } | |
| .eye.listening { | |
| animation: breathe-fast 1.2s ease-in-out infinite; | |
| box-shadow: | |
| 0 0 90px 20px rgba(255, 42, 26, 0.8), | |
| 0 0 200px 60px rgba(255, 42, 26, 0.4), | |
| inset 0 0 40px rgba(0, 0, 0, 0.6); | |
| } | |
| .eye.thinking { | |
| animation: none; | |
| filter: saturate(0.6) brightness(0.6); | |
| box-shadow: | |
| 0 0 30px 4px rgba(255, 42, 26, 0.3), | |
| inset 0 0 40px rgba(0, 0, 0, 0.7); | |
| } | |
| .eye.speaking { | |
| animation: breathe 2s ease-in-out infinite; | |
| filter: brightness(1.15); | |
| box-shadow: | |
| 0 0 100px 24px rgba(255, 42, 26, 0.9), | |
| 0 0 240px 70px rgba(255, 42, 26, 0.45), | |
| inset 0 0 40px rgba(0, 0, 0, 0.6); | |
| } | |
| .log { | |
| min-height: 3em; | |
| max-width: 680px; | |
| width: 100%; | |
| text-align: center; | |
| font-size: 13px; | |
| line-height: 1.6; | |
| color: #777; | |
| white-space: pre-wrap; | |
| } | |
| .log .you { color: #888; } | |
| .log .hal { color: #c74a3a; } | |
| .hint { | |
| position: fixed; | |
| bottom: 24px; | |
| font-size: 12px; | |
| color: #444; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="eye" class="eye" aria-label="Hold to speak"></div> | |
| <div id="log" class="log"></div> | |
| <div class="hint">Hold the eye to speak</div> | |
| <script> | |
| const eye = document.getElementById('eye'); | |
| const log = document.getElementById('log'); | |
| let recorder = null; | |
| let chunks = []; | |
| let busy = false; | |
| let audioCtx = null; | |
| let primedAudio = null; | |
| // iOS/Safari require audio playback to be initiated from a user gesture. | |
| // Prime an <audio> element and an AudioContext on first touch so later | |
| // programmatic play() calls are allowed. | |
| function primePlayback() { | |
| try { | |
| if (!audioCtx) { | |
| const Ctx = window.AudioContext || window.webkitAudioContext; | |
| if (Ctx) audioCtx = new Ctx(); | |
| } | |
| if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); | |
| if (!primedAudio) { | |
| primedAudio = new Audio(); | |
| primedAudio.playsInline = true; | |
| primedAudio.setAttribute('playsinline', ''); | |
| primedAudio.muted = true; | |
| primedAudio.src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA='; | |
| primedAudio.play().catch(() => {}); | |
| } | |
| } catch (e) {} | |
| } | |
| function setState(state) { | |
| eye.classList.remove('listening', 'thinking', 'speaking'); | |
| if (state !== 'idle') eye.classList.add(state); | |
| } | |
| function updateLog(userText, halText) { | |
| log.innerHTML = | |
| '<div class="you">You: ' + escapeHtml(userText || '—') + '</div>' + | |
| '<div class="hal">HAL: ' + escapeHtml(halText || '—') + '</div>'; | |
| } | |
| function escapeHtml(s) { | |
| return s.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); | |
| } | |
| async function startRecording() { | |
| if (busy) return; | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| recorder = new MediaRecorder(stream); | |
| chunks = []; | |
| recorder.ondataavailable = e => { if (e.data.size > 0) chunks.push(e.data); }; | |
| recorder.start(); | |
| setState('listening'); | |
| } catch (err) { | |
| console.error('mic error', err); | |
| setState('idle'); | |
| } | |
| } | |
| async function stopAndSend() { | |
| if (!recorder || recorder.state !== 'recording') return; | |
| busy = true; | |
| await new Promise(resolve => { | |
| recorder.onstop = resolve; | |
| recorder.stop(); | |
| }); | |
| recorder.stream.getTracks().forEach(t => t.stop()); | |
| const mime = recorder.mimeType || 'audio/webm'; | |
| const ext = mime.includes('mp4') ? 'mp4' : mime.includes('ogg') ? 'ogg' : 'webm'; | |
| const blob = new Blob(chunks, { type: mime }); | |
| recorder = null; | |
| const form = new FormData(); | |
| form.append('audio', blob, 'audio.' + ext); | |
| setState('thinking'); | |
| try { | |
| const res = await fetch('/api/talk', { method: 'POST', body: form, credentials: 'same-origin' }); | |
| if (res.status === 204) { | |
| setState('idle'); | |
| busy = false; | |
| return; | |
| } | |
| if (!res.ok) { | |
| console.error('server error', res.status, await res.text()); | |
| setState('idle'); | |
| busy = false; | |
| return; | |
| } | |
| const userText = decodeURIComponent(res.headers.get('X-User-Transcript') || ''); | |
| const halText = decodeURIComponent(res.headers.get('X-Hal-Transcript') || ''); | |
| updateLog(userText, halText); | |
| const audioBlob = await res.blob(); | |
| const url = URL.createObjectURL(audioBlob); | |
| const audio = primedAudio || new Audio(); | |
| audio.muted = false; | |
| audio.playsInline = true; | |
| audio.setAttribute('playsinline', ''); | |
| audio.src = url; | |
| audio.onended = () => { setState('idle'); busy = false; }; | |
| audio.onerror = () => { setState('idle'); busy = false; }; | |
| setState('speaking'); | |
| try { | |
| await audio.play(); | |
| } catch (err) { | |
| console.error('playback blocked', err); | |
| log.innerHTML += '<div class="hal">[tap eye to hear reply]</div>'; | |
| const resume = async () => { | |
| eye.removeEventListener('touchend', resume); | |
| eye.removeEventListener('click', resume); | |
| try { await audio.play(); } catch (e) { setState('idle'); busy = false; } | |
| }; | |
| eye.addEventListener('touchend', resume, { once: true }); | |
| eye.addEventListener('click', resume, { once: true }); | |
| } | |
| } catch (err) { | |
| console.error('request error', err); | |
| setState('idle'); | |
| busy = false; | |
| } | |
| } | |
| eye.addEventListener('mousedown', e => { e.preventDefault(); primePlayback(); startRecording(); }); | |
| eye.addEventListener('mouseup', e => { e.preventDefault(); stopAndSend(); }); | |
| eye.addEventListener('mouseleave', () => { if (recorder && recorder.state === 'recording') stopAndSend(); }); | |
| eye.addEventListener('touchstart', e => { e.preventDefault(); primePlayback(); startRecording(); }, { passive: false }); | |
| eye.addEventListener('touchend', e => { e.preventDefault(); stopAndSend(); }, { passive: false }); | |
| eye.addEventListener('touchcancel', e => { e.preventDefault(); stopAndSend(); }, { passive: false }); | |
| eye.addEventListener('contextmenu', e => e.preventDefault()); | |
| </script> | |
| </body> | |
| </html> | |