blender-voicevox-pl/operators.py

259 lines
8.9 KiB
Python

"""
オペレーター
ユーザーが実行できる操作を定義
"""
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
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
break
if not text_strip:
if seq_editor.active_strip and seq_editor.active_strip.type == 'TEXT':
text_strip = seq_editor.active_strip
else:
for strip in seq_editor.strips_all:
if strip.select and strip.type == 'TEXT':
text_strip = strip
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',
'channel',
}
if hasattr(text_strip, 'bl_rna') and hasattr(text_strip.bl_rna, 'properties'):
for prop in text_strip.bl_rna.properties:
prop_name = prop.identifier
if prop_name in skip_properties:
continue
try:
value = getattr(text_strip, prop_name)
if prop_name == 'location':
try:
settings['position_x'] = value[0]
settings['position_y'] = value[1]
except (TypeError, IndexError):
pass
elif prop_name == 'transform':
if hasattr(value, 'bl_rna') and hasattr(value.bl_rna, 'properties'):
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
except Exception as e:
print(f"[VoiceVox] transform.{transform_prop_name} の取得をスキップ: {e}")
else:
pass
else:
if prop.is_readonly:
pass
else:
settings[prop_name] = value
except Exception as e:
print(f"[VoiceVox] {prop_name} の取得をスキップ: {e}")
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:
api = VoiceVoxAPI(props.voicevox_host, props.voicevox_port)
output_dir = bpy.path.abspath(props.output_directory)
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)
return {'CANCELLED'}
self.report({'INFO'}, f"音声生成完了: {audio_file}")
if props.auto_add_to_sequencer:
self._add_to_sequencer(context, audio_file, props.text)
return {'FINISHED'}
except Exception as e:
error_msg = f"音声生成エラー: {str(e)}"
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
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
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'}