
통화 경로를 훔쳐보고 호(Call)를 추측해서는 안 된다 ― Amazon Connect + KVS로 시그널링을 가져오기
요약
Amazon Connect와 Kinesis Video Streams를 활용해 실시간 통화 받아쓰기 서비스를 구축하며 겪은 설계적 시행착오를 다룹니다. OpenAI Realtime API의 VAD 성능과 8kHz 음성 처리 경험, 그리고 스트림 폴링 방식의 한계를 분석합니다.
핵심 포인트
- OpenAI Realtime API의 VAD를 활용하면 REST 방식보다 정확한 구간 절삭이 가능함
- 8kHz 전화 음성도 업샘플링을 통해 API 요구사항을 충족하며 안정적으로 변환 가능
- KVS 스트림 폴링 방식은 통화 시작 시점의 데이터 손실과 식별 불가 문제를 야기함
- 통화 신호(Signaling)를 직접 수신하지 않고 데이터 흐름만 감시하는 설계의 위험성 경고
Amazon Connect의 통화 음성을 Kinesis Video Streams(KVS)로부터 받아, 화자별로 실시간 받아쓰기(Transcription)를 수행하는 소비 서비스를 작성했다. 그 과정에서 전화 엔지니어라면 설계 전에 깨달았어야 할 문제에 부딪혔기에 이를 공유한다.
수신을 위해 EC2를 세울 필요는 없다
처음에 고민했던 것은 "KVS에서 데이터를 흡수하는 상주 프로세스를 어디에 둘 것인가"였다. 결론은 어디에도 세울 필요가 없다. GetMedia는 단순한 AWS API이며, 인증 정보만 있다면 로컬 머신에서도 호출할 수 있다. 망 내에 수신 장치를 둘 필연성은 어디에도 없다.
클라우드에 상설하는 것은 "통화가 오면 자동으로 받는 운영 환경"이 된 이후에 해도 충분하며, 개발 중에 EC2에 SSH로 접속해 코드를 고치고 재시작하는 루프는 단순히 이터레이션(Iteration)을 늦출 뿐이었다.
Realtime API의 transcription 세션
OpenAI Realtime API에는 대화용이 아닌 받아쓰기 전용 세션이 있다. wss://api.openai.com/v1/realtime?intent=transcription에 연결하여 선언한다.
{
"type": "session.update",
"session": {
...
그 후에는 input_audio_buffer.append로 음성을 흘려넣기만 하면 된다. 발화의 끊김은 서버 VAD(Voice Activity Detection)가 판단해주므로, 이쪽에서 무음 감지를 직접 작성할 필요가 없다. 반환되는 값은 conversation.item.input_audio_transcription.delta(순차적 데이터)와 .completed(확정 데이터)이다.
REST보다 정확도가 높았다
흥미로웠던 점은 동일한 음성을 REST의 /v1/audio/transcriptions로도 던져보았다는 것이다. 결과는 다음과 같이 갈렸다.
| 경로 | 결과 |
|---|---|
| REST (파일 일괄 처리) | 말씀드립니다. 이상. |
| Realtime (VAD로 구간 절삭) | 여보세요 / 이상. |
REST는 "여보세요"를 "말씀드립니다"로 오인했다. 추측건대, 8kHz 전화 대역은 정보량이 적기 때문에 무음을 포함한 긴 구간을 통째로 처리하는 것보다, VAD로 발화 부분만 잘라내는 것이 더 정확도가 높을 것이다.
덧붙여 알게 된 사실은, 8kHz 상태에서도 일본어는 충분히 텍스트로 변환된다는 점이다. 24kHz로의 업샘플링(Up-sampling)은 정확도를 위해서가 아니라, API의 포맷 요구사항을 충족하기 위한 처리일 뿐이었다 (8kHz와 24kHz의 결과 차이는 없었다).
본론: 호(Call)를 특정할 수 없다
자, 동작하는 것은 만들어졌다. 하지만 근본적인 문제가 남아 있었다. 서비스 측에서는 지금 처리하고 있는 음성이 어떤 호(Call)의 것인지 알지 못한다.
초판 구현은 이랬다. KVS의 스트림 목록을 몇 초 간격으로 폴링(Polling)하다가, 본 적 없는 스트림이 나타나면 "호가 온 모양이다"라고 판단하여 수신을 시작한다.
동작은 한다. 하지만 전화망 용어로 바꾸면, 베어러(Bearer, 통화 경로)를 감시하여 호의 발생을 때려 맞추고 있는 구현일 뿐이다. 공통선 신호의 IAM(Initial Address Message)을 받지 않고, 통화 채널로 소리가 흐르기 시작했다는 사실만을 근거로 "누군가 연결된 모양이다"라고 결정하고 있다. 교환기 설계로서는 성립하지 않는다.
실질적인 피해도 세 가지 있었다.
- 통화의 앞부분을 놓친다. 인지한 후에
StartSelectorType=NOW로 연결하므로, 그 전까지의 음성은 영구히 손실된다. - 호의 정보를 아무것도 알 수 없다. 발신자 번호도 컨택트 ID(Contact ID)도 스트림 이름으로부터 추측할 수밖에 없다.
- 여러 호가 동시에 오면 매칭할 근거가 없다.
해결책: 제어 신호 선을 따로 뽑는다
콜 플로우(Call Flow)의 "외부 리소스 호출" 블록에서 Lambda를 실행할 수 있다. 이를 미디어 스트리밍 시작 직후에 배치하면, Lambda가 받는 이벤트에 호의 식별 정보가 담겨 온다.
{
"Details": {
"ContactData": {
...
호의 ID와 해당 호 전용 스트림, 그리고 통화 시작 부분의 프래그먼트(Fragment) 번호. 추측이 아닌 교환기로부터의 통지이다.
이 Lambda가 SQS로 메시지를 던지면, 소비 서비스 측은 롱 폴링(Long Polling)으로 대기한다. 통화 경로(KVS)와 시그널링(SQS)이 분리된 구성이 된다. SQS를 선택한 이유는 로컬 개발이든 Fargate 상설 환경이든 수신 측 코드가 바뀌지 않기 때문이다. HTTP로 직접 호출하는 방식이라면 개발 중에는 서비스가 공개되지 않아 메시지를 받을 수 없다.
얻은 효과는 매우 크다.
- 호(Call)와 스트림(Stream)의 대응이 확정되어, 혼동될 염려가 없다
StartFragmentNumber부터 읽기 때문에 통화의 시작부터 음성을 가져올 수 있다 - 발신자 번호를 화면에 표시할 수 있다- 여러 개의 호를 독립된 세션으로서 병렬 처리할 수 있다
부록: 무음 루프(Silence Loop)의 함정
브라우저까지 연결하여 동작시켜 보니, 상대방의 발언은 흐르는데 내 쪽의 트랙(Track)은 한 번도 말하지 않았다.
원인은 콜 플로우(Call Flow)에 있었다. "동의 안내(Consent Announcement) → 스트리밍 시작 → 무음 루프로 통화 유지"라는 흐름으로 되어 있었다. 안내는 모두 스트리밍 시작 "전"에 재생이 끝난다. 즉, 내 쪽 트랙에는 구조상 영원히 디지털 무음만 실리게 된다. 양쪽 화자를 분석하는 프로덕트인데, 한쪽 채널이 원리적으로 죽어 있었던 것이다.
요약
- KVS의
GetMedia는 단순한 API일 뿐이다. 수신을 위해 EC2는 필요 없다 - Realtime API의 transcription 세션은 VAD(Voice Activity Detection)에 구분을 맡길 수 있어 편리하다. 동일한 음성으로 REST 방식보다 정확도가 높았다 - 전화 대역폭인 8kHz에서도 일본어는 충분히 텍스트로 변환된다
- 통화 경로를 훔쳐보고 호를 추측해서는 안 된다. 시그널링(Signaling)을 별도로 가져오면, 호의 식별·통화의 시작점·발신자 번호·여러 호의 병렬 처리를 한 번에 얻을 수 있다
마지막 항목은 클라우드 API로서 바라보고 있으면 "스트림 목록을 폴링(Polling)하면 되겠지" 하고 넘어가기 쉽다. 하지만 이는 교환기의 이야기이며, 제어와 통화 경로는 별개라는 고전적인 원칙으로 되돌아온 격이었다.
EBML 파서(Parser)를 스트리밍 방식으로 개편한 이야기나 구성도가 포함된 상세 버전은 원문 블로그에서 확인하세요:
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기