
Gemini Live API (Bidi WebSocket)로 실시간 음성 아바타를 제작하며 알게 된 점 ―― Function Calling으로
요약
Gemini Live API와 WebSocket을 활용하여 실시간 음성 대화가 가능한 버추얼 휴먼 아바타를 구현한 기술 기록입니다. 중계 서버를 통한 API 키 보안 관리와 Function Calling을 이용한 아바타의 표정 제어 노하우를 다룹니다.
핵심 포인트
- WebSocket 중계 서버를 구축하여 API 키 노출을 방지하고 연결 안정성을 확보함
- Function Calling을 통해 모델이 대화 문맥에 따라 아바타의 표정을 스스로 결정하도록 설계
- Function Call 호출 시 반드시 결과를 반환해야 API 응답이 중단되지 않음
- 침묵 처리 로직을 별도 코드 대신 System Instruction을 통해 자연어로 제어
AI 기업인 크리스탈 메소드 주식회사(Crystal Method Co., Ltd.)에서 버추얼 휴먼(대화형 AI 아바타) 제품 개발에 Google의 Gemini Live API(양방향 음성 대화·BidiGenerateContent)를 통합한 구현 기록입니다. 감수는 대표인 카와이 케이(Kei Kawai, AI 멀티모달 관련 특허 16건의 발명자)가 맡았습니다.
"Gemini Live로 실시간 음성 대화 아바타를 만들 때 어디에서 막히는가"를 구현한 코드베이스를 바탕으로 구체적으로 작성합니다.
Gemini Live API는 WebSocket을 통해 직접 양방향으로 주고받는 API입니다. 브라우저에서 직접 연결하는 설계도 가능하지만, 저희는 다음과 같은 구성을 선택했습니다.
브라우저 (마이크 입력·음성 재생·아바타 묘사)
↕ WebSocket
Node.js 중계 서버
...
이유는 두 가지입니다.
- API 키를 브라우저에 노출시키고 싶지 않음 (클라이언트 직결 방식이면 키가 프론트엔드에 드러남)
- 기존의 WebSocket/Socket.IO 서버와 공존시킬 때, 전용 포트로 분리하는 것이 사고가 적음 (동일 프로세스 내에서 여러 WS 서버를 다루면 연결 혼선이나 라이프사이클 관리가 복잡해짐)
중계 서버 측은 심플한 메시지 패스스루(pass-through) 방식으로, setup(모델 설정 전송) → setupComplete 수신 → 클라이언트에 ready 통지, 라는 순서로 핸드셰이크(handshake)를 관리합니다. 이 과정을 생략하고 클라이언트 측의 음성 송신을 먼저 허용하면, Gemini 측의 초기화가 끝나기 전에 데이터가 전송되어 무시되는 초보적인 실수(ハマりどころ)가 있었습니다.
이번에 가장 실용적이었던 설계는 이것입니다. Gemini Live API에는 tools.functionDeclarations를 통해 함수를 전달할 수 있습니다. 저희는 control_avatar라는 함수를 하나 정의하고, action(nod=끄덕임 / smile=미소 / empathy=공감 / surprise=놀람 / think=생각 중 / wave=손 흔들기)을 Gemini 스스로 대화 문맥에 따라 판단하여 호출하도록 만들었습니다.
tools: [{
functionDeclarations: [{
name: 'control_avatar',
...
포인트는, "언제·왜 그 표정을 지을지"를 미리 너무 과하게 규칙화하지 않고, system instruction으로 방침만 전달하여 모델의 판단에 맡긴 것입니다. 방침으로 전달한 내용은 예를 들어 다음과 같습니다.
- 상대가 말하는 동안에는 적절히 끄덕인다.
- 상대가 정답이나 좋은 반응을 보이면 미소를 짓는다.
- 상대가 곤란해 보이면 공감하는 표정을 짓는다.
Function Call이 날아오면, 중계 서버는 그것을 그대로 클라이언트에 avatar_control 메시지로 전달하고, Gemini 측에는 toolResponse로 "실행했다"라고 응답합니다. 이 왕복 과정을 소홀히 하면 Gemini 측이 응답을 멈춰버리기 때문에, Function Call에는 반드시 결과를 반환하는 것이 Live API를 사용할 때의 필수 규칙입니다.
음성 대화에서 은근히 어려운 부분이 "상대가 침묵하는 시간"의 처리입니다. 무음을 감지하는 메커니즘을 별도로 구현하는 대신, 저희는 침묵에 대한 행동 역시 system instruction 기술만으로 제어했습니다.
- 5초 정도의 침묵 → 생각 중일 수도 있으므로 조금 더 기다림
- 15초 이상의 침묵 → "괜찮으신가요?"라고 부드럽게 말을 건넴
- 30초 이상의 침묵 → 힌트를 주거나 화제를 전환함
- 단, 어려운 문제를 생각하는 중에는 재촉하지 않음
이는 감지 로직을 코드로 작성한 것이 아니라, 모델에게 "얼마나 기다려야 하는지"에 대한 기준을 자연어로 전달했을 뿐입니다. 대화의 문맥(내용의 난이도)까지 고려하여 대기 시간을 조정해 준다는 점은 규칙 기반(rule-based) 구현보다 유연했습니다.
responseModalities: ['AUDIO'](음성으로만 응답)를 지정하더라도, serverContent.modelTurn.parts 안에 text 파트가 섞여서 돌아오는 경우가 있습니다. 자막 표시나 로그용 텍스트가 필요한 경우, 이 part.text를 가져오면 음성과 별개로 텍스트 스트림 API를 호출할 필요가 없었습니다. 반대로 part.inlineData(음성 데이터 본체)와 part.text는 동일한 parts 배열에 혼재되어 있으므로, 수신 측에서 type 분기를 하는 구현이 필수적입니다.
범용 대화형 AI 아바타를 만들다 보면, "교사에게는 교사답게", "면접관에게는 면접관답게" 행동을 구분하여 수행하고 있는지 걱정되기 마련입니다. 저희가 실제로 수행한 방법은 동일한 API 호출 프레임워크 내에서, system instruction(시스템 지침)만 교체한 6가지 직종의 테스트 케이스를 실행하여 응답을 나란히 놓고 비교하는 단순한 방식이었습니다.
검증한 역할의 예:
- 교사 (수학)
- 멘탈 헬스 카운슬러
- 병원 접수처
- 채용 면접관
- 부동산 영업 어시스턴트
- 노인 시설 AI 컴패니언
각 케이스에 대해 기대되는 키워드(예를 들어 수학 교사라면 "방정식", "이항" 등)를 사전에 정의하고, 실제 응답 텍스트에 해당 키워드가 포함되었는지, 아바타의 액션(Action)이 호출되었는지, 응답 완료까지 걸린 시간은 어느 정도인지를 로그로 남겨 비교했습니다. LLM(거대언어모델)의 응답은 결정론적(Deterministic)이지 않기 때문에 엄격한 자동 합불 판정 방식은 사용하지 않았지만, "system instruction의 한 마디를 바꾼 것만으로 역할로서의 자연스러움이 어떻게 변하는지"를 육안으로 비교할 수 있는 상태를 만든 것이 프롬프트 튜닝(Prompt Tuning)을 진행하는 데 있어 유효했습니다.
- Gemini Live API는 브라우저 직결보다 중계 서버를 거치는 구성이 안전함 (API 키 보호 및 연결 관리의 분리)
- 아바타의 비언어적 표현은 세세하게 코드로 제어하기보다, Function Calling(함수 호출) + 방침만을 담은 system instruction을 사용하는 것이 더 유연하게 동작함 - 침묵 대응과 같은 "간격(Pause)" 설계도 자연어 프롬프트만으로 충분히 실용적인 수준까지 가능함
- 음성 응답 모드에서도 텍스트는 취득할 수 있음 (parts 내의 type 분기 필요)
- 여러 직종의 system instruction을 나란히 놓고 테스트하는 간이적인 수법만으로도, 역할별 응답 품질의 차이를 확인할 수 있음
Gemini의 기술적인 전체 모습(모델 체계·요금·에이전트 기능)은 당사의 해설 기사에 정리해 두었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기