STTをリアルタイムに近づけるライブラリ「RealtimeSTT」を試す
以下で紹介したやつのSTT側のアプローチ
GitHubレポジトリ
RealtimeSTT
リアルタイムアプリケーション向けに設計された、使いやすく低遅延の音声認識ライブラリ
プロジェクト概要
RealtimeSTTはマイクを介して音声を聞き取り、リアルタイムでテキストに変換します。
ヒント: Linguflexもぜひご覧ください。このプロジェクトはRealtimeSTTの派生元で、音声を使って環境を制御できる、最も高度なオープンソースアシスタントの1つです。
以下に最適です:
- 音声アシスタント
- 高速かつ正確な音声認識を必要とするアプリケーション
特徴
- 音声活動検知(VAD: Voice Activity Detection:): 話し始めと終わりを自動検知。
- リアルタイム書き起こし: 音声をリアルタイムでテキストに変換。
- ウェイクワード機能: 指定したウェイクワードの検知で起動可能。
ヒント: RealtimeTTSも参照してください。本ライブラリの音声出力機能に対応しており、両者を組み合わせることで、大規模言語モデルを活用した強力なリアルタイムオーディオラッパーが構築できます。
技術スタック
このライブラリで使用されている技術:
- 音声活動検知:
- 音声認識:
- Faster_Whisper:GPU加速による即時認識。
- ウェイクワード検知:
- Porcupine または OpenWakeWord を使用。
これらのコンポーネントは、最新の高性能ソリューションを構築するための「業界標準」な最先端技術を提供します。
TTSに比べるとこちらのほうが色々考えないといけないことは多いかも。
インストール
ローカルのMac上で試す。Macの場合はCPUオンリーになると思うが、Linux+CUDA環境であればGPUも使える様子。とりあえず今回はMacで。
あらかじめportaudioのインストールが必要
brew install portaudio
Python仮想環境を作成して、RealtimeSTTをパッケージインストール。自分はmiseを使用しているが、適宜。
mkdir realtimestt-work && cd realtimestt-work
mise use python@3.12
cat << 'EOS' >> .mise.toml
[env]
_.python.venv = { path = ".venv", create = true }
EOS
mise trust
pip install RealtimeSTT
Quick Examplesのサンプルコードを試してみる。サンプルは2つ用意されていて、1つ目は発話内容を出力するだけ、2つ目はpyautoguiでテキストをタイプさせるというもの。1つ目のサンプルコードにpyautoguiがインポートされているが、不要。
from RealtimeSTT import AudioToTextRecorder
def process_text(text):
print(text)
if __name__ == '__main__':
print("'speak now'と表示されたら音声聞き取り・書き起こしをスタートします。")
recorder = AudioToTextRecorder()
while True:
recorder.text(process_text)
実行
python sample.py
実行すると初回はモデルのダウンロードが行われる。
'speak now'というと書き起こしをスタートします。
/Users/kun432/work/realtimestt-work/.venv/lib/python3.12/site-packages/torch/hub.py:330: UserWarning: You are about to download and run code from an untrusted repository. In a future release, this won't be allowed. To add the repository to your trusted list, change the command to {calling_fn}(..., trust_repo=False) and a command prompt will appear asking for an explicit confirmation of trust, or load(..., trust_repo=True), which will assume that the prompt is to be answered with 'yes'. You can also use load(..., trust_repo='check') which will only prompt for confirmation if the repo is not already trusted. This will eventually be the default behaviour
warnings.warn(
Downloading: "https://github.com/snakers4/silero-vad/zipball/master" to /Users/kun432/.cache/torch/hub/master.zip
tokenizer.json: 100%|█████████████████████████████████████████████████████████████████████████████████| 2.20M/2.20M [00:00<00:00, 4.86MB/s]
config.json: 100%|████████████████████████████████████████████████████████████████████████████████████| 2.25k/2.25k [00:00<00:00, 9.18MB/s]
vocabulary.txt: 100%|███████████████████████████████████████████████████████████████████████████████████| 460k/460k [00:00<00:00, 1.30MB/s]
model.bin: 100%|██████████████████████████████████████████████████████████████████████████████████████| 75.5M/75.5M [00:08<00:00, 8.96MB/s]
[2024-12-25 17:54:30.536] [ctranslate2] [thread 71591610] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
で、以下のように表示されれば発話待ちになるので、適当に発話してみる。
⠦ speak now
実際に発話してみると、上記の表示がrecording
→transcribing
と表示されて、その後発話内容がテキストで出力される。適当に日本語で発話してみた内容が以下。
おはようございます。
Let's do it.
Don't know, thank you, they shall accept you.
明日の電気を教えてください.
電気じゃなくて電気で.
Thank you.
電気じゃなくて、電気。
肯定咱們講.
ちゃんと認識してね.
日本語で発話しているのに、英語や中国語で認識している場合も見られる。言語の設定とかを行えるか?は後で確認する。
もう1つのサンプルはpyautoguiをよく知らないので割愛。
Quick Start
Quick Startで基本的な使い方を試していく。なお、READMEの上の方に
注意: 現在
multiprocessing
モジュールを使用しているため、特にWindowsプラットフォームでは、予期しない挙動を防ぐためにコード内でif __name__ == '__main__':
を含めるようにしてください。詳細な説明はPython公式ドキュメントのmultiprocessing
を参照してください。
とあるので注意。
録音の開始・終了(手動)
from RealtimeSTT import AudioToTextRecorder
if __name__ == '__main__':
print("'recording'と表示されたら発話してください。Enterを押すと文字起こしして録音を終了します。")
# AudioToTextRecorderでレコーダーを初期化
recorder = AudioToTextRecorder()
# `.start()`で録音開始
recorder.start()
input() # ENTER入力を受け取る
# `.stop()`で録音終了
recorder.stop()
# `.text()`で録音した内容のテキストを取得
print("Transcription: ", recorder.text())
# `.shutdown()`でレコーダーを終了
recorder.shutdown()
python sample.py
Press Enter to stop recording...
⠦ recording
Transcription: おはようございます。
RealtimeSTT shutting down
録音の開始・終了(VADによる自動)
with
で使うと、VADが有効になり、speak now
→recording
→transcribing
と処理が行われて、自動的に終了する。
from RealtimeSTT import AudioToTextRecorder
if __name__ == '__main__':
with AudioToTextRecorder() as recorder:
print(recorder.text())
おはようございます。
RealtimeSTT shutting down
ループにしたい場合は、Quick Exampleのように.text()
にコールバックを指定すると、文字起こし処理が非同期で行われる。
from RealtimeSTT import AudioToTextRecorder
def process_text(text):
print(text)
if __name__ == '__main__':
recorder = AudioToTextRecorder()
while True:
recorder.text(process_text)
ウェイクワード
ウェイクワード検出もできる。ウェイクワードエンジンはPorcupineとOpenWakeWordに対応していて、デフォルトだとPorcupineが選択される。
Macの場合、Porcupineだと以下となる。
RealTimeSTT: root - ERROR - Error initializing porcupine wake word detection engine: dlopen(/Users/kun432/work/realtimestt-work/.venv/lib/python3.12/site-packages/pvporcupine/lib/mac/x86_64/libpv_porcupine.dylib, 0x0006): tried: '/Users/kun432/work/realtimestt-work/.venv/lib/python3.12/site-packages/pvporcupine/lib/mac/x86_64/libpv_porcupine.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64e' or 'arm64')), '/System/Volumes/Preboot/Cryptexes/OS/Users/kun432/work/realtimestt-work/.venv/lib/python3.12/site-packages/pvporcupine/lib/mac/x86_64/libpv_porcupine.dylib' (no such file), '/Users/kun432/work/realtimestt-work/.venv/lib/python3.12/site-packages/pvporcupine/lib/mac/x86_64/libpv_porcupine.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64e' or 'arm64'))
Traceback (most recent call last):
Porcupine自体はMacOSにも対応していると思うのだけど、
v2.0.0 - Nov 25th, 2021
- Improved accuracy
- Added Rust SDK
- macOS arm64 support
- Added NodeJS support for Windows, NVIDIA Jetson Nano, and BeagleBone
- Added .NET support for NVIDIA Jetson Nano and BeagleBone
- Runtime optimization
RealtimeSTTが使用しているpvporcupineのバージョンを確認してみる
pip freeze | grep -i porcupine
pvporcupine==1.9.5
自分はM2 Proなので、おそらくこのバージョンではApple Siliconに対応してない可能性が考えられる。
ということでOpenWakeWordを使うことにする。以前試した記事は以下。ただちょっとこれもApple Silicon環境では色々手間取った記憶があるけどもとりあえず。
wakeword_backend
でバックエンドにOpenWakeWordを指定、そしてwake_words
は「Alexa」にした。
from RealtimeSTT import AudioToTextRecorder
if __name__ == '__main__':
with AudioToTextRecorder(
wakeword_backend="oww",
wake_words="alexa"
) as recorder:
print('録音を開始するには"アレクサ"といってから発話してください。')
print(recorder.text())
実行
python sample.py
初回実行時はモデルがダウンロードされる。いろいろなウェイクワードごとのモデルがダウンロードされているのがわかる。
embedding_model.tflite: 100%|████████████████████████████████████████████████████████████████████████| 1.33M/1.33M [00:00<00:00, 9.02MiB/s]
[2024-12-25 20:03:15.652] [ctranslate2] [thread 71681821] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 c
ompute type instead.
embedding_model.onnx: 100%|██████████████████████████████████████████████████████████████████████████| 1.33M/1.33M [00:00<00:00, 9.34MiB/s]
melspectrogram.tflite: 100%|█████████████████████████████████████████████████████████████████████████| 1.09M/1.09M [00:00<00:00, 9.28MiB/s]
melspectrogram.onnx: 100%|███████████████████████████████████████████████████████████████████████████| 1.09M/1.09M [00:00<00:00, 9.05MiB/s]
1 from RealtimeSTT import AudioToTextRecorder
silero_vad.onnx: 100%|███████████████████████████████████████████████████████████████████████████████| 1.81M/1.81M [00:00<00:00, 9.52MiB/s]
alexa_v0.1.tflite: 100%|███████████████████████████████████████████████████████████████████████████████| 855k/855k [00:00<00:00, 6.48MiB/s]
alexa_v0.1.onnx: 100%|█████████████████████████████████████████████████████████████████████████████████| 854k/854k [00:00<00:00, 8.02MiB/s]
hey_mycroft_v0.1.tflite: 100%|█████████████████████████████████████████████████████████████████████████| 860k/860k [00:00<00:00, 8.89MiB/s]
hey_mycroft_v0.1.onnx: 100%|███████████████████████████████████████████████████████████████████████████| 858k/858k [00:00<00:00, 8.55MiB/s]
hey_jarvis_v0.1.tflite: 100%|████████████████████████████████████████████████████████████████████████| 1.28M/1.28M [00:00<00:00, 9.26MiB/s]
hey_jarvis_v0.1.onnx: 100%|██████████████████████████████████████████████████████████████████████████| 1.27M/1.27M [00:00<00:00, 3.76MiB/s]
hey_rhasspy_v0.1.tflite: 100%|█████████████████████████████████████████████████████████████████████████| 416k/416k [00:00<00:00, 8.14MiB/s]
hey_rhasspy_v0.1.onnx: 100%|███████████████████████████████████████████████████████████████████████████| 204k/204k [00:00<00:00, 6.28MiB/s]
timer_v0.1.tflite: 100%|█████████████████████████████████████████████████████████████████████████████| 1.74M/1.74M [00:00<00:00, 9.69MiB/s]
timer_v0.1.onnx: 100%|███████████████████████████████████████████████████████████████████████████████| 1.74M/1.74M [00:00<00:00, 9.70MiB/s]
weather_v0.1.tflite: 100%|███████████████████████████████████████████████████████████████████████████| 1.15M/1.15M [00:00<00:00, 9.09MiB/s]
weather_v0.1.onnx: 100%|█████████████████████████████████████████████████████████████████████████████| 1.15M/1.15M [00:00<00:00, 7.97MiB/s]
モデルがロードされると以下となる。
録音を開始するには"アレクサ"といってから発話してください。
⠏ say alexa
「アレクサ」と発話すると以下のように表示が代わり録音が開始される。
⠇ speak now
あとは普通に発話すると、recording
→transcribing
と表示が代わって発話内容が出力される。
おはようございます。
RealtimeSTT shutting down
ちなみに、デフォルトだとウェイクワード後に5秒以内に発話がない場合は元に戻る。
コールバック
あらかじめいくつかのイベントが用意されているので、それぞれにコールバックを設定することができる。
イベント | 説明 |
---|---|
on_recording_start |
録音開始時 |
on_recording_stop |
録音終了時 |
on_transcription_start |
文字起こし開始時 |
on_recorded_chunk |
音声のチャンクが録音された時。チャンクデータが引数として渡される。 |
on_realtime_transcription_update |
リアルタイム文字起こしが更新された時。更新されたテキストが引数として渡される。 |
on_realtime_transcription_stabilized |
リアルタイム文字起こしが安定した(高品質のテキストに更新された)時。高品質のテキストが引数として渡される。 |
on_vad_detect_start |
音声活動検知開始時 |
on_vad_detect_stop |
音声活動検知終了時 |
on_wakeword_detected |
ウェイクワード検知時 |
on_wakeword_timeout |
ウェイクワード検知後、音声が検知されずタイムアウトになった時 |
on_wakeword_detection_start |
ウェイクワード検知が開始された時 |
on_wakeword_detection_end |
ウェイクワード検知が終了された時 |
ほぼ全部設定してみた。
from RealtimeSTT import AudioToTextRecorder
def recording_start_callback():
print("[イベント] 録音開始")
def recording_stop_callback():
print("[イベント] 録音終了")
def transcription_start_callback():
print("[イベント] 文字起こし開始")
def vad_detect_start_callback():
print("[イベント] VAD開始")
def vad_detect_stop_callback():
print("[イベント] VAD終了")
def wakeword_detection_start_callback():
print("[イベント] ウェイクワード検知開始")
def wakeword_detection_end_callback():
print("[イベント] ウェイクワード検知終了")
def wakeword_detected_callback():
print("[イベント] ウェイクワード検知")
def wakeword_timeout_callback():
print("[イベント] ウェイクワードタイムアウト")
if __name__ == '__main__':
with AudioToTextRecorder(
wakeword_backend="oww",
wake_words="alexa",
on_recording_start=recording_start_callback,
on_recording_stop=recording_stop_callback,
on_transcription_start=transcription_start_callback,
on_vad_detect_start=vad_detect_start_callback,
on_vad_detect_stop=vad_detect_stop_callback,
on_wakeword_detected=wakeword_detected_callback,
on_wakeword_timeout=wakeword_timeout_callback,
on_wakeword_detection_start=wakeword_detection_start_callback,
on_wakeword_detection_end=wakeword_detection_end_callback,
) as recorder:
print('録音を開始するには"アレクサ"といってから発話してください。')
print(recorder.text())
python sample.py
「アレクサ」→(タイムアウト)→「アレクサ」→「おはようございます」だとこんな感じになる。
録音を開始するには"アレクサ"といってから発話してください。
[イベント] VAD開始
⠙ speak now[イベント] VAD終了
[イベント] ウェイクワード検知開始
⠼ say alexa[イベント] ウェイクワード検知
[イベント] ウェイクワード検知終了
[イベント] VAD開始
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
⠴ speak now[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
⠦ speak now[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
⠦ speak now[イベント] ウェイクワードタイムアウト
[イベント] VAD終了
[イベント] ウェイクワード検知開始
⠹ say alexa[イベント] ウェイクワード検知
[イベント] ウェイクワード検知終了
[イベント] VAD開始
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
⠸ speak now[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
⠼ speak now[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
[イベント] ウェイクワード検知
⠹ speak now[イベント] VAD終了
[イベント] 録音開始
⠴ recording[イベント] 録音終了
[イベント] 文字起こし開始
我還有個在一起.
書き起こしには失敗してるけど、挙動が見えると思う。
チャンクのフィード
マイクを使わずにオーディオのチャンクを渡して処理することもできるみたい。ここはちょっと試してないので参考まで。
from RealtimeSTT import AudioToTextRecorder
if __name__ == '__main__':
recorder = AudioToTextRecorder(use_microphone=False)
with open("audio_chunk.pcm", "rb") as f:
audio_chunk = f.read()
recorder.feed_audio(audio_chunk)
print("Transcription: ", recorder.text())
realtime_transcription
を調べてみたけど、enable_realtime_transcription
を使えばいいみたい。コールバックも設定してやってみた。
from RealtimeSTT import AudioToTextRecorder
def realtime_transcription_update_callback(text):
print(f"\n[イベント] リアルタイム文字起こし更新: {text}")
def realtime_transcription_stabilized_callback(text):
print(f"\n[イベント] リアルタイム文字起こし安定: {text}")
if __name__ == '__main__':
with AudioToTextRecorder(
enable_realtime_transcription=True,
on_realtime_transcription_update=realtime_transcription_update_callback,
on_realtime_transcription_stabilized=realtime_transcription_stabilized_callback,
language="ja",
) as recorder:
while True:
print("\n[文字起こし] ", recorder.text())
「走れメロス」の冒頭を読み上げてみた。
[イベント] リアルタイム文字起こし安定: ウェイロースは
[イベント] リアルタイム文字起こし更新: ウェイロースは
⠙ recording
[イベント] リアルタイム文字起こし安定: メロスはゲッキロし
[イベント] リアルタイム文字起こし更新: メロスはゲッキロし
⠦ recording
[イベント] リアルタイム文字起こし安定: メロスは
[イベント] リアルタイム文字起こし更新: メロスは月色したカッ
⠹ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した
[イベント] リアルタイム文字起こし更新: メロスは月色した必ずか
⠇ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチブ
[イベント] リアルタイム文字起こし更新: メロスは月色した必ずかのジャチブ
⠼ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の
[イベント] リアルタイム文字起こし更新: メロスは月色した必ずかのジャチゴを逆の
⠙ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の王の
[イベント] リアルタイム文字起こし更新: メロスは月色した必ずかのジャチゴを逆の王の
⠏ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の
[イベント] リアルタイム文字起こし更新: ベロスは激労した必ずかのジャチボーザーザーを除かなければ
⠧ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の応募がなければならないときつい
[イベント] リアルタイム文字起こし更新: ベロスは激労した 必ずかのジャチゴを逆の応募がなければならないときつい
⠴ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の
[イベント] リアルタイム文字起こし更新: メロスは月色した必ずかのジャチゴー逆の王を乗るかなければならんと決意した
⠸ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の
[イベント] リアルタイム文字起こし更新: メロスは激労した必ずかのジャチゴー逆の王を乗るかなければならんと決意したメロスにはせい
⠙ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の
[イベント] リアルタイム文字起こし更新: メロスは激労した必ずかのジャチゴーギャックの王を乗るかなければならんと決意したメロスには政治化が分かる
⠙ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の
[イベント] リアルタイム文字起こし更新: メロスは激労した必ずかのジャチゴー逆の王を乗るかなければ何と決意したメロスには政治家がわからん メロス
⠋ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の
[イベント] リアルタイム文字起こし更新: メロスは激労した必ずかのジャチゴーギャックの王を乗るかなければならんと決意したメロスには政治家がわからんメロスは無らの
⠋ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の
[イベント] リアルタイム文字起こし更新: メロスは月々した必ずかのジャチゴーギャックの王を乗るかなければならんと決意したメロスには政治家がわからんメロスは無らの僕人
⠋ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の
[イベント] リアルタイム文字起こし更新: メロスは激労した必ずかのジャチゴーギャックの王を乗るかなければならんと決意したメロスには政治家がわからんメロスは無らの僕人である
⠙ recording
[イベント] リアルタイム文字起こし安定: メロスは月色した必ずかのジャチゴを逆の
[イベント] リアルタイム文字起こし更新: メロスは月々した必ずかのジャチゴーギャックの王を乗るかなければ何と決意したメロスには政治家がわからんメロスは村の僕人である増えを吹き必ず
[文字起こし] メロスは月々した必ずかのジャチボーギャックの王を乗るかなければ何と決意したメロスには政治家がわからんメロスは村の僕人である増えを吹き出した遊んで暮らしてきた.
品質についてはイマイチ感があるけども、リアルタイムだからなのかはわからない。ただ、モデルをより高精度なモデルに変更もできるはず(CPUで特に指定してないので多分小さめのモデルになってると思う)
以下のコードも参考になりそう。
あと、言語を指定する場合はAudioToTextRecorder
にlanguage
を渡すだけで良さそう。
with AudioToTextRecorder(language="ja") as recorder:
ということで、LLMと組み合わせ他サンプルを試してみる。
- LLM・TTSはOpenAIのものを使用
- RealtimeTTSで、TTSも有効化
- すこしOpenAI SDKの書き方が古いので修正
パッケージを追加インストール
pip install openai RealtimeTTS[all,ja]
OpenAI APIキーを環境変数にセット
import os
from openai import OpenAI
from RealtimeTTS import TextToAudioStream, OpenAIEngine
from RealtimeSTT import AudioToTextRecorder
if __name__ == '__main__':
# OpenAIクライアントの初期化
client = OpenAI()
# TTSのセットアップ
stream = TextToAudioStream(
OpenAIEngine(),
language="ja",
tokenizer="stanza",
log_characters=True
)
# STTのセットアップ
recorder = AudioToTextRecorder(
model="medium",
language="ja",
spinner=True,
)
system_prompt_message = {
'role': 'system',
'content': 'あなたは親切な日本語のアシスタントです。名前は「ジョン」です。ユーザと日本語で楽しく会話します。'
}
def generate_response(messages):
"""OpenAI Chat Completionで回答をストリーミングで取得"""
print("Assistant: ", end="")
for chunk in client.chat.completions.create(model="gpt-4o-mini", messages=messages, stream=True):
text_chunk = chunk.choices[0].delta.content
if text_chunk:
print(text_chunk)
yield text_chunk
history = []
def main():
"""Main loop for interaction."""
while True:
# ユーザの音声入力を取得
user_text = recorder.text().strip()
if not user_text:
continue
print(f'>>> {user_text}\n<<< ', end="", flush=True)
history.append({'role': 'user', 'content': user_text})
# LLMの回答を取得して再生
assistant_response = generate_response([system_prompt_message] + history[-10:])
stream.feed(assistant_response).play_async()
history.append({'role': 'assistant', 'content': stream.text()})
if __name__ == "__main__":
main()
一応実行結果。
>>> おはようございます。
⠹ speak nowお
おは
⠸ speak nowよう
ようございます
ございます!
!今日は
今日はど
どんな
んなこと
ことを
をする
する予定
⠼ speak nowです
⠦ speak nowか
か?
⠧ speak now
>>> あなたの名前を教えてください。
⠼ speak nowお
おは
はよう
ようございます
ございます!
!私
私の
の名前
名前は
はジョ
ジョン
ンです
です。
。あなた
あなたの
の名前
名前は
は何
何です
ですか
か?
?
>>> タロウです。
⠸ speak nowタ
タロ
ロウ
ウさん
さん、
、よろしく
よろしくお願いします
お願いします!
!今日は
今日は何
何を
を話
⠼ speak nowしま
⠦ speak nowしょう
⠇ speak nowか
⠏ speak now?
⠋ speak now
>>> 日本の総理大臣は誰?
⠼ speak now現在
現在の
の日本
日本の
の総
総理
⠴ speak now大
大臣
臣は
は岸
岸田
田文
文雄
雄(
(き
きし
しだ
だ
ふ
ふみ
みお
⠦ speak now)
⠧ speak nowさん
⠇ speak nowです
⠏ speak now。
。彼
彼は
は202
2021
1年
年に
に就
就任
任しました
しました。
。政治
政治や
や経
経済
済について
⠋ speak now何
⠙ speak nowか
⠹ speak now興
⠸ speak now味
⠼ speak nowが
がある
⠦ speak nowこと
⠏ speak nowは
⠋ speak nowあります
⠼ speak nowか
⠴ speak now?
⠦ speak now
やってみるとわかるのだけども、レスポンスが速くなったという感はそれほどしない。具体的には
- STT→LLMでストリーミング出力まではそこそこのレスポンスに収まっているように思う。
- 一番時間がかかってるのは、LLMのストリーミングをSTTするところ。
- RealtimeTTSではある程度バッファにためてから文節単位でTTSに渡している様子
- ここである程度の長さがないとTTSに渡されない、TTS自体に時間がかかっている、のだろうと思う
LLMの出力が多ければ多いほどストリーミング出力の分割はTTSで生きてくると思うので、効果が出るのかもしれないけど、シンプルな短い会話のやり取りではどうしようもなさそう。STTに渡す前の分割をもっと細かくすれば、というのはあるのかもしれないけど、そうなると十分な文章の長さにならないので発話やイントネーションに影響は出そう。
この手法だけでは難しいかなという感はある。
2024/12/26追記
すこしTTSのパラメータを見直してみたら多少改善した気がする。詳細は一番下。
まとめ
OpenAIのRealtime APIやGeminiのMultimodal Live APIなどに比べると、流石に分が悪いのだが、
- 既存の実装をあまり変えない
- 100%を目指さず、少なくとも今より高速化したい
とかならいいのかもしれない。あと、TTS・STTモデルの選択とか、パラメータなどは、今回深くはやっていないので、チューニング余地が多少はあるかもしれない。LLMもローカルモデルで試してみたいなというところもある。
ただ、仕組み上の限界はありそうな気がなんとなくしてて、やはりLLMの生成自体をある程度高速化しないと厳しいかなとも感じた。
高速化への道は遠い・・・
RealtimeTTSのパラメータ少し見直したら少しマシになった気がする。
上のSTT・LLM・TTSのチャットで試してみようと思う。