""" VoiceVox API連携 VoiceVoxエンジンとの通信を担当 """ import hashlib import json import os import urllib.error import urllib.request from typing import Optional class VoiceVoxAPI: """VoiceVox エンジンとの通信を管理""" def __init__(self, host: str = "127.0.0.1", port: int = 50021): self.base_url = f"http://{host}:{port}" self.last_error = "" # 最後のエラーメッセージを保存 def test_connection(self) -> bool: """接続テスト""" try: url = f"{self.base_url}/version" with urllib.request.urlopen(url, timeout=5) as response: return response.status == 200 except Exception as e: print(f"Connection test failed: {e}") return False def get_speakers(self) -> Optional[list]: """利用可能な話者一覧を取得""" try: url = f"{self.base_url}/speakers" with urllib.request.urlopen(url, timeout=10) as response: data = response.read() # UTF-8として明示的にデコード text = data.decode("utf-8") speakers = json.loads(text) print(f"[VoiceVox API] 話者を {len(speakers)} 人取得") return speakers except Exception as e: print(f"Failed to get speakers: {e}") import traceback traceback.print_exc() return None def generate_speech( self, text: str, speaker_id: int = 0, speed_scale: float = 1.0, pitch_scale: float = 0.0, intonation_scale: float = 1.0, volume_scale: float = 1.0, output_dir: str = "/tmp", ) -> Optional[str]: """ 音声を生成して保存 Returns: 生成された音声ファイルのパス、失敗時はNone """ try: print(f"[VoiceVox API] Step 1: 出力ディレクトリを作成: {output_dir}") os.makedirs(output_dir, exist_ok=True) # 1. 音声合成用のクエリを作成 print(f"[VoiceVox API] Step 2: 音声クエリを生成中...") query_url = f"{self.base_url}/audio_query" query_params = urllib.parse.urlencode( { "text": text, "speaker": speaker_id, } ) full_query_url = f"{query_url}?{query_params}" print(f"[VoiceVox API] リクエストURL: {full_query_url}") try: # POSTリクエストにするため空のdataを渡す req = urllib.request.Request(full_query_url, data=b"", method="POST") with urllib.request.urlopen(req, timeout=10) as response: query_data = json.loads(response.read()) print(f"[VoiceVox API] クエリ生成成功") except urllib.error.HTTPError as e: print(f"[VoiceVox API Error] HTTPエラー: {e.code} {e.reason}") print( f"[VoiceVox API Error] レスポンス: {e.read().decode('utf-8', errors='replace')}" ) raise except urllib.error.URLError as e: print(f"[VoiceVox API Error] 接続エラー: {e.reason}") print( f"[VoiceVox API Error] VoiceVoxエンジンが起動しているか確認してください" ) raisee # パラメータを適用 query_data["speedScale"] = speed_scale query_data["pitchScale"] = pitch_scale query_data["intonationScale"] = intonation_scale query_data["volumeScale"] = volume_scale # 2. 音声合成を実行 print(f"[VoiceVox API] Step 3: 音声合成を実行中...") synthesis_url = f"{self.base_url}/synthesis" synthesis_params = urllib.parse.urlencode({"speaker": speaker_id}) req = urllib.request.Request( f"{synthesis_url}?{synthesis_params}", data=json.dumps(query_data).encode("utf-8"), headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=30) as response: audio_data = response.read() print(f"[VoiceVox API] 音声合成成功 ({len(audio_data)} bytes)") except urllib.error.HTTPError as e: print(f"[VoiceVox API Error] 合成HTTPエラー: {e.code} {e.reason}") print( f"[VoiceVox API Error] レスポンス: {e.read().decode('utf-8', errors='replace')}" ) raise # 3. 音声ファイルを保存 print(f"[VoiceVox API] Step 4: 音声ファイルを保存中...") text_hash = hashlib.md5(text.encode("utf-8")).hexdigest()[:8] filename = f"voicevox_{text_hash}_{speaker_id}.wav" filepath = os.path.join(output_dir, filename) with open(filepath, "wb") as f: f.write(audio_data) print(f"[VoiceVox API] 音声保存完了: {filepath}") return filepath except urllib.error.URLError as e: error_msg = ( f"接続エラー: VoiceVoxエンジンに接続できません ({self.base_url})" ) print(f"[VoiceVox API Error] {error_msg}") print(f"[VoiceVox API Error] 詳細: {e}") self.last_error = error_msg import traceback traceback.print_exc() return None except urllib.error.HTTPError as e: error_msg = f"HTTPエラー {e.code}: {e.reason}" print(f"[VoiceVox API Error] {error_msg}") self.last_error = error_msg import traceback traceback.print_exc() return None except Exception as e: error_msg = f"{type(e).__name__}: {str(e)}" print(f"[VoiceVox API Error] 音声生成失敗: {error_msg}") self.last_error = error_msg import traceback traceback.print_exc() return None