310 lines
12 KiB
Python
310 lines
12 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
|
||
|
||
# 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'}
|