feat: reference text strip
This commit is contained in:
parent
aead0ff136
commit
04e62ba030
11
__init__.py
11
__init__.py
|
|
@ -17,6 +17,8 @@ bl_info = {
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from . import operators
|
from . import operators
|
||||||
|
from . import operators_reference
|
||||||
|
from . import operators_speaker
|
||||||
from . import panels
|
from . import panels
|
||||||
from . import properties
|
from . import properties
|
||||||
|
|
||||||
|
|
@ -26,6 +28,9 @@ classes = (
|
||||||
operators.VOICEVOX_OT_generate_speech,
|
operators.VOICEVOX_OT_generate_speech,
|
||||||
operators.VOICEVOX_OT_add_subtitle,
|
operators.VOICEVOX_OT_add_subtitle,
|
||||||
operators.VOICEVOX_OT_test_connection,
|
operators.VOICEVOX_OT_test_connection,
|
||||||
|
operators_reference.VOICEVOX_OT_set_reference_text,
|
||||||
|
operators_reference.VOICEVOX_OT_clear_reference_text,
|
||||||
|
operators_speaker.VOICEVOX_OT_refresh_speakers,
|
||||||
panels.VOICEVOX_PT_main_panel,
|
panels.VOICEVOX_PT_main_panel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -38,6 +43,12 @@ def register():
|
||||||
# シーンにプロパティを追加
|
# シーンにプロパティを追加
|
||||||
bpy.types.Scene.voicevox = bpy.props.PointerProperty(type=properties.VoiceVoxProperties)
|
bpy.types.Scene.voicevox = bpy.props.PointerProperty(type=properties.VoiceVoxProperties)
|
||||||
|
|
||||||
|
# 初回起動時に話者リストを取得
|
||||||
|
try:
|
||||||
|
properties.update_speaker_cache()
|
||||||
|
except:
|
||||||
|
pass # 初回はVoiceVoxが起動していない可能性があるのでスキップ
|
||||||
|
|
||||||
print("VoiceVox Plugin registered")
|
print("VoiceVox Plugin registered")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
12
flake.nix
12
flake.nix
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
{
|
||||||
self,
|
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
flake-utils,
|
flake-utils,
|
||||||
}:
|
}:
|
||||||
|
|
@ -20,21 +19,14 @@
|
||||||
{
|
{
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
# Python開発環境
|
|
||||||
python311
|
python311
|
||||||
python311Packages.pip
|
python311Packages.pip
|
||||||
python311Packages.requests
|
python311Packages.requests
|
||||||
python311Packages.pillow
|
python311Packages.pillow
|
||||||
|
|
||||||
# Blender (開発・テスト用)
|
|
||||||
blender
|
blender
|
||||||
|
|
||||||
# 開発ツール
|
|
||||||
git
|
git
|
||||||
|
voicevox-core
|
||||||
# VoiceVoxエンジン(オプション)
|
voicevox-engine
|
||||||
# voicevox-engine は Nix パッケージとして提供されていない可能性があるため
|
|
||||||
# 別途インストールが必要
|
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|
|
||||||
146
operators.py
146
operators.py
|
|
@ -9,6 +9,93 @@ from .voicevox import VoiceVoxAPI
|
||||||
from .subtitles import SubtitleManager
|
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):
|
class VOICEVOX_OT_test_connection(Operator):
|
||||||
"""VoiceVoxエンジンへの接続をテスト"""
|
"""VoiceVoxエンジンへの接続をテスト"""
|
||||||
bl_idname = "voicevox.test_connection"
|
bl_idname = "voicevox.test_connection"
|
||||||
|
|
@ -52,9 +139,12 @@ class VOICEVOX_OT_generate_speech(Operator):
|
||||||
output_dir = bpy.path.abspath(props.output_directory)
|
output_dir = bpy.path.abspath(props.output_directory)
|
||||||
print(f"[VoiceVox] 出力先: {output_dir}")
|
print(f"[VoiceVox] 出力先: {output_dir}")
|
||||||
|
|
||||||
|
# speakerプロパティからIDを取得
|
||||||
|
speaker_id = int(props.speaker) if props.speaker else 0
|
||||||
|
|
||||||
audio_file = api.generate_speech(
|
audio_file = api.generate_speech(
|
||||||
text=props.text,
|
text=props.text,
|
||||||
speaker_id=props.speaker_id,
|
speaker_id=speaker_id,
|
||||||
speed_scale=props.speed_scale,
|
speed_scale=props.speed_scale,
|
||||||
pitch_scale=props.pitch_scale,
|
pitch_scale=props.pitch_scale,
|
||||||
intonation_scale=props.intonation_scale,
|
intonation_scale=props.intonation_scale,
|
||||||
|
|
@ -93,6 +183,7 @@ class VOICEVOX_OT_generate_speech(Operator):
|
||||||
|
|
||||||
seq_editor = context.scene.sequence_editor
|
seq_editor = context.scene.sequence_editor
|
||||||
frame_current = context.scene.frame_current
|
frame_current = context.scene.frame_current
|
||||||
|
props = context.scene.voicevox
|
||||||
|
|
||||||
# 音声を追加
|
# 音声を追加
|
||||||
audio_strip = seq_editor.strips.new_sound(
|
audio_strip = seq_editor.strips.new_sound(
|
||||||
|
|
@ -106,17 +197,36 @@ class VOICEVOX_OT_generate_speech(Operator):
|
||||||
audio_duration_frames = audio_strip.frame_final_duration
|
audio_duration_frames = audio_strip.frame_final_duration
|
||||||
print(f"[VoiceVox] 音声の長さ: {audio_duration_frames} フレーム")
|
print(f"[VoiceVox] 音声の長さ: {audio_duration_frames} フレーム")
|
||||||
|
|
||||||
|
# 選択されているテキストストリップから設定を抽出(あれば)
|
||||||
|
text_settings = get_text_strip_settings(context)
|
||||||
|
|
||||||
# 字幕を追加(音声の長さに合わせる)
|
# 字幕を追加(音声の長さに合わせる)
|
||||||
props = context.scene.voicevox
|
|
||||||
subtitle_mgr = SubtitleManager()
|
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(
|
subtitle_mgr.add_subtitle(
|
||||||
context=context,
|
context=context,
|
||||||
text=text,
|
text=text,
|
||||||
frame_start=frame_current,
|
frame_start=frame_current,
|
||||||
duration_frames=audio_duration_frames, # 音声の長さに合わせる
|
duration_frames=audio_duration_frames, # 音声の長さに合わせる
|
||||||
font_size=props.subtitle_font_size,
|
font_size=font_size,
|
||||||
position_y=props.subtitle_position_y,
|
position_y=position_y,
|
||||||
channel=props.subtitle_channel,
|
channel=channel,
|
||||||
|
**extra_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -143,15 +253,35 @@ class VOICEVOX_OT_add_subtitle(Operator):
|
||||||
default_duration_sec = 3.0
|
default_duration_sec = 3.0
|
||||||
duration_frames = int(default_duration_sec * context.scene.render.fps)
|
duration_frames = int(default_duration_sec * context.scene.render.fps)
|
||||||
|
|
||||||
|
# 選択されているテキストストリップから設定を抽出(あれば)
|
||||||
|
text_settings = get_text_strip_settings(context)
|
||||||
|
|
||||||
subtitle_mgr = SubtitleManager()
|
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(
|
subtitle_mgr.add_subtitle(
|
||||||
context=context,
|
context=context,
|
||||||
text=props.text,
|
text=props.text,
|
||||||
frame_start=frame_current,
|
frame_start=frame_current,
|
||||||
duration_frames=duration_frames,
|
duration_frames=duration_frames,
|
||||||
font_size=props.subtitle_font_size,
|
font_size=font_size,
|
||||||
position_y=props.subtitle_position_y,
|
position_y=position_y,
|
||||||
channel=props.subtitle_channel,
|
channel=channel,
|
||||||
|
**extra_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.report({'INFO'}, "字幕を追加しました")
|
self.report({'INFO'}, "字幕を追加しました")
|
||||||
|
|
|
||||||
60
operators_reference.py
Normal file
60
operators_reference.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
"""
|
||||||
|
リファレンステキストストリップ管理用オペレーター
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
|
||||||
|
|
||||||
|
class VOICEVOX_OT_set_reference_text(Operator):
|
||||||
|
"""選択したテキストストリップをリファレンスとして設定"""
|
||||||
|
bl_idname = "voicevox.set_reference_text"
|
||||||
|
bl_label = "Set as Reference"
|
||||||
|
bl_description = "選択したテキストストリップをリファレンスとして設定します"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
if not context.scene.sequence_editor:
|
||||||
|
self.report({'ERROR'}, "シーケンスエディタがありません")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
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':
|
||||||
|
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:
|
||||||
|
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'}
|
||||||
|
|
||||||
|
|
||||||
|
class VOICEVOX_OT_clear_reference_text(Operator):
|
||||||
|
"""リファレンステキストストリップをクリア"""
|
||||||
|
bl_idname = "voicevox.clear_reference_text"
|
||||||
|
bl_label = "Clear Reference"
|
||||||
|
bl_description = "リファレンステキストストリップをクリアします"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
props = context.scene.voicevox
|
||||||
|
props.reference_text_strip = ""
|
||||||
|
self.report({'INFO'}, "リファレンステキストをクリアしました")
|
||||||
|
print(f"[VoiceVox] リファレンステキストをクリア")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
36
operators_speaker.py
Normal file
36
operators_speaker.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""
|
||||||
|
話者リスト更新用オペレーター
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
from .properties import update_speaker_cache
|
||||||
|
|
||||||
|
|
||||||
|
class VOICEVOX_OT_refresh_speakers(Operator):
|
||||||
|
"""VoiceVoxから話者リストを再取得"""
|
||||||
|
bl_idname = "voicevox.refresh_speakers"
|
||||||
|
bl_label = "Refresh Speakers"
|
||||||
|
bl_description = "VoiceVoxエンジンから話者リストを再取得します"
|
||||||
|
bl_options = {'REGISTER'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
props = context.scene.voicevox
|
||||||
|
|
||||||
|
# VoiceVox APIから話者リストを取得してキャッシュを更新
|
||||||
|
count = update_speaker_cache(props.voicevox_host, props.voicevox_port)
|
||||||
|
|
||||||
|
if count > 0:
|
||||||
|
self.report({'INFO'}, f"{count}人の話者を取得しました")
|
||||||
|
else:
|
||||||
|
self.report({'WARNING'}, "話者の取得に失敗しました")
|
||||||
|
|
||||||
|
# EnumPropertyを強制的に再評価
|
||||||
|
current = props.speaker
|
||||||
|
try:
|
||||||
|
props.speaker = '0'
|
||||||
|
props.speaker = current
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
14
panels.py
14
panels.py
|
|
@ -39,7 +39,9 @@ class VOICEVOX_PT_main_panel(Panel):
|
||||||
# 音声設定
|
# 音声設定
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.label(text="Voice Settings", icon='SPEAKER')
|
box.label(text="Voice Settings", icon='SPEAKER')
|
||||||
box.prop(props, "speaker_id")
|
row = box.row()
|
||||||
|
row.prop(props, "speaker", text="")
|
||||||
|
row.operator("voicevox.refresh_speakers", text="", icon='FILE_REFRESH')
|
||||||
box.prop(props, "speed_scale", slider=True)
|
box.prop(props, "speed_scale", slider=True)
|
||||||
box.prop(props, "pitch_scale", slider=True)
|
box.prop(props, "pitch_scale", slider=True)
|
||||||
box.prop(props, "intonation_scale", slider=True)
|
box.prop(props, "intonation_scale", slider=True)
|
||||||
|
|
@ -50,8 +52,14 @@ class VOICEVOX_PT_main_panel(Panel):
|
||||||
# 字幕設定
|
# 字幕設定
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.label(text="Subtitle Settings", icon='FONT_DATA')
|
box.label(text="Subtitle Settings", icon='FONT_DATA')
|
||||||
box.prop(props, "subtitle_font_size")
|
|
||||||
box.prop(props, "subtitle_position_y", slider=True)
|
# リファレンステキスト
|
||||||
|
if props.reference_text_strip:
|
||||||
|
row = box.row()
|
||||||
|
row.label(text=f"Ref: {props.reference_text_strip}", icon='LINKED')
|
||||||
|
row.operator("voicevox.clear_reference_text", text="", icon='X')
|
||||||
|
else:
|
||||||
|
box.operator("voicevox.set_reference_text", icon='EYEDROPPER')
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,63 @@ from bpy.props import (
|
||||||
EnumProperty,
|
EnumProperty,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# グローバル変数で話者リストをキャッシュ
|
||||||
|
_speaker_cache = []
|
||||||
|
|
||||||
|
|
||||||
|
def get_speaker_items(self, context):
|
||||||
|
"""キャッシュされた話者一覧を返す"""
|
||||||
|
global _speaker_cache
|
||||||
|
|
||||||
|
if not _speaker_cache:
|
||||||
|
# キャッシュが空の場合はデフォルトを返す
|
||||||
|
return [('0', 'リロードボタンを押してください', 'VoiceVoxから話者リストを取得')]
|
||||||
|
|
||||||
|
return _speaker_cache
|
||||||
|
|
||||||
|
|
||||||
|
def update_speaker_cache(host: str = "127.0.0.1", port: int = 50021):
|
||||||
|
"""VoiceVox APIから話者リストを取得してキャッシュを更新"""
|
||||||
|
global _speaker_cache
|
||||||
|
from .voicevox import VoiceVoxAPI
|
||||||
|
|
||||||
|
items = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
api = VoiceVoxAPI(host, port)
|
||||||
|
speakers = api.get_speakers()
|
||||||
|
|
||||||
|
if speakers:
|
||||||
|
for speaker in speakers:
|
||||||
|
name = speaker.get('name', 'Unknown')
|
||||||
|
styles = speaker.get('styles', [])
|
||||||
|
|
||||||
|
for style in styles:
|
||||||
|
style_name = style.get('name', '')
|
||||||
|
style_id = style.get('id', 0)
|
||||||
|
|
||||||
|
# 表示名: "キャラクター名 (スタイル名)"
|
||||||
|
display_name = f"{name} ({style_name})" if style_name else name
|
||||||
|
|
||||||
|
items.append((
|
||||||
|
str(style_id),
|
||||||
|
display_name,
|
||||||
|
f"Speaker ID: {style_id}",
|
||||||
|
))
|
||||||
|
|
||||||
|
_speaker_cache = items
|
||||||
|
return len(items)
|
||||||
|
else:
|
||||||
|
_speaker_cache = [('0', 'VoiceVoxに接続できません', '')]
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[VoiceVox] 話者一覧の取得に失敗: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
_speaker_cache = [('0', '接続エラー', str(e))]
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class VoiceVoxProperties(bpy.types.PropertyGroup):
|
class VoiceVoxProperties(bpy.types.PropertyGroup):
|
||||||
"""VoiceVoxプラグインのプロパティ"""
|
"""VoiceVoxプラグインのプロパティ"""
|
||||||
|
|
@ -38,6 +95,13 @@ class VoiceVoxProperties(bpy.types.PropertyGroup):
|
||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
speaker: EnumProperty(
|
||||||
|
name="Speaker",
|
||||||
|
description="話者を選択",
|
||||||
|
items=get_speaker_items,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 後方互換性のため残す(内部的にはspeakerを使用)
|
||||||
speaker_id: IntProperty(
|
speaker_id: IntProperty(
|
||||||
name="Speaker ID",
|
name="Speaker ID",
|
||||||
description="話者ID (VoiceVoxのキャラクター)",
|
description="話者ID (VoiceVoxのキャラクター)",
|
||||||
|
|
@ -77,23 +141,6 @@ class VoiceVoxProperties(bpy.types.PropertyGroup):
|
||||||
max=2.0,
|
max=2.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 字幕の設定
|
|
||||||
subtitle_font_size: IntProperty(
|
|
||||||
name="Font Size",
|
|
||||||
description="字幕のフォントサイズ",
|
|
||||||
default=48,
|
|
||||||
min=12,
|
|
||||||
max=200,
|
|
||||||
)
|
|
||||||
|
|
||||||
subtitle_position_y: FloatProperty(
|
|
||||||
name="Position Y",
|
|
||||||
description="字幕の垂直位置 (0.0 = 下, 1.0 = 上)",
|
|
||||||
default=0.1,
|
|
||||||
min=0.0,
|
|
||||||
max=1.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
# チャンネル設定
|
# チャンネル設定
|
||||||
audio_channel: IntProperty(
|
audio_channel: IntProperty(
|
||||||
name="Audio Channel",
|
name="Audio Channel",
|
||||||
|
|
@ -111,6 +158,13 @@ class VoiceVoxProperties(bpy.types.PropertyGroup):
|
||||||
max=128,
|
max=128,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# リファレンステキストストリップ
|
||||||
|
reference_text_strip: StringProperty(
|
||||||
|
name="Reference Text Strip",
|
||||||
|
description="設定を流用するリファレンステキストストリップの名前",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
# 出力設定
|
# 出力設定
|
||||||
output_directory: StringProperty(
|
output_directory: StringProperty(
|
||||||
name="Output Directory",
|
name="Output Directory",
|
||||||
|
|
|
||||||
70
subtitles.py
70
subtitles.py
|
|
@ -18,6 +18,7 @@ class SubtitleManager:
|
||||||
font_size: int = 48,
|
font_size: int = 48,
|
||||||
position_y: float = 0.1,
|
position_y: float = 0.1,
|
||||||
channel: int = 2,
|
channel: int = 2,
|
||||||
|
**extra_settings,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
シーケンサーに字幕を追加
|
シーケンサーに字幕を追加
|
||||||
|
|
@ -50,44 +51,45 @@ class SubtitleManager:
|
||||||
# テキスト内容を設定
|
# テキスト内容を設定
|
||||||
text_strip.text = text
|
text_strip.text = text
|
||||||
|
|
||||||
# Blender 5.0で利用可能なプロパティのみ設定
|
# デフォルト設定
|
||||||
try:
|
default_settings = {
|
||||||
text_strip.font_size = font_size
|
'use_shadow': True,
|
||||||
except AttributeError:
|
'shadow_color': (0.0, 0.0, 0.0, 0.8),
|
||||||
print(f"[Subtitle] font_size プロパティは利用できません")
|
'color': (1.0, 1.0, 1.0, 1.0),
|
||||||
|
'blend_type': 'ALPHA_OVER',
|
||||||
|
'align_x': 'CENTER',
|
||||||
|
'align_y': 'BOTTOM',
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
# デフォルト設定から開始し、extra_settingsで上書き
|
||||||
text_strip.use_shadow = True
|
all_settings = default_settings.copy()
|
||||||
text_strip.shadow_color = (0.0, 0.0, 0.0, 0.8)
|
if extra_settings:
|
||||||
except AttributeError:
|
all_settings.update(extra_settings)
|
||||||
print(f"[Subtitle] shadow プロパティは利用できません")
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 全てのプロパティを設定
|
||||||
|
for prop, value in all_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:
|
try:
|
||||||
text_strip.location[0] = 0.5 # 水平中央
|
text_strip.location[0] = position_x
|
||||||
text_strip.location[1] = position_y # 垂直位置
|
text_strip.location[1] = position_y_val
|
||||||
except (AttributeError, TypeError):
|
print(f"[Subtitle] location = ({position_x}, {position_y_val})")
|
||||||
print(f"[Subtitle] location プロパティは利用できません")
|
except (AttributeError, TypeError) as e:
|
||||||
|
print(f"[Subtitle] location プロパティは利用できません: {e}")
|
||||||
# テキストの配置(Blender 5.0では変更された可能性あり)
|
|
||||||
try:
|
|
||||||
text_strip.align_x = 'CENTER'
|
|
||||||
text_strip.align_y = 'BOTTOM'
|
|
||||||
except AttributeError:
|
|
||||||
# Blender 5.0では align_x/align_y が削除または変更された
|
|
||||||
print(f"[Subtitle] align_x/align_y プロパティは利用できません")
|
|
||||||
|
|
||||||
# テキストの色(白)
|
|
||||||
try:
|
|
||||||
text_strip.color = (1.0, 1.0, 1.0, 1.0)
|
|
||||||
except AttributeError:
|
|
||||||
print(f"[Subtitle] color プロパティは利用できません")
|
|
||||||
|
|
||||||
# ブレンドモード
|
|
||||||
try:
|
|
||||||
text_strip.blend_type = 'ALPHA_OVER'
|
|
||||||
except AttributeError:
|
|
||||||
print(f"[Subtitle] blend_type プロパティは利用できません")
|
|
||||||
|
|
||||||
print(f"Subtitle added: '{text}' at frame {frame_start}")
|
print(f"Subtitle added: '{text}' at frame {frame_start}")
|
||||||
|
|
||||||
|
|
|
||||||
70
voicevox.py
70
voicevox.py
|
|
@ -3,12 +3,12 @@ VoiceVox API連携
|
||||||
VoiceVoxエンジンとの通信を担当
|
VoiceVoxエンジンとの通信を担当
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import Optional
|
import json
|
||||||
import urllib.request
|
import os
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class VoiceVoxAPI:
|
class VoiceVoxAPI:
|
||||||
|
|
@ -34,9 +34,16 @@ class VoiceVoxAPI:
|
||||||
url = f"{self.base_url}/speakers"
|
url = f"{self.base_url}/speakers"
|
||||||
with urllib.request.urlopen(url, timeout=10) as response:
|
with urllib.request.urlopen(url, timeout=10) as response:
|
||||||
data = response.read()
|
data = response.read()
|
||||||
return json.loads(data)
|
# UTF-8として明示的にデコード
|
||||||
|
text = data.decode("utf-8")
|
||||||
|
speakers = json.loads(text)
|
||||||
|
print(f"[VoiceVox API] 話者を {len(speakers)} 人取得")
|
||||||
|
return speakers
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to get speakers: {e}")
|
print(f"Failed to get speakers: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def generate_speech(
|
def generate_speech(
|
||||||
|
|
@ -62,45 +69,51 @@ class VoiceVoxAPI:
|
||||||
# 1. 音声合成用のクエリを作成
|
# 1. 音声合成用のクエリを作成
|
||||||
print(f"[VoiceVox API] Step 2: 音声クエリを生成中...")
|
print(f"[VoiceVox API] Step 2: 音声クエリを生成中...")
|
||||||
query_url = f"{self.base_url}/audio_query"
|
query_url = f"{self.base_url}/audio_query"
|
||||||
query_params = urllib.parse.urlencode({
|
query_params = urllib.parse.urlencode(
|
||||||
'text': text,
|
{
|
||||||
'speaker': speaker_id,
|
"text": text,
|
||||||
})
|
"speaker": speaker_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
full_query_url = f"{query_url}?{query_params}"
|
full_query_url = f"{query_url}?{query_params}"
|
||||||
print(f"[VoiceVox API] リクエストURL: {full_query_url}")
|
print(f"[VoiceVox API] リクエストURL: {full_query_url}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# POSTリクエストにするため空のdataを渡す
|
# POSTリクエストにするため空のdataを渡す
|
||||||
req = urllib.request.Request(full_query_url, data=b'', method='POST')
|
req = urllib.request.Request(full_query_url, data=b"", method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=10) as response:
|
with urllib.request.urlopen(req, timeout=10) as response:
|
||||||
query_data = json.loads(response.read())
|
query_data = json.loads(response.read())
|
||||||
print(f"[VoiceVox API] クエリ生成成功")
|
print(f"[VoiceVox API] クエリ生成成功")
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
print(f"[VoiceVox API Error] HTTPエラー: {e.code} {e.reason}")
|
print(f"[VoiceVox API Error] HTTPエラー: {e.code} {e.reason}")
|
||||||
print(f"[VoiceVox API Error] レスポンス: {e.read().decode('utf-8', errors='replace')}")
|
print(
|
||||||
|
f"[VoiceVox API Error] レスポンス: {e.read().decode('utf-8', errors='replace')}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
except urllib.error.URLError as e:
|
except urllib.error.URLError as e:
|
||||||
print(f"[VoiceVox API Error] 接続エラー: {e.reason}")
|
print(f"[VoiceVox API Error] 接続エラー: {e.reason}")
|
||||||
print(f"[VoiceVox API Error] VoiceVoxエンジンが起動しているか確認してください")
|
print(
|
||||||
raise
|
f"[VoiceVox API Error] VoiceVoxエンジンが起動しているか確認してください"
|
||||||
|
)
|
||||||
|
raisee
|
||||||
|
|
||||||
# パラメータを適用
|
# パラメータを適用
|
||||||
query_data['speedScale'] = speed_scale
|
query_data["speedScale"] = speed_scale
|
||||||
query_data['pitchScale'] = pitch_scale
|
query_data["pitchScale"] = pitch_scale
|
||||||
query_data['intonationScale'] = intonation_scale
|
query_data["intonationScale"] = intonation_scale
|
||||||
query_data['volumeScale'] = volume_scale
|
query_data["volumeScale"] = volume_scale
|
||||||
|
|
||||||
# 2. 音声合成を実行
|
# 2. 音声合成を実行
|
||||||
print(f"[VoiceVox API] Step 3: 音声合成を実行中...")
|
print(f"[VoiceVox API] Step 3: 音声合成を実行中...")
|
||||||
synthesis_url = f"{self.base_url}/synthesis"
|
synthesis_url = f"{self.base_url}/synthesis"
|
||||||
synthesis_params = urllib.parse.urlencode({'speaker': speaker_id})
|
synthesis_params = urllib.parse.urlencode({"speaker": speaker_id})
|
||||||
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
f"{synthesis_url}?{synthesis_params}",
|
f"{synthesis_url}?{synthesis_params}",
|
||||||
data=json.dumps(query_data).encode('utf-8'),
|
data=json.dumps(query_data).encode("utf-8"),
|
||||||
headers={'Content-Type': 'application/json'},
|
headers={"Content-Type": "application/json"},
|
||||||
method='POST',
|
method="POST",
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -109,27 +122,32 @@ class VoiceVoxAPI:
|
||||||
print(f"[VoiceVox API] 音声合成成功 ({len(audio_data)} bytes)")
|
print(f"[VoiceVox API] 音声合成成功 ({len(audio_data)} bytes)")
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
print(f"[VoiceVox API Error] 合成HTTPエラー: {e.code} {e.reason}")
|
print(f"[VoiceVox API Error] 合成HTTPエラー: {e.code} {e.reason}")
|
||||||
print(f"[VoiceVox API Error] レスポンス: {e.read().decode('utf-8', errors='replace')}")
|
print(
|
||||||
|
f"[VoiceVox API Error] レスポンス: {e.read().decode('utf-8', errors='replace')}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# 3. 音声ファイルを保存
|
# 3. 音声ファイルを保存
|
||||||
print(f"[VoiceVox API] Step 4: 音声ファイルを保存中...")
|
print(f"[VoiceVox API] Step 4: 音声ファイルを保存中...")
|
||||||
text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()[:8]
|
text_hash = hashlib.md5(text.encode("utf-8")).hexdigest()[:8]
|
||||||
filename = f"voicevox_{text_hash}_{speaker_id}.wav"
|
filename = f"voicevox_{text_hash}_{speaker_id}.wav"
|
||||||
filepath = os.path.join(output_dir, filename)
|
filepath = os.path.join(output_dir, filename)
|
||||||
|
|
||||||
with open(filepath, 'wb') as f:
|
with open(filepath, "wb") as f:
|
||||||
f.write(audio_data)
|
f.write(audio_data)
|
||||||
|
|
||||||
print(f"[VoiceVox API] 音声保存完了: {filepath}")
|
print(f"[VoiceVox API] 音声保存完了: {filepath}")
|
||||||
return filepath
|
return filepath
|
||||||
|
|
||||||
except urllib.error.URLError as e:
|
except urllib.error.URLError as e:
|
||||||
error_msg = f"接続エラー: VoiceVoxエンジンに接続できません ({self.base_url})"
|
error_msg = (
|
||||||
|
f"接続エラー: VoiceVoxエンジンに接続できません ({self.base_url})"
|
||||||
|
)
|
||||||
print(f"[VoiceVox API Error] {error_msg}")
|
print(f"[VoiceVox API Error] {error_msg}")
|
||||||
print(f"[VoiceVox API Error] 詳細: {e}")
|
print(f"[VoiceVox API Error] 詳細: {e}")
|
||||||
self.last_error = error_msg
|
self.last_error = error_msg
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
|
|
@ -137,6 +155,7 @@ class VoiceVoxAPI:
|
||||||
print(f"[VoiceVox API Error] {error_msg}")
|
print(f"[VoiceVox API Error] {error_msg}")
|
||||||
self.last_error = error_msg
|
self.last_error = error_msg
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -144,5 +163,6 @@ class VoiceVoxAPI:
|
||||||
print(f"[VoiceVox API Error] 音声生成失敗: {error_msg}")
|
print(f"[VoiceVox API Error] 音声生成失敗: {error_msg}")
|
||||||
self.last_error = error_msg
|
self.last_error = error_msg
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user