Code › tail-villain
라이브 인터뷰의 턴 경계
음성 입력과 모델 응답의 턴 경계를 정리해 라이브 인터뷰를 안정화한 과정
라이브 인터뷰는 말이 오가기 시작한 뒤부터 더 어려워졌다. WebSocket이 열리고, 모델 음성이 나오고, 사용자의 말이 transcript로 남으면 겉으로는 기능이 된 상태다. 그런데 실제로 테스트해 보면 사용자가 잠깐 멈춘 사이에 turn이 끝나거나, 빈 push-to-talk가 답변처럼 처리되거나, assistant 음성의 앞부분이 잘리는 일이 생겼다.
이건 단순히 STT 품질 문제가 아니었다. 사용자가 언제 답변을 시작했고 언제 끝냈는지, 모델의 음성이 정말 끝났는지, transcript가 저장해도 되는 수준인지, UI가 지금 말해도 된다고 보여 줘도 되는지까지 한꺼번에 맞아야 했다.
라이브 인터뷰를 안정화하려면 turn의 경계를 다시 정의해야 했다.
처음에는 activity_end를 기준으로 생각하기 쉬웠다. 사용자가 push-to-talk를 놓거나 자동 감지가 말을 끝났다고 판단하면, 그 시점부터 assistant가 답하면 된다고 생각하기 쉽다. 하지만 Gemini Live의 activity_end는 사용자의 활동 구간이 닫혔다는 신호이지, 의미 있는 답변이 들어왔다는 보장은 아니었다.
빈 버튼 누름이나 아주 짧은 소음도 activity를 만들 수 있다. 그걸 그대로 assistant 응답 허용 조건으로 쓰면 모델은 사용자가 아무 말도 하지 않았는데도 다음 질문을 하거나, 무응답으로 처리하거나 빈 답변에 반응한다. 그래서 relay 쪽 기준을 바꿨다. finished input transcription이 있고, 그 안에 실제 글자나 숫자가 있어야 user turn으로 인정하고 assistant 응답을 허용한다.
이 기준은 완벽하지 않지만, 적어도 빈 turn을 제품의 대화로 승격시키지는 않는다. Live session 안에서는 작은 이벤트 하나가 곧바로 비용, transcript, evaluation, UX로 이어지기 때문에, 의미 없는 입력은 가능한 한 relay 경계에서 멈춰야 했다.
프론트도 같은 방향으로 정리했다. Live mode에서는 별도 TTS 버튼이나 재생 아이콘이 오히려 거짓 affordance가 된다. 이미 모델이 streaming audio를 만들고 있는데, text mode의 speaker 버튼을 그대로 보여 주면 사용자는 그 버튼이 Live audio를 제어한다고 오해할 수 있다. 그래서 Live bubble에서는 그런 제어를 숨기고, 현재 Live transcript와 저장된 메시지를 섞는 방식도 active session에서만 허용했다.
완료된 세션에서는 persisted message만 보여 줘야 한다. 그렇지 않으면 끝낸 직후에는 같은 메시지가 두 번 보이고, 새로고침하면 정상으로 돌아오는 이상한 경험이 생긴다.
다음 문제는 interruption이었다. 사용자가 interviewer가 말하는 중에 push-to-talk를 누르면 현재 답변을 끊고 말할 수 있다. 하지만 브라우저 스피커에서 나오는 assistant 음성이 마이크로 다시 들어가거나, 잘린 첫 프레임이 사용자 말처럼 보내지면 STT는 이상한 방향으로 무너진다.
실제 테스트에서는 엉뚱한 문자나 숫자로 읽히는 transcript가 나왔다. 별도의 STT 경로를 붙이면 더 깨끗해질 거라고 기대했지만, real browser 테스트에서는 오히려 언어가 더 흔들리고 UX timing도 밀렸다. canonical transcript는 Gemini Live input transcription으로 되돌렸다.
이때 배운 건 STT를 하나 더 붙이는 게 항상 정답은 아니라는 점이다. 별도 모델이 더 정확할 수는 있지만, 라이브 인터뷰에서는 transcript가 언제 도착하고, UI가 언제 보여 주고, 평가가 어떤 텍스트를 기준으로 도는지도 같이 중요하다. 더 깨끗해 보이는 아키텍처가 실제 대화에서는 더 느리고 더 혼란스러울 수 있다.
interruption은 기본값에서 다시 밀려났다. assistant가 말하는 동안 사용자의 mic를 열어 두는 순간, echo cancellation과 noise suppression에 너무 많은 책임을 넘기게 된다. 브라우저 제약을 켜는 건 필요했지만, 그것만으로 turn boundary가 해결되지는 않았다.
handoff 쪽에서는 반대 문제가 있었다. assistant가 말을 끝낸 것으로 표시된 뒤에도, Gemini Live가 뒤늦게 audio fragment를 더 보낼 수 있었다. relay가 turnComplete를 너무 강하게 믿으면 그 조각을 unsolicited output으로 보고 버리게 되고, 사용자는 interviewer 음성의 앞이나 끝이 잘린 것처럼 듣는다.
로그 레벨을 낮춰도 잘린 음성은 복구되지 않는다. 실제로는 late assistant audio를 어느 시점까지 받아 줄지, 사용자가 이미 real audio를 보내기 시작했는지, client가 listening 상태로 돌아가도 되는지를 더 조심스럽게 나눠야 했다.
그래서 assistant audio queue가 끝나기 전에는 PTT가 너무 빨리 열리지 않게 했고, 사용자가 PTT를 놓은 뒤에도 짧은 release tail을 남겨 마지막 음절이 activityEnd 전에 잘리지 않도록 했다. PTT는 명시적인 사용자 의도이므로 첫 프레임이 작다는 이유만으로 버리는 RMS gate도 제거했다.
이런 수정은 각각 작지만, voice UX에서는 작은 timing 차이가 바로 들린다. 텍스트 채팅에서는 300ms가 거의 보이지 않지만, 음성에서는 그 300ms가 마지막 음절이거나 다음 질문의 첫 단어가 된다.
turn이 안정되기 시작하자 다른 경계도 눈에 들어왔다. 한국어 roadmap에서 진행하는 인터뷰가 중간에 영어 요청을 받으면, 일반 interview prompt는 단순히 한국어로 답하라는 정도의 지시만 갖고 있었다. study mode에는 언어를 고정하고 사용자 요청으로 바꾸지 말라는 규칙이 있었는데, regular interview 경로에는 그 정도로 강한 lock이 없었다.
그래서 opening, follow-up, angle transition, closing, final wrap-up 쪽에 같은 언어 고정 규칙을 넣었다. 사용자가 영어 technical term이나 고유명사를 섞는 것은 허용하지만, learner-visible JSON field와 실제 답변 언어는 세션 언어를 따라야 한다. 언어는 취향 설정으로 끝나지 않고, 인터뷰 제품에서는 평가와 UX 일관성의 일부가 된다.
provider error도 비슷했다. Gemini나 Vertex 쪽에서 일시적인 429나 RESOURCE_EXHAUSTED가 나면, 그 raw message가 사용자에게 보이면 제품이 갑자기 내부 로그를 노출한 것처럼 읽힌다. structured request는 retry와 backoff를 붙였고, streaming request는 아직 chunk가 하나도 나가지 않았을 때만 retry하도록 제한했다.
한 번이라도 SSE chunk가 나간 뒤에 다시 시도하면 사용자는 같은 답변의 앞부분을 두 번 볼 수 있다. streaming retry는 중복 출력과 persistence를 함께 확인해야 한다.
이 시기의 작업을 기능 목록으로만 보면 서로 다른 버그를 많이 고친 기록이 된다. 빈 PTT, noisy STT, late audio, scroll, transcript duplicate, 언어 고정, provider retry는 표면적으로 각자 다른 문제다.
하지만 실제로는 하나의 질문으로 묶였다. 지금 이 turn을 제품이 믿어도 되는가.
사용자의 침묵을 답변으로 믿으면 안 된다. assistant의 turnComplete를 모든 audio가 끝났다는 뜻으로 믿어도 안 된다. 별도 STT가 더 낫다는 가정도, 모델이 session language를 알아서 지킬 거라는 기대도, provider가 항상 정상 응답을 줄 거라는 전제도 그대로 믿으면 안 된다.
라이브 인터뷰는 실시간이라서 실수가 빨리 드러난다. 텍스트 모드에서는 나중에 정리할 수 있는 경계도, 음성에서는 사용자가 바로 듣고 말하면서 경험한다. 그래서 이 작업에서는 더 많은 기능을 넣기보다, 대화 한 턴이 성립하는 조건을 계속 좁혔다.
꼬빌이 제대로 압박하려면 먼저 사용자의 답변을 정확한 turn으로 받아야 한다. 그 turn이 흔들리면 persona도, 평가도, 리포트도 그 위에 안정적으로 설 수 없다.