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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ class VoiceVoxAPI:
with urllib.request.urlopen(url, timeout=5) as response:
return response.status == 200
except Exception as e:
print(f"Connection test failed: {e}")
print(f"[VoiceVox API Error] Connection test failed: {e}")
return False
def get_speakers(self) -> Optional[list]:
@ -34,16 +34,11 @@ class VoiceVoxAPI:
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()
print(f"[VoiceVox API Error] Failed to get speakers: {e}")
return None
def generate_speech(
@ -63,11 +58,8 @@ class VoiceVoxAPI:
生成された音声ファイルのパス失敗時は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(
{
@ -77,14 +69,11 @@ class VoiceVoxAPI:
)
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(
@ -96,7 +85,7 @@ class VoiceVoxAPI:
print(
f"[VoiceVox API Error] VoiceVoxエンジンが起動しているか確認してください"
)
raisee
raise
# パラメータを適用
query_data["speedScale"] = speed_scale
@ -104,8 +93,6 @@ class VoiceVoxAPI:
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})
@ -119,7 +106,6 @@ class VoiceVoxAPI:
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(
@ -127,8 +113,6 @@ class VoiceVoxAPI:
)
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)
@ -136,7 +120,6 @@ class VoiceVoxAPI:
with open(filepath, "wb") as f:
f.write(audio_data)
print(f"[VoiceVox API] 音声保存完了: {filepath}")
return filepath
except urllib.error.URLError as e:
@ -144,25 +127,15 @@ class VoiceVoxAPI:
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