refactor: clean prints add LICENCE

This commit is contained in:
Keisuke Hirata 2026-02-04 22:22:51 +09:00
parent 4973220a3f
commit 25a555cc29
8 changed files with 18 additions and 217 deletions

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
Copyright 2026 Hare
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -2,48 +2,6 @@
Blender 5.0 向けの VoiceVox 音声合成と字幕表示を統合したアドオンです。 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 エンジンを起動** 1. **VoiceVox エンジンを起動**
@ -51,7 +9,7 @@ direnv allow
- VoiceVox エンジンをコマンドラインで起動 - VoiceVox エンジンをコマンドラインで起動
2. **Blender でシーケンスエディタを開く** 2. **Blender でシーケンスエディタを開く**
- ウィンドウタイプを "Video Sequencing" に変更 - "Video Editing"ワークスペース下部、"Video Sequencer"エディタを開く
3. **VoiceVox パネルを開く** 3. **VoiceVox パネルを開く**
- サイドバーN キー)の "VoiceVox" タブをクリック - サイドバーN キー)の "VoiceVox" タブをクリック
@ -64,54 +22,6 @@ direnv allow
- 話者や音声パラメータを調整 - 話者や音声パラメータを調整
- "Generate Speech & Subtitle" ボタンをクリック - "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 MIT License
## 作者
Your Name

View File

@ -5,7 +5,7 @@ VoiceVoxを使用した音声合成と字幕表示を同時に行うBlenderア
bl_info = { bl_info = {
"name": "VoiceVox TTS & Subtitles", "name": "VoiceVox TTS & Subtitles",
"author": "Your Name", "author": "Hare",
"version": (1, 0, 0), "version": (1, 0, 0),
"blender": (5, 0, 0), "blender": (5, 0, 0),
"location": "View3D > Sidebar > VoiceVox", "location": "View3D > Sidebar > VoiceVox",
@ -40,16 +40,12 @@ def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
# シーンにプロパティを追加
bpy.types.Scene.voicevox = bpy.props.PointerProperty(type=properties.VoiceVoxProperties) bpy.types.Scene.voicevox = bpy.props.PointerProperty(type=properties.VoiceVoxProperties)
# 初回起動時に話者リストを取得
try: try:
properties.update_speaker_cache() properties.update_speaker_cache()
except: except:
pass # 初回はVoiceVoxが起動していない可能性があるのでスキップ pass
print("VoiceVox Plugin registered")
def unregister(): def unregister():
@ -57,11 +53,8 @@ def unregister():
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
# シーンからプロパティを削除
del bpy.types.Scene.voicevox del bpy.types.Scene.voicevox
print("VoiceVox Plugin unregistered")
if __name__ == "__main__": if __name__ == "__main__":
register() register()

View File

@ -23,73 +23,53 @@ def get_text_strip_settings(context):
props = context.scene.voicevox props = context.scene.voicevox
text_strip = None text_strip = None
# 1. まずリファレンステキストストリップをチェック
if props.reference_text_strip: if props.reference_text_strip:
for strip in seq_editor.strips_all: for strip in seq_editor.strips_all:
if strip.name == props.reference_text_strip and strip.type == 'TEXT': if strip.name == props.reference_text_strip and strip.type == 'TEXT':
text_strip = strip text_strip = strip
print(f"[VoiceVox] リファレンステキスト '{strip.name}' を使用")
break break
# 2. リファレンスが見つからない場合は選択されているストリップを使用
if not text_strip: if not text_strip:
if seq_editor.active_strip and seq_editor.active_strip.type == 'TEXT': if seq_editor.active_strip and seq_editor.active_strip.type == 'TEXT':
text_strip = seq_editor.active_strip text_strip = seq_editor.active_strip
print(f"[VoiceVox] アクティブなテキスト '{text_strip.name}' を使用")
else: else:
# 選択されているストリップからテキストを探す
for strip in seq_editor.strips_all: for strip in seq_editor.strips_all:
if strip.select and strip.type == 'TEXT': if strip.select and strip.type == 'TEXT':
text_strip = strip text_strip = strip
print(f"[VoiceVox] 選択されたテキスト '{text_strip.name}' を使用")
break break
if not text_strip: if not text_strip:
return None return None
# すべてのプロパティを動的に抽出
settings = {} settings = {}
# ストリップ固有のプロパティ(コピーしないもの)
skip_properties = { skip_properties = {
'name', 'type', 'frame_start', 'frame_final_start', 'name', 'type', 'frame_start', 'frame_final_start',
'frame_final_end', 'frame_final_duration', 'frame_duration', 'frame_final_end', 'frame_final_duration', 'frame_duration',
'frame_offset_start', 'frame_offset_end', 'frame_still_start', 'frame_offset_start', 'frame_offset_end', 'frame_still_start',
'frame_still_end', 'select', 'select_left_handle', 'frame_still_end', 'select', 'select_left_handle',
'select_right_handle', 'mute', 'lock', 'text', # textは別途設定 'select_right_handle', 'mute', 'lock', 'text',
'channel', # channelはUIの設定を優先 'channel',
} }
# bl_rna.propertiesから全プロパティを取得
if hasattr(text_strip, 'bl_rna') and hasattr(text_strip.bl_rna, 'properties'): if hasattr(text_strip, 'bl_rna') and hasattr(text_strip.bl_rna, 'properties'):
# デバッグ: すべてのプロパティ名を出力
all_prop_names = [p.identifier for p in text_strip.bl_rna.properties]
print(f"[VoiceVox Debug] 利用可能なプロパティ: {all_prop_names}")
for prop in text_strip.bl_rna.properties: for prop in text_strip.bl_rna.properties:
prop_name = prop.identifier prop_name = prop.identifier
# スキップするプロパティ
if prop_name in skip_properties: if prop_name in skip_properties:
print(f"[VoiceVox Debug] スキップskip_properties: {prop_name}")
continue continue
# プロパティの値を取得
try: try:
value = getattr(text_strip, prop_name) value = getattr(text_strip, prop_name)
# locationは特別処理
if prop_name == 'location': if prop_name == 'location':
try: try:
settings['position_x'] = value[0] settings['position_x'] = value[0]
settings['position_y'] = value[1] settings['position_y'] = value[1]
except (TypeError, IndexError): except (TypeError, IndexError):
pass pass
# transformは子プロパティを個別にコピーreadonly だが子プロパティは書き込み可能)
elif prop_name == 'transform': elif prop_name == 'transform':
print(f"[VoiceVox Debug] transform プロパティを発見: {value}")
if hasattr(value, 'bl_rna') and hasattr(value.bl_rna, 'properties'): if hasattr(value, 'bl_rna') and hasattr(value.bl_rna, 'properties'):
print(f"[VoiceVox Debug] transform の子プロパティを列挙中...")
for transform_prop in value.bl_rna.properties: for transform_prop in value.bl_rna.properties:
transform_prop_name = transform_prop.identifier transform_prop_name = transform_prop.identifier
if transform_prop.is_readonly: if transform_prop.is_readonly:
@ -97,23 +77,18 @@ def get_text_strip_settings(context):
try: try:
transform_value = getattr(value, transform_prop_name) transform_value = getattr(value, transform_prop_name)
settings[f'transform_{transform_prop_name}'] = transform_value settings[f'transform_{transform_prop_name}'] = transform_value
print(f"[VoiceVox] コピー: transform.{transform_prop_name} = {transform_value}")
except Exception as e: except Exception as e:
print(f"[VoiceVox] transform.{transform_prop_name} の取得をスキップ: {e}") print(f"[VoiceVox] transform.{transform_prop_name} の取得をスキップ: {e}")
else: else:
print(f"[VoiceVox Debug] transform に bl_rna.properties がありません") pass
else: else:
# 通常のプロパティ: readonlyはスキップ
if prop.is_readonly: if prop.is_readonly:
print(f"[VoiceVox Debug] スキップreadonly: {prop_name}") pass
else: else:
settings[prop_name] = value settings[prop_name] = value
print(f"[VoiceVox] コピー: {prop_name} = {value}")
except Exception as e: except Exception as e:
print(f"[VoiceVox] {prop_name} の取得をスキップ: {e}") print(f"[VoiceVox] {prop_name} の取得をスキップ: {e}")
print(f"[VoiceVox] テキストストリップ '{text_strip.name}' から {len(settings)} 個のプロパティを抽出")
return settings return settings
@ -151,16 +126,10 @@ class VOICEVOX_OT_generate_speech(Operator):
return {'CANCELLED'} return {'CANCELLED'}
try: 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) api = VoiceVoxAPI(props.voicevox_host, props.voicevox_port)
output_dir = bpy.path.abspath(props.output_directory) output_dir = bpy.path.abspath(props.output_directory)
print(f"[VoiceVox] 出力先: {output_dir}")
# speakerプロパティからIDを取得
speaker_id = int(props.speaker) if props.speaker else 0 speaker_id = int(props.speaker) if props.speaker else 0
audio_file = api.generate_speech( audio_file = api.generate_speech(
@ -176,29 +145,22 @@ class VOICEVOX_OT_generate_speech(Operator):
if not audio_file: if not audio_file:
error_msg = f"音声生成に失敗: {api.last_error if api.last_error else '不明なエラー'}" error_msg = f"音声生成に失敗: {api.last_error if api.last_error else '不明なエラー'}"
self.report({'ERROR'}, error_msg) self.report({'ERROR'}, error_msg)
print(f"[VoiceVox] {error_msg}")
return {'CANCELLED'} return {'CANCELLED'}
self.report({'INFO'}, f"音声生成完了: {audio_file}") self.report({'INFO'}, f"音声生成完了: {audio_file}")
print(f"[VoiceVox] 音声生成完了: {audio_file}")
# シーケンサーに自動追加
if props.auto_add_to_sequencer: if props.auto_add_to_sequencer:
self._add_to_sequencer(context, audio_file, props.text) self._add_to_sequencer(context, audio_file, props.text)
return {'FINISHED'} return {'FINISHED'}
except Exception as e: except Exception as e:
import traceback
error_msg = f"音声生成エラー: {str(e)}" error_msg = f"音声生成エラー: {str(e)}"
print(f"[VoiceVox Error] {error_msg}")
traceback.print_exc()
self.report({'ERROR'}, error_msg) self.report({'ERROR'}, error_msg)
return {'CANCELLED'} return {'CANCELLED'}
def _add_to_sequencer(self, context, audio_file, text): def _add_to_sequencer(self, context, audio_file, text):
"""音声と字幕をシーケンサーに追加""" """音声と字幕をシーケンサーに追加"""
# シーケンスエディタのエリアを取得または作成
if not context.scene.sequence_editor: if not context.scene.sequence_editor:
context.scene.sequence_editor_create() context.scene.sequence_editor_create()
@ -206,7 +168,6 @@ class VOICEVOX_OT_generate_speech(Operator):
frame_current = context.scene.frame_current frame_current = context.scene.frame_current
props = context.scene.voicevox props = context.scene.voicevox
# 音声を追加
audio_strip = seq_editor.strips.new_sound( audio_strip = seq_editor.strips.new_sound(
name="VoiceVox Audio", name="VoiceVox Audio",
filepath=audio_file, filepath=audio_file,
@ -214,26 +175,19 @@ class VOICEVOX_OT_generate_speech(Operator):
frame_start=frame_current, frame_start=frame_current,
) )
# 音声の長さを取得(フレーム数)
audio_duration_frames = audio_strip.frame_final_duration audio_duration_frames = audio_strip.frame_final_duration
print(f"[VoiceVox] 音声の長さ: {audio_duration_frames} フレーム")
# 選択されているテキストストリップから設定を抽出(あれば)
text_settings = get_text_strip_settings(context) text_settings = get_text_strip_settings(context)
# 字幕を追加(音声の長さに合わせる)
subtitle_mgr = SubtitleManager() subtitle_mgr = SubtitleManager()
# テキストストリップから設定を取得できた場合はそれを優先
if text_settings: if text_settings:
font_size = text_settings.get('font_size', 48) # デフォルト48 font_size = text_settings.get('font_size', 48) # デフォルト48
position_y = text_settings.get('position_y', 0.1) # デフォルト0.1 position_y = text_settings.get('position_y', 0.1) # デフォルト0.1
channel = text_settings.get('channel', props.subtitle_channel) channel = text_settings.get('channel', props.subtitle_channel)
# その他の設定も渡す
extra_settings = {k: v for k, v in text_settings.items() extra_settings = {k: v for k, v in text_settings.items()
if k not in ['font_size', 'position_y', 'channel']} if k not in ['font_size', 'position_y', 'channel']}
else: else:
# リファレンスがない場合のデフォルト値
font_size = 48 font_size = 48
position_y = 0.1 position_y = 0.1
channel = props.subtitle_channel channel = props.subtitle_channel
@ -243,7 +197,7 @@ class VOICEVOX_OT_generate_speech(Operator):
context=context, context=context,
text=text, text=text,
frame_start=frame_current, frame_start=frame_current,
duration_frames=audio_duration_frames, # 音声の長さに合わせる duration_frames=audio_duration_frames,
font_size=font_size, font_size=font_size,
position_y=position_y, position_y=position_y,
channel=channel, channel=channel,
@ -270,25 +224,20 @@ class VOICEVOX_OT_add_subtitle(Operator):
frame_current = context.scene.frame_current frame_current = context.scene.frame_current
# 字幕のみの場合はデフォルト3秒
default_duration_sec = 3.0 default_duration_sec = 3.0
duration_frames = int(default_duration_sec * context.scene.render.fps) duration_frames = int(default_duration_sec * context.scene.render.fps)
# 選択されているテキストストリップから設定を抽出(あれば)
text_settings = get_text_strip_settings(context) text_settings = get_text_strip_settings(context)
subtitle_mgr = SubtitleManager() subtitle_mgr = SubtitleManager()
# テキストストリップから設定を取得できた場合はそれを優先
if text_settings: if text_settings:
font_size = text_settings.get('font_size', 48) # デフォルト48 font_size = text_settings.get('font_size', 48) # デフォルト48
position_y = text_settings.get('position_y', 0.1) # デフォルト0.1 position_y = text_settings.get('position_y', 0.1) # デフォルト0.1
channel = text_settings.get('channel', props.subtitle_channel) channel = text_settings.get('channel', props.subtitle_channel)
# その他の設定も渡す
extra_settings = {k: v for k, v in text_settings.items() extra_settings = {k: v for k, v in text_settings.items()
if k not in ['font_size', 'position_y', 'channel']} if k not in ['font_size', 'position_y', 'channel']}
else: else:
# リファレンスがない場合のデフォルト値
font_size = 48 font_size = 48
position_y = 0.1 position_y = 0.1
channel = props.subtitle_channel channel = props.subtitle_channel

View File

@ -21,7 +21,6 @@ class VOICEVOX_OT_set_reference_text(Operator):
seq_editor = context.scene.sequence_editor seq_editor = context.scene.sequence_editor
props = context.scene.voicevox props = context.scene.voicevox
# 選択されているテキストストリップを探す
text_strip = None text_strip = None
if seq_editor.active_strip and seq_editor.active_strip.type == 'TEXT': if seq_editor.active_strip and seq_editor.active_strip.type == 'TEXT':
@ -36,10 +35,8 @@ class VOICEVOX_OT_set_reference_text(Operator):
self.report({'ERROR'}, "テキストストリップが選択されていません") self.report({'ERROR'}, "テキストストリップが選択されていません")
return {'CANCELLED'} return {'CANCELLED'}
# リファレンスとして設定
props.reference_text_strip = text_strip.name props.reference_text_strip = text_strip.name
self.report({'INFO'}, f"リファレンステキスト: '{text_strip.name}'") self.report({'INFO'}, f"リファレンステキスト: '{text_strip.name}'")
print(f"[VoiceVox] リファレンステキストを設定: {text_strip.name}")
return {'FINISHED'} return {'FINISHED'}
@ -55,6 +52,5 @@ class VOICEVOX_OT_clear_reference_text(Operator):
props = context.scene.voicevox props = context.scene.voicevox
props.reference_text_strip = "" props.reference_text_strip = ""
self.report({'INFO'}, "リファレンステキストをクリアしました") self.report({'INFO'}, "リファレンステキストをクリアしました")
print(f"[VoiceVox] リファレンステキストをクリア")
return {'FINISHED'} return {'FINISHED'}

View File

@ -12,7 +12,6 @@ from bpy.props import (
EnumProperty, EnumProperty,
) )
# グローバル変数で話者リストをキャッシュ
_speaker_cache = [] _speaker_cache = []
@ -21,7 +20,6 @@ def get_speaker_items(self, context):
global _speaker_cache global _speaker_cache
if not _speaker_cache: if not _speaker_cache:
# キャッシュが空の場合はデフォルトを返す
return [('0', 'リロードボタンを押してください', 'VoiceVoxから話者リストを取得')] return [('0', 'リロードボタンを押してください', 'VoiceVoxから話者リストを取得')]
return _speaker_cache return _speaker_cache
@ -47,7 +45,6 @@ def update_speaker_cache(host: str = "127.0.0.1", port: int = 50021):
style_name = style.get('name', '') style_name = style.get('name', '')
style_id = style.get('id', 0) style_id = style.get('id', 0)
# 表示名: "キャラクター名 (スタイル名)"
display_name = f"{name} ({style_name})" if style_name else name display_name = f"{name} ({style_name})" if style_name else name
items.append(( items.append((
@ -64,8 +61,6 @@ def update_speaker_cache(host: str = "127.0.0.1", port: int = 50021):
except Exception as e: except Exception as e:
print(f"[VoiceVox] 話者一覧の取得に失敗: {e}") print(f"[VoiceVox] 話者一覧の取得に失敗: {e}")
import traceback
traceback.print_exc()
_speaker_cache = [('0', '接続エラー', str(e))] _speaker_cache = [('0', '接続エラー', str(e))]
return 0 return 0
@ -73,7 +68,6 @@ def update_speaker_cache(host: str = "127.0.0.1", port: int = 50021):
class VoiceVoxProperties(bpy.types.PropertyGroup): class VoiceVoxProperties(bpy.types.PropertyGroup):
"""VoiceVoxプラグインのプロパティ""" """VoiceVoxプラグインのプロパティ"""
# VoiceVox エンジンの設定
voicevox_host: StringProperty( voicevox_host: StringProperty(
name="VoiceVox Host", name="VoiceVox Host",
description="VoiceVoxエンジンのホストアドレス", description="VoiceVoxエンジンのホストアドレス",
@ -88,7 +82,6 @@ class VoiceVoxProperties(bpy.types.PropertyGroup):
max=65535, max=65535,
) )
# 音声合成の設定
text: StringProperty( text: StringProperty(
name="Text", name="Text",
description="音声合成するテキスト", description="音声合成するテキスト",
@ -101,7 +94,6 @@ class VoiceVoxProperties(bpy.types.PropertyGroup):
items=get_speaker_items, items=get_speaker_items,
) )
# 後方互換性のため残す内部的にはspeakerを使用
speaker_id: IntProperty( speaker_id: IntProperty(
name="Speaker ID", name="Speaker ID",
description="話者ID (VoiceVoxのキャラクター)", description="話者ID (VoiceVoxのキャラクター)",
@ -141,7 +133,6 @@ class VoiceVoxProperties(bpy.types.PropertyGroup):
max=2.0, max=2.0,
) )
# チャンネル設定
audio_channel: IntProperty( audio_channel: IntProperty(
name="Audio Channel", name="Audio Channel",
description="音声を配置するチャンネル番号", description="音声を配置するチャンネル番号",
@ -158,14 +149,12 @@ class VoiceVoxProperties(bpy.types.PropertyGroup):
max=128, max=128,
) )
# リファレンステキストストリップ
reference_text_strip: StringProperty( reference_text_strip: StringProperty(
name="Reference Text Strip", name="Reference Text Strip",
description="設定を流用するリファレンステキストストリップの名前", description="設定を流用するリファレンステキストストリップの名前",
default="", default="",
) )
# 出力設定
output_directory: StringProperty( output_directory: StringProperty(
name="Output Directory", name="Output Directory",
description="音声ファイルの出力先ディレクトリ", description="音声ファイルの出力先ディレクトリ",

View File

@ -39,7 +39,6 @@ class SubtitleManager:
seq_editor = scene.sequence_editor seq_editor = scene.sequence_editor
# テキストストリップを追加
text_strip = seq_editor.strips.new_effect( text_strip = seq_editor.strips.new_effect(
name="Subtitle", name="Subtitle",
type='TEXT', type='TEXT',
@ -48,10 +47,8 @@ class SubtitleManager:
length=duration_frames, length=duration_frames,
) )
# テキスト内容を設定
text_strip.text = text text_strip.text = text
# デフォルト設定
default_settings = { default_settings = {
'use_shadow': True, 'use_shadow': True,
'shadow_color': (0.0, 0.0, 0.0, 0.8), 'shadow_color': (0.0, 0.0, 0.0, 0.8),
@ -61,61 +58,48 @@ class SubtitleManager:
'align_y': 'BOTTOM', 'align_y': 'BOTTOM',
} }
# デフォルト設定から開始し、extra_settingsで上書き
all_settings = default_settings.copy() all_settings = default_settings.copy()
if extra_settings: if extra_settings:
all_settings.update(extra_settings) all_settings.update(extra_settings)
# font_sizeは常にパラメータの値を使用
all_settings['font_size'] = font_size all_settings['font_size'] = font_size
# 位置の処理
position_x = all_settings.pop('position_x', 0.5) position_x = all_settings.pop('position_x', 0.5)
position_y_val = all_settings.pop('position_y', position_y) position_y_val = all_settings.pop('position_y', position_y)
# Transform関連のプロパティを分離
transform_settings = {} transform_settings = {}
other_settings = {} other_settings = {}
for prop, value in all_settings.items(): for prop, value in all_settings.items():
if prop.startswith('transform_'): if prop.startswith('transform_'):
# transform_offset_x → offset_x
transform_prop_name = prop[len('transform_'):] transform_prop_name = prop[len('transform_'):]
transform_settings[transform_prop_name] = value transform_settings[transform_prop_name] = value
else: else:
other_settings[prop] = value other_settings[prop] = value
# 通常のプロパティを設定
for prop, value in other_settings.items(): for prop, value in other_settings.items():
try: try:
setattr(text_strip, prop, value) setattr(text_strip, prop, value)
print(f"[Subtitle] 設定: {prop} = {value}")
except AttributeError: except AttributeError:
print(f"[Subtitle] {prop} プロパティは利用できません(スキップ)") print(f"[Subtitle] {prop} プロパティは利用できません(スキップ)")
except Exception as e: except Exception as e:
print(f"[Subtitle] {prop} の設定に失敗: {e}") print(f"[Subtitle] {prop} の設定に失敗: {e}")
# 位置を設定
try: try:
text_strip.location[0] = position_x text_strip.location[0] = position_x
text_strip.location[1] = position_y_val text_strip.location[1] = position_y_val
print(f"[Subtitle] location = ({position_x}, {position_y_val})")
except (AttributeError, TypeError) as e: except (AttributeError, TypeError) as e:
print(f"[Subtitle] location プロパティは利用できません: {e}") print(f"[Subtitle] location プロパティは利用できません: {e}")
# Transformプロパティを設定
if transform_settings and hasattr(text_strip, 'transform'): if transform_settings and hasattr(text_strip, 'transform'):
for prop, value in transform_settings.items(): for prop, value in transform_settings.items():
try: try:
setattr(text_strip.transform, prop, value) setattr(text_strip.transform, prop, value)
print(f"[Subtitle] 設定: transform.{prop} = {value}")
except AttributeError: except AttributeError:
print(f"[Subtitle] transform.{prop} プロパティは利用できません(スキップ)") print(f"[Subtitle] transform.{prop} プロパティは利用できません(スキップ)")
except Exception as e: except Exception as e:
print(f"[Subtitle] transform.{prop} の設定に失敗: {e}") print(f"[Subtitle] transform.{prop} の設定に失敗: {e}")
print(f"Subtitle added: '{text}' at frame {frame_start}")
return text_strip return text_strip
def update_subtitle(self, text_strip, **kwargs): def update_subtitle(self, text_strip, **kwargs):

View File

@ -25,7 +25,7 @@ class VoiceVoxAPI:
with urllib.request.urlopen(url, timeout=5) as response: with urllib.request.urlopen(url, timeout=5) as response:
return response.status == 200 return response.status == 200
except Exception as e: except Exception as e:
print(f"Connection test failed: {e}") print(f"[VoiceVox API Error] Connection test failed: {e}")
return False return False
def get_speakers(self) -> Optional[list]: def get_speakers(self) -> Optional[list]:
@ -34,16 +34,11 @@ class VoiceVoxAPI:
url = f"{self.base_url}/speakers" url = f"{self.base_url}/speakers"
with urllib.request.urlopen(url, timeout=10) as response: with urllib.request.urlopen(url, timeout=10) as response:
data = response.read() data = response.read()
# UTF-8として明示的にデコード
text = data.decode("utf-8") text = data.decode("utf-8")
speakers = json.loads(text) speakers = json.loads(text)
print(f"[VoiceVox API] 話者を {len(speakers)} 人取得")
return speakers return speakers
except Exception as e: except Exception as e:
print(f"Failed to get speakers: {e}") print(f"[VoiceVox API Error] Failed to get speakers: {e}")
import traceback
traceback.print_exc()
return None return None
def generate_speech( def generate_speech(
@ -63,11 +58,8 @@ class VoiceVoxAPI:
生成された音声ファイルのパス失敗時はNone 生成された音声ファイルのパス失敗時はNone
""" """
try: try:
print(f"[VoiceVox API] Step 1: 出力ディレクトリを作成: {output_dir}")
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
# 1. 音声合成用のクエリを作成
print(f"[VoiceVox API] Step 2: 音声クエリを生成中...")
query_url = f"{self.base_url}/audio_query" query_url = f"{self.base_url}/audio_query"
query_params = urllib.parse.urlencode( query_params = urllib.parse.urlencode(
{ {
@ -77,14 +69,11 @@ class VoiceVoxAPI:
) )
full_query_url = f"{query_url}?{query_params}" full_query_url = f"{query_url}?{query_params}"
print(f"[VoiceVox API] リクエストURL: {full_query_url}")
try: try:
# POSTリクエストにするため空のdataを渡す
req = urllib.request.Request(full_query_url, data=b"", method="POST") req = urllib.request.Request(full_query_url, data=b"", method="POST")
with urllib.request.urlopen(req, timeout=10) as response: with urllib.request.urlopen(req, timeout=10) as response:
query_data = json.loads(response.read()) query_data = json.loads(response.read())
print(f"[VoiceVox API] クエリ生成成功")
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
print(f"[VoiceVox API Error] HTTPエラー: {e.code} {e.reason}") print(f"[VoiceVox API Error] HTTPエラー: {e.code} {e.reason}")
print( print(
@ -96,7 +85,7 @@ class VoiceVoxAPI:
print( print(
f"[VoiceVox API Error] VoiceVoxエンジンが起動しているか確認してください" f"[VoiceVox API Error] VoiceVoxエンジンが起動しているか確認してください"
) )
raisee raise
# パラメータを適用 # パラメータを適用
query_data["speedScale"] = speed_scale query_data["speedScale"] = speed_scale
@ -104,8 +93,6 @@ class VoiceVoxAPI:
query_data["intonationScale"] = intonation_scale query_data["intonationScale"] = intonation_scale
query_data["volumeScale"] = volume_scale query_data["volumeScale"] = volume_scale
# 2. 音声合成を実行
print(f"[VoiceVox API] Step 3: 音声合成を実行中...")
synthesis_url = f"{self.base_url}/synthesis" synthesis_url = f"{self.base_url}/synthesis"
synthesis_params = urllib.parse.urlencode({"speaker": speaker_id}) synthesis_params = urllib.parse.urlencode({"speaker": speaker_id})
@ -119,7 +106,6 @@ class VoiceVoxAPI:
try: try:
with urllib.request.urlopen(req, timeout=30) as response: with urllib.request.urlopen(req, timeout=30) as response:
audio_data = response.read() audio_data = response.read()
print(f"[VoiceVox API] 音声合成成功 ({len(audio_data)} bytes)")
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
print(f"[VoiceVox API Error] 合成HTTPエラー: {e.code} {e.reason}") print(f"[VoiceVox API Error] 合成HTTPエラー: {e.code} {e.reason}")
print( print(
@ -127,8 +113,6 @@ class VoiceVoxAPI:
) )
raise raise
# 3. 音声ファイルを保存
print(f"[VoiceVox API] Step 4: 音声ファイルを保存中...")
text_hash = hashlib.md5(text.encode("utf-8")).hexdigest()[:8] text_hash = hashlib.md5(text.encode("utf-8")).hexdigest()[:8]
filename = f"voicevox_{text_hash}_{speaker_id}.wav" filename = f"voicevox_{text_hash}_{speaker_id}.wav"
filepath = os.path.join(output_dir, filename) filepath = os.path.join(output_dir, filename)
@ -136,7 +120,6 @@ class VoiceVoxAPI:
with open(filepath, "wb") as f: with open(filepath, "wb") as f:
f.write(audio_data) f.write(audio_data)
print(f"[VoiceVox API] 音声保存完了: {filepath}")
return filepath return filepath
except urllib.error.URLError as e: except urllib.error.URLError as e:
@ -144,25 +127,15 @@ class VoiceVoxAPI:
f"接続エラー: VoiceVoxエンジンに接続できません ({self.base_url})" f"接続エラー: VoiceVoxエンジンに接続できません ({self.base_url})"
) )
print(f"[VoiceVox API Error] {error_msg}") print(f"[VoiceVox API Error] {error_msg}")
print(f"[VoiceVox API Error] 詳細: {e}")
self.last_error = error_msg self.last_error = error_msg
import traceback
traceback.print_exc()
return None return None
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
error_msg = f"HTTPエラー {e.code}: {e.reason}" error_msg = f"HTTPエラー {e.code}: {e.reason}"
print(f"[VoiceVox API Error] {error_msg}") print(f"[VoiceVox API Error] {error_msg}")
self.last_error = error_msg self.last_error = error_msg
import traceback
traceback.print_exc()
return None return None
except Exception as e: except Exception as e:
error_msg = f"{type(e).__name__}: {str(e)}" error_msg = f"{type(e).__name__}: {str(e)}"
print(f"[VoiceVox API Error] 音声生成失敗: {error_msg}") print(f"[VoiceVox API Error] 音声生成失敗: {error_msg}")
self.last_error = error_msg self.last_error = error_msg
import traceback
traceback.print_exc()
return None return None