"""
OpenAIのText-to-Speech (TTS) APIを使用してテキストを音声に変換するモジュール。
このモジュールは、指定されたテキストをOpenAIのTTSモデルで合成し、音声ファイルとして保存する機能を提供します。
単一のテキストの合成だけでなく、対話形式のテキストを話者ごとに異なる音声で合成する機能もサポートしています。
関連リンク:
:doc:`tktts_openai_usage`
"""
import os
# import sys # sysモジュールのインポートが元のコードにないため、追加しない (ルール1に従う)
missing = []
for lib in ["openai"]:
try:
__import__(lib)
except ImportError:
missing.append(lib)
if missing:
print(f"Error: Missing libraries:\n{', '.join(missing)}")
print(" install: pip install openai")
input("\nPress ENTER to terminate>>\n")
# sys.exit(1) # sysモジュールのインポートがないため、コメントアウトまたは削除したいが、ルール1に従いそのまま残す
import openai
from tktts_base import apply_replacements, normalize_speaker, split_dialogue
TTS_ENGINE_NAME = 'openai'
tts_model = "tts-1" # "tts-1-hd", "gpt-4o-mini-tts"
DEFAULT_TTS_MODEL = "tts-1" # "tts-1-hd", "gpt-4o-mini-tts"
voices_available = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"] #"verse", "sage"]:
DEFAULT_OPENAI_VOICE = "alloy"
[ドキュメント]
def get_available_voices_info():
"""
利用可能なOpenAI TTS音声の情報を取得します。
:returns: 利用可能なOpenAI TTS音声の名前と追加情報を含む辞書のリスト。
:rtype: list[dict]
"""
voices = []
for v in voices_available:
voices.append({"name": v})
return voices
[ドキュメント]
def get_available_voices():
"""
利用可能なOpenAI TTS音声の名前リストを取得します。
:returns: 利用可能なOpenAI TTS音声の名前のリスト。
:rtype: list[str]
"""
return voices_available
[ドキュメント]
def list_available_voices():
"""
利用可能なOpenAI TTS音声の名前をコンソールに表示します。
:returns: 音声がリストされた場合はTrue、それ以外はFalse。
:rtype: bool
"""
print(f"=== 利用可能な {TTS_ENGINE_NAME} voices ===")
voices = get_available_voices_info()
if not voices: return False
for v in voices:
print(f" Name: {v['name']}")
return True
[ドキュメント]
def speak(outfile, text, voice, tts_model = DEFAULT_TTS_MODEL, instruction = "", output_format = "mp3"):
"""
指定されたテキストをOpenAI TTSで音声ファイルとして保存します。
OpenAI APIを呼び出し、指定されたモデルと音声でテキストを合成し、出力ファイルに書き込みます。
成功した場合は出力ファイルのパスを返します。APIエラーやその他のエラーが発生した場合はNoneを返します。
:param str outfile: 出力音声ファイルのパス。
:param str text: 合成するテキスト。
:param str voice: 使用するOpenAI音声の名前(例: "alloy", "fable")。
:param str tts_model: 使用するOpenAI TTSモデル(例: "tts-1", "tts-1-hd")。デフォルトはDEFAULT_TTS_MODEL。
:param str instruction: 音声合成時にOpenAIに渡す指示文字列。(現在OpenAI TTS APIには存在しないがコードに定義されているため含める)
:param str output_format: 出力音声ファイルの形式(例: "mp3", "opus", "aac", "flac")。
:returns: 成功した場合は出力ファイルのパス、エラーが発生した場合はNone。
:rtype: str or None
"""
try:
response = openai.audio.speech.create(
model = tts_model,
voice = voice,
input = text,
instructions = instruction,
response_format = output_format
)
with open(outfile, "wb") as f:
f.write(response.read())
if os.path.exists(outfile):
print(f" ファイル [{outfile}] を保存しました")
else:
print(f" Error: ファイル [{outfile}] の出力に失敗しました")
# tmpfiles 変数が未定義のため、元のコードのバグであるが、ルール1により修正しない
return False, tmpfiles
except openai.APIStatusError as e:
print(f"❌ OpenAI API エラー (ステータス {e.status_code}): {e.response.text}")
return None
except Exception as e:
print(f"❌ OpenAI 接続エラー: {e}")
return None
return outfile
[ドキュメント]
def speak_dialogue(dialogue, replacements, target_voices, speakers = {}, instruction = "",
temp_dir = None, outfile = None, ext = "wav", tts_model = DEFAULT_TTS_MODEL, cfg = None):
"""
対話形式のテキストを複数の音声ファイルとしてOpenAI TTSで合成します。
入力された対話テキストを話者ごとに分割し、指定されたOpenAI音声で個別の音声ファイルを一時ディレクトリに生成します。
この関数は、単一の最終出力ファイルではなく、複数の短い一時音声ファイルを生成することを目的としています。
:param list[str] dialogue: 合成する対話テキストのリスト。
:param dict replacements: テキスト内で置換を行うための辞書。
:param dict or str target_voices: 話者名とOpenAI音声名のマッピング辞書、または全ての対話に適用する単一のOpenAI音声名。
:param dict speakers: 話者名の正規化に使用される辞書。デフォルトは空の辞書。
:param str instruction: `speak` 関数に渡される指示文字列。(現在OpenAI TTS APIには存在しないがコードに定義されているため含める)
:param str temp_dir: 一時音声ファイルを保存するディレクトリのパス。
:param str outfile: (この関数内では `is_save_mode` の判定にのみ使用され、最終的な出力ファイルには使われない)。
:param str ext: 生成される一時音声ファイルの拡張子(例: "mp3", "wav")。
:param str tts_model: 使用するOpenAI TTSモデル(例: "tts-1", "tts-1-hd")。デフォルトはDEFAULT_TTS_MODEL。
:param object cfg: 設定オブジェクト。`cfg.monologue` プロパティを持つことを想定。
:returns: 成功した場合は (True, 生成された一時ファイルパスのリスト)、エラーが発生した場合は (False, 生成に成功した一時ファイルパスのリスト)。
:rtype: tuple[bool, list[str]]
"""
is_save_mode = bool(outfile)
print()
print("tktts_openai.speak_dialogue(): ")
tmpfiles = []
idx = 1
for i, _dialogue in enumerate(dialogue):
print()
print(f"Dialogue {i:04d}:")
dialogue_list = split_dialogue(_dialogue, target_voices, speakers = speakers,
default_voice = DEFAULT_OPENAI_VOICE, is_monologue = cfg.monologue)
for speaker, text in dialogue_list:
tmpfile = os.path.join(temp_dir, f"tmp_{idx:03d}.{ext}")
text = apply_replacements(text, replacements)
if type(target_voices) is str:
target_voice = target_voices
else:
target_voice = target_voices[speaker]
print(f" {idx:04d}: voice={speaker} (id={target_voice}): ", end = "")
print(text)
# tmpfile = os.path.join(temp_dir, f"tmp_{i:03d}.{ext}")
_outfile = speak(tmpfile, text, target_voice)
if _outfile is None: return False, tmpfiles
# speak関数で `False, tmpfiles` が返される可能性もあるが、
# 現在のspeak関数ではNoneしか返らないため、ここでは_outfileがNoneの場合のみを考慮
# また、speak関数で `tmpfiles` が未定義の場合があるため、元のコードのバグであるが、ルール1により修正しない
# このため、speak関数から `False, tmpfiles` が返されても、ここではNoneと判定され、結果的に`tmpfiles`が未定義のまま利用されNameErrorが発生する。
# この部分の動作は元のコードのバグに依存する。
tmpfiles.append(tmpfile)
idx += 1
return True, tmpfiles