엔지니어링 노트음성과 회의

회의를 끝내면 마지막 발언이 빠진다, 음성은 받아쓰기를 마친 뒤 지운다

회의를 끝내는 순간에도 아직 받아쓰지 못한 소리가 남아 있습니다. 라이브 회의는 60초 슬롯이 찰 때마다 창 하나를 받아쓰는데(「회의가 겹치면 받아쓰기가 멈춘다, 큐는 쓰는 자원마다 나눈다」), 마지막 슬롯은 주기를 채우지 못한 채 끝납니다. 기본 60초 주기에서는 평균 30초, 5분 주기를 쓰는 OwlAI SaaS에서는 평균 2분 30초입니다. 음성 인식 재시도를 기다리는 창이 섞여 있을 때도 있습니다. 회의가 끝나면 원본 음성을 지우므로, 이 구간을 받아쓰기 전에 지우면 회의의 마지막 발언은 되살릴 길이 없습니다. 회의록 모델이 몇 분 응답하지 않았을 뿐인데 작업 큐가 그것을 실패로 세어 회의를 접어도 음성이 함께 지워집니다.

이 글은 회의 종료 작업이 음성을 지우기 전에 남은 구간을 모두 받아쓰는 순서를 다룹니다. 그리고 기다리는 일과 실패한 일을 따로 세어, 일시적인 장애가 음성 삭제로 이어지지 않게 하는 방법을 설명합니다.

회의를 끝낸 시점에 받아쓰지 못한 구간은 네 가지다

회의 종료를 누르면 회의는 정리 중 상태가 되고, 10초 유예(finalizeGraceSeconds) 뒤에 종료 작업 하나가 남은 일을 맡습니다. 브라우저는 녹음을 5초마다 청크로 잘라 올리는데, 유예는 그 마지막 청크가 도착할 시간입니다. 서버는 청크를 받는 즉시 객체 저장소에 쓰므로, 탭이 닫히거나 웹 프로세스가 바뀌어도 종료 작업은 마지막으로 올라온 청크까지 읽습니다. 이 시점에 받아쓰기가 없는 구간은 네 가지입니다.

구간 남는 이유 종료 작업이 하는 일
마지막 구간 창은 주기를 채워야 열린다 주기를 채우지 못했어도 창으로 확정해 직접 받아쓴다
오디오가 덜 온 창 절전이나 네트워크 끊김으로 청크가 비었다 유예가 지났으니 더 올 청크가 없다고 보고, 온 만큼만 받아쓴다
작업 등록에 실패한 창 창을 예약한 뒤 큐에 넣는 순간 DB 커넥션이 끊겼다 기다려도 올 작업이 없으니 직접 받아쓴다
음성 인식 재시도 중인 창 오디오는 다 왔는데 모델 호출이 실패했다 재시도가 끝날 때까지 기다린다

앞의 셋은 기다려도 달라지는 것이 없고, 넷째만 기다리면 끝납니다. 넷째가 까다로운 이유는 큐의 순서에 있습니다. 창 받아쓰기 작업은 회의 단위 잠금 없이 큐에 들어가므로, 큐가 종료 작업을 받아쓰기 뒤에 세워 주지 않습니다. 음성 인식 재시도 간격은 15·30·45초라 모델이 한 번만 실패해도 종료 작업이 먼저 돕니다.

흔한 구성 세 가지는 모두 음성을 먼저 지운다

가장 단순한 종료 처리는 회의록을 만들고 음성을 지우는 두 단계입니다. 그러면 위 표의 네 구간이 모두 받아쓰기 없이 사라집니다.

마지막 구간을 다른 창처럼 받아쓰기 작업으로 큐에 넣는 구성도 자연스럽습니다. 받아쓰기 경로가 하나로 모이기 때문입니다. 그런데 종료 작업에는 큐에 넣은 다른 작업이 끝나기를 기다릴 방법이 없고, 기다리지 못하면 그 작업이 돌기 전에 음성을 지웁니다.

받아쓰기를 기다리는 데 작업 큐의 재시도를 쓰는 방법도 있습니다. 예외를 올리면 큐가 정해진 간격으로 다시 돌려 주니 구현이 짧습니다. 하지만 재시도 횟수는 「이 회의를 처리하다 실패했다」를 세는 값입니다. 기다림이 그 횟수를 쓰면 마지막 시도가 그만큼 앞당겨지고, 마지막 시도의 실패는 회의를 접고 음성을 지웁니다.

종료 작업은 받아쓰기를 끝낸 뒤에만 음성을 지운다

종료 작업은 다음 순서로만 움직입니다.

  1. 받아쓰기가 없는 창을 정리한다. 오디오가 덜 온 창과 작업 등록에 실패한 창은 이 자리에서 확정한다.
  2. 음성 인식 재시도 중인 창이 남았으면 종료 작업을 15초 뒤로 다시 걸고, 지금 작업은 정상 종료한다. 기다림은 최대 10분이다.
  3. 마지막 구간을 직접 받아쓴다.
  4. 회의록을 만든다.
  5. 음성을 지우고 회의를 완료로 바꾼다.
회의 종료 작업의 순서와 단계마다 갈라지는 대기·재시도 경로
그림 1. 회의 종료 작업의 순서와 단계마다 갈라지는 대기·재시도 경로

기다림의 상한은 대기를 시작한 시각부터 실제로 흐른 시간으로 잽니다. 다시 걸 때마다 15초씩 더하면 계산은 단순하지만, 회차마다의 실행 시간과 큐 대기가 빠져 10분 상한이 실제로는 몇 배로 늘어납니다. 상한이 있어야 하는 이유는 작업이 큐에서 사라진 창이 영원히 끝나지 않기 때문입니다. 상한에 닿으면 종료 작업은 그 창을 남긴 채 다음 단계로 가고, 그 사실을 로그에 남깁니다.

받아쓰기가 없는 창은 두 가지로 셉니다. 아직 시도 중인 창과, 재시도를 다 써서 더 시도하지 않는 창입니다. 기다릴지는 첫째만 보고 정합니다. 더 시도하지 않는 창까지 기다리면 회의가 끝나지 않습니다. 음성을 지울 때 남기는 경고는 둘을 합쳐 셉니다. 더 시도하지 않는 창에는 글자가 없으니, 음성까지 지우면 그 구간에는 아무것도 남지 않습니다. 한 수로 두 질문에 답하면 정작 음성을 지워 잃는 구간이 경고에서 빠집니다.

마지막 구간을 받아쓰다 실패하면 그 창만 받아쓰기 실패로 표시하고 다음 단계로 갑니다. 이 단계에 왔다면 회의의 나머지는 이미 받아써져 있습니다. 창 하나 때문에 회의를 정리 중에 묶어 두면 사용자는 회의록 자체를 받지 못합니다.

기다림과 일시적인 장애는 재시도 횟수를 쓰지 않는다

종료 작업의 재시도는 5회이고, 간격은 15·30·45초처럼 15초씩 늘어납니다. 이 횟수는 실패에만 씁니다. 상황마다 세는 방법은 다음과 같습니다.

상황 재시도 횟수 다시 거는 간격 상한 상한에 닿으면
음성 인식 재시도 중인 창이 남음 쓰지 않음 15초 10분 그 창을 남긴 채 진행하고 로그에 남김
회의록 모델에 닿지 못함 쓰지 않음 1분 1시간 회의를 실패로 접음
사용 한도·결제 한도 소진 쓰지 않음 다시 걸지 않음 — 회의록 없이 완료
워커가 죽어 작업이 멈춤 씀 1분·5분 주기로 회수 회수 3회 회의를 실패로 접음
그 밖의 오류 씀 15·30·45초… 재시도 5회 중 남은 횟수 회의를 실패로 접고 음성 삭제

회의록 모델에 닿지 못한 실패는 이 회의의 실패로 세지 않습니다. 연결 실패, 시간 초과, 요청 한도, 5xx, 과부하로 분류한 실패는 1분 간격으로 다시 걸며 최대 1시간을 기다립니다. 받아쓰기 대기보다 훨씬 긴 이유는 기다리는 대상이 밖에서 회복되기 때문입니다. 상한을 짧게 잡으면 곧 돌아올 모델 때문에 멀쩡한 회의를 실패로 접고 음성까지 지웁니다. 1시간이 지나도 돌아오지 않으면 자동 복구로 풀 일이 아니라 운영 장애로 보고 회의를 접습니다. 분류는 채팅의 실패 판정과 같은 기준을 씁니다. 분류하지 못한 실패는 이 대기에 넣지 않습니다. 넣으면 제품 결함이 외부 장애처럼 보이고, 회의는 결과가 같은 재시도를 1시간 동안 되풀이합니다.

사용 한도나 프로바이더 결제 한도가 바닥나면 한도가 풀릴 때까지 결과가 같습니다. 이 시점에는 마지막 구간까지 받아쓰기가 끝났으므로, 종료 작업은 회의를 실패로 접는 대신 회의록 없이 완료합니다. 회의 화면의 회의록 자리에는 회의록이 없는 이유가 보이고, 한도가 돌아오면 사용자가 다시 만들기로 회의록을 만듭니다. 이 판정은 외부 장애 판정보다 먼저 합니다. 한 프로바이더는 한도 소진을 429로 돌려주는데, 순서가 바뀌면 일시적인 요청 한도로 읽고 1시간을 헛되이 기다립니다.

워커가 작업 도중 죽으면 그 작업은 실행 중 상태로 멈춥니다. 배포로 컨테이너가 바뀔 때마다 생기는 일입니다. 워커 프로세스는 1분마다, 주기 청소 작업은 5분마다 멈춘 작업을 찾아 대기열로 되돌립니다. 새 작업을 하나 더 거는 대신 같은 작업을 되돌리는 이유는 멈춘 작업이 회의의 잠금을 쥐고 있어 새 작업이 시작되지 못하기 때문입니다. 되돌릴 때마다 재시도 횟수가 하나 올라가므로, 종료 작업의 재시도 5회는 오류 몫 2회에 회수 몫 3회를 더한 값입니다. 회수 몫을 더하지 않으면 배포로 한 번 회수된 작업이 다음 오류 한 번에 마지막 시도가 되어 음성을 지웁니다. 회수에도 상한이 있는 이유는 워커를 죽이는 작업이 있으면 되돌릴 때마다 워커를 다시 죽이기 때문입니다.

그 밖의 오류는 재시도 횟수가 남은 동안 예외를 그대로 올려 큐의 재시도를 탑니다. 그동안 음성은 남아 있어 다음 시도가 마지막 구간부터 다시 받아쓸 수 있습니다. 마지막 시도까지 실패하면 회의를 실패로 접고 화면에 「이 회의는 처리가 중단됐습니다」와 사유를 보이며, 이때도 음성은 지웁니다.

DB가 재시작되면 끊긴 커넥션을 버리고, 빠진 창은 종료 작업이 채운다

DB가 업그레이드나 장애 조치로 재시작되면 앱이 들고 있던 커넥션이 모두 끊깁니다. 워커가 쓰는 작업 큐 라이브러리는 커넥션을 꺼낼 때 살아 있는지 스스로 검사합니다. 계속 떠 있는 웹 프로세스는 작업을 큐에 넣을 때 따로 커넥션 풀을 쓰는데, 이 풀이 검사 없이 커넥션을 꺼내면 끊긴 커넥션이 줄에 남아 있다가 다음 작업 등록을 실패시킵니다. 회의 중이라면 그 시각에 찬 창의 받아쓰기 작업이 큐에 들어가지 못합니다.

그래서 작업 등록용 커넥션 풀은 커넥션을 꺼낼 때마다 빈 쿼리로 살아 있는지 확인하고, 끊긴 커넥션은 버린 뒤 다음 커넥션을 씁니다. 5초 안에 살아 있는 커넥션을 얻지 못하면 바로 실패를 돌려줍니다. 그래도 등록에 실패한 창은 앞에서 본 대로 종료 작업이 찾아내 직접 받아씁니다. 커넥션 검사는 빠지는 창을 줄이고, 종료 작업은 빠진 창을 음성이 남아 있는 동안 채웁니다.

설계에서 고른 것

실패한 회의의 음성을 남겨 두는 대신, 실패한 회의에서도 음성을 지웁니다. 남겨 두면 원인을 고친 뒤 다시 받아쓸 길이 열립니다. 하지만 그 예외를 두면 「회의가 끝나면 원본 음성은 보관하지 않는다」는 약속이 실패한 회의마다 깨집니다. 대신 지우기 전까지의 순서를 촘촘히 짜서, 회의록 모델이 잠깐 멈추거나 배포로 워커가 바뀌는 정도로는 실패에 닿지 않게 했습니다. 회의를 실패로 접거나 받아쓰기 없는 구간을 안은 채 음성을 지울 때는 로그와 회의 화면의 사유로 드러납니다.

음성 삭제 시점을 회의록 성공 대신 받아쓰기 완료에 묶었습니다. 받아쓴 글이 남아 있으면 음성 없이도 회의록을 다시 쓸 수 있습니다. 그래서 한도 소진이나 모델의 빈 응답으로 회의록이 없을 때도 회의는 받아쓰기를 온전히 가진 채 완료되고, 회의록은 나중에 다시 만들기로 만듭니다.

마지막 구간은 별도 작업으로 거는 대신 종료 작업 안에서 받아씁니다. 대가는 종료 작업이 라이브 받아쓰기와 GPU를 다툰다는 점이고, 종료 처리 큐의 동시성을 낮게 둔 이유가 그것입니다(「회의가 겹치면 받아쓰기가 멈춘다, 큐는 쓰는 자원마다 나눈다」).

받아쓰기 대기(기본 10분)와 회의록 모델 대기(기본 1시간)는 길이를 다르게 잡았습니다. 창 받아쓰기 작업 자체의 재시도는 약 5분 안에 끝나도록 짧게 둡니다. GPU 한 대를 향한 요청이라 실패가 몰리면 재시도가 곧 부하가 되기 때문입니다. 회의록 모델은 밖에서 회복되기를 기다리므로 길게 둡니다. 두 대기 시간은 설치 설정으로 바꿀 수 있습니다. 길게 잡을수록 회의가 정리 중에 머무는 시간이 늘고, 짧게 잡을수록 곧 돌아올 모델 때문에 회의를 실패로 접습니다. 회의록 모델을 자체 호스팅하고 그 서버를 자주 재시작하는 환경이라면, 모델 대기를 재시작에 걸리는 시간보다 넉넉히 잡아야 재시작이 음성 삭제로 이어지지 않습니다.

어떤 문제를 해결하나요?

음성과 회의의 다른 글