カテゴリー【Python、Windows、OpenAI/ChatGPT】
キーボードで直接入力したテキストを音声合成しOBS Studioで配信する
POSTED BY
2026-03-09
2026-03-09


マイクで喋った日本語をテキスト変換してOBS Studioにリアルタイムで表示する【1】
マイクで喋った日本語をテキスト変換してOBS Studioにリアルタイムで表示する【2】
マイクで喋った日本語をテキスト変換してOBS Studioにリアルタイムで表示する【3】
マイクで喋った日本語をテキスト変換+音声合成しOBS Studioで配信する
手に余裕があるなら無理にマイク→テキスト変換という精度に難ありなことしなくても、キーボードで直接打つほうがそりゃ正確です。
前回のコードを少し改変するだけでOKです。誤送信を防ぐために、リターンキー2連続押下で送信というふうにしています。
| Python | obs_live_by_keyboard_voicevox.py | GitHub Source |
import io
import queue
import threading
import time
import requests
import sounddevice as sd
import soundfile as sf
from openai import OpenAI
from obsws_python import ReqClient
# =========================
# OpenAI
# =========================
client = OpenAI()
TRANS_MODEL = "gpt-4o-mini"
TRANS_TEMPERATURE = 0.2
# =========================
# OBS websocket
# =========================
OBS_HOST = "127.0.0.1"
OBS_PORT = 4455
OBS_PASS = "yourpass"
TEXT_SOURCE_JP = "Caption"
TEXT_SOURCE_EN = "Caption_EN"
obs = ReqClient(host=OBS_HOST, port=OBS_PORT, password=OBS_PASS)
# =========================
# VOICEVOX
# =========================
VOICEVOX_BASE = "https://your.voicevox.api.server/voicevox_engine"
VOICEVOX_SPEAKER = 0
SPEAK_JP = True # True: 日本語読み上げ / False: 英訳を読み上げ
# =========================
# VB-CABLE playback
# =========================
VB_PLAY_DEVICE_NAME = "CABLE Input"
VB_PLAY_DEVICE_INDEX = None # 自動検出できない時だけ番号固定
# =========================
# 表示設定
# =========================
WRAP_JP_WIDTH = 0
WRAP_EN_WIDTH = 0
# =========================
# TTS制御
# =========================
MIN_TTS_INTERVAL_SEC = 0.2
# =========================
# 非同期ワーカー用
# =========================
work_q: queue.Queue[str] = queue.Queue(maxsize=50)
stop_event = threading.Event()
def wrap_fixed(s: str, width: int) -> str:
s = (s or "").strip()
if not s or width <= 0:
return s
return "\n".join(s[i:i+width] for i in range(0, len(s), width))
def translate_to_english_openai(jp_text: str) -> str:
jp_text = (jp_text or "").strip()
if not jp_text:
return ""
resp = client.chat.completions.create(
model=TRANS_MODEL,
temperature=TRANS_TEMPERATURE,
messages=[
{
"role": "system",
"content": (
"You are a live-stream subtitle translator.\n"
"Translate Japanese to natural, concise spoken English.\n"
"Rules:\n"
"- Keep it short and readable as subtitles.\n"
"- Do NOT add explanations.\n"
"- Do NOT add extra information.\n"
"- Keep names as-is when unsure.\n"
),
},
{"role": "user", "content": jp_text},
],
)
return (resp.choices[0].message.content or "").strip()
def obs_set_text(source_name: str, text: str):
obs.set_input_settings(
name=source_name,
settings={"text": text},
overlay=True
)
def voicevox_tts_wav_bytes(text: str, speaker: int) -> bytes:
r = requests.post(
f"{VOICEVOX_BASE}/audio_query",
params={"text": text, "speaker": speaker},
timeout=10,
)
r.raise_for_status()
query = r.json()
r = requests.post(
f"{VOICEVOX_BASE}/synthesis",
params={"speaker": speaker},
json=query,
timeout=30,
)
r.raise_for_status()
return r.content
def find_output_device_index_by_name(name_substr: str) -> int | None:
name_substr = (name_substr or "").lower()
try:
devices = sd.query_devices()
except Exception:
return None
for i, d in enumerate(devices):
if d.get("max_output_channels", 0) <= 0:
continue
nm = (d.get("name") or "").lower()
if name_substr in nm:
return i
return None
def play_wav_bytes_to_device(wav_bytes: bytes, device_index: int):
bio = io.BytesIO(wav_bytes)
data, fs = sf.read(bio, dtype="float32")
# 必要なら音量調整
gain = 0.5
data = data * gain
sd.play(data, fs, device=device_index)
sd.wait()
def worker_translate_tts_play(dev_index: int):
last_tts = 0.0
while not stop_event.is_set():
try:
jp = work_q.get(timeout=0.5)
except queue.Empty:
continue
try:
# 英訳
en = translate_to_english_openai(jp)
obs_set_text(TEXT_SOURCE_EN, wrap_fixed(en, WRAP_EN_WIDTH) if en else "")
# 読み上げ対象
speak_text = jp if SPEAK_JP else (en or jp)
# 連打抑制
now = time.time()
wait = MIN_TTS_INTERVAL_SEC - (now - last_tts)
if wait > 0:
time.sleep(wait)
# VOICEVOX → VB-CABLE
tts_wav = voicevox_tts_wav_bytes(speak_text, VOICEVOX_SPEAKER)
last_tts = time.time()
play_wav_bytes_to_device(tts_wav, dev_index)
except Exception as e:
print("[Worker] error:", e)
finally:
try:
work_q.task_done()
except Exception:
pass
def submit_text(jp: str):
jp = (jp or "").strip()
if not jp:
return
obs_set_text(TEXT_SOURCE_JP, wrap_fixed(jp, WRAP_JP_WIDTH))
print("JP:", jp)
try:
work_q.put_nowait(jp)
except queue.Full:
try:
_ = work_q.get_nowait()
work_q.task_done()
except Exception:
pass
try:
work_q.put_nowait(jp)
except Exception:
pass
def main():
# VB-CABLE 出力先決定
dev_index = VB_PLAY_DEVICE_INDEX
if dev_index is None:
dev_index = find_output_device_index_by_name(VB_PLAY_DEVICE_NAME)
if dev_index is None:
print("[VB-CABLE] 出力デバイスを自動検出できませんでした。")
print("sd.query_devices() で 'CABLE Input' の番号を確認し、VB_PLAY_DEVICE_INDEX に設定してください。")
print(sd.query_devices())
return
print(f"[VB-CABLE] playback device index = {dev_index} (name contains '{VB_PLAY_DEVICE_NAME}')")
# 非同期ワーカー起動
t = threading.Thread(target=worker_translate_tts_play, args=(dev_index,), daemon=True)
t.start()
print("Ready.")
print("日本語を入力してください。")
print("・空行 Enter 1回目: まだ送信しない")
print("・空行 Enter 2回連続: 送信確定")
print("・/send : その場で送信")
print("・/clear : 入力中テキスト破棄")
print("・/quit : 終了")
print("-" * 50)
lines: list[str] = []
blank_count = 0
while True:
try:
line = input()
except (EOFError, KeyboardInterrupt):
print("\nStopping...")
break
cmd = line.strip()
if cmd == "/quit":
print("Stopping...")
break
if cmd == "/clear":
lines.clear()
blank_count = 0
print("[cleared]")
continue
if cmd == "/send":
text = "\n".join(lines).strip()
if text:
submit_text(text)
else:
print("[empty: nothing sent]")
lines.clear()
blank_count = 0
continue
if cmd == "":
blank_count += 1
if blank_count >= 2:
text = "\n".join(lines).strip()
if text:
submit_text(text)
else:
print("[empty: nothing sent]")
lines.clear()
blank_count = 0
print("-" * 50)
else:
# 1回目の空行は「段落内改行」として保持したいなら追加
# ただし末尾空行が増えすぎるので、ここでは何もしない
print("[blank once: press Enter once more to send]")
continue
# 通常行
blank_count = 0
lines.append(line)
stop_event.set()
time.sleep(0.2)
print("Stopped.")
if __name__ == "__main__":
main()
(venv) python obs_live_by_keyboard_voicevox.py
OpenAI APIはデータ従量課金であるので、このように使用したらこまめに課金画面をチェックしておかないといけない。
https://platform.openai.com/settings/organization/billing/overview
Android
iPhone/iPad
Flutter
MacOS
Windows
Debian
Ubuntu
CentOS
FreeBSD
RaspberryPI
HTML/CSS
C/C++
PHP
Java
JavaScript
Node.js
Swift
Python
MatLab
Amazon/AWS
CORESERVER
Google
仮想通貨
LINE
OpenAI/ChatGPT
IBM Watson
Microsoft Azure
Xcode
VMware
MySQL
PostgreSQL
Redis
Groonga
Git/GitHub
Apache
nginx
Postfix
SendGrid
Hackintosh
Hardware
Fate/Grand Order
ウマ娘
将棋
ドラレコ
※本記事は当サイト管理人の個人的な備忘録です。本記事の参照又は付随ソースコード利用後にいかなる損害が発生しても当サイト及び管理人は一切責任を負いません。
※本記事内容の無断転載を禁じます。
※本記事内容の無断転載を禁じます。
【WEBMASTER/管理人】
自営業プログラマーです。お仕事ください!ご連絡は以下アドレスまでお願いします★
【キーワード検索】
【最近の記事】【全部の記事】
キーボードで直接入力したテキストを音声合成しOBS Studioで配信するマイクで喋った日本語をテキスト変換+音声合成しOBS Studioで配信する
マイクで喋った日本語をテキスト変換してOBS Studioにリアルタイムで表示する【3】
マイクで喋った日本語をテキスト変換してOBS Studioにリアルタイムで表示する【2】
マイクで喋った日本語をテキスト変換してOBS Studioにリアルタイムで表示する【1】
Raspberry PI 2 bookworm 32bitでCanon IP4300プリンタ印刷する
【VMware】Apple silicon M2 MacでWindows11を無償で動かす
A4用紙タテ2ページ分をA3用紙ヨコ1ページに印刷するには
【Android】apkのインストールができたのにアプリ一覧に出ない場合
【Node.js】chrono-nodeを使用して自然言語を日付に変換する
【人気の記事】【全部の記事】
進研ゼミチャレンジタッチをAndroid端末化する【Windows10】リモートデスクトップ間のコピー&ペーストができなくなった場合の対処法
Windows11+WSL2でUbuntuを使う【2】ブリッジ接続+固定IPの設定
CUDA13環境下でGPU使用版のPyTorchを導入する
Googleファミリーリンクで子供の端末の現在地がエラーで取得できない場合
Windows版Google Driveが使用中と言われアンインストールできない場合
DebianにウェブサーバーApache2をセットアップ
VirtualBoxの仮想マシンをWindows起動時に自動起動し終了時に自動サスペンドする
【Apache】サーバーに同時接続可能なクライアント数を調整する
Androidホームで左にスワイプすると出てくるニュース共を一切表示させない方法
【カテゴリーリンク】
Android
iPhone/iPad
Flutter
MacOS
Windows
Debian
Ubuntu
CentOS
FreeBSD
RaspberryPI
HTML/CSS
C/C++
PHP
Java
JavaScript
Node.js
Swift
Python
MatLab
Amazon/AWS
CORESERVER
Google
仮想通貨
LINE
OpenAI/ChatGPT
IBM Watson
Microsoft Azure
Xcode
VMware
MySQL
PostgreSQL
Redis
Groonga
Git/GitHub
Apache
nginx
Postfix
SendGrid
Hackintosh
Hardware
Fate/Grand Order
ウマ娘
将棋
ドラレコ