commit aead0ff1369e67d7a831eccc03b9cd83d9ac6665 Author: Hare Date: Wed Feb 4 19:51:39 2026 +0900 init diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d579d23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ + +# Blender +*.blend1 +*.blend2 + +# VoiceVox音声ファイル +*.wav +audio_cache/ + +# 環境 +.direnv/ +.envrc.local +result + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..effb74d --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# Blender VoiceVox Plugin + +Blender 5.0 向けの VoiceVox 音声合成と字幕表示を統合したアドオンです。 + +## 機能 + +- **音声合成**: VoiceVox エンジンを使用してテキストから音声を生成 +- **字幕表示**: 生成した音声と同期する字幕をシーケンサーに追加 +- **カスタマイズ可能**: 話者、話速、音高、抑揚、音量などを調整可能 +- **自動統合**: 生成した音声と字幕を自動的にシーケンサーに追加 + +## 必要なものeee + +- Blender 5.0 以上 +- VoiceVox エンジン(ローカルで実行中であること) + - https://voicevox.hiroshiba.jp/ からダウンロード + - デフォルトでは `127.0.0.1:50021` で動作 + +## インストール + +### 開発環境のセットアップ(Nix + direnv) + +```bash +# direnv を許可 +direnv allow + +# 開発環境に入る(自動的に有効化されます) +# または手動で: nix develop +``` + +### Blender へのインストール + +1. このディレクトリ全体を Blender のアドオンフォルダにシンボリックリンク: + ```bash + ln -s $(pwd) ~/.config/blender/5.0/scripts/addons/blender-voicevox-pl + ``` + +2. または、Blender の UI からインストール: + - Edit > Preferences > Add-ons + - Install ボタンをクリック + - このディレクトリを zip 化したファイルを選択 + +3. アドオンを有効化: + - "VoiceVox TTS & Subtitles" を検索 + - チェックボックスをオンにする + +## 使い方 + +1. **VoiceVox エンジンを起動** + - VoiceVox アプリケーションを起動するか + - VoiceVox エンジンをコマンドラインで起動 + +2. **Blender でシーケンスエディタを開く** + - ウィンドウタイプを "Video Sequencing" に変更 + +3. **VoiceVox パネルを開く** + - サイドバー(N キー)の "VoiceVox" タブをクリック + +4. **接続をテスト** + - "Test Connection" ボタンをクリックして VoiceVox に接続できるか確認 + +5. **音声と字幕を生成** + - テキストを入力 + - 話者や音声パラメータを調整 + - "Generate Speech & Subtitle" ボタンをクリック + +## 開発 + +### プロジェクト構成 + +``` +. +├── __init__.py # アドオンのエントリーポイント +├── operators.py # ユーザー操作の実装 +├── panels.py # UI パネルの定義 +├── properties.py # プロパティの定義 +├── voicevox.py # VoiceVox API 連携 +├── subtitles.py # 字幕管理 +├── flake.nix # Nix 開発環境設定 +└── README.md # このファイル +``` + +### Blender でのテスト + +```bash +# バックグラウンドでテスト +blender --background --python-expr "import bpy; bpy.ops.preferences.addon_enable(module='blender-voicevox-pl')" + +# GUI でテスト(開発用) +blender +``` + +## トラブルシューティング + +### VoiceVox に接続できない + +- VoiceVox エンジンが起動しているか確認 +- ホスト・ポート設定が正しいか確認(デフォルト: 127.0.0.1:50021) +- ファイアウォールがブロックしていないか確認 + +### 音声ファイルが生成されない + +- 出力ディレクトリの書き込み権限を確認 +- VoiceVox エンジンのログを確認 + +### 字幕が表示されない + +- シーケンスエディタで正しいチャンネルを表示しているか確認 +- レンダー設定で字幕のストリップが有効になっているか確認 + +## ライセンス + +MIT License + +## 作者 + +Your Name diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..2c6f56c --- /dev/null +++ b/__init__.py @@ -0,0 +1,56 @@ +""" +Blender VoiceVox Plugin +VoiceVoxを使用した音声合成と字幕表示を同時に行うBlenderアドオン +""" + +bl_info = { + "name": "VoiceVox TTS & Subtitles", + "author": "Your Name", + "version": (1, 0, 0), + "blender": (5, 0, 0), + "location": "View3D > Sidebar > VoiceVox", + "description": "VoiceVoxによる音声合成と字幕表示", + "warning": "", + "doc_url": "", + "category": "Sequencer", +} + +import bpy +from . import operators +from . import panels +from . import properties + + +classes = ( + properties.VoiceVoxProperties, + operators.VOICEVOX_OT_generate_speech, + operators.VOICEVOX_OT_add_subtitle, + operators.VOICEVOX_OT_test_connection, + panels.VOICEVOX_PT_main_panel, +) + + +def register(): + """アドオンを登録""" + for cls in classes: + bpy.utils.register_class(cls) + + # シーンにプロパティを追加 + bpy.types.Scene.voicevox = bpy.props.PointerProperty(type=properties.VoiceVoxProperties) + + print("VoiceVox Plugin registered") + + +def unregister(): + """アドオンを登録解除""" + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + # シーンからプロパティを削除 + del bpy.types.Scene.voicevox + + print("VoiceVox Plugin unregistered") + + +if __name__ == "__main__": + register() diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..668ac53 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1770115704, + "narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e6eae2ee2110f3d31110d5c222cd395303343b08", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..1daeddb --- /dev/null +++ b/flake.nix @@ -0,0 +1,54 @@ +{ + description = "Blender VoiceVox Plugin Development Environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + # Python開発環境 + python311 + python311Packages.pip + python311Packages.requests + python311Packages.pillow + + # Blender (開発・テスト用) + blender + + # 開発ツール + git + + # VoiceVoxエンジン(オプション) + # voicevox-engine は Nix パッケージとして提供されていない可能性があるため + # 別途インストールが必要 + ]; + + shellHook = '' + python --version + blender --version | head -n 1 + + # Pythonパスにカレントディレクトリを追加 + export PYTHONPATH="$PWD:$PYTHONPATH" + + # アドオンのインストールパスを環境変数として設定 + export BLENDER_USER_SCRIPTS="$HOME/.config/blender/5.0/scripts" + export BLENDER_USER_ADDONS="$BLENDER_USER_SCRIPTS/addons" + ''; + }; + } + ); +} diff --git a/operators.py b/operators.py new file mode 100644 index 0000000..4adcb69 --- /dev/null +++ b/operators.py @@ -0,0 +1,158 @@ +""" +オペレーター +ユーザーが実行できる操作を定義 +""" + +import bpy +from bpy.types import Operator +from .voicevox import VoiceVoxAPI +from .subtitles import SubtitleManager + + +class VOICEVOX_OT_test_connection(Operator): + """VoiceVoxエンジンへの接続をテスト""" + bl_idname = "voicevox.test_connection" + bl_label = "Test Connection" + bl_description = "VoiceVoxエンジンへの接続をテストします" + bl_options = {'REGISTER'} + + def execute(self, context): + props = context.scene.voicevox + api = VoiceVoxAPI(props.voicevox_host, props.voicevox_port) + + if api.test_connection(): + self.report({'INFO'}, "VoiceVoxエンジンに接続成功!") + return {'FINISHED'} + else: + self.report({'ERROR'}, "VoiceVoxエンジンに接続できません") + return {'CANCELLED'} + + +class VOICEVOX_OT_generate_speech(Operator): + """VoiceVoxで音声を生成""" + bl_idname = "voicevox.generate_speech" + bl_label = "Generate Speech" + bl_description = "VoiceVoxを使用して音声を生成します" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + props = context.scene.voicevox + + if not props.text: + self.report({'ERROR'}, "テキストを入力してください") + return {'CANCELLED'} + + try: + # VoiceVox APIで音声生成 + print(f"[VoiceVox] 音声生成開始: '{props.text}'") + print(f"[VoiceVox] 接続先: {props.voicevox_host}:{props.voicevox_port}") + + api = VoiceVoxAPI(props.voicevox_host, props.voicevox_port) + + output_dir = bpy.path.abspath(props.output_directory) + print(f"[VoiceVox] 出力先: {output_dir}") + + audio_file = api.generate_speech( + text=props.text, + speaker_id=props.speaker_id, + speed_scale=props.speed_scale, + pitch_scale=props.pitch_scale, + intonation_scale=props.intonation_scale, + volume_scale=props.volume_scale, + output_dir=output_dir, + ) + + if not audio_file: + error_msg = f"音声生成に失敗: {api.last_error if api.last_error else '不明なエラー'}" + self.report({'ERROR'}, error_msg) + print(f"[VoiceVox] {error_msg}") + return {'CANCELLED'} + + self.report({'INFO'}, f"音声生成完了: {audio_file}") + print(f"[VoiceVox] 音声生成完了: {audio_file}") + + # シーケンサーに自動追加 + if props.auto_add_to_sequencer: + self._add_to_sequencer(context, audio_file, props.text) + + return {'FINISHED'} + + except Exception as e: + import traceback + error_msg = f"音声生成エラー: {str(e)}" + print(f"[VoiceVox Error] {error_msg}") + traceback.print_exc() + self.report({'ERROR'}, error_msg) + return {'CANCELLED'} + + def _add_to_sequencer(self, context, audio_file, text): + """音声と字幕をシーケンサーに追加""" + # シーケンスエディタのエリアを取得または作成 + if not context.scene.sequence_editor: + context.scene.sequence_editor_create() + + seq_editor = context.scene.sequence_editor + frame_current = context.scene.frame_current + + # 音声を追加 + audio_strip = seq_editor.strips.new_sound( + name="VoiceVox Audio", + filepath=audio_file, + channel=props.audio_channel, + frame_start=frame_current, + ) + + # 音声の長さを取得(フレーム数) + audio_duration_frames = audio_strip.frame_final_duration + print(f"[VoiceVox] 音声の長さ: {audio_duration_frames} フレーム") + + # 字幕を追加(音声の長さに合わせる) + props = context.scene.voicevox + subtitle_mgr = SubtitleManager() + subtitle_mgr.add_subtitle( + context=context, + text=text, + frame_start=frame_current, + duration_frames=audio_duration_frames, # 音声の長さに合わせる + font_size=props.subtitle_font_size, + position_y=props.subtitle_position_y, + channel=props.subtitle_channel, + ) + + +class VOICEVOX_OT_add_subtitle(Operator): + """字幕のみを追加""" + bl_idname = "voicevox.add_subtitle" + bl_label = "Add Subtitle Only" + bl_description = "字幕のみをシーケンサーに追加します" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + props = context.scene.voicevox + + if not props.text: + self.report({'ERROR'}, "テキストを入力してください") + return {'CANCELLED'} + + if not context.scene.sequence_editor: + context.scene.sequence_editor_create() + + frame_current = context.scene.frame_current + + # 字幕のみの場合はデフォルト3秒 + default_duration_sec = 3.0 + duration_frames = int(default_duration_sec * context.scene.render.fps) + + subtitle_mgr = SubtitleManager() + subtitle_mgr.add_subtitle( + context=context, + text=props.text, + frame_start=frame_current, + duration_frames=duration_frames, + font_size=props.subtitle_font_size, + position_y=props.subtitle_position_y, + channel=props.subtitle_channel, + ) + + self.report({'INFO'}, "字幕を追加しました") + return {'FINISHED'} diff --git a/panels.py b/panels.py new file mode 100644 index 0000000..7c35a53 --- /dev/null +++ b/panels.py @@ -0,0 +1,79 @@ +""" +UIパネル +Blenderのサイドバーに表示されるUIを定義 +""" + +import bpy +from bpy.types import Panel + + +class VOICEVOX_PT_main_panel(Panel): + """VoiceVoxメインパネル""" + bl_label = "VoiceVox TTS" + bl_idname = "VOICEVOX_PT_main_panel" + bl_space_type = 'SEQUENCE_EDITOR' + bl_region_type = 'UI' + bl_category = 'VoiceVox' + + def draw(self, context): + layout = self.layout + props = context.scene.voicevox + + # 接続設定 + box = layout.box() + box.label(text="VoiceVox Engine", icon='NETWORK_DRIVE') + row = box.row() + row.prop(props, "voicevox_host") + row.prop(props, "voicevox_port") + box.operator("voicevox.test_connection", icon='FILE_REFRESH') + + layout.separator() + + # テキスト入力 + box = layout.box() + box.label(text="Text to Speech", icon='FILE_TEXT') + box.prop(props, "text", text="") + + layout.separator() + + # 音声設定 + box = layout.box() + box.label(text="Voice Settings", icon='SPEAKER') + box.prop(props, "speaker_id") + box.prop(props, "speed_scale", slider=True) + box.prop(props, "pitch_scale", slider=True) + box.prop(props, "intonation_scale", slider=True) + box.prop(props, "volume_scale", slider=True) + + layout.separator() + + # 字幕設定 + box = layout.box() + box.label(text="Subtitle Settings", icon='FONT_DATA') + box.prop(props, "subtitle_font_size") + box.prop(props, "subtitle_position_y", slider=True) + + layout.separator() + + # チャンネル設定 + box = layout.box() + box.label(text="Channel Settings", icon='LINENUMBERS_ON') + row = box.row() + row.prop(props, "audio_channel") + row.prop(props, "subtitle_channel") + + layout.separator() + + # 出力設定 + box = layout.box() + box.label(text="Output Settings", icon='FILE_FOLDER') + box.prop(props, "output_directory") + box.prop(props, "auto_add_to_sequencer") + + layout.separator() + + # アクション + col = layout.column(align=True) + col.scale_y = 1.5 + col.operator("voicevox.generate_speech", icon='PLAY_SOUND', text="Generate Speech & Subtitle") + col.operator("voicevox.add_subtitle", icon='FONT_DATA', text="Add Subtitle Only") diff --git a/properties.py b/properties.py new file mode 100644 index 0000000..b0b6637 --- /dev/null +++ b/properties.py @@ -0,0 +1,126 @@ +""" +プロパティ定義 +アドオンで使用する設定値やパラメータを定義 +""" + +import bpy +from bpy.props import ( + StringProperty, + IntProperty, + FloatProperty, + BoolProperty, + EnumProperty, +) + + +class VoiceVoxProperties(bpy.types.PropertyGroup): + """VoiceVoxプラグインのプロパティ""" + + # VoiceVox エンジンの設定 + voicevox_host: StringProperty( + name="VoiceVox Host", + description="VoiceVoxエンジンのホストアドレス", + default="127.0.0.1", + ) + + voicevox_port: IntProperty( + name="VoiceVox Port", + description="VoiceVoxエンジンのポート番号", + default=50021, + min=1, + max=65535, + ) + + # 音声合成の設定 + text: StringProperty( + name="Text", + description="音声合成するテキスト", + default="", + ) + + speaker_id: IntProperty( + name="Speaker ID", + description="話者ID (VoiceVoxのキャラクター)", + default=0, + min=0, + ) + + speed_scale: FloatProperty( + name="Speed", + description="話速", + default=1.0, + min=0.5, + max=2.0, + ) + + pitch_scale: FloatProperty( + name="Pitch", + description="音高", + default=0.0, + min=-0.15, + max=0.15, + ) + + intonation_scale: FloatProperty( + name="Intonation", + description="抑揚", + default=1.0, + min=0.0, + max=2.0, + ) + + volume_scale: FloatProperty( + name="Volume", + description="音量", + default=1.0, + min=0.0, + max=2.0, + ) + + # 字幕の設定 + subtitle_font_size: IntProperty( + name="Font Size", + description="字幕のフォントサイズ", + default=48, + min=12, + max=200, + ) + + subtitle_position_y: FloatProperty( + name="Position Y", + description="字幕の垂直位置 (0.0 = 下, 1.0 = 上)", + default=0.1, + min=0.0, + max=1.0, + ) + + # チャンネル設定 + audio_channel: IntProperty( + name="Audio Channel", + description="音声を配置するチャンネル番号", + default=1, + min=1, + max=128, + ) + + subtitle_channel: IntProperty( + name="Subtitle Channel", + description="字幕を配置するチャンネル番号", + default=2, + min=1, + max=128, + ) + + # 出力設定 + output_directory: StringProperty( + name="Output Directory", + description="音声ファイルの出力先ディレクトリ", + default="//audio_cache/", + subtype='DIR_PATH', + ) + + auto_add_to_sequencer: BoolProperty( + name="Auto Add to Sequencer", + description="生成した音声と字幕を自動的にシーケンサーに追加", + default=True, + ) diff --git a/subtitles.py b/subtitles.py new file mode 100644 index 0000000..1357d90 --- /dev/null +++ b/subtitles.py @@ -0,0 +1,104 @@ +""" +字幕管理 +Blenderのシーケンサーに字幕を追加 +""" + +import bpy + + +class SubtitleManager: + """字幕の追加と管理を担当""" + + def add_subtitle( + self, + context, + text: str, + frame_start: int, + duration_frames: int, + font_size: int = 48, + position_y: float = 0.1, + channel: int = 2, + ): + """ + シーケンサーに字幕を追加 + + Args: + context: Blenderコンテキスト + text: 字幕テキスト + frame_start: 開始フレーム + duration_frames: 表示期間(フレーム数) + font_size: フォントサイズ + position_y: 垂直位置 (0.0-1.0) + channel: チャンネル番号 + """ + scene = context.scene + + if not scene.sequence_editor: + scene.sequence_editor_create() + + seq_editor = scene.sequence_editor + + # テキストストリップを追加 + text_strip = seq_editor.strips.new_effect( + name="Subtitle", + type='TEXT', + channel=channel, + frame_start=frame_start, + length=duration_frames, + ) + + # テキスト内容を設定 + text_strip.text = text + + # Blender 5.0で利用可能なプロパティのみ設定 + try: + text_strip.font_size = font_size + except AttributeError: + print(f"[Subtitle] font_size プロパティは利用できません") + + try: + text_strip.use_shadow = True + text_strip.shadow_color = (0.0, 0.0, 0.0, 0.8) + except AttributeError: + print(f"[Subtitle] shadow プロパティは利用できません") + + # 位置を設定 + try: + text_strip.location[0] = 0.5 # 水平中央 + text_strip.location[1] = position_y # 垂直位置 + except (AttributeError, TypeError): + print(f"[Subtitle] location プロパティは利用できません") + + # テキストの配置(Blender 5.0では変更された可能性あり) + try: + text_strip.align_x = 'CENTER' + text_strip.align_y = 'BOTTOM' + except AttributeError: + # Blender 5.0では align_x/align_y が削除または変更された + print(f"[Subtitle] align_x/align_y プロパティは利用できません") + + # テキストの色(白) + try: + text_strip.color = (1.0, 1.0, 1.0, 1.0) + except AttributeError: + print(f"[Subtitle] color プロパティは利用できません") + + # ブレンドモード + try: + text_strip.blend_type = 'ALPHA_OVER' + except AttributeError: + print(f"[Subtitle] blend_type プロパティは利用できません") + + print(f"Subtitle added: '{text}' at frame {frame_start}") + + return text_strip + + def update_subtitle(self, text_strip, **kwargs): + """既存の字幕を更新""" + for key, value in kwargs.items(): + if hasattr(text_strip, key): + setattr(text_strip, key, value) + + def remove_subtitle(self, seq_editor, text_strip): + """字幕を削除""" + seq_editor.strips.remove(text_strip) diff --git a/voicevox.py b/voicevox.py new file mode 100644 index 0000000..79e617b --- /dev/null +++ b/voicevox.py @@ -0,0 +1,148 @@ +""" +VoiceVox API連携 +VoiceVoxエンジンとの通信を担当 +""" + +import os +import json +import hashlib +from typing import Optional +import urllib.request +import urllib.error + + +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() + return json.loads(data) + except Exception as e: + print(f"Failed to get speakers: {e}") + 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エンジンが起動しているか確認してください") + raise + + # パラメータを適用 + 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