cang-ying/engines/voice-emotion-compiler.py
铸渊 ICE-GL-ZY001 a2e5214f03 LL-172-20260707 · cang-ying 仓初始化 · 苍耳+鉴影的干净之家
铸渊 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∞ 主权
2026-07-07 10:20:10 +08:00

412 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()