概要
低遅延でモジュラー式の音声エージェントパイプライン(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 オーディオチャンクの形状を (1, samples) と想定して [0] を適用しているため、TTS/chatTTS_handler.py で発生します。ChatTTS が形状 (samples,) の平坦な配列を返す場合、[0] は単一の numpy.int16 サンプルを選択するため、len(audio_chunk) が失敗します。
末尾の [0] を削除して、ストリーミングブランチを修正してください:
# Before
audio_chunk = (audio_chunk * 32768).astype(np.int16)[0]
# After
audio_chunk = (audio_chunk * 32768).astype(np.int16)
(samples,) と (1, samples) の両方のチャンク形状をサポートするには、次を使用してください:
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'が発生した場合、どのように修正すればよいですか?
speech-to-speech サーバーを、Open WebUI 互換性の修正を含む最新バージョンに更新してください。この問題は、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 では、リアルタイム WebSocket エンドポイントを
/v1/realtime に設定したままにし、設定済みのモデル(例: faster-qwen3-tts)をそのまま使用します。4. 新しい音声通話を開始します。失敗した既存のセッションは、閉じてから再作成する必要がある場合があります。
更新されたサーバーは、Open WebUI のスナップショット ID を受け入れ、スナップショット削除イベントを認識し、audio.input.turn_detection.create_response が false の場合に自動的な重複レスポンスが生成されるのを回避します。すぐに更新できない場合は、プロジェクトに同梱されている Web デモを一時的な回避策として使用してください。
トラブル対応Open WebUI Realtime で `create_response: false` が無視される場合、重複する音声応答を修正するにはどうすればよいですか?
audio.input.turn_detection.create_response フラグの修正を含むビルドにアップグレードしてください。この不具合は RealtimeService._on_transcription_completed に存在します。セッションが create_response: false で設定されている場合でも、空でない文字起こしの後に GenerateResponseRequest を無条件でキューに追加します。その後 Open WebUI が明示的な response.create を送信するため、同じターンが二度発話されることになります。
ソースからビルドする場合は、実効的な create_response 設定が有効になっている場合にのみ自動生成がキューに追加されるように、文字起こし完了パスとダイレクトオーディオパスにパッチを適用してください。
{
"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()
アップグレードできない場合の一時的な回避策:レスポンス機構は1つだけを使用してください。つまり、自動生成を有効にしたまま Open WebUI の明示的な response.create を削除するか、create_response: false を維持してサーバーがレスポンスを自動的にキューに追加しないようにしてください。アップグレードまたはパッチの適用が推奨される解決策です。
トラブル対応Kokoro itself has no concept of sessions or CLI flags — the model just takes a voice ID per synthesis call — so this behavior is coming from whatever application is wrapping it. The usual cause looks like this:
Kokoro voices are language-bound (voice IDs are prefixed by language: `af_*`/`am_*` for American English, `jf_*` for Japanese, etc.). When the model replies in another language, the app detects that language, loads the matching pipeline, and — because your session-selected voice doesn't exist for that language — falls back to a default, which is typically the voice from the CLI flag. If the app writes that fallback into its "current voice" state instead of a temporary variable, or simply never restores the session voice afterward, all subsequent replies keep using the CLI voice.
Things to try:
1. **Update the app.** Fallback/state bugs like this tend to get fixed quickly; check the changelog or issues for anything mentioning voice persistence or language switching.
2. **Set your preferred voice as the default** (config file, env var, or the CLI flag itself) so that even when the fallback fires, it lands on the voice you actually want.
3. **Look for a per-language voice mapping option.** If the app lets you define voices per language, fill in every language you use so the fallback never has to guess.
4. **Disable automatic language detection/voice switching** if you don't need multilingual output — that eliminates the code path entirely.
5. **If it's your own code:** keep the session voice in a separate variable from the "active" voice, resolve the voice per utterance through a language→voice map, and never write the fallback value back into session state.
If you can tell me which tool you're using (e.g., the `kokoro-tts` CLI, Kokoro-FastAPI, or a chat/assistant integration), how you set the session voice, and the version, I can point you at the specific setting or code path responsible.
「Preserve Kokoro session voices across language switches」というPR(issue #659の修正)を含むビルドに更新するか、以下のソース変更を適用してください。
上書きは1回の 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. _lang_and_voice_for がCLIや言語ごとのデフォルトを適用する前に、その音声を返すようにしてください:
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 に設定し、スペイン語で1回応答させてから英語で応答させます。発話される音声は ef_dora の次に am_puck となるはずで、ef_dora の次に af_heart ではありません。
パッチを適用しない場合の一時的な回避策:--kokoro_voice をクライアントが選択するのと同じ音声に設定するか、セットアップ時の言語から切り替えないようにしてください。セッションごとに異なる音声は、設定だけでは維持できません。
トラブル対応AIが無音の間、Speech-to-SpeechがWebRTCクライアントごとにCPUを使用するのはなぜですか?また、アイドル状態の無音オーディオパケットを停止するにはどうすればよいですか?
バックグラウンドのCPU使用率は、送信PCMバッファが空の場合でも、aiortcが1秒あたり約50個の無音RTPオーディオパケットをエンコードして送信し続けることが原因です。PR #570を含むSpeech-to-Speechリリースにアップグレードするか、まだリリースされていない場合はそのPRからビルドして実行してください。この修正により、送信オーディオトラックのrecv()はバッファが空の間イベントで待機し、新しいPCMフレームが到着するかトラックが停止すると起動します。これにより、RTCRtpSenderの停止/再作成やWebRTC再ネゴシエーションを行うことなく、継続的な無音フレームのエンコードを防止でき、音声が再開されたときもRTPタイムスタンプは連続性を維持します。一時的な回避策として、src/speech_to_speech/api/openai_realtime/webrtc_session.pyで送信キューが空のときにセンダーを一時停止/無効化し、次のフレームを送信する前に再度有効化してください。replaceTrack(None)は、aiortcが引き続き50 Hzでトラックをポーリングするため効果が低く、センダーの停止/再作成ではRTCP BYEが送信され、再起動のオーバーヘッドが追加される可能性があります。それでもなおCPU使用率が残る場合は、_send_loop_forを10 msのキューポーリングから非同期のawait queue.get()スタイルの待機に変更してください。