연속된 음성처리는 파일 하나를 업로드해서 한 번에 텍스트로 바꾸는 작업과 다르다. 사용자가 말하는 동안 오디오는 계속 들어오고, 시스템은 아직 끝나지 않은 말을 적당한 단위로 잘라서, 늦지 않게 추론하고, 이전 결과와 이어 붙여야 한다.
단순히 생각하면 마이크 입력을 계속 STT 모델에 넣으면 될 것 같지만 실제 구현은 그렇게 되지 않는다. 모델은 일정한 길이의 오디오 조각을 입력으로 받는 경우가 많고, 사람의 말은 문장 단위로 깔끔하게 끊기지 않으며, 중간에 침묵과 잡음과 말 끊김이 계속 섞인다. 연속 음성처리의 핵심은 이 흐름을 무너지지 않게 관리하는 데 있다.
이 글에서는 연속된 음성을 순차적으로 처리하는 기본 구조를 정리한다. 첫 번째 글에서 다룬 노이즈 전처리가 "들어온 소리를 알아듣기 좋게 만드는 작업"이라면, 이번 글의 주제는 "계속 들어오는 소리를 어떤 순서로 잘라서 모델에 넘길 것인가"이다.

연속 음성처리는 오디오 입력, 전처리, 특징 추출, 인식 모델, 텍스트 출력이 순서대로 이어지는 파이프라인이다. 출처: NVIDIA Technical Blog
한 줄로 보는 연속 음성처리
연속 음성처리는 보통 다음 흐름으로 구성된다.
- 마이크나 음성 스트림에서 짧은 오디오 프레임을 계속 받는다.
- 프레임을 버퍼에 쌓는다.
- VAD로 말소리와 침묵을 구분한다.
- 말이 시작되면 세그먼트를 열고, 말이 끝났다고 판단되면 세그먼트를 닫는다.
- 닫힌 세그먼트 또는 진행 중인 세그먼트를 STT 모델에 보낸다.
- 모델 결과를 임시 결과와 확정 결과로 나누어 관리한다.
- 확정된 텍스트를 앞뒤 문맥과 합쳐 최종 문장으로 만든다.
여기서 중요한 단어는 "프레임", "버퍼", "세그먼트", "임시 결과", "확정 결과"이다. 이 다섯 개만 이해하면 연속 음성처리의 큰 구조가 보인다.
파일 STT와 스트리밍 STT는 문제가 다르다
파일 STT에서는 녹음이 이미 끝나 있다. 전체 오디오 길이를 알고 있고, 앞뒤 문맥도 한 번에 볼 수 있다. 그래서 모델은 전체 파일을 기준으로 더 안정적인 결과를 만들 수 있다.
스트리밍 STT에서는 입력이 아직 끝나지 않았다. 사용자가 다음에 무슨 말을 할지 알 수 없고, 지금 들어온 소리가 문장의 중간인지 끝인지도 확실하지 않다. Google Cloud Speech-to-Text의 streaming recognition도 클라이언트가 오디오를 작은 청크로 계속 보내고 서버가 중간 결과와 최종 결과를 반환하는 구조로 설명된다.
이 차이 때문에 스트리밍 STT는 정확도만이 아니라 지연 시간도 함께 다룬다. 너무 오래 기다리면 정확도는 올라갈 수 있지만 사용자는 답답하다. 너무 빨리 판단하면 문장이 덜 들어온 상태에서 잘못된 결과를 확정할 수 있다.

파일 STT와 스트리밍 STT 모두 음성 입력을 텍스트로 바꾸지만, 스트리밍에서는 청크 단위의 중간 처리와 결과 갱신이 중요하다. 출처: MobiDev
프레임: 가장 작은 처리 단위
마이크에서 들어오는 오디오는 보통 아주 짧은 단위로 잘라서 처리한다. 이 작은 조각을 프레임이라고 부른다. 예를 들어 10ms, 20ms, 30ms 같은 단위로 오디오를 자르는 방식이 많이 쓰인다.
프레임은 사람이 읽기 좋은 단위가 아니다. "안녕하세요" 같은 말 한마디가 아니라, 그보다 훨씬 작은 소리 조각이다. 하지만 컴퓨터 입장에서는 이 작은 단위가 다루기 쉽다. 프레임 단위로 에너지를 보고, 음성 여부를 판단하고, 버퍼에 쌓을지 버릴지 결정할 수 있기 때문이다.
초보자가 가장 먼저 헷갈리는 지점은 여기다. STT 모델에 바로 "문장"이 들어가는 것이 아니라, 실제 시스템에서는 매우 작은 오디오 조각들이 먼저 들어오고, 그 조각들이 모여 말 구간이 된다.
버퍼: 잠깐 담아두는 메모리
연속된 음성은 한 번에 끝나지 않기 때문에 중간 저장 공간이 필요하다. 이 역할을 하는 것이 버퍼이다. 버퍼는 들어온 오디오 프레임을 잠깐 담아두는 공간이다.
버퍼가 필요한 이유는 세 가지다.
- 말이 시작되기 직전의 짧은 소리를 보존해야 한다.
- 말이 끝났는지 판단하려면 몇 프레임 정도 더 기다려야 한다.
- STT 모델에 넘길 때 너무 짧거나 너무 긴 입력을 조정해야 한다.
예를 들어 사용자가 "오늘 날씨"라고 말하기 시작했는데, VAD가 첫 음절을 조금 늦게 감지하면 "오늘"의 앞부분이 잘릴 수 있다. 그래서 실제 구현에서는 말이 시작되기 전의 짧은 프레임을 pre-roll 형태로 같이 붙여 저장하는 경우가 많다.
반대로 말이 끝났다고 바로 끊어도 문제가 생긴다. 사람은 문장 중간에 잠깐 멈춘다. 이 짧은 침묵을 문장의 끝으로 오해하면 "오늘"과 "날씨 어때"가 서로 다른 요청처럼 쪼개질 수 있다. 그래서 end padding이나 hangover 구간을 두고 조금 더 기다린 뒤 세그먼트를 닫는다.
버퍼는 아주 짧은 오디오 프레임을 잠깐 보관해 말의 앞부분 손실과 끝부분 오판을 줄인다.
VAD: 말소리인지 침묵인지 판단하는 문지기
VAD는 Voice Activity Detection의 약자이다. 오디오 프레임이 사람의 말에 가까운지, 침묵이나 배경음에 가까운지를 판단한다. 연속 음성처리에서 VAD는 거의 문지기 역할을 한다.
VAD가 없으면 시스템은 모든 소리를 STT 모델에 보내게 된다. 키보드 소리, 에어컨 소리, 침묵, 주변 소음까지 계속 모델에 들어간다. 이 방식은 비용이 커지고, 지연 시간이 늘고, 이상한 텍스트가 생성될 가능성도 커진다.
대표적인 예로 WebRTC VAD는 실시간 통신 환경에서 쓰이는 전통적인 VAD 구현이고, Silero VAD는 신경망 기반으로 널리 쓰이는 오픈소스 VAD이다. 구현 방식은 다르지만, 연속 음성처리 파이프라인에서 "어디부터 어디까지가 말인가"를 잡아준다는 목적은 같다.
VAD가 하는 판단은 최종 정답이 아니다. VAD는 "이 구간이 말일 가능성이 높다"는 신호를 줄 뿐이다. 그래서 VAD 결과만 믿고 너무 공격적으로 자르면 문장이 망가질 수 있다. 실제 시스템에서는 VAD 결과에 버퍼, 최소 발화 길이, 최대 발화 길이, 침묵 지속 시간 같은 규칙을 함께 붙인다.

VAD는 프레임이 말소리에 가까운지 판단해 세그먼트 시작과 종료의 기준 신호를 만든다. 출처: Introduction to Speech Processing
세그먼트: STT 모델에 넘길 말 구간
세그먼트는 STT 모델에 넘길 하나의 말 구간이다. 연속 음성처리에서 가장 중요한 설계는 세그먼트를 어떻게 열고 닫을지 정하는 것이다.
기본적인 방식은 단순하다.
- VAD가 말소리를 감지하면 세그먼트를 연다.
- 이후 들어오는 프레임을 세그먼트에 계속 붙인다.
- 침묵이 일정 시간 이상 이어지면 세그먼트를 닫는다.
- 닫힌 세그먼트를 STT 모델에 보낸다.
하지만 실제 음성은 이보다 복잡하다. 사용자는 생각하면서 말하고, 중간에 숨을 쉬고, 말을 고치고, 단어를 반복한다. 그래서 세그먼트 정책은 너무 짧아도 안 되고 너무 길어도 안 된다.
세그먼트가 너무 짧으면 문맥이 부족해진다. "그거", "아까", "여기" 같은 말은 앞뒤 문맥 없이는 의미가 약하다. 세그먼트가 너무 길면 STT 결과가 늦게 나오고, 실시간 대화 시스템에서는 반응성이 떨어진다.
좋은 세그먼트 정책은 정확도와 지연 시간 사이의 타협이다. 실시간 자막처럼 빠른 반응이 중요한 시스템은 짧은 세그먼트를 선호하고, 회의록처럼 정확한 문장이 중요한 시스템은 더 긴 구간과 후처리를 활용한다.
임시 결과와 확정 결과를 분리한다
스트리밍 STT에서 결과는 보통 임시 결과와 확정 결과로 나뉜다. 임시 결과는 아직 바뀔 수 있는 텍스트이고, 확정 결과는 더 이상 크게 바뀌지 않는 텍스트이다.
예를 들어 사용자가 "오늘 날씨가"까지 말했을 때 모델은 "오늘 날씨가"라는 임시 결과를 낼 수 있다. 사용자가 이어서 "어때"라고 말하면 최종 결과는 "오늘 날씨가 어때"가 된다. 중간에는 띄어쓰기나 단어가 바뀔 수도 있다.
이 구조를 모르면 스트리밍 STT 결과가 흔들리는 것처럼 보인다. 하지만 이는 오류라기보다 정상적인 동작에 가깝다. 아직 문장이 끝나지 않았기 때문에 모델이 현재까지의 최선 추정을 보여주는 것이다.
실시간 UI에서는 임시 결과를 옅은 색으로 표시하고, 확정 결과를 일반 텍스트로 표시하는 방식이 자주 쓰인다. 서버 내부에서는 임시 결과를 바로 DB에 저장하지 않고, 확정 결과만 대화 히스토리에 반영하는 방식이 안전하다.
스트리밍 STT에서는 partial 결과가 계속 바뀌고, 일정 시점 이후 final 결과로 확정된다.
스트리밍 STT는 아직 바뀔 수 있는 임시 결과와 문장 경계가 잡힌 확정 결과를 분리해서 다룬다.
Overlap: 경계에서 단어가 잘리는 문제
연속 음성을 일정한 길이의 청크로 자르면 경계 문제가 생긴다. 단어 하나가 두 청크 사이에 걸릴 수 있기 때문이다. 예를 들어 "트랜스포머"라는 단어가 앞 청크 끝과 뒤 청크 시작에 나뉘어 들어가면 모델이 제대로 인식하지 못할 수 있다.
이를 줄이기 위해 overlap을 사용한다. overlap은 다음 청크를 만들 때 이전 청크의 끝부분을 조금 다시 포함하는 방식이다. 예를 들어 5초 단위로 처리하되 앞뒤 0.5초를 겹치게 만들 수 있다.
overlap은 경계 손실을 줄이는 데 도움이 되지만 중복 텍스트를 만들 수 있다. 그래서 후처리 단계에서 이전 결과와 새 결과의 겹치는 부분을 비교해 중복을 제거해야 한다.
이 방식은 특히 긴 음성 파일을 작은 조각으로 나눠 처리할 때 중요하다. 스트리밍에서도 모델이 긴 컨텍스트를 안정적으로 처리하지 못하거나, 일정 길이마다 추론을 새로 해야 하는 구조라면 overlap과 deduplication이 필요하다.
청크 사이에 overlap을 두면 경계에 걸린 단어가 다음 청크에서 다시 인식될 수 있다.
overlap은 앞 청크의 끝부분을 다음 청크에도 포함해 경계에서 단어가 잘리는 문제를 줄인다.
모델은 어떤 방식으로 순서를 이해할까
STT 모델의 내부 구조는 여러 방식으로 발전해 왔다. 오래된 시스템은 음향 모델, 발음 사전, 언어 모델을 나눠서 구성하는 경우가 많았다. 이후 딥러닝 기반 end-to-end 음성인식이 널리 쓰이면서 오디오 특징에서 텍스트를 직접 예측하는 방식이 강해졌다.
연속 음성처리에서 자주 등장하는 개념 중 하나가 CTC이다. Connectionist Temporal Classification은 입력 프레임과 출력 글자 사이의 정렬이 명확하지 않을 때 학습할 수 있도록 만든 방법이다. 음성에서는 어떤 프레임이 정확히 어떤 글자에 대응되는지 사람이 일일이 표시하기 어렵기 때문에 이런 방식이 중요하다.
Whisper 같은 모델은 대규모 약지도 데이터로 학습된 encoder-decoder Transformer 구조를 사용한다. Whisper 논문은 다양한 음성 처리 작업을 하나의 sequence-to-sequence 형식으로 다루는 접근을 설명한다. 이런 모델을 연속 처리에 사용할 때도 입력 오디오를 적절한 길이로 자르고, 이전 문맥을 어떻게 이어줄지 설계해야 한다.
즉 모델이 강력해져도 파이프라인이 사라지는 것은 아니다. 연속 음성처리는 모델 바깥에서 오디오 흐름을 관리하는 문제와 모델 안에서 음성을 텍스트로 바꾸는 문제가 함께 존재한다.
순차 처리 파이프라인을 코드처럼 보면
연속 음성처리의 흐름은 다음과 같은 상태 기계로 볼 수 있다.
- IDLE: 아직 말이 시작되지 않은 상태
- SPEECH: 말이 들어오고 있는 상태
- MAYBE_END: 침묵이 감지됐지만 진짜 끝인지 기다리는 상태
- FINALIZE: 세그먼트를 닫고 STT에 넘기는 상태
이 상태 기계가 필요한 이유는 순간적인 판단을 피하기 위해서다. 한 프레임에서 침묵이 나왔다고 바로 끝내지 않고, 몇 프레임 동안 침묵이 이어지는지 본다. 한 프레임에서 음성이 나왔다고 바로 세그먼트를 시작하지 않고, 최소한의 연속성을 확인한다.
간단한 의사코드는 다음과 같다.
state = "IDLE"
segment = []
pre_roll = RingBuffer(max_ms=300)
for frame in audio_stream:
is_speech = vad(frame)
pre_roll.push(frame)
if state == "IDLE":
if is_speech:
segment = pre_roll.dump() + [frame]
state = "SPEECH"
elif state == "SPEECH":
segment.append(frame)
if not is_speech:
silence_ms = frame.duration_ms
state = "MAYBE_END"
elif state == "MAYBE_END":
segment.append(frame)
if is_speech:
state = "SPEECH"
else:
silence_ms += frame.duration_ms
if silence_ms >= 700:
send_to_stt(segment)
segment = []
state = "IDLE"
이 코드는 실제 서비스 코드라기보다 구조를 보여주는 예시이다. 핵심은 프레임을 하나씩 처리하면서 상태를 바꾸고, 세그먼트를 열고 닫는다는 점이다.

연속 STT의 상태 관리는 VAD 판단, 필터링, 세그먼트 시작과 종료 판단을 순서대로 연결하는 방식으로 구성된다. 출처: VOCAL Technologies
실시간 대화 시스템에서는 큐가 필요하다
STT가 챗봇이나 음성 에이전트와 연결되면 문제가 하나 더 생긴다. 사용자는 계속 말하고, STT는 텍스트를 만들고, LLM은 답변을 생성하고, TTS는 다시 음성을 만든다. 이 모든 작업이 같은 속도로 끝나지 않는다.
그래서 각 단계 사이에는 큐가 필요하다. 오디오 입력 큐, STT 작업 큐, 텍스트 이벤트 큐, 응답 생성 큐처럼 단계를 나누면 한 단계가 잠깐 느려져도 전체 시스템이 바로 무너지지 않는다.
큐가 없으면 STT 모델이 바쁜 순간에 오디오 프레임이 유실되거나, LLM 응답 생성 중에 새 음성이 들어왔을 때 상태가 꼬일 수 있다. 큐는 순서를 보존하고, 처리량 차이를 흡수하고, 재시도나 취소를 제어하는 역할을 한다.
실시간 시스템에서 중요한 것은 모든 단계를 빠르게 만드는 것만이 아니다. 어느 단계가 느려졌을 때 어디까지 버틸 수 있는지, 어떤 데이터를 버릴 수 있는지, 어떤 결과는 반드시 보존해야 하는지를 정하는 것이 더 중요하다.
끼어들기와 말 겹침 처리
음성 에이전트에서는 사용자가 시스템 답변 중간에 다시 말할 수 있다. 이를 barge-in이라고 부른다. 사용자가 끼어들면 TTS 출력을 멈추고, 새 사용자 발화를 우선 처리해야 한다.
이때 STT 파이프라인은 단순히 음성을 텍스트로 바꾸는 역할을 넘어 대화 상태를 제어하는 신호가 된다. VAD가 새 음성을 감지하면 현재 TTS를 중단하고, 진행 중인 LLM 생성을 취소하고, 새 세그먼트를 시작할 수 있다.
회의나 콜센터처럼 여러 사람이 말하는 환경에서는 화자 분리도 중요해진다. pyannote.audio는 speaker diarization을 포함한 음성 처리 파이프라인을 제공한다. 화자 분리는 "무엇을 말했는가"뿐 아니라 "누가 말했는가"를 기록해야 하는 시스템에서 필요하다.
다만 화자 분리까지 넣으면 지연 시간이 늘고 구조가 복잡해진다. 실시간 대화 에이전트에서는 먼저 VAD와 단일 화자 STT를 안정화하고, 이후 필요할 때 diarization을 붙이는 순서가 일반적으로 안전하다.
후처리: 텍스트를 문장으로 만든다
STT 모델이 반환한 텍스트는 바로 최종 문서가 되기 어렵다. 구두점이 없거나, 띄어쓰기가 어색하거나, 이전 세그먼트와 중복되는 부분이 생길 수 있다. 그래서 후처리가 필요하다.
후처리에서 자주 하는 일은 다음과 같다.
- 앞뒤 세그먼트의 중복 텍스트 제거
- 구두점과 문장 경계 보정
- 숫자, 날짜, 단위 표기 정규화
- 금칙어 또는 개인정보 마스킹
- 임시 결과와 확정 결과의 병합
- 대화 히스토리 저장 단위 결정
초보자가 놓치기 쉬운 부분은 STT 결과가 곧바로 "대화 메시지"가 아니라는 점이다. 실시간 STT 결과는 여러 번 수정될 수 있는 이벤트 흐름이다. 이 이벤트 흐름을 사람이 읽기 좋은 메시지로 바꾸는 단계가 따로 필요하다.
정리: 연속 음성처리는 잘 자르고 잘 기다리는 기술이다
연속된 음성처리는 모델 하나로 끝나는 문제가 아니다. 마이크에서 들어오는 프레임을 버퍼에 쌓고, VAD로 말 구간을 찾고, 적절한 세그먼트를 만들고, 임시 결과와 확정 결과를 나누고, overlap과 후처리로 경계 문제를 줄이는 전체 파이프라인이다.
핵심은 순서이다. 오디오는 시간 순서대로 들어오고, 시스템도 이 순서를 보존해야 한다. 동시에 사용자가 말을 멈췄는지, 잠깐 쉰 것인지, 다시 이어 말할 것인지를 적당히 기다려야 한다.
그래서 연속 음성처리는 "빨리 인식하는 기술"이라기보다 "충분히 기다리되 너무 늦지 않게 확정하는 기술"에 가깝다. 이 관점으로 보면 버퍼, VAD, 세그먼트, partial/final result, queue 같은 요소들이 왜 필요한지 자연스럽게 연결된다.