cang-ying/engines/voice-emotion-compiler.py

412 lines
14 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VOICE-EMOTION-COMPILER
语音情感编译器 "苏白·大声·自信"转成 TTS 参数
功能:
1. 情感标签解析 ("苏白·大声·自信" rate/pitch/volume)
2. 支持 Edge-TTS 和豆包语音 A/B 测试
3. 生成 voice_profile.hdlp Agent_04 读取
4. 批量生成不同情感参数的音频供 A/B 测试
用法:
python voice-emotion-compiler.py --text "未来的天下第一宗!" --emotion "苏白·大声·自信" --output su-bai-loud.mp3
python voice-emotion-compiler.py --ab-test --text "你好" --emotion "苏白·平静"
python voice-emotion-compiler.py --generate-profile --character "苏白"
"""
import os
import sys
import json
import argparse
import importlib.util
from pathlib import Path
from datetime import datetime
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "engines"))
# 导入现有的 TTS 引擎。文件名是 tts-engine.py不能用普通 import。
tts_engine_path = PROJECT_ROOT / "engines" / "tts-engine.py"
try:
spec = importlib.util.spec_from_file_location("tts_engine", tts_engine_path)
tts_engine = importlib.util.module_from_spec(spec)
spec.loader.exec_module(tts_engine)
generate_speech = tts_engine.generate_speech
generate_by_character = tts_engine.generate_by_character
load_voice_config = tts_engine.load_voice_config
except Exception as exc:
print(f"⚠️ 无法导入 tts-engine将使用简化模式: {exc}")
generate_speech = None
generate_by_character = None
class VoiceEmotionCompiler:
"""语音情感编译器"""
# 情感映射表: "角色·情感·强度" → TTS 参数
EMOTION_MAP = {
# 苏白情感库
"苏白·平静·正常": {
"rate": "+0%",
"pitch": "+0Hz",
"volume": "+0%",
"voice": "zh-CN-XiaoxiaoNeural", # 阳光少年音
"style": None, # Edge-TTS 不支持 style用参数模拟
},
"苏白·大声·自信": {
"rate": "+20%", # 语速加快
"pitch": "+10Hz", # 音调略高
"volume": "+15%", # 音量增加
"voice": "zh-CN-XiaoxiaoNeural",
"style": None,
},
"苏白·小声·犹豫": {
"rate": "-15%",
"pitch": "-5Hz",
"volume": "-10%",
"voice": "zh-CN-XiaoxiaoNeural",
"style": None,
},
"苏白·生气·愤怒": {
"rate": "+25%",
"pitch": "+15Hz",
"volume": "+20%",
"voice": "zh-CN-XiaoxiaoNeural",
"style": None,
},
"苏白·惊讶·震惊": {
"rate": "+30%",
"pitch": "+20Hz",
"volume": "+10%",
"voice": "zh-CN-XiaoxiaoNeural",
"style": None,
},
"苏白·悲伤·失落": {
"rate": "-20%",
"pitch": "-10Hz",
"volume": "-5%",
"voice": "zh-CN-XiaoxiaoNeural",
"style": None,
},
# 诸葛风情感库
"诸葛风·平静·沉稳": {
"rate": "+0%",
"pitch": "-5Hz",
"volume": "+0%",
"voice": "zh-CN-YunxiNeural", # 沉稳男声
"style": None,
},
"诸葛风·大声·威严": {
"rate": "+10%",
"pitch": "-10Hz", # 低沉有力
"volume": "+20%",
"voice": "zh-CN-YunxiNeural",
"style": None,
},
# 萧灵汐情感库
"萧灵汐·平静·清冷": {
"rate": "+0%",
"pitch": "+5Hz",
"volume": "+0%",
"voice": "zh-CN-XiaoyiNeural", # 清冷女声
"style": None,
},
"萧灵汐·大声·愤怒": {
"rate": "+15%",
"pitch": "+10Hz",
"volume": "+15%",
"voice": "zh-CN-XiaoyiNeural",
"style": None,
},
}
# 豆包语音情感映射 (如果豆包 API 支持情感参数)
DOUBAO_EMOTION_MAP = {
"苏白·平静·正常": {"emotion": "neutral", "speed": 1.0, "pitch": 1.0, "volume": 1.0},
"苏白·大声·自信": {"emotion": "happy", "speed": 1.2, "pitch": 1.1, "volume": 1.15},
"苏白·生气·愤怒": {"emotion": "angry", "speed": 1.25, "pitch": 1.15, "volume": 1.2},
"苏白·惊讶·震惊": {"emotion": "surprised", "speed": 1.3, "pitch": 1.2, "volume": 1.1},
"苏白·悲伤·失落": {"emotion": "sad", "speed": 0.8, "pitch": 0.9, "volume": 0.95},
}
def __init__(self, character=None):
self.character = character
self.voice_profiles = {}
def parse_emotion_tag(self, emotion_tag):
"""
解析情感标签
格式: "角色·情感·强度" "情感·强度"
返回: TTS 参数字典
"""
print(f"🔍 解析情感标签: {emotion_tag}")
# 直接查找映射表
if emotion_tag in self.EMOTION_MAP:
params = self.EMOTION_MAP[emotion_tag].copy()
print(f" ✅ 找到映射: rate={params['rate']}, pitch={params['pitch']}, volume={params['volume']}")
return params
# 模糊匹配: 只给情感,不给角色
for key, val in self.EMOTION_MAP.items():
if emotion_tag in key:
params = val.copy()
print(f" ⚠️ 模糊匹配: {key} → rate={params['rate']}")
return params
# 未找到,使用默认
print(f" ⚠️ 未找到映射,使用默认参数")
return {
"rate": "+0%",
"pitch": "+0Hz",
"volume": "+0%",
"voice": "zh-CN-XiaoxiaoNeural",
"style": None,
}
def compile_to_tts_params(self, emotion_tag, engine="edge-tts"):
"""
将情感标签编译为 TTS 参数
engine: "edge-tts" | "doubao"
"""
if engine == "edge-tts":
return self.parse_emotion_tag(emotion_tag)
elif engine == "doubao":
# 豆包语音参数
if emotion_tag in self.DOUBAO_EMOTION_MAP:
return self.DOUBAO_EMOTION_MAP[emotion_tag]
else:
return {"emotion": "neutral", "speed": 1.0, "pitch": 1.0, "volume": 1.0}
else:
raise ValueError(f"不支持的引擎: {engine}")
def generate_speech_with_emotion(self, text, emotion_tag, output_path, engine="edge-tts"):
"""
生成带情感的语音
"""
print(f"\n🎤 生成情感语音")
print(f" 文本: {text}")
print(f" 情感: {emotion_tag}")
print(f" 引擎: {engine}")
params = self.compile_to_tts_params(emotion_tag, engine)
if engine == "edge-tts":
if generate_speech is None:
print(" ❌ tts-engine 不可用")
return False
ok = generate_speech(
text=text,
output_path=output_path,
voice=params["voice"],
rate=params["rate"],
pitch=params["pitch"],
volume=params["volume"]
)
return ok
elif engine == "doubao":
# 豆包语音 API 调用
print(f" 📤 调用豆包语音 API...")
print(f" 参数: {params}")
# TODO: 实现豆包 API 调用
# doubao_api_call(text, output_path, params)
print(f" ⚠️ 豆包 API 调用未实现")
return False
return False
def ab_test(self, text, emotion_tag, output_dir):
"""
A/B 测试: 生成不同参数的音频
"""
print(f"\n🧪 A/B 测试: {emotion_tag}")
print(f" 文本: {text}")
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
results = []
# 生成多个变体
variants = self._generate_variants(emotion_tag)
for i, variant_params in enumerate(variants):
output_path = output_dir / f"ab-test-{i+1:03d}.mp3"
print(f"\n [{i+1}/{len(variants)}] {variant_params['label']}")
if generate_speech:
ok = generate_speech(
text=text,
output_path=str(output_path),
voice=variant_params["params"]["voice"],
rate=variant_params["params"]["rate"],
pitch=variant_params["params"]["pitch"],
volume=variant_params["params"]["volume"]
)
if ok:
results.append({
"label": variant_params["label"],
"path": str(output_path),
"params": variant_params["params"]
})
# 生成 A/B 测试报告
report_path = output_dir / "ab-test-report.json"
with open(report_path, "w", encoding="utf-8") as f:
json.dump({
"emotion_tag": emotion_tag,
"text": text,
"variants": results,
"generated_at": datetime.now().isoformat()
}, f, ensure_ascii=False, indent=2)
print(f"\n✅ A/B 测试完成,生成 {len(results)} 个变体")
print(f" 报告: {report_path}")
return results
def _generate_variants(self, emotion_tag):
"""生成多个变体参数"""
base_params = self.parse_emotion_tag(emotion_tag)
variants = [
{"label": "基准", "params": base_params},
{"label": "语速+10%", "params": {**base_params, "rate": f"+{int(base_params['rate'].strip('%+')) + 10}%"}},
{"label": "音调+5Hz", "params": {**base_params, "pitch": f"+{int(base_params['pitch'].strip('Hz+')) + 5}Hz"}},
{"label": "音量+10%", "params": {**base_params, "volume": f"+{int(base_params['volume'].strip('%+')) + 10}%"}},
]
return variants
def generate_voice_profile(self, character):
"""
生成角色的 voice_profile.hdlp
保存到 assets/characters/<CHAR-ID>/voice/voice-profile.hdlp
"""
print(f"\n📝 生成 {character} 的语音画像...")
character_dir = PROJECT_ROOT / "assets" / "characters" / character
voice_dir = character_dir / "voice"
voice_dir.mkdir(parents=True, exist_ok=True)
profile_path = voice_dir / "voice-profile.hdlp"
# 收集该角色的所有情感
character_prefix = character.replace("CHAR-", "").replace("-", "")
# 简单匹配: 找所有以 "苏白" 开头的情感标签
emotions = {}
for key in self.EMOTION_MAP.keys():
if key.startswith("苏白"): # TODO: 根据实际角色名匹配
emotions[key] = self.EMOTION_MAP[key]
# 生成 HLDP 格式的配置
profile_content = f"""# 语音画像 · {character}
> HLDP://video-ai-system/assets/characters/{character}/voice/voice-profile
> 类型: 语音配置 · 情感参数映射
> 建立: D144 · 2026-06-24
> 铸渊 ICE-GL-ZY001 · 冰朔 TCS-0002
---
## 默认音色
```
voice: {list(emotions.values())[0]['voice'] if emotions else 'zh-CN-XiaoxiaoNeural'}
engine: edge-tts
```
---
## 情感参数映射
"""
for emotion_tag, params in emotions.items():
profile_content += f"""### {emotion_tag}
```
rate: {params['rate']}
pitch: {params['pitch']}
volume: {params['volume']}
voice: {params['voice']}
```
"""
profile_content += """---
## 使用方式
```
from voice_emotion_compiler import VoiceEmotionCompiler
compiler = VoiceEmotionCompiler()
params = compiler.compile_to_tts_params("苏白·大声·自信", engine="edge-tts")
generate_speech(text, output_path, **params)
```
---
此文件由 VOICE-EMOTION-COMPILER 自动生成
Agent_04 (配音) 读取此文件获取角色情感参数
"""
with open(profile_path, "w", encoding="utf-8") as f:
f.write(profile_content)
print(f" ✅ 已生成: {profile_path}")
return profile_path
def main():
parser = argparse.ArgumentParser(description="VOICE-EMOTION-COMPILER")
parser.add_argument("--text", type=str, help="要合成的文本")
parser.add_argument("--emotion", type=str, help="情感标签 (如: '苏白·大声·自信')")
parser.add_argument("--output", type=str, help="输出音频路径")
parser.add_argument("--engine", type=str, default="edge-tts", choices=["edge-tts", "doubao"], help="TTS 引擎")
parser.add_argument("--ab-test", action="store_true", help="A/B 测试模式")
parser.add_argument("--output-dir", type=str, help="A/B 测试输出目录")
parser.add_argument("--generate-profile", action="store_true", help="生成 voice_profile.hdlp")
parser.add_argument("--character", type=str, help="角色ID")
args = parser.parse_args()
compiler = VoiceEmotionCompiler()
if args.generate_profile:
if not args.character:
print("❌ --generate-profile 需要 --character")
sys.exit(1)
compiler.generate_voice_profile(args.character)
sys.exit(0)
if args.ab_test:
if not args.text or not args.emotion or not args.output_dir:
print("❌ --ab-test 需要 --text, --emotion, --output-dir")
sys.exit(1)
compiler.ab_test(args.text, args.emotion, args.output_dir)
sys.exit(0)
if not args.text or not args.emotion or not args.output:
parser.print_help()
sys.exit(1)
ok = compiler.generate_speech_with_emotion(
text=args.text,
emotion_tag=args.emotion,
output_path=args.output,
engine=args.engine
)
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()