신성혁
개인2026.07 (하루)

Talk Bot — Discord 실시간 음성 AI 봇

Discord 음성 채널에서 낮은 지연으로 실시간 대화하는 AI 봇을 하루 만에 만들어 개인 미니PC에서 24/7 운영하고 있습니다.

48k→16k Mono
입력 리샘플
20ms / 3840B
출력 프레임
약 2.4%
패킷 손실률
5분
유휴 자동 종료

개요

Discord 음성 채널에서 사람과 낮은 지연으로 실시간 음성 대화를 나누는 개인 프로젝트로, 추론과 음성 생성 전체를 Gemini Live API(bidiGenerateContent, 양방향 WebSocket)에 위임해 API 키가 노출되지 않는 server-to-server 구조로 설계했습니다. 음성 흐름을 Discord 수신(48kHz Stereo PCM16) → 16kHz Mono 리샘플 후 20ms 청크 전송 → Gemini 응답(24kHz Mono)을 48kHz Stereo·20ms(3840byte) 프레임으로 재생 → 5분 무발화 시 자동 세션 종료의 4개 파이프라인으로 분리하고, discord_sink/discord_source/discord_commands와 gemini_live 모듈 경계를 나눠 파이프라인 변경 없이 AI 제공자를 교체할 수 있게 했습니다. Ubuntu 24.04 미니PC에서 systemd로 24/7 상시 구동합니다.

가장 비중 있게 해결한 문제는 Discord의 E2EE(DAVE) 롤아웃으로 py-cord 음성 수신 기능이 PyPI 릴리즈 기준으로는 아예 동작하지 않는 것이었습니다. 관련 공식 이슈가 ETA 없이 열려 있어 PyPI 대신 미병합 fix 브랜치를 소스에서 직접 빌드해 사용했고, 그마저 Sink 인터페이스가 미완성(`__sink_listeners__`, `is_opus()`, `walk_children()` 등은 참조되지만 내장 구현이 없는 상태)이라 직접 구현해 채워 넣었습니다. `start_recording()`이 `Sink.init(vc)`를 호출하지 않아 내부적으로 `assert self.sink.client`가 실패하는 문제도 로그 분석으로 원인을 찾아 수동으로 배선했습니다.

실기 테스트에서 Gemini 서버 측 자동 VAD로는 발화의 절반 가까이 봇이 응답하지 않는 현상을 발견해, DAVE 복호화 과정에서 발생하는 약 2.4% 패킷 손실(사람 귀엔 들리지 않는 수준)이 자동 VAD의 발화 감지를 가끔 놓치게 만든다는 원인을 특정했습니다. Discord Sink가 이미 갖고 있는 신뢰도 높은 오디오 입력 신호를 Gemini의 수동 VAD API(`activity_start`/`activity_end`)로 직접 전달하도록 아키텍처를 교체하고, 버스트로 몰려 들어오는 20ms 프레임을 재-페이싱하는 로직과 응답 타임아웃 시 자동 재연결을 추가해 문제를 해소했습니다. 세션 압축·재개로 15분 제한을 회피하고, 상한이 있는 지수 백오프 재연결과 barge-in(`interrupted` 수신 즉시 재생 버퍼 클리어)으로 안정성을 확보했으며, 오디오·전사 로그를 저장하지 않아 프라이버시를 지켰습니다. docs/에 스펙 문서를 먼저 작성해 합의한 뒤 구현하는 방식으로 진행해 결정 로그와 재결정 이력을 추적 가능하게 관리했고, 오디오 변환은 단위 테스트로, 전체 join→대화→barge-in→leave 플로우는 미니PC 실기 음성으로 검증했습니다.

원인 — 들리지 않는 손실이 만든 무응답

발화의 절반 가까이 봇이 응답하지 않는 현상을, DAVE 복호화 과정의 약 2.4% 패킷 손실(사람 귀엔 들리지 않는 수준)이 Gemini 자동 VAD의 발화 감지를 가끔 놓치게 만드는 것으로 특정했습니다. Discord Sink가 이미 갖고 있던 신뢰도 높은 '지금 오디오가 들어온다'는 신호를 수동 VAD API로 직접 넘기는 쪽으로 아키텍처를 바꿔 해소했습니다.

핵심

  • DAVE E2EE 롤아웃으로 미동작하던 py-cord 음성 수신을 fix 브랜치 + 직접 구현으로 안정화
  • 패킷 손실로 인한 자동 VAD 무응답 ~50%를 Discord 신호 기반 수동 VAD로 교체해 해결
  • 20ms 프레임 재-페이싱, 세션 재개(resumption), barge-in으로 저지연·상시 운영 확보
  • /persona 명령으로 보이스+시스템 프롬프트 캐릭터 프리셋 4종 런타임 전환

직접 해보기

48kHz 원본과 16kHz 다운샘플 파형을 비교하며 앨리어싱과 Nyquist 한계를 체험합니다.

Resample Scope
48kHz 원본 (위)
샘플 481개 / 10ms
16kHz 다운샘플 (아래)
샘플 161개 / 10ms

Nyquist(8kHz) 이내 — 16kHz 다운샘플로도 원신호를 보존 가능

기술 스택

Python 3.12py-cordGemini Live APIgoogle-genai SDKsoxrnumpysystemd