""" オペレーター ユーザーが実行できる操作を定義 """ 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'): for prop in text_strip.bl_rna.properties: prop_name = prop.identifier # スキップするプロパティ if prop_name in skip_properties: continue # 読み取り専用プロパティはスキップ if prop.is_readonly: 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 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'}