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