비전·음성·추천

DOMAIN / 45번째 글

실시간 음성 API — 말이 끝나고 첫 소리까지의 예산

음성 대화를 붙일 때 실제로 다루게 되는 것은 지연 예산과 상태 관리입니다. ASR·LLM·TTS 3단 구성과 통합 음성 모델의 차이, 말이 끝났다고 판정하는 문제, 끼어들기가 왜 단순한 멈춤이 아닌지, 그리고 스트리밍 중 도구 호출을 다루는 법을 정리합니다.

PALDYN Team17 MIN READ

지난 글까지 이미지 쪽을 다뤘으니 이번에는 소리다. 음성 대화를 붙이는 일은 텍스트 챗봇을 붙이는 일과 겉보기에 비슷해 보인다. 입력을 받고, 모델을 부르고, 결과를 돌려준다. 그런데 실제로 만들어 보면 완전히 다른 종류의 문제를 다루게 된다.

차이는 하나다. 텍스트 대화에서 사용자는 답이 늦으면 기다린다. 음성 대화에서 사용자는 답이 늦으면 다시 말한다. 사람끼리의 대화에서 말 사이 침묵이 1초를 넘으면 어색하고 2초를 넘으면 상대가 못 들었다고 생각한다. 그 감각이 그대로 적용되기 때문에, 음성 시스템의 설계는 처음부터 끝까지 지연 예산을 어디에 쓸 것인가의 문제가 된다.

예산은 몇 백 밀리초다

목표를 먼저 정하자. 사용자가 말을 멈춘 순간부터 시스템의 첫 소리가 나오기까지가 예산이다. 사람끼리 대화할 때 이 간격은 보통 200ms 안팎이고, 800ms를 넘으면 "느리다"고 느끼기 시작한다. 실용적인 목표는 500~800ms 정도다.

이 예산을 어디에 쓰는지 보면 설계가 보인다.

3단 파이프라인과 통합 음성 모델의 지연이 쌓이는 자리

여기서 눈여겨볼 것은 가장 큰 조각이 모델이 아니라는 사실이다. 침묵을 기다리는 시간이 절반 가까이를 먹는다. LLM을 더 빠른 것으로 바꿔 400ms를 250ms로 줄여도 전체는 1200ms에서 1050ms가 될 뿐이다. 앞쪽을 안 건드리면 뒤를 아무리 줄여도 체감이 안 바뀐다.

말이 끝났는지 아는 것이 첫 번째 어려움

사람은 상대가 말을 끝냈는지를 침묵만으로 판단하지 않는다. 억양이 내려갔는지, 문장이 완결됐는지, 시선을 줬는지를 함께 본다. 기계는 대개 침묵의 길이만 본다.

여기서 딜레마가 생긴다. 침묵 임계값을 짧게 잡으면(300ms) 반응은 빠른데 사용자가 생각하느라 잠깐 멈춘 것을 말 끝으로 착각해 말허리를 자른다. 길게 잡으면(1초) 자르지는 않는데 매 턴이 1초씩 느려진다.

음성 활동 감지(VAD)는 오디오 프레임마다 "여기 사람 말소리가 있는가"를 판단하는 모듈이다. Silero VAD 같은 작은 신경망이 표준으로 쓰이고, 브라우저나 클라이언트에서 돌 만큼 가볍다. VAD는 소리가 있는지 없는지만 알려 주므로 "얼마나 기다릴 것인가"는 여전히 우리가 정해야 한다.

실무에서 쓰는 완화책이 몇 가지 있다.

  • 상황에 따라 임계값을 바꾼다. 전화번호나 주소를 받는 중이면 사람이 중간에 멈추므로 길게 잡고, 예/아니오를 받는 중이면 짧게 잡는다
  • 문장이 끝났는지를 함께 본다. 부분 인식 결과가 "그러니까 제 계좌번호가"에서 멈췄으면 아직 안 끝난 것이다. 텍스트가 완결된 문장인지 값싼 모델로 판단해 임계값을 조절한다
  • 기다리는 동안 채운다. 임계값이 지나면 곧바로 "네," 같은 짧은 반응을 먼저 내보내 체감 지연을 덮는다. 어색해지기 쉬우니 아껴 쓴다

최근의 통합 음성 모델들은 이 판정을 모델 안에서 한다. 억양과 문맥을 함께 보므로 침묵만 보는 것보다 낫고, 무엇보다 판정과 응답 생성이 한 모델 안에서 이어지므로 그 사이의 왕복이 사라진다.

3단 구성과 통합 모델

지금 선택지는 크게 둘이다.

ASR → LLM → TTS 통합 음성 모델
지연 단계마다 쌓인다 짧다
억양·감정 텍스트를 지나며 사라진다 입력 억양이 답에 반영된다
중간 텍스트 남는다 (로그·검수·필터링) 따로 요청해야 한다
모델 선택 단계마다 자유롭게 고른다 그 벤더의 모델에 묶인다
도구 호출 LLM 단계에서 평범하게 지원 범위가 모델마다 다르다
비용 세 번 청구 오디오 토큰 단가가 비싼 편
디버깅 단계별로 끊어 본다 안이 안 보인다

중간 텍스트가 남는지가 생각보다 중요하다. 콜센터라면 통화 기록을 남겨야 하고, 금융이라면 답변을 내보내기 전에 가드레일을 통과시켜야 한다. 3단 구성은 LLM 출력이 텍스트로 나오니 그 사이에 검사를 끼울 수 있지만, 통합 모델은 오디오가 바로 나오므로 끼울 자리가 없다. 통합 모델을 쓰면서 텍스트 전사를 함께 받는 옵션이 있긴 한데, 그건 검사용이지 차단용은 아니다 — 이미 소리가 나간 뒤다.

반대로 억양과 감정은 통합 모델만 다룰 수 있다. 사용자가 짜증 난 목소리로 말했다는 사실은 텍스트를 지나는 순간 사라진다. 상담 품질이 중요한 자리에서는 이게 결정적일 수 있다.

스트리밍은 양쪽 다 흐른다

실시간 음성 API는 요청-응답이 아니라 양방향으로 계속 흐르는 연결이다. WebSocket이나 WebRTC 위에서 이벤트를 주고받는다.

클라이언트 → 서버   오디오 조각 (20~100ms 단위로 계속)
서버 → 클라이언트   부분 인식 결과 (사용자가 말하는 중에도)
서버 → 클라이언트   응답 오디오 조각 (생성되는 대로)
서버 → 클라이언트   응답 텍스트 (전사·로그용)
클라이언트 → 서버   취소 (사용자가 끼어들었다)

WebRTC를 쓰는 이유는 브라우저에서 마이크와 스피커를 다루는 일이 그 위에 이미 얹혀 있기 때문이다. 지터 버퍼, 에코 제거, 패킷 손실 은닉이 공짜로 따라온다. 서버끼리 붙이거나 전화망과 연결할 때는 WebSocket으로 원시 오디오를 흘리는 편이 단순하다.

여기서 중요한 설계 원칙이 하나 있다. 서버는 오디오를 앞질러 보낸다. 응답 5초 분량이 생성됐다면 5초치를 다 보내 두는 편이 네트워크가 불안정해도 끊기지 않는다. 그런데 이 앞질러 보내기가 다음 문제를 만든다.

끼어들기는 멈춤이 아니라 되감기다

사용자가 시스템의 말을 자르고 끼어드는 것을 끼어들기(barge-in)라고 한다. 이게 자연스럽게 동작하지 않으면 대화가 아니라 안내방송이 된다.

단순하게 생각하면 "재생을 멈추고 새 입력을 받으면 되는 일"인데, 앞질러 보내기 때문에 그렇지 않다.

끼어들었을 때 서버가 보낸 것과 사용자가 들은 것이 어긋나는 모습

서버는 스무 조각을 보냈고 대화 기록에 "이만큼 말했다"고 적어 뒀다. 사용자는 세 조각만 들었다. 이 어긋남을 안 고치면 다음 턴부터 서로 다른 대화를 한다 — 사용자는 못 들은 내용을 시스템은 이미 말한 것으로 치고, "아까 말씀드렸듯이"라고 이어 간다.

그래서 끼어들기 처리는 세 가지를 한꺼번에 해야 한다.

  1. 클라이언트가 남은 버퍼를 버린다. 즉시 소리가 멎어야 한다
  2. 클라이언트가 얼마나 재생됐는지를 서버에 알린다. 밀리초 단위 위치를 함께 보낸다
  3. 서버가 대화 기록을 그 지점에서 자른다. 실제로 들린 데까지만 남긴다

세 번째가 자주 빠진다. 그리고 빠져도 당장은 멀쩡해 보이므로 며칠 뒤 "왜 못 들은 걸 들었다고 하지"에서 발견된다. 주요 실시간 API들이 취소 이벤트에 재생 위치를 함께 실어 보내게 설계된 이유가 이것이다.

여기에 딸린 문제가 에코다. 스피커에서 나오는 시스템 목소리가 마이크로 다시 들어가면 VAD가 그걸 사용자 발화로 착각해 스스로 끼어든다. 헤드셋이 아니라 스피커폰 환경이면 반드시 에코 제거를 켜야 한다. WebRTC를 쓰면 브라우저가 해 주고, 아니면 직접 붙여야 한다.

도구 호출은 침묵을 만든다

음성 에이전트가 조회를 해야 할 때 문제가 생긴다. 텍스트 챗봇이라면 "잠시만요"라는 표시가 뜨고 사용자는 기다린다. 음성에서는 아무 소리도 안 나므로 사용자가 연결이 끊긴 줄 안다.

대응은 단순하지만 명시적으로 해야 한다.

  • 도구를 부르기 전에 말한다. "조회해 볼게요" 한마디를 먼저 내보내고 호출을 시작한다
  • 오래 걸리면 중간에 다시 말한다. 2초를 넘기면 "거의 다 됐어요"를 한 번 더
  • 가능하면 미리 부른다. 사용자가 계좌번호를 다 부르기 전에 앞자리로 후보를 좁혀 두는 식으로, 부분 인식 결과에서 미리 시작할 수 있는 조회가 있다

그리고 도구 호출 중에도 연결과 VAD는 계속 살아 있어야 한다. 조회하는 동안 사용자가 "아 아니에요, 다른 계좌요"라고 말하면 그걸 받아 진행 중인 호출을 버려야 한다. 이 부분을 동기 호출로 짜 두면 그 시간 동안 입력이 통째로 막힌다.

음성 에이전트에서 다룬 설계 원칙들이 여기에 그대로 얹힌다. 다른 점은 여기서는 프로토콜 수준의 이야기 — 어떤 이벤트를 언제 보내고 어떤 상태를 누가 들고 있는가 — 라는 것이다.

무엇을 재야 하는가

음성 시스템의 품질은 텍스트 지표로 안 잡힌다. 최소한 이 넷은 따로 재야 한다.

첫 소리까지의 지연. 사용자 발화 종료부터 첫 오디오 바이트까지. 평균이 아니라 95번째 백분위로 본다 — 열 번에 한 번 2초씩 걸리면 사용자는 그 시스템을 느리다고 기억한다.

말허리 자름 비율. 사용자가 말하는 중에 시스템이 끼어든 비율. VAD 임계값을 줄이면 지연은 좋아지고 이 값은 나빠진다. 둘을 함께 봐야 튜닝이 된다.

끼어들기 성공률. 사용자가 끼어들었을 때 1초 안에 소리가 멎었는가. 그리고 대화 기록이 제대로 잘렸는가.

턴 완결률. 사용자가 하려던 일을 몇 번의 턴 안에 마쳤는가. 결국 이게 제품 지표다. 앞의 셋이 다 좋은데 이게 나쁘면 문제는 음성이 아니라 대화 설계에 있다.

이 값들은 실제 통화 로그에서만 제대로 나온다. 오디오와 이벤트 타임스탬프를 함께 저장해 두는 것이 사실상 필수다 — 나중에 "이 통화에서 왜 3초가 걸렸나"를 재구성할 수 있어야 한다.

정리

  • 음성 대화의 설계는 지연 예산을 어디에 쓸 것인가의 문제다. 목표는 500~800ms
  • 가장 큰 조각은 모델이 아니라 말이 끝났는지 판정하는 데 드는 시간이다. 여기를 안 건드리면 뒤를 줄여도 소용없다
  • 침묵 임계값은 짧으면 말허리를 자르고 길면 느리다. 상황별 임계값과 문장 완결 판단으로 완화한다
  • 3단 구성은 중간 텍스트가 남아 검수·필터링이 되고, 통합 모델은 짧고 억양을 살린다. 검사를 끼울 자리가 필요한지로 고른다
  • 서버는 오디오를 앞질러 보낸다. 그래서 끼어들기는 멈춤이 아니라 되감기다 — 들린 지점까지 대화 기록을 잘라야 한다
  • 도구 호출 중의 침묵은 반드시 말로 채운다. 그리고 그동안에도 입력은 열려 있어야 한다
  • 지표는 첫 소리 지연(95백분위), 말허리 자름 비율, 끼어들기 성공률, 턴 완결률 넷을 함께 본다

읽어주셔서 감사합니다. 😊

LATEST

비전·음성·추천의 최신 글

비전·음성·추천2026.09.07

오프라인 강화학습 — 쌓인 로그만으로 정책을 배우기

실제 서비스에서 탐색은 곧 사용자에게 나쁜 행동을 해 보는 일입니다. 이미 쌓인 로그만으로 정책을 배우려 할 때 왜 Q값이 혼자 부풀어 오르는지, 그 부풀음을 누르는 세 갈래 대응, 행동 복제라는 기준선의 무게, 그리고 배포 전에 성능을 재는 일이 왜 가장 어려운지를 정리합니다.

18 MIN
비전·음성·추천2026.09.07

다국어 전이 — 라벨 없는 언어에서 모델이 동작하는 이유

영어 라벨만으로 학습한 분류기가 한국어 문장을 그대로 처리하는 일이 실제로 일어납니다. 여러 언어가 한 표현 공간에 겹쳐 놓이는 원리, 그 겹침이 무너지는 조건, 번역해서 학습할지 번역해서 추론할지 고르는 기준, 그리고 언어별로 나눠 재야 하는 이유를 정리합니다.

16 MIN
비전·음성·추천2026.09.07

정보 추출 — 글 한 덩이를 표 한 줄로 바꾸는 일

계약서와 이메일을 데이터베이스에 넣으려면 글에서 값을 뽑아 칸에 채워야 합니다. 개체명·관계·사건의 세 층위, 값을 정규화하는 일이 왜 절반인지, 근거 위치를 함께 남겨야 하는 이유, 규칙·전용 모델·언어 모델의 갈림길, 그리고 필드별로 재는 평가법을 정리합니다.

17 MIN