blender-voicevox-pl/operators.py
2026-02-04 21:49:33 +09:00

310 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
オペレーター
ユーザーが実行できる操作を定義
"""
import bpy
from bpy.types import Operator
from .voicevox import VoiceVoxAPI
from .subtitles import SubtitleManager
def get_text_strip_settings(context):
"""
リファレンスまたは選択されているテキストストリップから設定を抽出
Returns:
dict: 抽出された設定、見つからない場合はNone
"""
if not context.scene.sequence_editor:
return None
seq_editor = context.scene.sequence_editor
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の設定を優先
}
# 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:
continue
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 がありません")
else:
# 通常のプロパティ: readonlyはスキップ
if prop.is_readonly:
print(f"[VoiceVox Debug] スキップreadonly: {prop_name}")
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
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}")
# speakerプロパティからIDを取得
speaker_id = int(props.speaker) if props.speaker else 0
audio_file = api.generate_speech(
text=props.text,
speaker_id=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
props = context.scene.voicevox
# 音声を追加
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} フレーム")
# 選択されているテキストストリップから設定を抽出(あれば)
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
extra_settings = {}
subtitle_mgr.add_subtitle(
context=context,
text=text,
frame_start=frame_current,
duration_frames=audio_duration_frames, # 音声の長さに合わせる
font_size=font_size,
position_y=position_y,
channel=channel,
**extra_settings,
)
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)
# 選択されているテキストストリップから設定を抽出(あれば)
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
extra_settings = {}
subtitle_mgr.add_subtitle(
context=context,
text=props.text,
frame_start=frame_current,
duration_frames=duration_frames,
font_size=font_size,
position_y=position_y,
channel=channel,
**extra_settings,
)
self.report({'INFO'}, "字幕を追加しました")
return {'FINISHED'}