"자비스"라고 부르면 초록 링이 빛난다 — Canvas 2D와 Web Speech만으로 만드는 AI 음성비서
인스타 릴스에서 초록색 발광 오브가 빙글빙글 돌면서 AI 답변을 스트리밍하는 영상 보셨나요?
그걸 브라우저 HTML 파일 하나로 직접 만들어봤습니다.
음성으로 AI 비서에게 물으면 — 토러스가 파동치고, 글자가 흘러나오고, Yuna 목소리로 답합니다.
완성된 모습
- 초록 발광 토러스가 내 목소리에 맞춰 맥동·회전
- 내가 말하는 중 실시간 자막(interim) 즉시 표시
- AI 비서 답변이 글자 단위로 흘러나오며 동시에 음성 출력
- IDLE / LISTENING / THINKING / SPEAKING 상태 실시간 표시
- 스페이스바 한 번 = 푸시투토크, 🎙버튼 = 상시 대기 모드
필요한 것
- macOS (Yuna 보이스 사용 — 다른 OS는 시스템 TTS로 대체 가능)
- Python 3 (로컬 서버용, 별도 패키지 불필요)
- Chrome 브라우저
- AI 비서 백엔드 (OpenClaw + Claude 세션 — 또는 직접 API 호출로 대체 가능)
파일 구조
jarvis-dashboard/
├── orchestrator.html ← 오케스트레이션 HUD 본체
├── server.py ← 로컬 백엔드 (/ask 엔드포인트)
└── launch.sh ← 실행 스크립트
1단계: 로컬 서버 만들기 (server.py)
브라우저에서 마이크를 쓰려면 localhost HTTP 서버가 필요합니다.file://로 열면 마이크 권한이 막힙니다.
from http.server import HTTPServer, SimpleHTTPRequestHandler
import json
class Handler(SimpleHTTPRequestHandler):
def do_POST(self):
if self.path != '/ask':
self.send_error(404)
return
length = int(self.headers['Content-Length'])
body = json.loads(self.rfile.read(length))
text = body.get('text', '').strip()
if not text:
self._json({'reply': '(빈 질문)'})
return
reply = call_ai(text)
self._json({'reply': reply})
def _json(self, obj):
data = json.dumps(obj, ensure_ascii=False).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data)
def log_message(self, *_): pass
def call_ai(text):
# 여기를 본인 AI 호출 코드로 교체하세요
# 예: return openai_chat(text)
return f"'{text}' 에 대한 AI 답변입니다."
HTTPServer(('127.0.0.1', 8777), Handler).serve_forever()
2단계: 오케스트레이션 HUD 기본 구조
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>JARVIS ORCHESTRATION</title>
<style>
body { background: #000; color: #00ff88; font-family: monospace; }
</style>
</head>
<body>
<div id="status">IDLE</div>
<canvas id="torus"></canvas>
<div id="say">말씀하세요...</div>
<div id="log"></div>
<canvas id="wave"></canvas>
</body>
</html>
3단계: 초록 토러스 — Canvas 2D로 구현
three.js 없이 Canvas 2D만으로 토러스(링 파티클)를 구현합니다.
마이크 진폭에 따라 반경·밝기·회전이 바뀝니다.
const canvas = document.getElementById('torus');
const ctx = canvas.getContext('2d');
const PARTICLES = 220;
let orbAmp = 0;
function drawTorus(t) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const baseR = 120;
for (let i = 0; i < PARTICLES; i++) {
const angle = (i / PARTICLES) * Math.PI * 2 + t * 0.5;
const r = baseR + orbAmp * 40 * Math.sin(i * 0.3 + t * 2);
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle) * 0.4;
const alpha = 0.4 + orbAmp * 0.6;
const size = 2 + orbAmp * 3;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0, 255, 136, ${alpha})`;
ctx.fill();
}
}
const audioCtx = new AudioContext();
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
const source = audioCtx.createMediaStreamSource(stream);
const analyser = audioCtx.createAnalyser();
source.connect(analyser);
const data = new Uint8Array(analyser.frequencyBinCount);
let t = 0;
function loop() {
analyser.getByteFrequencyData(data);
const avg = data.reduce((a, b) => a + b, 0) / data.length;
orbAmp += ((avg / 255) - orbAmp) * 0.15;
drawTorus(t);
t += 0.02;
requestAnimationFrame(loop);
}
loop();
});
4단계: 음성 인식 — Web Speech API
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
const sr = new SR();
sr.lang = 'ko-KR';
sr.interimResults = true;
sr.continuous = false;
let busy = false;
sr.onresult = (e) => {
const result = e.results[e.results.length - 1];
const text = result[0].transcript;
document.getElementById('say').textContent = text;
if (result.isFinal && !busy) ask(text);
};
sr.onerror = (e) => {
if (e.error === 'no-speech' || e.error === 'aborted') return;
setTimeout(() => sr.start(), 500);
};
sr.onend = () => {
if (!busy) setTimeout(() => sr.start(), 350);
};
5단계: AI 비서 호출 + 음성 출력
async function ask(text) {
busy = true;
setStatus('THINKING');
const res = await fetch('/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
const { reply } = await res.json();
document.getElementById('log').textContent = reply;
setStatus('SPEAKING');
const utter = new SpeechSynthesisUtterance(reply);
utter.lang = 'ko-KR';
const voices = speechSynthesis.getVoices();
utter.voice = voices.find(v => v.name === 'Yuna') || voices.find(v => v.lang === 'ko-KR');
utter.onend = () => {
busy = false;
setTimeout(() => {
setStatus('LISTENING');
sr.start();
}, 700);
};
speechSynthesis.speak(utter);
}
실행하기
# 1. 서버 시작
python3 server.py &
# 2. 브라우저에서 열기
open -a "Google Chrome" http://127.0.0.1:8777/orchestrator.html
처음 열면 마이크 권한 팝업이 뜹니다. 허용하면 됩니다.
이후엔 🎙 버튼 누르고 말하면 바로 작동합니다.
흔한 문제들
음성인식이 자비스 목소리를 따라 말함
→ busy = true로 AI가 말하는 동안 인식을 막고, onend 후 0.7초 딜레이 필수.
file://로 열었더니 마이크가 안 됨
→ 반드시 http://127.0.0.1:8777/으로 접속해야 합니다.
Yuna 목소리가 안 나옴
→ 시스템 설정 > 손쉬운 사용 > 말하기에서 Yuna 보이스 다운로드 후 재시도.
말이 끝나도 AI가 답 안 함
→ isFinal 체크 확인. Chrome에서는 continuous: true 대신 onend 재시작 방식이 더 안정적.
마치며
three.js나 복잡한 라이브러리 없이, 순수 Canvas 2D와 Web Speech API만으로
인스타 릴스에서 보던 그 자비스 느낌을 낼 수 있습니다.
백엔드 /ask만 본인 AI에 연결하면 완전히 나만의 음성비서 HUD가 됩니다.
2026-06-10 작성
'AI 바이브 코딩' 카테고리의 다른 글
| 요즘 엑셀 트렌드 2026 — AI로 넘어간 엑셀 (0) | 2026.06.12 |
|---|---|
| AI랑 코딩하다 보면 모르는 말 천지 — 바이브 코딩 용어, 비전공자 눈높이로 (0) | 2026.06.11 |
| 바이브 코딩의 기준을 만들다 (0) | 2026.06.11 |
| 내 머릿속에 거장 8명을 앉혔다 — AI 자문단으로 사업 결정하기 (0) | 2026.06.10 |
| 집에 둔 AI 비서를, 회사에서도 — 원격으로 쓰는 3가지 방법 (0) | 2026.06.10 |