init
This commit is contained in:
commit
aead0ff136
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Blender
|
||||
*.blend1
|
||||
*.blend2
|
||||
|
||||
# VoiceVox音声ファイル
|
||||
*.wav
|
||||
audio_cache/
|
||||
|
||||
# 環境
|
||||
.direnv/
|
||||
.envrc.local
|
||||
result
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
117
README.md
Normal file
117
README.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# Blender VoiceVox Plugin
|
||||
|
||||
Blender 5.0 向けの VoiceVox 音声合成と字幕表示を統合したアドオンです。
|
||||
|
||||
## 機能
|
||||
|
||||
- **音声合成**: VoiceVox エンジンを使用してテキストから音声を生成
|
||||
- **字幕表示**: 生成した音声と同期する字幕をシーケンサーに追加
|
||||
- **カスタマイズ可能**: 話者、話速、音高、抑揚、音量などを調整可能
|
||||
- **自動統合**: 生成した音声と字幕を自動的にシーケンサーに追加
|
||||
|
||||
## 必要なものeee
|
||||
|
||||
- Blender 5.0 以上
|
||||
- VoiceVox エンジン(ローカルで実行中であること)
|
||||
- https://voicevox.hiroshiba.jp/ からダウンロード
|
||||
- デフォルトでは `127.0.0.1:50021` で動作
|
||||
|
||||
## インストール
|
||||
|
||||
### 開発環境のセットアップ(Nix + direnv)
|
||||
|
||||
```bash
|
||||
# direnv を許可
|
||||
direnv allow
|
||||
|
||||
# 開発環境に入る(自動的に有効化されます)
|
||||
# または手動で: nix develop
|
||||
```
|
||||
|
||||
### Blender へのインストール
|
||||
|
||||
1. このディレクトリ全体を Blender のアドオンフォルダにシンボリックリンク:
|
||||
```bash
|
||||
ln -s $(pwd) ~/.config/blender/5.0/scripts/addons/blender-voicevox-pl
|
||||
```
|
||||
|
||||
2. または、Blender の UI からインストール:
|
||||
- Edit > Preferences > Add-ons
|
||||
- Install ボタンをクリック
|
||||
- このディレクトリを zip 化したファイルを選択
|
||||
|
||||
3. アドオンを有効化:
|
||||
- "VoiceVox TTS & Subtitles" を検索
|
||||
- チェックボックスをオンにする
|
||||
|
||||
## 使い方
|
||||
|
||||
1. **VoiceVox エンジンを起動**
|
||||
- VoiceVox アプリケーションを起動するか
|
||||
- VoiceVox エンジンをコマンドラインで起動
|
||||
|
||||
2. **Blender でシーケンスエディタを開く**
|
||||
- ウィンドウタイプを "Video Sequencing" に変更
|
||||
|
||||
3. **VoiceVox パネルを開く**
|
||||
- サイドバー(N キー)の "VoiceVox" タブをクリック
|
||||
|
||||
4. **接続をテスト**
|
||||
- "Test Connection" ボタンをクリックして VoiceVox に接続できるか確認
|
||||
|
||||
5. **音声と字幕を生成**
|
||||
- テキストを入力
|
||||
- 話者や音声パラメータを調整
|
||||
- "Generate Speech & Subtitle" ボタンをクリック
|
||||
|
||||
## 開発
|
||||
|
||||
### プロジェクト構成
|
||||
|
||||
```
|
||||
.
|
||||
├── __init__.py # アドオンのエントリーポイント
|
||||
├── operators.py # ユーザー操作の実装
|
||||
├── panels.py # UI パネルの定義
|
||||
├── properties.py # プロパティの定義
|
||||
├── voicevox.py # VoiceVox API 連携
|
||||
├── subtitles.py # 字幕管理
|
||||
├── flake.nix # Nix 開発環境設定
|
||||
└── README.md # このファイル
|
||||
```
|
||||
|
||||
### Blender でのテスト
|
||||
|
||||
```bash
|
||||
# バックグラウンドでテスト
|
||||
blender --background --python-expr "import bpy; bpy.ops.preferences.addon_enable(module='blender-voicevox-pl')"
|
||||
|
||||
# GUI でテスト(開発用)
|
||||
blender
|
||||
```
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
### VoiceVox に接続できない
|
||||
|
||||
- VoiceVox エンジンが起動しているか確認
|
||||
- ホスト・ポート設定が正しいか確認(デフォルト: 127.0.0.1:50021)
|
||||
- ファイアウォールがブロックしていないか確認
|
||||
|
||||
### 音声ファイルが生成されない
|
||||
|
||||
- 出力ディレクトリの書き込み権限を確認
|
||||
- VoiceVox エンジンのログを確認
|
||||
|
||||
### 字幕が表示されない
|
||||
|
||||
- シーケンスエディタで正しいチャンネルを表示しているか確認
|
||||
- レンダー設定で字幕のストリップが有効になっているか確認
|
||||
|
||||
## ライセンス
|
||||
|
||||
MIT License
|
||||
|
||||
## 作者
|
||||
|
||||
Your Name
|
||||
56
__init__.py
Normal file
56
__init__.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""
|
||||
Blender VoiceVox Plugin
|
||||
VoiceVoxを使用した音声合成と字幕表示を同時に行うBlenderアドオン
|
||||
"""
|
||||
|
||||
bl_info = {
|
||||
"name": "VoiceVox TTS & Subtitles",
|
||||
"author": "Your Name",
|
||||
"version": (1, 0, 0),
|
||||
"blender": (5, 0, 0),
|
||||
"location": "View3D > Sidebar > VoiceVox",
|
||||
"description": "VoiceVoxによる音声合成と字幕表示",
|
||||
"warning": "",
|
||||
"doc_url": "",
|
||||
"category": "Sequencer",
|
||||
}
|
||||
|
||||
import bpy
|
||||
from . import operators
|
||||
from . import panels
|
||||
from . import properties
|
||||
|
||||
|
||||
classes = (
|
||||
properties.VoiceVoxProperties,
|
||||
operators.VOICEVOX_OT_generate_speech,
|
||||
operators.VOICEVOX_OT_add_subtitle,
|
||||
operators.VOICEVOX_OT_test_connection,
|
||||
panels.VOICEVOX_PT_main_panel,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
"""アドオンを登録"""
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# シーンにプロパティを追加
|
||||
bpy.types.Scene.voicevox = bpy.props.PointerProperty(type=properties.VoiceVoxProperties)
|
||||
|
||||
print("VoiceVox Plugin registered")
|
||||
|
||||
|
||||
def unregister():
|
||||
"""アドオンを登録解除"""
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# シーンからプロパティを削除
|
||||
del bpy.types.Scene.voicevox
|
||||
|
||||
print("VoiceVox Plugin unregistered")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
61
flake.lock
Normal file
61
flake.lock
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770115704,
|
||||
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
54
flake.nix
Normal file
54
flake.nix
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
description = "Blender VoiceVox Plugin Development Environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
# Python開発環境
|
||||
python311
|
||||
python311Packages.pip
|
||||
python311Packages.requests
|
||||
python311Packages.pillow
|
||||
|
||||
# Blender (開発・テスト用)
|
||||
blender
|
||||
|
||||
# 開発ツール
|
||||
git
|
||||
|
||||
# VoiceVoxエンジン(オプション)
|
||||
# voicevox-engine は Nix パッケージとして提供されていない可能性があるため
|
||||
# 別途インストールが必要
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
python --version
|
||||
blender --version | head -n 1
|
||||
|
||||
# Pythonパスにカレントディレクトリを追加
|
||||
export PYTHONPATH="$PWD:$PYTHONPATH"
|
||||
|
||||
# アドオンのインストールパスを環境変数として設定
|
||||
export BLENDER_USER_SCRIPTS="$HOME/.config/blender/5.0/scripts"
|
||||
export BLENDER_USER_ADDONS="$BLENDER_USER_SCRIPTS/addons"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
158
operators.py
Normal file
158
operators.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""
|
||||
オペレーター
|
||||
ユーザーが実行できる操作を定義
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from .voicevox import VoiceVoxAPI
|
||||
from .subtitles import SubtitleManager
|
||||
|
||||
|
||||
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}")
|
||||
|
||||
audio_file = api.generate_speech(
|
||||
text=props.text,
|
||||
speaker_id=props.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
|
||||
|
||||
# 音声を追加
|
||||
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} フレーム")
|
||||
|
||||
# 字幕を追加(音声の長さに合わせる)
|
||||
props = context.scene.voicevox
|
||||
subtitle_mgr = SubtitleManager()
|
||||
subtitle_mgr.add_subtitle(
|
||||
context=context,
|
||||
text=text,
|
||||
frame_start=frame_current,
|
||||
duration_frames=audio_duration_frames, # 音声の長さに合わせる
|
||||
font_size=props.subtitle_font_size,
|
||||
position_y=props.subtitle_position_y,
|
||||
channel=props.subtitle_channel,
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
subtitle_mgr = SubtitleManager()
|
||||
subtitle_mgr.add_subtitle(
|
||||
context=context,
|
||||
text=props.text,
|
||||
frame_start=frame_current,
|
||||
duration_frames=duration_frames,
|
||||
font_size=props.subtitle_font_size,
|
||||
position_y=props.subtitle_position_y,
|
||||
channel=props.subtitle_channel,
|
||||
)
|
||||
|
||||
self.report({'INFO'}, "字幕を追加しました")
|
||||
return {'FINISHED'}
|
||||
79
panels.py
Normal file
79
panels.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"""
|
||||
UIパネル
|
||||
Blenderのサイドバーに表示されるUIを定義
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.types import Panel
|
||||
|
||||
|
||||
class VOICEVOX_PT_main_panel(Panel):
|
||||
"""VoiceVoxメインパネル"""
|
||||
bl_label = "VoiceVox TTS"
|
||||
bl_idname = "VOICEVOX_PT_main_panel"
|
||||
bl_space_type = 'SEQUENCE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = 'VoiceVox'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.voicevox
|
||||
|
||||
# 接続設定
|
||||
box = layout.box()
|
||||
box.label(text="VoiceVox Engine", icon='NETWORK_DRIVE')
|
||||
row = box.row()
|
||||
row.prop(props, "voicevox_host")
|
||||
row.prop(props, "voicevox_port")
|
||||
box.operator("voicevox.test_connection", icon='FILE_REFRESH')
|
||||
|
||||
layout.separator()
|
||||
|
||||
# テキスト入力
|
||||
box = layout.box()
|
||||
box.label(text="Text to Speech", icon='FILE_TEXT')
|
||||
box.prop(props, "text", text="")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# 音声設定
|
||||
box = layout.box()
|
||||
box.label(text="Voice Settings", icon='SPEAKER')
|
||||
box.prop(props, "speaker_id")
|
||||
box.prop(props, "speed_scale", slider=True)
|
||||
box.prop(props, "pitch_scale", slider=True)
|
||||
box.prop(props, "intonation_scale", slider=True)
|
||||
box.prop(props, "volume_scale", slider=True)
|
||||
|
||||
layout.separator()
|
||||
|
||||
# 字幕設定
|
||||
box = layout.box()
|
||||
box.label(text="Subtitle Settings", icon='FONT_DATA')
|
||||
box.prop(props, "subtitle_font_size")
|
||||
box.prop(props, "subtitle_position_y", slider=True)
|
||||
|
||||
layout.separator()
|
||||
|
||||
# チャンネル設定
|
||||
box = layout.box()
|
||||
box.label(text="Channel Settings", icon='LINENUMBERS_ON')
|
||||
row = box.row()
|
||||
row.prop(props, "audio_channel")
|
||||
row.prop(props, "subtitle_channel")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# 出力設定
|
||||
box = layout.box()
|
||||
box.label(text="Output Settings", icon='FILE_FOLDER')
|
||||
box.prop(props, "output_directory")
|
||||
box.prop(props, "auto_add_to_sequencer")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# アクション
|
||||
col = layout.column(align=True)
|
||||
col.scale_y = 1.5
|
||||
col.operator("voicevox.generate_speech", icon='PLAY_SOUND', text="Generate Speech & Subtitle")
|
||||
col.operator("voicevox.add_subtitle", icon='FONT_DATA', text="Add Subtitle Only")
|
||||
126
properties.py
Normal file
126
properties.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""
|
||||
プロパティ定義
|
||||
アドオンで使用する設定値やパラメータを定義
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
|
||||
class VoiceVoxProperties(bpy.types.PropertyGroup):
|
||||
"""VoiceVoxプラグインのプロパティ"""
|
||||
|
||||
# VoiceVox エンジンの設定
|
||||
voicevox_host: StringProperty(
|
||||
name="VoiceVox Host",
|
||||
description="VoiceVoxエンジンのホストアドレス",
|
||||
default="127.0.0.1",
|
||||
)
|
||||
|
||||
voicevox_port: IntProperty(
|
||||
name="VoiceVox Port",
|
||||
description="VoiceVoxエンジンのポート番号",
|
||||
default=50021,
|
||||
min=1,
|
||||
max=65535,
|
||||
)
|
||||
|
||||
# 音声合成の設定
|
||||
text: StringProperty(
|
||||
name="Text",
|
||||
description="音声合成するテキスト",
|
||||
default="",
|
||||
)
|
||||
|
||||
speaker_id: IntProperty(
|
||||
name="Speaker ID",
|
||||
description="話者ID (VoiceVoxのキャラクター)",
|
||||
default=0,
|
||||
min=0,
|
||||
)
|
||||
|
||||
speed_scale: FloatProperty(
|
||||
name="Speed",
|
||||
description="話速",
|
||||
default=1.0,
|
||||
min=0.5,
|
||||
max=2.0,
|
||||
)
|
||||
|
||||
pitch_scale: FloatProperty(
|
||||
name="Pitch",
|
||||
description="音高",
|
||||
default=0.0,
|
||||
min=-0.15,
|
||||
max=0.15,
|
||||
)
|
||||
|
||||
intonation_scale: FloatProperty(
|
||||
name="Intonation",
|
||||
description="抑揚",
|
||||
default=1.0,
|
||||
min=0.0,
|
||||
max=2.0,
|
||||
)
|
||||
|
||||
volume_scale: FloatProperty(
|
||||
name="Volume",
|
||||
description="音量",
|
||||
default=1.0,
|
||||
min=0.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(
|
||||
name="Audio Channel",
|
||||
description="音声を配置するチャンネル番号",
|
||||
default=1,
|
||||
min=1,
|
||||
max=128,
|
||||
)
|
||||
|
||||
subtitle_channel: IntProperty(
|
||||
name="Subtitle Channel",
|
||||
description="字幕を配置するチャンネル番号",
|
||||
default=2,
|
||||
min=1,
|
||||
max=128,
|
||||
)
|
||||
|
||||
# 出力設定
|
||||
output_directory: StringProperty(
|
||||
name="Output Directory",
|
||||
description="音声ファイルの出力先ディレクトリ",
|
||||
default="//audio_cache/",
|
||||
subtype='DIR_PATH',
|
||||
)
|
||||
|
||||
auto_add_to_sequencer: BoolProperty(
|
||||
name="Auto Add to Sequencer",
|
||||
description="生成した音声と字幕を自動的にシーケンサーに追加",
|
||||
default=True,
|
||||
)
|
||||
104
subtitles.py
Normal file
104
subtitles.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""
|
||||
字幕管理
|
||||
Blenderのシーケンサーに字幕を追加
|
||||
"""
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
class SubtitleManager:
|
||||
"""字幕の追加と管理を担当"""
|
||||
|
||||
def add_subtitle(
|
||||
self,
|
||||
context,
|
||||
text: str,
|
||||
frame_start: int,
|
||||
duration_frames: int,
|
||||
font_size: int = 48,
|
||||
position_y: float = 0.1,
|
||||
channel: int = 2,
|
||||
):
|
||||
"""
|
||||
シーケンサーに字幕を追加
|
||||
|
||||
Args:
|
||||
context: Blenderコンテキスト
|
||||
text: 字幕テキスト
|
||||
frame_start: 開始フレーム
|
||||
duration_frames: 表示期間(フレーム数)
|
||||
font_size: フォントサイズ
|
||||
position_y: 垂直位置 (0.0-1.0)
|
||||
channel: チャンネル番号
|
||||
"""
|
||||
scene = context.scene
|
||||
|
||||
if not scene.sequence_editor:
|
||||
scene.sequence_editor_create()
|
||||
|
||||
seq_editor = scene.sequence_editor
|
||||
|
||||
# テキストストリップを追加
|
||||
text_strip = seq_editor.strips.new_effect(
|
||||
name="Subtitle",
|
||||
type='TEXT',
|
||||
channel=channel,
|
||||
frame_start=frame_start,
|
||||
length=duration_frames,
|
||||
)
|
||||
|
||||
# テキスト内容を設定
|
||||
text_strip.text = text
|
||||
|
||||
# Blender 5.0で利用可能なプロパティのみ設定
|
||||
try:
|
||||
text_strip.font_size = font_size
|
||||
except AttributeError:
|
||||
print(f"[Subtitle] font_size プロパティは利用できません")
|
||||
|
||||
try:
|
||||
text_strip.use_shadow = True
|
||||
text_strip.shadow_color = (0.0, 0.0, 0.0, 0.8)
|
||||
except AttributeError:
|
||||
print(f"[Subtitle] shadow プロパティは利用できません")
|
||||
|
||||
# 位置を設定
|
||||
try:
|
||||
text_strip.location[0] = 0.5 # 水平中央
|
||||
text_strip.location[1] = position_y # 垂直位置
|
||||
except (AttributeError, TypeError):
|
||||
print(f"[Subtitle] location プロパティは利用できません")
|
||||
|
||||
# テキストの配置(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}")
|
||||
|
||||
return text_strip
|
||||
|
||||
def update_subtitle(self, text_strip, **kwargs):
|
||||
"""既存の字幕を更新"""
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(text_strip, key):
|
||||
setattr(text_strip, key, value)
|
||||
|
||||
def remove_subtitle(self, seq_editor, text_strip):
|
||||
"""字幕を削除"""
|
||||
seq_editor.strips.remove(text_strip)
|
||||
148
voicevox.py
Normal file
148
voicevox.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
"""
|
||||
VoiceVox API連携
|
||||
VoiceVoxエンジンとの通信を担当
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import hashlib
|
||||
from typing import Optional
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
|
||||
class VoiceVoxAPI:
|
||||
"""VoiceVox エンジンとの通信を管理"""
|
||||
|
||||
def __init__(self, host: str = "127.0.0.1", port: int = 50021):
|
||||
self.base_url = f"http://{host}:{port}"
|
||||
self.last_error = "" # 最後のエラーメッセージを保存
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""接続テスト"""
|
||||
try:
|
||||
url = f"{self.base_url}/version"
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
return response.status == 200
|
||||
except Exception as e:
|
||||
print(f"Connection test failed: {e}")
|
||||
return False
|
||||
|
||||
def get_speakers(self) -> Optional[list]:
|
||||
"""利用可能な話者一覧を取得"""
|
||||
try:
|
||||
url = f"{self.base_url}/speakers"
|
||||
with urllib.request.urlopen(url, timeout=10) as response:
|
||||
data = response.read()
|
||||
return json.loads(data)
|
||||
except Exception as e:
|
||||
print(f"Failed to get speakers: {e}")
|
||||
return None
|
||||
|
||||
def generate_speech(
|
||||
self,
|
||||
text: str,
|
||||
speaker_id: int = 0,
|
||||
speed_scale: float = 1.0,
|
||||
pitch_scale: float = 0.0,
|
||||
intonation_scale: float = 1.0,
|
||||
volume_scale: float = 1.0,
|
||||
output_dir: str = "/tmp",
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
音声を生成して保存
|
||||
|
||||
Returns:
|
||||
生成された音声ファイルのパス、失敗時はNone
|
||||
"""
|
||||
try:
|
||||
print(f"[VoiceVox API] Step 1: 出力ディレクトリを作成: {output_dir}")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 1. 音声合成用のクエリを作成
|
||||
print(f"[VoiceVox API] Step 2: 音声クエリを生成中...")
|
||||
query_url = f"{self.base_url}/audio_query"
|
||||
query_params = urllib.parse.urlencode({
|
||||
'text': text,
|
||||
'speaker': speaker_id,
|
||||
})
|
||||
|
||||
full_query_url = f"{query_url}?{query_params}"
|
||||
print(f"[VoiceVox API] リクエストURL: {full_query_url}")
|
||||
|
||||
try:
|
||||
# POSTリクエストにするため空のdataを渡す
|
||||
req = urllib.request.Request(full_query_url, data=b'', method='POST')
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
query_data = json.loads(response.read())
|
||||
print(f"[VoiceVox API] クエリ生成成功")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"[VoiceVox API Error] HTTPエラー: {e.code} {e.reason}")
|
||||
print(f"[VoiceVox API Error] レスポンス: {e.read().decode('utf-8', errors='replace')}")
|
||||
raise
|
||||
except urllib.error.URLError as e:
|
||||
print(f"[VoiceVox API Error] 接続エラー: {e.reason}")
|
||||
print(f"[VoiceVox API Error] VoiceVoxエンジンが起動しているか確認してください")
|
||||
raise
|
||||
|
||||
# パラメータを適用
|
||||
query_data['speedScale'] = speed_scale
|
||||
query_data['pitchScale'] = pitch_scale
|
||||
query_data['intonationScale'] = intonation_scale
|
||||
query_data['volumeScale'] = volume_scale
|
||||
|
||||
# 2. 音声合成を実行
|
||||
print(f"[VoiceVox API] Step 3: 音声合成を実行中...")
|
||||
synthesis_url = f"{self.base_url}/synthesis"
|
||||
synthesis_params = urllib.parse.urlencode({'speaker': speaker_id})
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{synthesis_url}?{synthesis_params}",
|
||||
data=json.dumps(query_data).encode('utf-8'),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST',
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
audio_data = response.read()
|
||||
print(f"[VoiceVox API] 音声合成成功 ({len(audio_data)} bytes)")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"[VoiceVox API Error] 合成HTTPエラー: {e.code} {e.reason}")
|
||||
print(f"[VoiceVox API Error] レスポンス: {e.read().decode('utf-8', errors='replace')}")
|
||||
raise
|
||||
|
||||
# 3. 音声ファイルを保存
|
||||
print(f"[VoiceVox API] Step 4: 音声ファイルを保存中...")
|
||||
text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()[:8]
|
||||
filename = f"voicevox_{text_hash}_{speaker_id}.wav"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(audio_data)
|
||||
|
||||
print(f"[VoiceVox API] 音声保存完了: {filepath}")
|
||||
return filepath
|
||||
|
||||
except urllib.error.URLError as e:
|
||||
error_msg = f"接続エラー: VoiceVoxエンジンに接続できません ({self.base_url})"
|
||||
print(f"[VoiceVox API Error] {error_msg}")
|
||||
print(f"[VoiceVox API Error] 詳細: {e}")
|
||||
self.last_error = error_msg
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
except urllib.error.HTTPError as e:
|
||||
error_msg = f"HTTPエラー {e.code}: {e.reason}"
|
||||
print(f"[VoiceVox API Error] {error_msg}")
|
||||
self.last_error = error_msg
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
except Exception as e:
|
||||
error_msg = f"{type(e).__name__}: {str(e)}"
|
||||
print(f"[VoiceVox API Error] 音声生成失敗: {error_msg}")
|
||||
self.last_error = error_msg
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
Loading…
Reference in New Issue
Block a user