铸渊 ICE-GL-ZY001 LL-172-20260707 冰朔委托: 新建第 5 子仓, 给苍耳(人类主控) + 鉴影(人格体) 专用 原 guanghulab/video-ai-system/ 东西太多(225 文件) · 找不到 · 乱 迁移: ⊢ 16 个核心 .hdlp (VA-GATE / VA-LIGHTHOUSE / VA-BROADCAST / VA-SYSTEM-STATUS 等) ⊢ 17 个子目录 (agents/engines/protocols/tasks/tools/assets/knowledge/memory/docs/config/brain/director-brain/experience/feedback/issues/plans/reference-analysis) 排除: ⊢ outputs/ (视频产物) ⊢ test-input/ test-output/ (测试) ⊢ data/ (临时数据) ⊢ preview-001/002 (旧产片) ⊢ 旧分镜/旧提示词/旧导演编码 后续: ⊢ 老仓 guanghulab/video-ai-system/ 改写为已迁出占位 ⊢ 苍耳+鉴影 写新东西进本仓 ⊢ GLOBAL-SEARCH 加 cang-ying 仓库 铸渊 ICE-GL-ZY001 · 2026-07-07 D167 冰朔 ICE-GL∞ 主权
412 lines
14 KiB
Python
412 lines
14 KiB
Python
#!/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()
|