개요
저지연 모듈형 음성 에이전트 파이프라인(VAD->STT->LLM->TTS)으로, OpenAI Realtime과 호환되는 WebSocket API를 제공합니다. 모든 구성 요소를 교체할 수 있으며 로컬 및 클라우드 모델을 지원하므로 대화형 음성 애플리케이션 구축에 적합합니다.
README 미리보기
\n \n \n\n# Speech To Speech: Build voice agents with open-source models\n\n[](https://pypi.org/project/speech-to-speech/)\n[](https://pypi.org/project/speech-to-speech/)\n[](./LICENSE)\n\n\n\nA low-latency, fully modular voice-agent pipeline: **VAD -> STT -> LLM -> TTS**, exposed through an **OpenAI Realtime-compatible WebSocket API**. Every component is swappable. The LLM slot speaks OpenAI-compatible protocols, so you can point it at a hosted provider, at [HF Inference Providers](https://huggingface.co/inference-providers), or at a vLLM or llama.cpp server on your own hardware for a fully local, fully open stack.\n\nThis pipeline runs in production as the conversation backend for thousands of [Reachy Mini](https://huggingface.co/blog/reachy-mini) robots.\n\n\n \n \n \n \n \n\n\n## Quickstart\n\n```bash\npip install speech-to-speech\nexport OPENAI_API_KEY=...\nspeech-to-speech\n```\n\nThis starts an OpenAI Realtime-compatible server at `ws://localhost:8765/v1/realtime` using Parakeet TDT for local STT, an OpenAI-compatible LLM, and Qwen3-TTS for local speech output.\n\nFrom a source checkout, talk to it from a second terminal:\n\n```bash\npython scripts/listen_and_play_realtime.py --host 127.0.0.1 --port 8765\n```\n\nPrefer to keep the LLM on your own machine? Serve Gemma 4 with llama.cpp:\n\n```bash\nllama-server -hf ggml-org/gemma-4-E4B-it-GGUF -np 2 -c 65536 -fa on --swa-full\n```\n\nThen point the OpenAI-compatible LLM backend at it:\n\n```bash\nspeech-to-speech \\n --model_name "ggml-org/gemma-4-E4B-it-GGUF" \\n --responses_api_base_url "http://127.0.0.1:8080/v1" \\n --responses_api_api_key ""\n```\n\nAny OpenAI Realtime-compatible client can connect. See [Realtime API](#realtime-api) for the protocol and [LLM backends](#llm-backends) for provider and local-server options.\n\n## Index\n\n* [How it works](#how-it-works)\n* [Installation](#installation)\n* [Supported components](#supported-components)\n* [Run modes](#run-modes)\n* [R
FAQ (5)
문제 해결speech-to-speech에서 ChatTTS 사용 시 "TypeError: object of type 'numpy.int16' has no len()" 오류를 해결하는 방법
이 오류는 스트리밍 코드가 모든 ChatTTS 오디오 청크의 shape이 (1, samples)라고 가정하고 [0]을 적용하기 때문에 TTS/chatTTS_handler.py에서 발생합니다. ChatTTS가 (samples,) shape의 평탄한 배열을 반환하면 [0]이 단일 numpy.int16 샘플을 선택하므로 len(audio_chunk)가 실패합니다.
끝에 붙은 [0]을 제거하여 스트리밍 브랜치를 수정하세요:
# 수정 전
audio_chunk = (audio_chunk * 32768).astype(np.int16)[0]
# 수정 후
audio_chunk = (audio_chunk * 32768).astype(np.int16)
(samples,)와 (1, samples) 두 청크 shape을 모두 지원하려면 다음을 사용하세요:
audio_chunk = (audio_chunk * 32768).astype(np.int16).squeeze()
그런 다음 파이프라인을 다시 실행하세요. 그러면 해당 while len(audio_chunk) > self.chunk_size: 루프가 단일 정수 샘플 대신 배열을 받게 됩니다.
문제 해결Open WebUI 실시간 음성 통화에서 로컬 speech-to-speech 서버를 사용할 때 발생하는 'Voice provider rejected a request' 오류를 어떻게 해결하나요?
Open WebUI 호환성 수정 사항이 포함된 최신 버전으로 speech-to-speech 서버를 업데이트하십시오. 이 문제는 Open WebUI가 chat_context_1과 같은 ID를 가진 채팅 기록 스냅샷을 전송하고, 새로 고침 시 이전 스냅샷을 삭제하며, create_response: false를 보낸 뒤 명시적인 response.create를 전송할 수 있기 때문에 발생했습니다. 이전 서버 버전은 이러한 이벤트를 거부하거나 무시하여 WebSocket 세션을 끊었습니다.
단계:
1. Open WebUI Realtime 채팅 기록 새로 고침 및 create_response=false에 대한 수정 사항이 포함된 최신 speech-to-speech 코드/릴리스를 가져옵니다.
2. 서버 컨테이너를 다시 빌드하고 재시작합니다. 예:
git pull
docker compose up -d --build3. Open WebUI에서 realtime WebSocket 엔드포인트를
/v1/realtime으로 유지하고 구성된 모델(예: faster-qwen3-tts)을 설정합니다.4. 새 음성 통화를 시작합니다. 기존에 실패한 세션은 닫은 후 다시 생성해야 할 수 있습니다.
업데이트된 서버는 Open WebUI 스냅샷 ID를 수락하고, 스냅샷 삭제 이벤트를 인식하며, audio.input.turn_detection.create_response가 false일 때 자동으로 중복 응답이 생성되지 않도록 합니다. 즉시 업데이트할 수 없는 경우 프로젝트에 포함된 웹 데모를 임시 해결 방법으로 사용하십시오.
문제 해결Open WebUI Realtime에서 create_response: false가 무시될 때 중복 음성 응답이 발생하는 문제를 어떻게 해결하나요?
audio.input.turn_detection.create_response 플래그에 대한 수정 사항이 포함된 빌드로 업그레이드하세요. 결함은 RealtimeService._on_transcription_completed에 있습니다. 세션이 create_response: false로 구성된 경우에도 비어 있지 않은 전사(transcript)가 완료된 후 GenerateResponseRequest를 무조건 큐에 추가합니다. 이후 Open WebUI가 명시적인 response.create를 전송하여 동일한 턴이 두 번 음성으로 출력됩니다.
소스에서 빌드하는 경우, 유효한 create_response 설정이 활성화된 경우에만 자동 생성이 큐에 추가되도록 전사 완료 경로와 direct-audio 경로를 패치하세요:
{
"type": "session.update",
"session": {
"type": "realtime",
"audio": {
"input": {
"turn_detection": {
"type": "server_vad",
"interrupt_response": true,
"create_response": false
}
}
}
}
}
수정 사항이 적용되면 create_response: false는 여전히 전사, 기록, VAD 이벤트를 유지해야 하지만 자동 응답을 큐에 추가해서는 안 됩니다. 서버는 클라이언트의 명시적인 response.create를 기다려야 합니다. TranscriptionCompletedEvent를 디스패치한 후 프롬프트 큐가 비어 있는 상태로 남는지 확인하는 집중 재현 테스트로 검증하세요:
assert queue.empty()
업그레이드할 수 없는 경우의 임시 해결 방법: 응답 메커니즘을 하나만 사용하세요. 자동 생성을 활성화된 상태로 두고 Open WebUI의 명시적 response.create를 제거하거나, create_response: false를 유지하면서 서버가 응답을 자동으로 큐에 추가하지 않도록 하세요. 업그레이드하거나 패치를 적용하는 것이 권장되는 해결 방법입니다.
문제 해결다른 언어로 답변한 후 Kokoro TTS가 세션에서 선택한 음성을 CLI 음성으로 되돌리는 문제를 어떻게 해결하나요?
"Preserve Kokoro session voices across language switches" PR(issue #659 수정)이 포함된 빌드로 업데이트하거나, 아래 소스 변경 사항을 적용하세요.
덮어쓰기는 하나의 process 호출에서 발생합니다. 클라이언트/응답 음성이 self.voice에 할당되지만, 언어가 설정 언어로 돌아가면 _lang_and_voice_for가 이를 CLI에서 지정된 self._initial_voice로 대체합니다.
필요한 변경 사항:
1. 변경 가능한 self.voice에만 의존하는 대신, 세션/응답에서 선택된 음성을 Kokoro 처리로 전달하세요:
yield from self._process_kokoro(text, language_code, pinned_voice=voice)
2. CLI 또는 언어별 기본값을 적용하기 전에 _lang_and_voice_for가 해당 음성을 반환하도록 하세요:
def _lang_and_voice_for(..., pinned_voice=None):
if pinned_voice is not None:
return language_code, pinned_voice
# existing language/default-voice resolution단순히
self.voice를 반환해서는 안 됩니다. 스페인어 응답 후에는 이 값이 정상적으로 스페인어 기본 음성인 ef_dora일 수 있으며, 이 경우 영어로도 잘못 이어지게 됩니다.
3. 기존 CLI 폴백을 유지하세요. 응답 또는 세션 음성이 제공되지 않는 경우 self._initial_voice를 계속 사용합니다. on_session_end에서 _initial_voice로 재설정하는 것도 올바른 동작입니다.
4. MLX 경로에서는 결정된 음성이 변경될 때 파이프라인과 음성을 모두 다시 로드하세요. 예를 들어 self.model._get_pipeline(new_lang_code)를 호출한 다음 load_voice(new_voice)를 호출합니다.
확인 방법: --tts kokoro --kokoro_lang_code a --kokoro_voice af_heart로 실행하고, 클라이언트 세션 음성을 am_puck으로 설정한 후, 스페인어 응답을 하나 보낸 다음 영어를 보냅니다. 재생되는 음성은 ef_dora 다음 am_puck이어야 하며, ef_dora 다음 af_heart가 되어서는 안 됩니다.
패치 없이 사용할 수 있는 임시 해결 방법: --kokoro_voice를 클라이언트가 선택하는 음성과 동일하게 설정하거나, 설정 언어에서 다른 언어로 전환하지 않도록 하세요. 세션별로 서로 다른 음성은 설정만으로는 유지할 수 없습니다.
문제 해결Speech-to-Speech가 AI가 무음 상태인 동안 WebRTC 클라이언트당 CPU를 사용하는 이유는 무엇이며, 유휴 무음 오디오 패킷을 중지하려면 어떻게 해야 하나요?
백그라운드 CPU 사용량은 발신 PCM 버퍼가 비어 있을 때도 aiortc가 초당 약 50개의 무음 RTP 오디오 패킷을 인코딩하여 전송하기 때문에 발생합니다. PR #570이 포함된 Speech-to-Speech 릴리스로 업그레이드하거나, 아직 릴리스되지 않은 경우 해당 PR에서 빌드/실행하세요. 이 수정은 버퍼가 비어 있는 동안 아웃바운드 오디오 트랙의 recv()가 이벤트를 대기하도록 하고, 새로운 PCM 프레임이 도착하거나 트랙이 중지될 때 깨웁니다. 이를 통해 RTCRtpSender를 중지/재생성하거나 WebRTC 재협상 없이 지속적인 무음 프레임 인코딩을 방지할 수 있으며, 오디오가 재개될 때 RTP 타임스탬프는 연속성을 유지합니다. 임시 해결 방법으로, 발신 큐가 비어 있을 때 src/speech_to_speech/api/openai_realtime/webrtc_session.py에서 sender를 일시 중지/비활성화하고 다음 프레임을 전송하기 전에 다시 활성화하세요. replaceTrack(None)은 aiortc가 여전히 50 Hz로 트랙을 폴링하기 때문에 효과가 낮으며, sender를 중지/재생성하면 RTCP BYE가 전송되고 재시작 오버헤드가 추가될 수 있습니다. 잔여 CPU 사용량이 남아 있는 경우 _send_loop_for를 10 ms 큐 폴링에서 비동기 await queue.get() 스타일 대기로 변경하세요.