This commit is contained in:
Keisuke Hirata 2026-02-04 19:51:39 +09:00
commit aead0ff136
11 changed files with 937 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

33
.gitignore vendored Normal file
View File

@ -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

117
README.md Normal file
View File

@ -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

56
__init__.py Normal file
View File

@ -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()

61
flake.lock Normal file
View File

@ -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
}

54
flake.nix Normal file
View File

@ -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"
'';
};
}
);
}

158
operators.py Normal file
View File

@ -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'}

79
panels.py Normal file
View File

@ -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")

126
properties.py Normal file
View File

@ -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,
)

104
subtitles.py Normal file
View File

@ -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)

148
voicevox.py Normal file
View File

@ -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