ffmpeg VAD와 barge-in을 사용한 AI 어시스턴트 핸즈프리 음성 모드 구축: npm 패키지 제로
요약
본 글은 ffmpeg의 VAD(Voice Activity Detection) 기능과 'barge-in' 기능을 활용하여 외부 npm 패키지 의존성 없이 AI 어시스턴트 핸즈프리 음성 모드를 구축하는 방법을 설명합니다. 단일 JavaScript 모듈로 녹음, 음성 감지, 실시간 대화 처리 및 오디오 재생을 구현하며, 특히 블루투스 환경에서의 문제 해결책도 제시했습니다.
핵심 포인트
- ffmpeg의 `silencedetect` 필터로 VAD를 직접 구현하여 외부 라이브러리 의존성을 제거함.
- barge-in 기능을 통해 어시스턴트 답변 도중 사용자의 발화를 실시간으로 감지하고 대화 흐름을 유지함.
- 블루투스 헤드셋 환경에서 오디오 재생 경로(A2DP -> Hands-Free)를 재라우팅하여 기능적 안정성을 확보함.
내가 만든 AI 어시스턴트를 위한 핸즈프리 음성 모드: ffmpeg VAD, barge-in, 그리고 0개의 npm 패키지
음성 인터페이스는 스피치 SDK와 스트리밍 프레임워크, 그리고 적지 않은 네이티브 종속성을 필요로 하는 것처럼 보입니다. 하지만 제 것은 단 하나의 1,100줄짜리 JavaScript 모듈이며, npm 종속성은 전혀 없고, 여러분이 이미 가지고 있을 가능성이 높은 바이너리인 ffmpeg와 ffplay에서 실행됩니다.
이 모듈은 Ankita 리포지토리의 src/channels/voice.mjs에 있습니다. 각 부분이 어떻게 작동하는지 설명하겠습니다.
하나의 ffmpeg 프로세스로 녹음과 음성 감지를 동시에 처리하기
제가 가장 좋아하는 트릭은 다음과 같습니다. 저는 VAD(Voice Activity Detection, 음성 활동 감지) 라이브러리를 직접 작성하지 않았습니다. ffmpeg는 silencedetect 오디오 필터를 제공하므로, 단일 ffmpeg 프로세스가 마이크를 녹음하는 동시에 그 표준 에러 스트림(stderr)에 음성 경계를 전송합니다:
silencedetect=noise=-35dB:d=1.2
마이크는 16kHz 모노 PCM(-ac 1 -ar 16000 -c:a pcm_s16le)으로 캡처되며, 이는 Whisper가 원하는 정확한 형식입니다. 동시에 stderr에는 silence_start: 3.4 / silence_end: 5.1 | silence_duration: 1.7과 같은 줄이 포함됩니다. 작은 상태 기반 파서(createSilenceParser)가 이를 이벤트로 변환하고, createTurnDetector는 이 이벤트를 작은 상태 기계에 매핑합니다: '침묵 후 소리'는 `
Groq 키가 존재하면 Orpheus TTS로 전환할 수 있습니다 (canopylabs/orpheus-v1-english, 이름이 지정된 9가지 음성 중 기본값은 tara). "auto" 제공자 설정은 키가 존재하면 Groq를, 그렇지 않으면 Edge를 선택합니다. 긴 답변은 최대 190자 길이의 문장으로 분할되어 청크 단위로 합성된 후 WAV 파일로 연결됩니다. (Edge의 엔드포인트는 짧은 입력에 더 안정적입니다.)
Barge-in: 어시스턴트가 말하는 도중에 끼어들기
실제 대화란 방해할 수 있다는 것을 의미합니다. 답변이 ffplay를 통해 재생되는 동안 마이크는 열려 있고 동일한 턴 감지기가 계속 이벤트를 공급합니다. 음성이 재생 시작 후 약 ~400ms 후에 시작되면, 그것은 '바지인(barge)': 플레이어를 SIGKILL로 종료하고, playbackGeneration 카운터를 증가시켜 디스크에 아직 쓰이고 있는 오디오가 새로운 발화 위에서 재생되지 못하게 합니다 (실제로 제가 겪었던 레이스 조건입니다. 파일을 삭제한 직후 잠시 후에 파일이 생성됩니다). 그런 다음 말한 내용을 전사하여 이를 다음 턴으로 루프에 바로 공급합니다.
400ms의 유예 시간은 의도적입니다: 재생이 시작된 후 '음성'의 첫 몇 분의 초는 종종 어시스턴트가 마이크를 통해 자신의 목소리를 듣는 경우이기 때문입니다.
거의 출시할 뻔했던 블루투스 문제
이것 때문에 저녁 시간을 날렸습니다. 블루투스 헤드셋은 마이크가 열리는 순간 고품질 A2DP 오디오 엔드포인트를 끊어버립니다. 따라서 재생이 A2DP를 통해 라우팅되었다면, 바지인 답변은 _음소거_로 나왔습니다. 해결책은 src/channels/audio-device.mjs에 있습니다: 바지인이 켜져 있고 마이크가 블루투스 장치인 경우, 음성 모드가 시작되기 전에 재생을 헤드셋의 Hands-Free 엔드포인트로 재라우팅하고 종료 시 복원합니다. 화려하지는 않지만, 이것 없이는 가장 일반적인 하드웨어에서 전체 기능이 작동하지 않습니다.
답변 전체를 읽어주지 않음
긴 도구 출력은 음성으로 작동하지 않습니다. 기본적으로 어시스턴트는 자신이 수행한 작업을 짧은 구두 메모로 요약합니다 (최대 8개 항목, 6문장, 1200자) — 그리고 나머지 내용이 어디에 있는지 알려줍니다:
"{address}, 그게 {spoken}입니다. 총합은 {total}입니다. 나머지는 화면에 있습니다, {address}." (기본 주소는 "sir"이며, 설정 가능합니다. 그리고 네, 템플릿이 비워두면 구두점을 스스로 정리해줍니다.) VOICE_FULL_READ를 사용하면 전체 내용을 읽게 할 수 있지만, 저는 기본적으로 꺼두었습니다. 왜냐하면 전체 읽기는 2,000 토큰 분량의 npm 오류 추적을 듣게 되는 결과를 초래하기 때문입니다.
다르게 하고 싶은 것들
VAD(Voice Activity Detection)는 투박한 도구입니다. 고정된 −35dB 임계값으로는 키보드 클릭과 음성을 구분할 수 없기 때문에, 노이즈 레벨, 무음 구간 길이, 바지어지 임계값, 최대 발화 길이가 모두 조정 가능합니다 (VOICE_NOISE_DB, VOICE_SILENCE_MS, VOICE_BARGE_DB, VOICE_MAX_UTTERANCE_MS) 그저 영리한 기능이 아닙니다. 제대로 된 ML 기반 VAD가 진정한 해결책일 것이지만, 그것은 또한 네이티브 종속성(native dependency)이 될 것이고, 핵심 목표는 종속성 없이 유지하는 것이었습니다. 때로는 "ffmpeg가 설치된 어떤 기기에서도 작동함"이라는 점이 "똑똑하다"는 것보다 더 중요합니다.
이는 test/channels/voice.test.mjs의 30개 단위 테스트로 커버됩니다. 파서와 턴 감지기(turn detector)는 순수 함수(pure functions)라서 마이크에 전혀 손대지 않고도 테스트하기 쉬웠습니다.
핵심은 이렇습니다: 설득력 있는 음성 모드는 별도의 음성 플랫폼을 요구하지 않습니다. 단지 하나의 ffmpeg 프로세스, 두 개의 API 호출, 4단계 상태 전이 기계(4-state turn machine), 그리고 블루투스에 대한 건강한 존중만 필요합니다.
이것으로 무언가를 구축하거나 제가 놓친 엣지 케이스를 발견한다면 — 댓글에서 정말 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기