huggingface/speech-to-speech · 라이선스 Apache-2.0 · 언어 Python 3.10+ · 버전 0.2.11 · 소스 파일 91개 · TrendShift Daily 트렌딩 진입 · pip install speech-to-speech로 바로 설치)
사람과 AI가 말로 대화하려면 최소 네 가지 일이 순서대로 일어나야 한다. (1) 상대가 지금 말하고 있는지를 알아채고, (2) 그 말을 글자로 받아적고, (3) 그 글자에 대한 답을 생각해내고, (4) 그 답을 다시 소리로 바꿔 들려줘야 한다. speech-to-speech는 이 네 단계를 각각 하나의 핸들러(handler)로 만들고, 이들을 큐로 연결해 하나의 흐름으로 묶은 프로그램이다.
손님(사용자)이 주문을 말하면 → 웨이터(VAD)가 "주문이 끝났다"를 알아채고 → 주방 보조(STT)가 주문서를 종이에 받아적고 → 요리사(LLM)가 요리를 만들고 → 서빙(TTS)이 접시에 담아 내온다.
핵심은 요리사가 마음에 안 들면 다른 요리사로 바꿔도 라인 전체는 그대로 돈다는 것. 이게 이 프로젝트가 말하는 "모듈러(modular)"의 의미다.
결과물은 pip install speech-to-speech 한 줄로 설치되는 파이썬 패키지 + speech-to-speech CLI 명령어다. 실행하면 ws://localhost:8765/v1/realtime에 OpenAI Realtime 호환 서버가 뜨고, 어떤 OpenAI Realtime 클라이언트든 접속할 수 있다.
음성 AI 시장에서 지금 뜨거운 키워드는 Realtime API다. OpenAI가 실시간 음성 대화 표준을 만들었지만, 그건 클라우드 종속 + 종량제 과금 + 데이터 외부 전송이라는 대가를 요구한다. speech-to-speech는 정확히 그 지점을 노린다.
서버가 OpenAI Realtime 프로토콜을 그대로 구현했기 때문에, 기존에 OpenAI로 음성 앱을 짰다면 클라이언트의 base_url만 localhost:8765로 바꾸면 된다. 코드 재작성이 거의 없다. "OpenAI를 쓰다가 비용·프라이버시 때문에 셀프호스팅으로 넘어가고 싶다"는 수요를 정확히 겨냥한다.
STT(Parakeet)·TTS(Qwen3-TTS)는 로컬에서 돌고, LLM만 llama.cpp/vLLM으로 내 컴퓨터에 띄우면 인터넷 없이 100% 로컬로 음성 대화가 돌아간다. 의료·법률·기업 내부처럼 데이터가 밖으로 나가면 안 되는 곳에서 결정적 장점이다.
Reachy Mini 로봇 수천 대의 대화 백엔드로 실제 운영 중이라고 README가 명시한다. GitHub의 흔한 "데모 수준 토이 프로젝트"와 달리, 끊김·중간 끼어들기(interruption)·턴 관리 같은 실전 문제를 이미 다뤄본 코드라는 신뢰 신호다.
Transformers·HF Hub·HF Inference Providers와 자연스럽게 연결된다. 기본 STT인 Parakeet TDT는 25개 유럽 언어를, 기본 TTS인 Qwen3-TTS는 다국어를 지원한다. --language auto로 말할 때마다 언어를 자동 감지해 스위칭할 수도 있다.
부품을 이어붙이는 구조라 지연(latency)이 각 단계 합산으로 쌓인다. VAD가 "말 끝"을 감지하고 → STT가 받아적고 → LLM이 생각하고 → TTS가 소리를 만드는 시간이 순차적으로 더해진다. 그래서 이 레포는 스레드 병렬화, 스트리밍, 화자 끼어들기(speculative turns) 같은 기법으로 그 지연을 깎는 데 상당한 코드를 쏟는다. 통짜 엔드투엔드 음성 모델의 즉답성과는 근본 결이 다르다.
| 레이어 | 기술 | 역할 |
|---|---|---|
| 언어 | Python 3.10+ | 전체 파이프라인. 타입힌트·mypy·ruff 적용 |
| 웹서버 | FastAPI + uvicorn | /v1/realtime WebSocket 엔드포인트 서빙 |
| 실시간 통신 | websockets, WebRTC(옵션) | 오디오 스트림 양방향 전송 |
| 동시성 | threading + Queue | 각 부품을 독립 스레드로, 큐로 연결 |
| 설정 검증 | Pydantic v2 | RuntimeConfig — 세션 설정을 공유 모델로 |
| 수치·오디오 | numpy, scipy, soundfile, sounddevice | PCM 처리·리샘플링·마이크/스피커 I/O |
| ML 프레임워크 | PyTorch 2.4+, Transformers 4.57+ | 로컬 모델 추론(CUDA/CPU) |
| Apple 실리콘 | MLX, mlx-lm, mlx-audio | 맥에서 GPU(MPS) 가속 경로 |
| LLM 클라이언트 | openai 2.28 | OpenAI 호환 API 호출(Responses/Chat) |
pyproject.toml을 보면 torch==2.11.0; platform_system == 'Darwin'처럼 OS별로 다른 의존성이 걸려 있다. 맥(Darwin)에서는 MLX 기반, 리눅스에서는 CUDA 기반 패키지가 자동 선택된다. "한 코드베이스, 여러 하드웨어"를 pip 레벨에서 푸는 방식 — 배울 만한 실전 패턴이다.| 부품 | 기본값 | 대안들 |
|---|---|---|
| VAD 음성감지 | Silero VAD v5 | (단일) — 임계값·묵음 길이만 튜닝 |
| STT 받아쓰기 | Parakeet TDT 0.6B | Whisper, Faster-Whisper, Paraformer, Lightning Whisper MLX |
| LLM 답 생성 | gpt-5.4-mini (Responses API) | transformers·mlx-lm(로컬), vLLM·llama.cpp(자체서버), 모든 OpenAI 호환 제공자 |
| TTS 음성합성 | Qwen3-TTS 1.7B | Kokoro-82M, Pocket TTS(음성 복제), ChatTTS, MMS TTS |
이 표를 레고 블록 카탈로그라고 보면 된다. 4개의 슬롯(VAD·STT·LLM·TTS)이 있고, 각 슬롯에 끼울 블록이 여러 개 준비돼 있다. CLI 플래그(--stt, --tts, --llm_backend)가 곧 "어떤 블록을 끼울지" 고르는 스위치다. 조합만 바꾸면 "완전 로컬 프라이버시 세팅"도, "클라우드 최고성능 세팅"도 같은 코드로 만든다.
이 프로젝트의 심장은 baseHandler.py의 BaseHandler 클래스다. 모든 부품(VAD·STT·LLM·TTS)이 이걸 상속한다. 규칙은 단순하다 — 입력 큐에서 꺼내(queue_in) → process()로 처리 → 출력 큐에 넣는다(queue_out). 각 핸들러는 자기 스레드에서 이 루프를 무한 반복한다. 부품끼리는 서로를 직접 부르지 않고 오직 큐를 통해서만 데이터를 주고받는다.
--thresh(민감도), --min_silence_ms(몇 ms 조용하면 말이 끝난 것으로 볼지) 같은 값으로 턴 감지를 정교하게 튜닝한다.왜 굳이 스레드로 쪼갤까? 겹쳐서 일하기 위해서다. STT가 방금 문장을 넘기면, STT 스레드는 곧바로 다음 오디오 청크를 받기 시작한다. 동시에 LLM 스레드는 넘겨받은 문장으로 답을 만든다. 만약 한 스레드에서 순차로 돌렸다면 STT가 끝날 때까지 LLM은 놀아야 한다. 큐로 이어진 파이프라인은 공장 컨베이어벨트처럼 각 공정이 쉬지 않고 돌게 만들어 전체 지연을 줄인다.
PIPELINE_END라는 특수 신호를 큐에 흘려보내 "이제 끝, 다들 정리해"를 전파한다. SESSION_END는 스레드를 죽이지 않고 세션 상태만 리셋하는 부드러운 신호다(다음 대화를 위해 스레드는 살려둠).대화 도중 사용자가 "목소리를 바꿔줘"나 "다른 지시를 줄게"라고 하면? 클라이언트가 session.update 이벤트를 보내고, 이게 Pydantic으로 검증된 RuntimeConfig에 deep-merge된다. VAD(턴 감지 임계값)·LLM(지시문·툴)·TTS(목소리)는 매 처리 시점에 이 공유 설정을 읽으므로, 파이프라인을 재시작하지 않고도 실시간으로 동작이 바뀐다. 설정을 한 곳(단일 진실원)에 모으고 여러 스레드가 읽게 하는 전형적 패턴이다.
실제 대화의 묘미는 말 자르기다. AI가 답하는 도중 사용자가 끼어들면 AI는 즉시 입을 다물어야 한다. VAD가 speech_started를 감지하면 라우터로 신호가 가고, 진행 중이던 응답이 취소된다(response.cancel). 여기서 어려운 건 "진짜 끼어든 건지, 잠깐 숨 고른 건지" 구분이다.
speculative_turns.py의 SpeculativeTurnTracker는 "말이 끝난 것 같다"를 확정이 아니라 잠정(speculative)으로 처리한다. 살짝 멈췄다가 이어 말하면(reopen window 안이면) 턴을 다시 열어 하나로 잇는다. CancelScope는 각 처리 항목에 "세대(generation)" 번호를 붙여, 취소된 옛 세대의 결과물(stale output)이 뒤늦게 큐에 흘러들어도 버리도록 한다. 실시간 음성 UX의 자연스러움이 여기서 갈린다.| 모드 | 전송 방식 | 언제 쓰나 |
|---|---|---|
| realtime (기본) | WebSocket, OpenAI Realtime 프로토콜 | 표준 음성 API로 앱·기기를 만들 때 |
| local | 내 PC의 마이크·스피커 | 클라이언트 없이 바로 대화해보고 싶을 때 |
| websocket | 원시 PCM over WebSocket | 프로토콜 없는 최소 커스텀 클라이언트 |
| socket | 원시 PCM over TCP | 모델은 원격 서버, 마이크·재생만 로컬 |
폴더 이름만 봐도 구조가 읽힌다 — VAD/ STT/ LLM/ TTS/는 곧 4단 사슬 그 자체다. 새 STT 모델을 추가하고 싶으면 STT/ 폴더에 base_stt_handler.py를 상속한 파일 하나를 더하고 arguments_classes/에 인자 dataclass를 추가하면 끝. "어디를 고쳐야 하는지"가 폴더 구조에 그대로 드러나는 게 잘 설계된 코드베이스의 특징이다.
asyncio만 배운 사람에게 이 레포는 스레드 + 큐라는 다른 동시성 모델의 교과서다. 각 스레드가 블로킹 I/O(모델 추론)를 맡고, 큐가 스레드 간 안전한 데이터 전달과 백프레셔(backpressure)를 담당한다. 데드락을 피하는 종료 신호(PIPELINE_END) 설계까지 실전 그대로다.
BaseHandler를 상속해 "입력 텍스트를 대문자로 바꾸는" 더미 핸들러를 만들어 파이프라인에 끼워보기. 큐 in/out만 이해하면 어떤 처리든 삽입 가능함을 체감할 수 있다.이 레포의 상업적 가치의 핵심은 "OpenAI Realtime을 그대로 흉내낸다"는 것. 남의 API 스펙(이벤트 종류·JSON 형태)을 읽고 자기 시스템에 매핑하는 어댑터(adapter) 패턴을 실물로 배울 수 있다. websocket_router.py가 내부 메시지를 response.output_audio.delta 같은 프로토콜 이벤트로 번역하는 부분이 백미다.
min_speech_continuation_ms) 튜닝.LMOutputProcessor가 문장 단위로 잘라 TTS에 넘긴다.pyproject.toml의 플랫폼 마커로 "맥=MLX, 리눅스=CUDA"를 pip 레벨에서 분기하는 법, extras([kokoro], [pocket])로 선택 의존성을 관리하는 법을 배운다. numpy<2(DeepFilterNet)와 numpy>=2(Pocket TTS)의 충돌을 문서로 경고하는 대목은, 파이썬 의존성 지옥의 현실을 보여주는 살아있는 사례다.
| 세팅 | 필요 하드웨어 | 비고 |
|---|---|---|
| 클라우드 LLM | 일반 노트북 (CPU) | STT·TTS만 로컬, LLM은 OpenAI/HF API. 진입장벽 최저 |
| Apple 실리콘 로컬 | M1~M4 Mac (MPS) | --local_mac_optimal_settings. MLX로 STT·LLM·TTS 전부 로컬 |
| Linux GPU 로컬 | NVIDIA GPU + CUDA 12/13 | Qwen3-TTS GGML 백엔드는 CUDA 런타임 필요(휠 매칭 주의) |
| CPU-only | 서버/PC (GPU 없음) | 가능하나 지연 큼. +cpu 휠 설치 |
| Docker | NVIDIA Container Toolkit | docker compose up → llama.cpp(Gemma 4) + 소켓서버, 포트 8080/12345/12346 |
qwentts-cpp-python)는 PyPI 기본 휠이 CUDA 12.8을 기대한다. 내 GPU의 CUDA 버전이 다르면 허깅페이스 휠하우스에서 맞는 버전(+cu130/+cu124/+cpu)을 먼저 깔아야 한다. 로컬 GPU 세팅에서 가장 흔한 설치 함정이다.최소 요건은 Python 3.10+ 하나뿐이다. "일단 돌려보기"는 클라우드 LLM 세팅으로 노트북에서도 되고, "완전 로컬 프라이버시"로 갈수록 GPU/통합메모리 요구가 커지는 구조다.
pip install speech-to-speech → export OPENAI_API_KEY=... → speech-to-speech. 서버가 뜨면 다른 터미널에서 python scripts/listen_and_play_realtime.py --host 127.0.0.1 --port 8765로 말을 걸어본다. STT·TTS는 로컬, 답만 클라우드로 오는 구조를 체감.
speech-to-speech --local_mac_optimal_settings 한 줄이면 STT·LLM·TTS 전부 로컬(MLX)로 돌아간다. 인터넷을 꺼도 대화가 되는지 확인해보고, "완전 로컬 음성 AI"가 실제로 가능함을 눈으로 본다.
--tts kokoro vs --tts pocket, --stt whisper vs --stt parakeet-tdt로 조합을 바꿔가며 음질·지연을 비교한다. python scripts/benchmark_tts.py로 Qwen3-TTS 양자화(bf16/4bit/6bit/8bit) 속도를 측정해 표로 정리해보기.
터미널 1에서 llama-server -hf ggml-org/gemma-4-E4B-it-GGUF ...로 LLM을 띄우고, 터미널 2에서 speech-to-speech의 --responses_api_base_url을 그 서버로 가리킨다. 클라우드 API 없이 100% 로컬로 돌리는 걸 성공시키기. Docker(docker compose up)로도 재현.
BaseHandler를 상속해 새 부품을 만든다. 예: STT와 LLM 사이에 "욕설 필터" 핸들러를 끼워 전사문을 검열하기. 혹은 TTS/에 새 음성 모델 핸들러를 추가하고 arguments_classes/에 CLI 인자를 등록해 --tts mytts로 부를 수 있게 만들기. 큐 in/out 규약만 지키면 파이프라인이 그대로 받아준다.
| 주차 | 주제 | 할 것 |
|---|---|---|
| 1주차 | 파이프라인 감 잡기 | 실습 1·2 완료. 4단 사슬(VAD→STT→LLM→TTS)을 그림으로 직접 그려보기 |
| 2주차 | VAD·오디오 기초 | 16kHz PCM·int16이 뭔지, Silero VAD가 확률로 말/침묵을 나누는 원리. vad_iterator.py 정독 |
| 3주차 | STT 심화 | Whisper vs Parakeet 아키텍처 비교. 스트리밍 전사(smart_progressive_streaming.py)가 부분 결과를 어떻게 내는지 |
| 4주차 | TTS 심화 | Qwen3-TTS·Kokoro·Pocket 비교, 음성 복제(voice cloning) 원리. 양자화(6bit)가 속도·품질에 주는 영향 벤치마크 |
| 5주차 | LLM 백엔드 | Responses API vs Chat Completions 차이, reasoning 끄기(reasoning_effort none)로 음성 지연 줄이기. vLLM·llama.cpp 서버 직접 띄우기 |
| 6주차 | 파이썬 동시성 | threading + Queue 패턴 정복. 데드락·백프레셔·종료 신호 설계. baseHandler.py 필사 |
| 7주차 | 실시간 프로토콜 | OpenAI Realtime 이벤트 스펙 학습, WebSocket vs WebRTC. websocket_router.py 이벤트 번역 읽기 |
| 8주차 | 실전 UX | 끼어들기·speculative turns·cancel scope로 자연스러운 대화 만들기. 실습 5로 나만의 핸들러 완성 |
이 로드맵은 바깥에서 안으로 파고드는 순서다. 먼저 "돌려보며 감을 잡고"(1주) → "각 부품을 하나씩 뜯고"(2~5주) → "부품을 잇는 뼈대(동시성·프로토콜)를 이해하고"(6~7주) → "실전 UX 디테일까지"(8주). 처음부터 speculative_turns.py 같은 어려운 코드를 보면 지치니, 반드시 실행 경험을 먼저 쌓고 들어갈 것.
BaseHandler를 상속한다.session.update로 실시간 변경./v1/responses(기본) vs /v1/chat/completions. 제공자의 툴콜 스트리밍 안정성·reasoning 제어에 따라 골라 쓴다.src/speech_to_speech/api/openai_realtime/README.md — 프로토콜·아키텍처 상세