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