음성 비서 파이프라인 구축 — Phase 6 Capstone(Build a Voice Assistant Pipeline — The Phase 6 Capstone)

Lesson 01-11에서 다룬 모든 내용을 하나로 이어 붙입니다. 듣고, 추론하고, 다시 말하는 음성 비서(voice assistant)를 만듭니다. 2026년 기준으로 이것은 더 이상 연구 문제가 아니라 풀린 엔지니어링 문제이지만, 통합 단계의 세부 설계가 실제 서비스 배포 여부를 가릅니다.

유형: Build 언어: Python 선수 강의: Phase 6 · 04, 05, 06, 07, 11; Phase 11 · 09 (Function Calling); Phase 14 · 01 (Agent Loop) 예상 시간: 약 120분

학습 목표

  • 음성 비서 파이프라인을 구성하는 일곱 가지 구성요소(component)를 end-to-end로 설명할 수 있습니다.
  • 음성 활동 감지(VAD), 스트리밍 음성 인식(streaming STT), 도구 호출(tool calling)이 가능한 거대 언어 모델(LLM), 스트리밍 음성 합성(streaming TTS), 끼어들기 처리기(interruption handler)를 하나의 흐름으로 연결할 수 있습니다.
  • 지연 시간(latency) 목표와 품질(quality) 목표를 단계별로 나누어 설정할 수 있습니다.
  • 개인정보(PII) 로그 보존, 끼어들기(barge-in), 환각(hallucination), 호출어(wake word) 처리 등 운영 환경에서 발생하는 위험을 식별할 수 있습니다.

문제

다음과 같은 end-to-end 비서를 만듭니다.

  1. 마이크 입력(16 kHz, mono)을 캡처합니다.
  2. 사용자 발화의 시작과 끝을 감지합니다.
  3. 스트리밍 방식으로 텍스트로 변환(transcribe)합니다.
  4. 변환된 텍스트(transcript)를 타이머·날씨·캘린더 같은 도구(tool)를 호출할 수 있는 거대 언어 모델(LLM)에 전달합니다.
  5. LLM이 생성하는 텍스트를 음성 합성(TTS)으로 스트리밍합니다.
  6. 합성된 오디오를 사용자에게 재생합니다.
  7. 사용자가 응답 도중 끼어들면(interrupt) 즉시 멈춥니다.

지연 시간 목표는 노트북 CPU 환경에서 사용자의 발화가 끝난 시점부터 첫 TTS 오디오 바이트가 나오기까지 800 ms 이내입니다. 품질 목표는 단어 누락이 없고, 무음 구간에서 환각된 자막이 없으며, 음성 복제(voice cloning) 누출이 없고, 프롬프트 인젝션(prompt injection)이 성공하지 않는 것입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.음성 비서 파이프라인(voice assistant pipeline)은 일곱 가지 구성요소를 종단간(end-to-end)으로 연결합니다. 다음 중 핵심 흐름을 올바르게 식별한 것은 무엇인가요?

2.음성 비서가 VAD 트리거 전 200-400 ms의 오디오를 저장하는 프리롤(pre-roll) 버퍼가 필요한 이유는 무엇인가요?

0/2 답변 완료

개념

voice assistant pipeline — 7 components, 1 turn 1. mic 16 kHz mono 20 ms chunks 2. VAD Silero + pre-roll 3. STT Whisper / Parakeet streaming 4. LLM + tools GPT-4o / Claude stream + JSON tools 5. TTS Kokoro / Cartesia stream from token 20 6. speaker opus out PLC enabled 7. user ear barge-in: if user speaks, VAD cancels TTS + LLM, restart from (2) the three failure modes you will hit 1. first-word clip — VAD triggers late; user's "hey" is lost. Keep 200-400 ms pre-roll buffer. 2. barge-in confusion — LLM keeps generating after user interrupts. Wire VAD → cancel-LLM. 3. silence hallucinations — Whisper says "Thanks for watching" on silent warm-up. Always VAD-gate. in production, log every turn + P50/P95/P99 per-stage histograms before shipping. 2026 reference stacks (pick by target latency + license) LiveKit + Deepgram + GPT-4o + Cartesia 350-500 ms industry default Pipecat + Whisper-stream + GPT-4o + Kokoro 500-800 ms DIY open-source Moshi (full-duplex, single model) 200-300 ms different architecture — lesson 15 Vapi / Retell (managed) 300-500 ms fastest to ship whisper.cpp + llama.cpp + Kokoro-ONNX offline edge / privacy add wake-word gate (Porcupine) for always-on consumer devices; never log raw audio > 30 days.

일곱 가지 구성요소

  1. 오디오 캡처(Audio capture). 마이크에서 16 kHz mono로, 20 ms 단위 청크(chunk)로 받습니다. Python에서는 보통 sounddevice를 쓰고, 운영 환경에서는 플랫폼 네이티브 API인 AudioUnit/ALSA/WASAPI를 사용합니다.
  2. 음성 활동 감지(VAD; Lesson 11). Silero VAD를 임곗값(threshold) 0.5, 최소 발화 길이 250 ms, 무음 유지 시간(silence hang-over) 500 ms로 운용합니다. 발화의 시작과 끝 신호를 전달합니다.
  3. 스트리밍 음성 인식(Streaming STT; Lesson 4-5). Whisper-streaming, Parakeet-TDT, 또는 Deepgram Nova-3(API)을 사용합니다. 부분 변환 결과(partial)와 최종 변환 결과(final)를 함께 내보냅니다.
  4. 도구 호출이 가능한 LLM(LLM with tool calling). GPT-4o, Claude 3.5, Gemini 2.5 Flash 등이 후보입니다. 도구는 JSON 스키마로 정의하고, 토큰을 스트리밍 방식으로 받습니다.
  5. 스트리밍 음성 합성(Streaming TTS; Lesson 7). Kokoro-82M(공개 모델 중 가장 빠름)이나 Cartesia Sonic(상용)을 사용합니다. LLM 토큰이 20개 정도 누적되면 TTS를 시작합니다.
  6. 재생(Playback). 스피커로 출력합니다. 대역폭이 좁은 네트워크에서는 Opus 코덱으로 인코딩합니다.
  7. 끼어들기 처리기(Interruption handler). TTS 재생 도중 VAD가 발화 시작을 감지하면 재생을 멈추고, LLM 생성을 취소하고, STT를 다시 시작합니다.

반드시 만나게 되는 세 가지 실패 양상

  1. 첫 단어 잘림(First-word clip). VAD가 한 박자 늦게 시작해 사용자의 "hey"가 잘립니다. 시작 임곗값을 0.5가 아니라 0.3에서 출발하세요.
  2. 응답 중 끼어들기 혼란(Mid-response interrupt confusion). 사용자가 끼어든 뒤에도 LLM이 계속 토큰을 생성해서, 비서가 사용자 위에 겹쳐 말합니다. VAD 신호를 LLM 취소 경로에 직접 연결해야 합니다.
  3. 무음 환각(Silence hallucination). Whisper가 워밍업 단계의 무음 프레임에 대해 "Thanks for watching" 같은 자막을 환각하여 내놓습니다. 항상 VAD로 입력을 게이팅(gating)해야 합니다.

2026년 운영 환경 참조 스택(2026 production reference stacks)

스택(Stack)지연 시간(Latency)라이선스(License)비고(Notes)
LiveKit + Deepgram + GPT-4o + Cartesia350-500 ms상용 API2026년 산업 표준 기본 구성
Pipecat + Whisper-streaming + GPT-4o + Kokoro500-800 ms대부분 오픈소스직접 구축에 적합
Moshi(full-duplex)200-300 msCC-BY 4.0단일 모델, 다른 아키텍처(architecture). Lesson 15에서 다룸
Vapi / Retell(managed)300-500 ms상용가장 빠르게 출시 가능, 커스터마이징(customization) 제약
Whisper.cpp + llama.cpp + Kokoro-ONNXoffline오픈소스프라이버시(privacy) / 엣지(edge) 환경

직접 만들기

Step 1: 청크 단위 마이크 캡처(의사 코드)

import sounddevice as sd

def mic_stream(chunk_ms=20, sr=16000):
    q = queue.Queue()
    def cb(indata, frames, time, status):
        q.put(indata.copy().flatten())
    with sd.InputStream(channels=1, samplerate=sr, blocksize=int(sr * chunk_ms/1000), callback=cb):
        while True:
            yield q.get()

Step 2: VAD로 게이팅한 턴(turn) 캡처

def capture_turn(stream, vad, pre_roll_ms=300, silence_ms=500):
    buf, pre, triggered = [], collections.deque(maxlen=pre_roll_ms // 20), False
    silent = 0
    for chunk in stream:
        pre.append(chunk)
        if vad(chunk):
            if not triggered:
                buf = list(pre)
                triggered = True
            buf.append(chunk)
            silent = 0
        elif triggered:
            silent += 20
            buf.append(chunk)
            if silent >= silence_ms:
                return b"".join(buf)

프리롤(pre-roll)은 VAD가 트리거되기 직전 200-400 ms 분량의 오디오를 함께 보관해서 첫 단어 잘림을 방지합니다.

Step 3: 스트리밍 STT → LLM → TTS

async def turn(audio_bytes):
    transcript = await stt.transcribe(audio_bytes)
    async for token in llm.stream(transcript):
        async for audio in tts.stream(token):
            await speaker.play(audio)

Step 4: LLM 루프 안에서의 도구 호출

tools = [
    {"name": "get_weather", "parameters": {"location": "string"}},
    {"name": "set_timer", "parameters": {"seconds": "int"}},
]

async for chunk in llm.stream(user_text, tools=tools):
    if chunk.type == "tool_call":
        result = dispatch(chunk.name, chunk.args)
        continue_streaming(result)
    if chunk.type == "text":
        await tts.stream(chunk.text)

Step 5: 끼어들기 처리

tts_task = asyncio.create_task(tts_loop())
while True:
    chunk = await mic.get()
    if vad(chunk):
        tts_task.cancel()
        await speaker.stop()
        await new_turn()
        break

사용해보기

code/main.py는 실제 하드웨어 없이도 파이프라인의 전체 모양을 볼 수 있도록, 일곱 가지 구성요소를 stub 모델로 연결한 실행 가능한 시뮬레이션(runnable simulation)입니다. 실제 구현에서는 stub을 다음 구성요소로 교체합니다.

  • silero-vad (pip install silero-vad)
  • deepgram-sdk 또는 openai-whisper
  • openai(gpt-4o) 또는 anthropic
  • kokoro 또는 cartesia
  • 입출력용 sounddevice

흔한 함정

  • 개인정보(PII)를 영구 로깅(logging). 전체 턴의 오디오는 대부분의 관할권(jurisdiction)에서 개인정보입니다. 30일 보존 정책과 저장 시 암호화(encrypted at rest)를 적용하세요.
  • 끼어들기(barge-in) 미지원. 사용자는 반드시 중간에 끼어듭니다. 비서는 즉시 발화를 멈춰야 합니다.
  • 블로킹 TTS. 동기식(synchronous) TTS는 이벤트 루프(event loop)를 막습니다. 비동기(async) 또는 별도 스레드(thread)를 사용하세요.
  • 도구 호출 오류 처리 부재. 도구는 실패하기 마련입니다. LLM에 오류를 돌려주고 한 번 재시도(retry)한 뒤, 그래도 실패하면 부드럽게 기능을 축소(graceful degrade)해야 합니다.
  • 과도한 환각 필터. 너무 강하게 거르면 비서가 "I can't help with that."만 반복합니다. 너무 느슨하면 아무 말이나 합니다. 별도 평가 세트(held-out set)로 보정(calibrate)하세요.
  • 호출어(wake-word) 옵션 부재. 항상 듣기(always-listening)는 프라이버시 책임 문제입니다. Porcupine이나 openWakeWord 같은 호출어 게이트를 추가하세요.

산출물 만들기

outputs/skill-voice-assistant-architect.md로 저장합니다. 예산, 규모, 언어, 컴플라이언스(compliance) 제약을 받아서 풀스택(full-stack) 설계 스펙(spec)을 만들어 주는 스킬(skill)입니다.

연습문제

  1. 쉬움. code/main.py를 실행합니다. stub 모듈로 하나의 턴(turn)을 end-to-end로 시뮬레이션하고 단계별 지연 시간을 출력합니다.
  2. 중간. STT stub을 미리 녹음된 .wav 파일에 대해 동작하는 실제 Whisper 모델로 교체합니다. 단어 오류율(WER; Word Error Rate)과 end-to-end 지연 시간을 측정합니다.
  3. 어려움. 도구 호출을 추가합니다. get_weather(아무 API나)와 set_timer를 구현하고, LLM이 도구를 거치도록 라우팅(routing)한 다음, 사용자가 "set a 5 minute timer"라고 말했을 때 올바른 함수가 호출되고 합성된 응답이 이를 확인해주는지 검증합니다.

핵심 용어

용어흔한 설명실제 의미
턴(Turn)사용자 + 비서의 한 번의 왕복VAD로 경계가 잡힌 사용자 발화 하나 + 그에 대응되는 LLM-TTS 응답 하나입니다.
끼어들기(Barge-in)인터럽션(interruption)사용자가 비서가 말하는 도중 발화를 시작하고, 비서가 즉시 멈추는 상황입니다.
호출어(Wake word)"Hey assistant" 같은 짧은 단어짧은 키워드 검출기(detector)입니다. Porcupine, Snowboy, openWakeWord 등이 있습니다.
종료점 결정(End-pointing)턴이 끝나는 지점VAD와 최소 무음 시간으로 사용자가 발화를 끝냈는지 판단합니다.
프리롤(Pre-roll)발화 직전 버퍼(pre-speech buffer)VAD가 트리거되기 전 200-400 ms 분량 오디오를 보관해 첫 단어 잘림을 방지합니다.
도구 호출(Tool call)함수 호출(function invocation)LLM이 JSON을 내보내면 런타임이 해당 함수에 디스패치(dispatch)하고, 결과를 다시 루프 안으로 돌려보냅니다.

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

voice-assistant-architect

Produce a full-stack voice-assistant spec — components, latency budget, observability, compliance — for a given workload.

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.음성 비서가 동기식(synchronous) TTS를 사용하여 전체 LLM 응답이 완료될 때까지 기다린 후 음성 합성을 시작합니다. 사용자들은 시스템이 멈춘 것 같다고 보고합니다. 어떤 아키텍처 변경이 이 문제를 해결하나요?

2.테스트 중에, Whisper 기반 STT가 사용자 턴 사이의 3초 무음 동안 'Thanks for watching'이라는 전사를 생성합니다. 원인과 해결 방법은 무엇인가요?

3.음성 비서가 날씨 API 도구를 호출하는데, API가 5초 후 타임아웃됩니다. 비서가 이 시간 동안 침묵하다가 크래시(crash)합니다. 이것이 강의에서 언급된 어떤 두 가지 설계 원칙을 위반하나요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다