대시보드-따라하기 시리즈 마지막 편. 버튼을 누르면 오늘의 브리핑을 읽어 주게 만든다. 화면을 볼 수 없는 시간에 쓰려고 붙였는데, 시리즈 전체에서 가장 손이 많이 간 부분이다.
1단계: 브라우저 음성으로 시작
처음에는 브라우저의 Web Speech API 로 읽었다. 무료이고 코드 몇 줄이면 된다. 다만 기기마다 목소리가 다르고 긴 글에서 어색하다. 지금도 음성 합성 서버가 막혔을 때의 대체 수단으로 남겨 두었다.
2단계: 서버에서 합성
LLM 계열 음성 합성 API 로 옮겼다. 텍스트를 보내면 PCM 오디오가 온다. 붙이면서 알게 된 것들:
- 입력 길이 제한이 있어서 긴 원고는 2,500자 안팎으로 나눠 합성한 뒤 PCM 을 이어 붙이고 WAV 헤더를 씌운다.
- 말투 지시문을 본문에 넣지 않는다. "다음 글을 차분하게 읽어 줘" 같은 문장을 앞에 붙였더니, 가끔 그 지시문까지 소리 내어 읽었다. 내가 쓴 모델은 시스템 지시를 따로 받지 않아서, 결국 지시문을 전부 뺐다.
- 가끔 비정상적으로 긴 음성이 나온다. 글자 수로 예상 길이를 계산해 두고(한국어는 글자당 0.2초쯤), 그보다 훨씬 길면 다시 합성해서 가장 짧은 것을 쓴다.
- 숫자 읽기. "13도"를 "열세 도", "40분"을 "마흔 분"으로 읽을 때가 있다. 합성 직전에 단위가 붙은 숫자를 한글로 풀어 써서("십삼 도", "사십 분") 넘긴다.
- 요금제를 먼저 본다. 후보였던 다른 음성 서비스는 호출이 없어도 월 기본요금이 꽤 컸다. 콘솔에서 확인하고 접었다.
3단계: 기다리지 않게 하기
원고를 다 쓰고 통째로 합성하니 버튼을 누르고 1분 넘게 조용했다. 재서 보니 원고 작성에 40~80초, 합성에 30초 남짓이었다. 미리 만들어 두는 방법도 있지만, 아무도 듣지 않는 날에도 비용이 나간다. 대신 조각내어 흘려보내기로 했다.
- 오프닝은 가벼운 모델로 2~4초 만에 만든다. 인사와 가장 중요한 한두 가지.
- 본문은 큰 모델에서 스트리밍으로 받으면서, 문장이 240자쯤 쌓일 때마다 한 파트로 잘라 합성 큐에 넣는다. 합성은 동시에 두 개까지.
- 클라이언트는 첫 파트가 준비되는 대로 재생을 시작하고, 재생 중에 다음 파트가 준비됐는지 확인해 큐 뒤에 붙인다.
이렇게 해서 첫 소리가 나기까지의 시간이 몇 초로 줄었다. 파트 끝에는 짧은 무음을 붙여 이어지는 느낌을 맞췄다.
호출 한도: 분당, 그리고 하루
- 분당 한도. 서버가 재시작한 직후 예약된 합성과 사용자의 요청이 겹치자 429 가 났다. 전역 동시 실행을 2개로 묶고, 429 면 8·16·32초 뒤에 다시 시도한다.
- 하루 한도. 어느 날 오후부터 음성이 전혀 나오지 않았다. 그날 시험을 많이 돌려 모델별 하루 요청 한도를 다 쓴 것이다. 문제는 그다음이었다. 429 응답이 "약 23시간 뒤에 다시 시도"라고 알려 줬는데, 내 재시도 코드가 그 시간을 그대로 기다렸다. 합성 슬롯 두 개가 모두 하루짜리 대기에 묶여서 새 요청은 영영 차례가 오지 않았다.
고친 방식: 서버가 알려 준 대기 시간이 2분을 넘으면 기다리지 않고 그 모델을 "소진"으로 표시한 뒤 한도가 따로인 예비 모델로 넘긴다. 예비도 막히면 즉시 실패를 돌려주고, 클라이언트는 브라우저 음성으로 읽는다. 소진 상태는 상태 확인 주소에 노출한다. 재시도는 "언제까지 기다릴 가치가 있는가"의 상한이 있어야 한다.
모바일 브라우저의 제약
- Range 요청. iOS Safari 는 오디오를 부분 요청(Range)으로 받는다. 서버가 206 응답을 지원하지 않으면 재생이 안 되거나 탐색이 안 된다.
- 자동 재생. 사용자의 터치로 시작된 재생이 끊기면 다음 재생을 코드로 시작할 수 없다. 퀴즈의 "생각할 시간"이나 다음 파트를 기다리는 동안에는 무음 오디오를 큐에 끼워 재생이 이어지게 했다.
- 재생 취소 처리. 재생을 새로 시작할 때마다 일련번호를 올리고, 진행 중이던 비동기 작업은 자기 번호가 최신이 아니면 물러나게 했다. 그런데 준비 상태를 확인하는 타이머가 번호를 올리기 전에 읽어 두는 바람에 첫 확인에서 스스로 꺼졌고, 파트 사이마다 2초씩 공백이 생겼다. 기준 번호는 재생을 시작한 뒤의 값이어야 한다.
- 차량 블루투스. 안드로이드 Chrome 에서 음성 질문을 받으려고 마이크를 여는 순간, 운영체제가 블루투스를 통화 모드로 바꿔 차량 화면에 "통화 중"이 떴다. 웹에서는 확실히 피할 방법을 찾지 못했다. 그래서 질문은 글 입력을 기본으로 하고, 음성 입력은 필요할 때만 켜게 바꿨다.
시험할 때 주의
로컬에서 서버를 띄우면 그날 치 음성이 없다는 이유로 합성을 처음부터 다시 한다. 하루 한도를 쓰는 작업이므로, 화면만 고칠 때는 음성 수집기를 꺼 두는 환경 변수를 만들어 두었다. 시험이 운영의 한도를 깎아 먹는다는 것은 2편의 데이터 소스에서도, 여기서도 똑같이 배웠다.
시리즈를 마치며
돌아보면 대부분의 시간은 기능이 아니라 "실패했을 때 어떻게 되는가"에 썼다. 수집이 실패하면 직전 값, LLM 이 실패하면 템플릿, 음성 합성이 막히면 브라우저 음성. 매일 아침 쓰는 물건은 가끔 덜 똑똑해도 되지만 멈추면 안 된다.
댓글 없음:
댓글 쓰기