Claude Code에게 내 Windows 노트북에 목소리를 입히다. 가장 느린 부분은 음성이다.
요약
본 글은 음성 인식(STT)과 AI 에이전트 기능을 결합하여 노트북에서 작동하는 개인화된 인터페이스를 구축한 과정을 다룹니다. 특히, Whisper와 같은 로컬 STT 도구부터 시작해 Core Ultra 9 등 최신 하드웨어의 NPU/GPU 자원을 활용하고, Claude Code와 같은 강력한 에이전트 기능을 음성 출력과 연결하는 기술적 여정을 설명합니다.
핵심 포인트
- 로컬 환경에서 Whisper를 포크하여 Ctrl+Space 기반 STT 기능을 구현했습니다.
- Core Ultra 9 (NPU 및 Arc iGPU)와 RTX 4070 등 최신 하드웨어의 성능을 활용했습니다.
- Claude Code 같은 에이전트가 웹 검색, 로그 읽기 등 복잡한 작업을 음성으로 수행하도록 연결하는 것이 핵심입니다.
- 로컬 LLM(Gemma 4 E4B 등)과 상용 에이전트(Claude Code)의 장단점을 비교 분석했습니다.
토요일 18시 59분, 나는 포르투갈어로 내 노트북에게 이번 주 가장 중요한 기술 뉴스의 짧은 요약을 요청했다. 내가 복제한 목소리가 "Um instante"("잠깐만")라고 말했다. Claude Code가 웹 검색을 실행했고, 내가 말을 멈춘 지 약 17초 후에 그녀의 답변이 음성으로 나오기 시작했다. 그녀가 선택한 주제는 Google이 Project Suncatcher의 첫 번째 시제품 위성을 궤도에 올린 것이었다.
그 과정에서 타이핑된 단어는 아무것도 없었다. 음성 인식, 목소리, 그리고 오디오 파이프라인 전체가 노트북에서 실행되었다. 기기 밖으로 나가는 유일한 것은 Claude Code가 자체 API로 보내는 텍스트뿐이었다.
데모는 나의 첫 언어인 포르투갈어로 진행됩니다. 영어 자막을 켜주세요.
AI 공개: 이 프로젝트의 코딩 작업 대부분은 내가 지시한 하에 코딩 에이전트(Claude Code, OpenAI Codex, Google Antigravity)들이 작성했다. 나는 리포지토리 커밋 히스토리, 디자인 노트 및 로그를 이용해 Claude와 함께 이 글을 초안 작성했고, 이후 직접 편집했다. 아래의 모든 숫자는 해당 로그에서 가져온 것이다.
Débora가 시작된 곳
Débora는 받아쓰기 도구로 시작했다. 나는 Intel의 NPU에서 Whisper를 실행하는 Windows 앱인 npu-whisper를 포크하여, 매일 사용하는 무언가로 만들었다: Ctrl+Space를 누르고 말하면 텍스트가 커서에 나타난다. 나는 faster-whisper를 통해 NVIDIA 지원을 추가했으며, 이는 장치 우선순위 목록(RTX, 다음 NPU, 그다음 iGPU, 그다음 CPU)과 uv tool install 설정을 포함하여, 오후 내내 걸리는 가상 환경 설정 대신 한 번의 명령으로 설치되도록 했다.
이후 모든 것에서 노트북이 중요했다: Core Ultra 9 185H (NPU 및 Arc iGPU 탑재)와 8GB VRAM을 가진 RTX 4070 Laptop GPU를 갖춘 모델이었다.
내가 LinkedIn에 이 내용을 올렸을 때, 다음 단계는 이 음성 레이어를 터미널에서 이미 사용하고 있는 AI 에이전트들과 연결하는 것이라고 말했다. 약 30시간과 55개의 커밋 후에, 그 단계가 완료되었다. 이것들이 그것을 형성한 결정들이며, 여전히 느린 부분이기도 하다.
결정 1: 채팅하기에는 로컬 LLM으로 충분했고, 나는 여전히 그것을 사용하지 않았다
Voice Chat을 위한 로컬 모델 비교 및 아키텍처 결정
첫 번째 버전의 음성 채팅은 로컬 모델을 두뇌로 사용했습니다. 이를 교체하기 전에, 저는 Arc iGPU와 OpenVINO GenAI를 사용하여 int4 형식으로 동일한 포르투갈어 대화 샘플 3개씩 총 세 가지 모델을 벤치마킹했습니다:
| Model | First token | Tokens/s | Followed the long persona prompt |
|---|---|---|---|
| Qwen3-8B | 0.3 s | ~11 | 나쁨: 제 이름을 잘못 읽고, 날짜를 숫자로 작성했으며, 이모지를 사용함 |
| ... |
Gemma 4 E4B가 명확하게 승리했습니다. 이 모델은 음성 엔진을 위해 전체 날짜를 풀어 말하며 "Débora, que dia é hoje?"라는 질문에 답했고, 전사된 내용이 깨져 나오자 저에게 반복해 달라고 요청했습니다. Qwen3-8B의 경우, 프롬프트에 추가하는 모든 규칙들이 답변을 더 나쁘게 만들었습니다.
어쨌든 저는 Claude Code를 사용하기로 했습니다. 로컬 4B 모델은 즐거운 대화를 유지할 수 있지만, 제 저장소(repositories)를 열거나, 컨테이너를 확인하거나, 앱 자체의 로그를 읽거나, 웹 검색을 할 수는 없습니다. Claude Code는 이미 도구와 프로젝트 컨텍스트, 그리고 저의 지침을 가지고 있습니다. 따라서 분리는 다음과 같이 이루어졌습니다: Débora가 귀와 입(Whisper 및 TTS)을 소유하고, 제가 작성하지 않은 하네스(harness)가 사고를 담당합니다. 로컬 모델은 여전히 모든 것을 오프라인으로 하고 싶을 때 백엔드로서 코드에 남아 있습니다.
분명한 비용은 지연 시간(latency)입니다. 콜드 호출(claude -p)은 짧은 질문에 대해 6.1초가 걸렸습니다. 저는 다른 CLI들도 같은 방식으로 측정했습니다:
| CLI | Version | Cold call |
|---|---|---|
claude -p | 2.1.295 | 6.1 s |
| ... |
매 문장마다 6초의 침묵은 음성 대화를 망칠 것입니다. 이것이 다음 결정으로 이어졌습니다.
결정 2: 하나의 Claude Code 프로세스를 활성화 상태로 유지하기
Claude Code는 stdio를 통해 JSON 라인을 읽고 쓰는 장기간 실행되는(long-lived) 프로세스로 작동할 수 있습니다. Débora가 이를 시작하는 방식은 대략 다음과 같습니다:
claude -p --input-format stream-json --output-format stream-json --verbose \
--include-partial-messages \
--session-id <uuid> \
...
각 플래그는 하나의 음성 문제를 해결합니다:
--include-partial-messages는 텍스트가 생성되는 대로 스트리밍하므로, TTS(Text-to-Speech)가 전체 답변을 기다릴 필요 없이 첫 구절부터 시작할 수 있습니다.
--session-id와 --resume은 앱 재시작 시에도 동일한 대화 세션을 유지합니다. 또한 claude --resume <id> 명령어를 사용하여 일반 터미널에서 해당 세션을 열고 그녀가 수행한 모든 것을 확인할 수 있습니다.
--permission-prompt-tool stdio는 모든 도구 권한을 stdout에서 subtype: "can_use_tool"을 가진 control_request로 변환합니다. Débora는 stdin으로 답변합니다.
--append-system-prompt-file은 프로젝트가 이미 가지고 있는 어떤 CLAUDE.md 파일 위에 음성 채널 규칙(짧은 구두 답변, 마크다운 사용 금지, 숫자와 날짜를 풀어서 작성, 세션의 날짜 및 언어)을 추가합니다.
권한 응답은 다음과 같이 작습니다:
response = ({"behavior": "allow", "updatedInput": request["input"]} if allow else
{"behavior": "deny", "message": "Permissão negada pela Débora."})
return {"type": "control_response", "response": {
...
기본값은 거부(deny)이며, 읽기 전용 진단(read-only diagnostics)에 대한 허가 목록이 있습니다. 데모를 위해 저는 이를 열어두었으며, 이것이 로그에 Claude pede permissão: WebSearch 다음에 permission allowed가 표시되는 이유입니다.
이 내용을 앱에 작성하기 전에, 저는 임시 스크립트를 사용하여 테스트했습니다. 동일한 프로세스 내에서 두 번의 턴 동안 테스트 단어("jabuticaba")를 컨텍스트에 유지했습니다. 첫 번째 텍스트 델타는 콜드(cold) 상태에서 3.8초, 웜(warm) 상태에서 1.8초가 걸렸습니다. subtype: "interrupt"를 가진 control_request로 전송된 인터럽트는 terminal_reason: "aborted_streaming"으로 턴을 종료시켰고, 프로세스는 살아남았습니다. 이 마지막 부분이 중요합니다. 왜냐하면 사람들은 음성 비서와 대화하는 동안 계속 말을 끊기 때문입니다.
결정 3: 오류가 포함된 원본 전사(raw transcription) 전송
Whisper는 브라질식 억양으로 'dictation engine'을 'dictêixon engine'이라고 듣습니다. 제 첫 번째 아이디어는 로컬 LLM이 Claude에게 도달하기 전에 전사(transcription) 내용을 정리하도록 하는 것이었습니다. 테스트해 봤지만, 정리가 오히려 상황을 악화시켰습니다. 명백한 오류는 수정했지만, 프로젝트별 오류들은 잘못된 무언가로 '수정'했고('dictêixon engine'이 'Decision Engine'으로 바뀜), 약 1초의 지연 시간도 추가했습니다.
Claude Code는 네 가지 원본 테스트 요청을 모두 이해했습니다. 왜냐하면 저장소(repository)를 열어두고 dictation_engine.py가 바로 거기에 있는 것을 볼 수 있었기 때문입니다. 그래서 Débora가 원본 텍스트를 보내면, 시스템 프롬프트에는 이것이 음성 인식에서 왔으며 오류를 포함할 수 있다고 명시되어 있습니다.
인식 오류에 대한 수정은 파이프라인의 다른 쪽으로 넘어갔습니다:
-
Claude는
~/.debora/harness/voice_memory.md에 작은 음성 메모리를 유지하며, 수정 사항당 한 줄을 기록합니다: `- -
Whisper는 제가 말하는 동안 약 1초마다 늘어나는 구간을 재전사(re-transcribes)합니다 (데모에서 RTX를 사용하여 0.2초 만에 11.9초의 음성을 전사했으며, 이는 실시간 계수(real-time factor)가 0.02라는 의미입니다). 초안이 "그리고..."와 같은 미완성되거나 줄임표처럼 보인다면, VAD(Voice Activity Detection)는 자르기 전에 0.8초 대신 2초의 침묵을 기다립니다.
-
캡처 및 전사 작업은 Claude가 생각하는 동안 그리고 Débora가 말하는 동안 계속 실행됩니다. 아무것도 음소거되지 않습니다.
-
답변은 직렬 대기열(serial queue)을 통해 진행됩니다. 끼어들기(Barge-in) 기능은 활성 턴(active turn)을 중단시키고, Claude의 나머지 응답을 모두 소진시킨 다음, 다음 발화를 순서대로 전송할 수 있습니다.
-
에코 필터는 마이크가 들은 것과 Débora가 실제로 재생한 문장을 비교합니다 (토큰 오버랩이 60%를 의미하면 에코). 따라서 노트북 스피커에서는 자신에게 대답하지 않습니다.
데모 로그에서 가져온 수치들
| 단계 | 턴 1: "Oi Débora, tudo bem?" | 턴 2: 기술 뉴스 |
|---|---|---|
| 제가 멈춘 후의 최종 전사 | ~0.2초 | ~0.2초 |
| ... |
Claude는 첫 번째 턴에서 빨랐고 오디오가 느렸습니다. 첫 문장은 2.3초에 대기열에 들어갔지만, 합성(synthesis)은 28초까지 시작되지 않았습니다. 이 시간 동안 VRAM이 1.3 GB에서 5.0 GB로 증가했는데, 제가 이해하기로는 TTS 서버가 여전히 모델을 로딩하고 있었던 것 같습니다. 해결책은 첫 번째 턴 전에 더 일찍 시작하여 워밍업(warm up)하는 것입니다.
두 번째 턴에서는 실제 문제가 드러납니다. Débora의 목소리는 짧은 참고 녹음에서 Chatterbox Multilingual로 만든 클론 음성이며, 이 GPU에서는 실시간보다 느리게 합성됩니다. 해당 답변의 청크들은 자신의 오디오 길이 대비 1.1배에서 1.4배가 걸렸습니다 (예를 들어, 5.1초 분량의 말에 대해 5.9초 작업). 모든 문장은 이전 문장을 기다리기 때문에 긴 답변일수록 간격이 벌어집니다.
Claude Code는 병목 현상이 아니었습니다. 프로세스가 워밍업된 상태에서 간단한 턴의 첫 토큰은 1.4초 만에 나왔고, 웹 검색 턴은 도구 사용에 시간을 할애했습니다. 대화를 느리게 만드는 것은 목소리입니다. 노트북에서 Whisper와 8 GB의 VRAM을 공유하는 음성 클로닝 TTS 모델 때문입니다.
실제로 코드를 작성한 사람
55개의 커밋 중 대부분은 에이전트들이 작성했습니다. Claude Code는 작업을 분해하고, 대부분의 하네스(harness)를 작성했으며, 태스크들을 배포하는 역할을 했습니다. Codex는 주로 제가 여유 할당량(quota)을 가지고 있었기 때문에 codex exec을 통해 병렬로 작업을 수행했으며, 여기에는 머지(merging) 전 코드 및 보안 검토가 포함되었습니다. Antigravity는 음성 모드를 트레이 아이콘과 오버레이 모두에 표시하는 방법 같은 연구 질문과 UI 아이디어를 담당했습니다. 초기 작업 과정에서 일부 지저분한 부분이 남았고, 이는 나중에 Claude가 정리했습니다.
제가 맡은 부분은 에이전트들이 할 수 없는 일이었습니다. 저는 테스트를 하는 동안 그녀와 대화하며 귀로 버그("마스코트 옆에 텍스트가 잘려요", "말을 끊었더니 문장의 절반이 사라졌어요")를 발견했고, 로그를 함께 읽으며 어떤 것을 유지할지 결정했습니다. 그 결정 중 일부는 '무엇을 만들지 않을 것'에 관한 것이었습니다: 사전 정리 LLM(pre-cleanup LLM)은 없애고, 당분간 터미널 UI도 없애며, 새로운 Windows 전용 의존성도 배제했습니다.
제한 사항 (Limitations)
- Débora는 자체적인 헤드리스 Claude Code 세션을 실행합니다. 사용자가 이미 열어둔 터미널을 직접 구동할 수는 없습니다.
- Claude는 마크다운(markdown) 형식으로 답변합니다. 음성 합성(TTS)에 전달되는 부분은 말로 할 수 있는 내용만 해당하며, 코드와 목록은 오버레이 및 로그에 남아 있습니다.
- 현재 Windows 전용이며, 원활한 음성 채팅을 위해서는 GPU가 필요합니다.
- Whisper의
language설정이 중요합니다. 만약 영어로 설정되어 있다면 포르투갈어 음성은 Claude가 인식하기 전에 번역됩니다.
다음 계획 (What's next)
다음 단계는 자동 하드웨어 분배입니다. Débora는 시작 시 NPU, iGPU, RTX를 모두 탐지한 후, Whisper, TTS, 그리고 로컬 모델이 각각 어디에서 실행되어야 할지 결정할 것입니다. 이때 하나의 전역 장치에 의존하는 대신 각 구성 요소별로 폴백(fallback) 방식을 적용합니다. 목표는 음성 처리를 핵심 경로(critical path)에서 분리하는 것입니다. 이 노트북의 한 가지 옵션은 Whisper를 NPU에서 실행하고, 나머지 전체 RTX 자원을 TTS에 할당하는 방식입니다.
코드는 MIT 라이선스를 따릅니다: github.com/alexandre-machado/debora-whisper.
한 가지 질문이 있습니다. 아직 해결하지 못한 부분인데요. VRAM 8GB로 실시간보다 빠르게 작동하는 로컬 음성 클론 TTS를 구현해 본 분 계신가요? 어떤 모델을 사용하셨고, 그 결과 품질은 어땠나요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기