""" Style-BERT-VITS2 Custom Handler for Hugging Face Inference Endpoints 日本語テキスト読み上げ用カスタムハンドラー """ import os import json import logging import traceback from typing import Dict, List, Any, Optional import torch import numpy as np from io import BytesIO import base64 # ログ設定 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class EndpointHandler: """Style-BERT-VITS2用のカスタムハンドラー""" def __init__(self, path: str = ""): """ ハンドラーの初期化 Args: path: モデルファイルのパス """ logger.info("Style-BERT-VITS2 Handler初期化開始") try: self.device = "cuda" if torch.cuda.is_available() else "cpu" logger.info(f"使用デバイス: {self.device}") # Style-BERT-VITS2の依存関係をインポート self._import_dependencies() # モデル初期化 self._load_model(path) # デフォルト設定 self.default_config = { "speaker_id": 0, "emotion": "neutral", "speed": 1.0, "pitch": 0.0, "intonation": 1.0, "volume": 1.0, "pre_phoneme_length": 0.1, "post_phoneme_length": 0.1, "sample_rate": 44100 } logger.info("Handler初期化完了") except Exception as e: logger.error(f"Handler初期化エラー: {e}") logger.error(traceback.format_exc()) raise def _import_dependencies(self): """必要な依存関係をインポート""" try: # Style-BERT-VITS2の主要モジュール try: global style_bert_vits2 import style_bert_vits2 self.has_style_bert_vits2 = True logger.info("Style-BERT-VITS2依存関係インポート完了") except ImportError: logger.warning("Style-BERT-VITS2がインストールされていません - モックモードで動作") self.has_style_bert_vits2 = False except Exception as e: logger.error(f"依存関係インポートエラー: {e}") raise def _load_model(self, path: str): """モデルをロード""" try: logger.info(f"モデルロード開始: {path}") # モデル設定ファイルのパス config_path = os.path.join(path, "config.json") model_path = os.path.join(path, "model.safetensors") if not os.path.exists(config_path): logger.warning(f"設定ファイルが見つかりません: {config_path}") # デフォルト設定を使用 self.model_config = self.default_config.copy() else: with open(config_path, "r", encoding="utf-8") as f: self.model_config = json.load(f) # モデルの実際のロード処理 if self.has_style_bert_vits2: # 実際のStyle-BERT-VITS2モデルをロード logger.info("実際のStyle-BERT-VITS2モデルロード開始") # ここで実際のモデルロード処理を実装 logger.info("モデルロード完了") else: # モックモード logger.info("モックモードでモデル初期化完了") except Exception as e: logger.error(f"モデルロードエラー: {e}") raise def __call__(self, data: Dict[str, Any]) -> List[Dict[str, Any]]: """ 推論実行のメインメソッド Args: data: リクエストデータ - inputs: テキスト(必須) - parameters: 音声生成パラメータ(オプション) Returns: 音声データとメタデータのリスト """ try: logger.info("推論開始") # 入力データの検証 inputs = data.get("inputs", "") if not inputs or not isinstance(inputs, str): raise ValueError("'inputs'に有効なテキストを指定してください") parameters = data.get("parameters", {}) # パラメータのマージ config = self.default_config.copy() config.update(parameters) logger.info(f"入力テキスト: {inputs[:50]}...") logger.info(f"使用パラメータ: {config}") # 音声合成実行 audio_result = self._synthesize_speech(inputs, config) # 結果の準備 result = [ { "audio_base64": audio_result["audio_base64"], "sample_rate": audio_result["sample_rate"], "duration": audio_result["duration"], "text": inputs, "parameters_used": config, "model_info": { "name": "Style-BERT-VITS2", "language": "ja", "device": self.device } } ] logger.info("推論完了") return result except Exception as e: logger.error(f"推論エラー: {e}") logger.error(traceback.format_exc()) # エラー情報を返す return [ { "error": str(e), "error_type": type(e).__name__, "traceback": traceback.format_exc(), "inputs": data.get("inputs", ""), "status": "error" } ] def _synthesize_speech(self, text: str, config: Dict[str, Any]) -> Dict[str, Any]: """ テキストから音声を合成 Args: text: 合成するテキスト config: 音声合成設定 Returns: 音声データとメタデータ """ try: logger.info("音声合成開始") sample_rate = config["sample_rate"] if self.has_style_bert_vits2: # 実際のStyle-BERT-VITS2による音声合成 logger.info("実際のStyle-BERT-VITS2で音声合成実行") # ここで実際の音声合成処理を実装 duration = len(text) * 0.1 # テキスト長に基づく概算時間 samples = int(sample_rate * duration) # 実際の音声データを生成 audio_data = np.zeros(samples) # プレースホルダー else: # モックモード - ダミー音声データ(サイン波) logger.info("モックモードでダミー音声生成") duration = len(text) * 0.1 # テキスト長に基づく概算時間 samples = int(sample_rate * duration) t = np.linspace(0, duration, samples) frequency = 440 # A4音程 audio_data = np.sin(2 * np.pi * frequency * t) * 0.3 # 16bit PCMに変換 audio_int16 = (audio_data * 32767).astype(np.int16) # WAVファイル形式でエンコード audio_bytes = self._encode_wav(audio_int16, sample_rate) # Base64エンコード audio_base64 = base64.b64encode(audio_bytes).decode('utf-8') result = { "audio_base64": audio_base64, "sample_rate": sample_rate, "duration": duration, "format": "wav" } logger.info(f"音声合成完了 - 時間: {duration:.2f}秒, サンプル数: {samples}") return result except Exception as e: logger.error(f"音声合成エラー: {e}") raise def _encode_wav(self, audio_data: np.ndarray, sample_rate: int) -> bytes: """ 音声データをWAV形式でエンコード Args: audio_data: 音声データ(int16) sample_rate: サンプリングレート Returns: WAVファイルのバイナリデータ """ import struct import wave # BytesIOでWAVファイルを作成 wav_buffer = BytesIO() with wave.open(wav_buffer, 'wb') as wav_file: wav_file.setnchannels(1) # モノラル wav_file.setsampwidth(2) # 16bit wav_file.setframerate(sample_rate) wav_file.writeframes(audio_data.tobytes()) wav_buffer.seek(0) return wav_buffer.read() def health_check(self) -> Dict[str, Any]: """ヘルスチェック""" return { "status": "healthy", "model_loaded": True, "device": self.device, "timestamp": str(torch.tensor([1.0]).item()) }