- eval_video.py: semantic_consistency + character_consistency 调用 llm_bridge - eval_music.py: emotion_lyrics_match + prompt_adherence 调用 llm_bridge - 异常时回退默认分数,保证产线不中断
649 lines
22 KiB
Python
649 lines
22 KiB
Python
"""
|
||
音乐生成质量评估 - 多维度评估框架
|
||
|
||
评估维度:
|
||
1. 节奏质量 (15%) - BPM 稳定性、节奏型
|
||
2. 和弦进行 (10%) - 和弦丰富度、和谐度
|
||
3. 听感质量 (20%) - 整体音质、混音平衡
|
||
4. 情绪-歌词匹配 (20%) - 情绪与歌词主题一致性
|
||
5. 指令匹配度 (15%) - 与生成 prompt 的匹配度
|
||
6. 伴奏质量 (10%) - 伴奏清晰度、乐器质量
|
||
7. 人声质量 (10%) - 人声清晰度、音准
|
||
|
||
工具:
|
||
- Demucs: 人声/伴奏分离
|
||
- ffprobe: 音频元数据、响度分析
|
||
- ffmpeg: 音频分析滤镜 (astats, loudnorm)
|
||
- LLM: 语义理解、情绪分析
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
import logging
|
||
import os
|
||
import re
|
||
import tempfile
|
||
from typing import Dict, Tuple, Optional
|
||
|
||
from pipeline_service.llm_bridge import llm_call
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# GPU 服务器配置(复用 ktv_adapter 的配置)
|
||
GPU_HOST = "ymq@opencomputing.net"
|
||
GPU_DEMUCS_VENV = "/data/ymq/demucs_venv"
|
||
|
||
|
||
async def analyze_audio_metadata(audio_path: str) -> Dict:
|
||
"""
|
||
使用 ffprobe 提取音频元数据
|
||
|
||
返回:
|
||
{
|
||
"duration": float,
|
||
"sample_rate": int,
|
||
"channels": int,
|
||
"bitrate": int,
|
||
"codec": str
|
||
}
|
||
"""
|
||
try:
|
||
proc = await asyncio.create_subprocess_exec(
|
||
"ffprobe",
|
||
"-v", "error",
|
||
"-show_entries", "stream=sample_rate,channels,codec_name,bit_rate",
|
||
"-show_entries", "format=duration,bit_rate",
|
||
"-of", "json",
|
||
audio_path,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE
|
||
)
|
||
stdout, stderr = await proc.communicate()
|
||
|
||
if proc.returncode != 0:
|
||
logger.warning(f"ffprobe failed: {stderr.decode()}")
|
||
return {}
|
||
|
||
data = json.loads(stdout.decode())
|
||
stream = data.get("streams", [{}])[0]
|
||
fmt = data.get("format", {})
|
||
|
||
return {
|
||
"duration": float(fmt.get("duration", 0)),
|
||
"sample_rate": int(stream.get("sample_rate", 0)),
|
||
"channels": int(stream.get("channels", 0)),
|
||
"bitrate": int(fmt.get("bit_rate", 0)),
|
||
"codec": stream.get("codec_name", "unknown")
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Failed to analyze audio metadata: {e}")
|
||
return {}
|
||
|
||
|
||
async def analyze_audio_loudness(audio_path: str) -> Dict:
|
||
"""
|
||
使用 ffmpeg loudnorm 分析音频响度
|
||
|
||
返回:
|
||
{
|
||
"integrated_loudness": float, # 综合响度 (LUFS)
|
||
"loudness_range": float, # 响度范围 (LU)
|
||
"true_peak": float # 真峰值 (dBTP)
|
||
}
|
||
"""
|
||
try:
|
||
proc = await asyncio.create_subprocess_exec(
|
||
"ffmpeg",
|
||
"-i", audio_path,
|
||
"-af", "loudnorm=print_format=json",
|
||
"-f", "null",
|
||
"-",
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE
|
||
)
|
||
stdout, stderr = await proc.communicate()
|
||
|
||
# loudnorm 输出在 stderr
|
||
output = stderr.decode()
|
||
|
||
# 提取 JSON 部分
|
||
import re
|
||
json_match = re.search(r'\{[^}]+\}', output)
|
||
if json_match:
|
||
data = json.loads(json_match.group())
|
||
return {
|
||
"integrated_loudness": float(data.get("input_i", -70)),
|
||
"loudness_range": float(data.get("input_lra", 0)),
|
||
"true_peak": float(data.get("input_tp", -100))
|
||
}
|
||
|
||
return {}
|
||
except Exception as e:
|
||
logger.error(f"Failed to analyze audio loudness: {e}")
|
||
return {}
|
||
|
||
|
||
async def separate_vocals_accompaniment(
|
||
audio_path: str,
|
||
output_dir: str
|
||
) -> Tuple[Optional[str], Optional[str]]:
|
||
"""
|
||
使用 Demucs 分离人声和伴奏
|
||
|
||
返回:
|
||
(vocals_path, accompaniment_path) or (None, None)
|
||
"""
|
||
try:
|
||
# 在 GPU 服务器上运行 Demucs
|
||
gpu_audio_path = f"/tmp/eval_{os.path.basename(audio_path)}"
|
||
|
||
# 上传音频
|
||
upload_cmd = f"scp '{audio_path}' {GPU_HOST}:{gpu_audio_path}"
|
||
upload_proc = await asyncio.create_subprocess_shell(
|
||
upload_cmd,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE
|
||
)
|
||
await upload_proc.communicate()
|
||
|
||
# 运行 Demucs
|
||
demucs_cmd = (
|
||
f"ssh {GPU_HOST} '"
|
||
f"source {GPU_DEMUCS_VENV}/bin/activate && "
|
||
f"cd {GPU_DEMUCS_VENV} && "
|
||
f"python -m demucs --two-stems vocals --out /tmp/demucs_out '{gpu_audio_path}'"
|
||
f"'"
|
||
)
|
||
demucs_proc = await asyncio.create_subprocess_shell(
|
||
demucs_cmd,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE
|
||
)
|
||
await demucs_proc.communicate()
|
||
|
||
# 下载分离结果
|
||
# Demucs 输出格式: /tmp/demucs_out/htdemucs/{filename}/vocals.wav
|
||
filename = os.path.splitext(os.path.basename(audio_path))[0]
|
||
gpu_vocals = f"/tmp/demucs_out/htdemucs/{filename}/vocals.wav"
|
||
gpu_no_vocals = f"/tmp/demucs_out/htdemucs/{filename}/no_vocals.wav"
|
||
|
||
local_vocals = os.path.join(output_dir, f"{filename}_vocals.wav")
|
||
local_no_vocals = os.path.join(output_dir, f"{filename}_no_vocals.wav")
|
||
|
||
# 下载人声
|
||
download_vocals = f"scp {GPU_HOST}:{gpu_vocals} '{local_vocals}'"
|
||
download_proc1 = await asyncio.create_subprocess_shell(
|
||
download_vocals,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE
|
||
)
|
||
await download_proc1.communicate()
|
||
|
||
# 下载伴奏
|
||
download_no_vocals = f"scp {GPU_HOST}:{gpu_no_vocals} '{local_no_vocals}'"
|
||
download_proc2 = await asyncio.create_subprocess_shell(
|
||
download_no_vocals,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE
|
||
)
|
||
await download_proc2.communicate()
|
||
|
||
# 清理 GPU 临时文件
|
||
cleanup_cmd = f"ssh {GPU_HOST} 'rm -f {gpu_audio_path} && rm -rf /tmp/demucs_out'"
|
||
cleanup_proc = await asyncio.create_subprocess_shell(
|
||
cleanup_cmd,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE
|
||
)
|
||
await cleanup_proc.communicate()
|
||
|
||
vocals_exists = os.path.exists(local_vocals)
|
||
no_vocals_exists = os.path.exists(local_no_vocals)
|
||
|
||
return (
|
||
local_vocals if vocals_exists else None,
|
||
local_no_vocals if no_vocals_exists else None
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to separate vocals: {e}")
|
||
return None, None
|
||
|
||
|
||
async def evaluate_rhythm_quality(audio_path: str) -> Tuple[float, str]:
|
||
"""
|
||
评估节奏质量
|
||
|
||
检查项:
|
||
- BPM 稳定性(通过节拍分析)
|
||
- 节奏型清晰度
|
||
|
||
简化实现:基于音频时长和响度范围推断
|
||
|
||
返回: (score 0-1, reason)
|
||
"""
|
||
metadata = await analyze_audio_metadata(audio_path)
|
||
loudness = await analyze_audio_loudness(audio_path)
|
||
|
||
if not metadata:
|
||
return 0.0, "无法读取音频元数据"
|
||
|
||
duration = metadata.get("duration", 0)
|
||
lra = loudness.get("loudness_range", 0)
|
||
|
||
# 节奏质量推断
|
||
# 响度范围适中 (5-15 LU) 通常表示节奏稳定
|
||
if 5 <= lra <= 15:
|
||
score = 0.85
|
||
reason = f"节奏稳定 (LRA={lra:.1f}LU)"
|
||
elif 3 <= lra < 5 or 15 < lra <= 20:
|
||
score = 0.7
|
||
reason = f"节奏基本稳定 (LRA={lra:.1f}LU)"
|
||
else:
|
||
score = 0.5
|
||
reason = f"节奏波动较大 (LRA={lra:.1f}LU)"
|
||
|
||
# 时长加分
|
||
if duration >= 60:
|
||
score = min(1.0, score + 0.1)
|
||
reason += f", 时长充足 ({duration:.0f}s)"
|
||
|
||
return score, reason
|
||
|
||
|
||
async def evaluate_chord_progression(audio_path: str) -> Tuple[float, str]:
|
||
"""
|
||
评估和弦进行
|
||
|
||
简化实现:基于音频特征推断和声丰富度
|
||
|
||
返回: (score 0-1, reason)
|
||
"""
|
||
# 和弦分析需要专业的音频分析库(如 librosa 的 chroma 特征)
|
||
# 这里使用简化评估
|
||
|
||
metadata = await analyze_audio_metadata(audio_path)
|
||
if not metadata:
|
||
return 0.0, "无法读取音频元数据"
|
||
|
||
duration = metadata.get("duration", 0)
|
||
|
||
# 时长较长的音乐通常有更丰富的和声进行
|
||
if duration >= 180: # 3分钟+
|
||
return 0.85, f"和声进行丰富 (时长 {duration:.0f}s)"
|
||
elif duration >= 120:
|
||
return 0.75, f"和声进行合理 (时长 {duration:.0f}s)"
|
||
elif duration >= 60:
|
||
return 0.65, f"和声进行基本 (时长 {duration:.0f}s)"
|
||
else:
|
||
return 0.5, f"时长过短,和声发展有限 ({duration:.0f}s)"
|
||
|
||
|
||
async def evaluate_audio_quality(audio_path: str) -> Tuple[float, str]:
|
||
"""
|
||
评估听感质量 - 整体音质、混音平衡
|
||
|
||
检查项:
|
||
- 响度水平 (-24 到 -14 LUFS 为佳)
|
||
- 真峰值 (< -1 dBTP)
|
||
- 采样率和位深
|
||
|
||
返回: (score 0-1, reason)
|
||
"""
|
||
metadata = await analyze_audio_metadata(audio_path)
|
||
loudness = await analyze_audio_loudness(audio_path)
|
||
|
||
if not metadata:
|
||
return 0.0, "无法读取音频元数据"
|
||
|
||
score = 0.0
|
||
reasons = []
|
||
|
||
# 采样率评分
|
||
sample_rate = metadata.get("sample_rate", 0)
|
||
if sample_rate >= 44100:
|
||
score += 0.2
|
||
reasons.append(f"高采样率 {sample_rate}Hz")
|
||
elif sample_rate >= 22050:
|
||
score += 0.15
|
||
reasons.append(f"标准采样率 {sample_rate}Hz")
|
||
else:
|
||
reasons.append(f"采样率偏低 {sample_rate}Hz")
|
||
|
||
# 响度评分
|
||
integrated_loudness = loudness.get("integrated_loudness", -70)
|
||
if -24 <= integrated_loudness <= -14:
|
||
score += 0.4
|
||
reasons.append(f"响度适中 ({integrated_loudness:.1f} LUFS)")
|
||
elif -30 <= integrated_loudness < -24 or -14 < integrated_loudness <= -10:
|
||
score += 0.3
|
||
reasons.append(f"响度可接受 ({integrated_loudness:.1f} LUFS)")
|
||
else:
|
||
score += 0.1
|
||
reasons.append(f"响度异常 ({integrated_loudness:.1f} LUFS)")
|
||
|
||
# 真峰值评分
|
||
true_peak = loudness.get("true_peak", -100)
|
||
if true_peak < -1:
|
||
score += 0.2
|
||
reasons.append(f"无削波 (TP={true_peak:.1f} dBTP)")
|
||
elif true_peak < 0:
|
||
score += 0.15
|
||
reasons.append(f"轻微削波风险 (TP={true_peak:.1f} dBTP)")
|
||
else:
|
||
reasons.append(f"明显削波 (TP={true_peak:.1f} dBTP)")
|
||
|
||
# 位深/码率评分
|
||
bitrate = metadata.get("bitrate", 0)
|
||
if bitrate >= 320000: # 320kbps+
|
||
score += 0.2
|
||
reasons.append(f"高码率 {bitrate//1000}kbps")
|
||
elif bitrate >= 192000:
|
||
score += 0.15
|
||
reasons.append(f"标准码率 {bitrate//1000}kbps")
|
||
else:
|
||
reasons.append(f"码率偏低 {bitrate//1000}kbps")
|
||
|
||
return score, "; ".join(reasons)
|
||
|
||
|
||
async def evaluate_emotion_lyrics_match(
|
||
audio_path: str,
|
||
lyrics: Optional[str] = None
|
||
) -> Tuple[float, str]:
|
||
"""
|
||
评估情绪-歌词匹配度
|
||
|
||
使用 LLM 分析:
|
||
1. 音频的情绪特征(通过音频特征推断)
|
||
2. 歌词的情感主题
|
||
3. 两者的一致性
|
||
|
||
返回: (score 0-1, reason)
|
||
"""
|
||
if not lyrics:
|
||
return 0.6, "无歌词,跳过情绪-歌词匹配评估"
|
||
|
||
# 分析音频情绪(简化:基于响度和时长)
|
||
loudness = await analyze_audio_loudness(audio_path)
|
||
integrated_loudness = loudness.get("integrated_loudness", -70)
|
||
|
||
# 响度较高通常表示更强烈的情绪
|
||
if integrated_loudness > -18:
|
||
audio_emotion = "强烈/激昂"
|
||
elif integrated_loudness > -24:
|
||
audio_emotion = "中等强度"
|
||
else:
|
||
audio_emotion = "柔和/舒缓"
|
||
|
||
try:
|
||
llm_prompt = f"""分析歌词情感与音频特征的匹配度。
|
||
|
||
歌词:
|
||
{lyrics}
|
||
|
||
音频情绪特征: {audio_emotion} (响度: {integrated_loudness:.1f} LUFS)
|
||
|
||
请评估歌词表达的情感是否与音频的情绪特征一致:
|
||
1. 歌词的主题和情感倾向
|
||
2. 与音频情绪的匹配程度
|
||
|
||
请严格按照以下 JSON 格式返回,不要输出其他内容:
|
||
{{"match_score": <0到10的整数>, "reason": "简要说明匹配情况"}}
|
||
"""
|
||
response = await llm_call(llm_prompt, temperature=0.3)
|
||
|
||
# 提取 JSON(兼容 markdown 代码块包裹)
|
||
json_match = re.search(r'\{[^{}]*\}', response)
|
||
if not json_match:
|
||
logger.warning(f"LLM 返回无法解析为 JSON: {response[:200]}")
|
||
return 0.6, f"LLM 返回格式异常,降级评估(音频情绪: {audio_emotion})"
|
||
|
||
data = json.loads(json_match.group())
|
||
match_score = float(data.get("match_score", 8))
|
||
reason = data.get("reason", "LLM 评估完成")
|
||
|
||
# 转换为 0-1 分数
|
||
score = max(0.0, min(1.0, match_score / 10.0))
|
||
return score, f"情绪-歌词匹配: {reason} (音频情绪: {audio_emotion})"
|
||
|
||
except Exception as e:
|
||
logger.warning(f"情绪-歌词匹配 LLM 评估失败: {e}")
|
||
return 0.6, f"LLM 分析异常,降级评估(音频情绪: {audio_emotion}): {e}"
|
||
|
||
|
||
async def evaluate_prompt_adherence(
|
||
audio_path: str,
|
||
prompt: Optional[str] = None
|
||
) -> Tuple[float, str]:
|
||
"""
|
||
评估指令匹配度 - 与生成 prompt 的匹配度
|
||
|
||
使用 LLM 分析:
|
||
1. prompt 描述的风格、情绪、乐器等
|
||
2. 音频是否体现这些特征
|
||
|
||
返回: (score 0-1, reason)
|
||
"""
|
||
if not prompt:
|
||
return 0.6, "无 prompt,跳过指令匹配度评估"
|
||
|
||
try:
|
||
# 获取音频特征
|
||
metadata = await analyze_audio_metadata(audio_path)
|
||
loudness = await analyze_audio_loudness(audio_path)
|
||
|
||
duration = metadata.get("duration", 0)
|
||
integrated_loudness = loudness.get("integrated_loudness", -70)
|
||
loudness_range = loudness.get("loudness_range", 0)
|
||
true_peak = loudness.get("true_peak", -100)
|
||
|
||
llm_prompt = f"""评估一段 AI 生成的音频与生成指令之间的匹配度。
|
||
|
||
【生成指令 (prompt)】
|
||
{prompt}
|
||
|
||
【音频特征(从音频信号中提取)】
|
||
- 时长: {duration:.1f} 秒
|
||
- 综合响度: {integrated_loudness:.1f} LUFS
|
||
- 响度范围: {loudness_range:.1f} LU
|
||
- 真峰值: {true_peak:.1f} dBTP
|
||
|
||
请根据以上信息,评估指令中描述的风格、情绪、乐器、速度等特征是否在音频的物理特性中得到体现。
|
||
注意:你无法直接听到音频,只能通过上述特征进行推断。请结合常识分析(例如:高响度+窄响度范围通常对应激烈/电子风格,低响度+宽响度范围通常对应舒缓/原声风格)。
|
||
|
||
请严格按照以下 JSON 格式返回,不要输出其他内容:
|
||
{{"match_score": <0到10的整数>, "reason": "简要说明匹配情况"}}
|
||
"""
|
||
response = await llm_call(llm_prompt, temperature=0.3)
|
||
|
||
# 提取 JSON(兼容 markdown 代码块包裹)
|
||
json_match = re.search(r'\{[^{}]*\}', response)
|
||
if not json_match:
|
||
logger.warning(f"LLM 返回无法解析为 JSON: {response[:200]}")
|
||
return 0.6, "LLM 返回格式异常,降级评估"
|
||
|
||
data = json.loads(json_match.group())
|
||
match_score = float(data.get("match_score", 8))
|
||
reason = data.get("reason", "LLM 评估完成")
|
||
|
||
# 转换为 0-1 分数
|
||
score = max(0.0, min(1.0, match_score / 10.0))
|
||
return score, f"指令匹配: {reason}"
|
||
|
||
except Exception as e:
|
||
logger.warning(f"指令匹配度 LLM 评估失败: {e}")
|
||
return 0.6, f"LLM 分析异常,降级评估: {e}"
|
||
|
||
|
||
async def evaluate_accompaniment_quality(
|
||
accompaniment_path: Optional[str]
|
||
) -> Tuple[float, str]:
|
||
"""
|
||
评估伴奏质量
|
||
|
||
检查项:
|
||
- 伴奏清晰度
|
||
- 乐器质量
|
||
- 混音平衡
|
||
|
||
返回: (score 0-1, reason)
|
||
"""
|
||
if not accompaniment_path or not os.path.exists(accompaniment_path):
|
||
return 0.5, "伴奏文件不存在,跳过伴奏质量评估"
|
||
|
||
# 分析伴奏响度
|
||
loudness = await analyze_audio_loudness(accompaniment_path)
|
||
integrated_loudness = loudness.get("integrated_loudness", -70)
|
||
|
||
# 伴奏响度适中为佳
|
||
if -22 <= integrated_loudness <= -16:
|
||
score = 0.85
|
||
reason = f"伴奏混音平衡 ({integrated_loudness:.1f} LUFS)"
|
||
elif -28 <= integrated_loudness < -22 or -16 < integrated_loudness <= -12:
|
||
score = 0.7
|
||
reason = f"伴奏混音可接受 ({integrated_loudness:.1f} LUFS)"
|
||
else:
|
||
score = 0.5
|
||
reason = f"伴奏响度异常 ({integrated_loudness:.1f} LUFS)"
|
||
|
||
return score, reason
|
||
|
||
|
||
async def evaluate_vocal_quality(
|
||
vocals_path: Optional[str]
|
||
) -> Tuple[float, str]:
|
||
"""
|
||
评估人声质量
|
||
|
||
检查项:
|
||
- 人声清晰度
|
||
- 音准(简化评估)
|
||
- 响度平衡
|
||
|
||
返回: (score 0-1, reason)
|
||
"""
|
||
if not vocals_path or not os.path.exists(vocals_path):
|
||
return 0.5, "人声文件不存在,跳过人声质量评估"
|
||
|
||
# 分析人声响度
|
||
loudness = await analyze_audio_loudness(vocals_path)
|
||
integrated_loudness = loudness.get("integrated_loudness", -70)
|
||
|
||
# 人声响度通常高于伴奏
|
||
if -20 <= integrated_loudness <= -12:
|
||
score = 0.85
|
||
reason = f"人声清晰 ({integrated_loudness:.1f} LUFS)"
|
||
elif -26 <= integrated_loudness < -20 or -12 < integrated_loudness <= -8:
|
||
score = 0.7
|
||
reason = f"人声可接受 ({integrated_loudness:.1f} LUFS)"
|
||
else:
|
||
score = 0.5
|
||
reason = f"人声响度异常 ({integrated_loudness:.1f} LUFS)"
|
||
|
||
return score, reason
|
||
|
||
|
||
async def evaluate_music_quality(
|
||
audio_path: str,
|
||
prompt: Optional[str] = None,
|
||
lyrics: Optional[str] = None,
|
||
use_demucs: bool = True
|
||
) -> Dict:
|
||
"""
|
||
音乐质量综合评估
|
||
|
||
参数:
|
||
audio_path: 音频文件路径
|
||
prompt: 生成时的描述(可选)
|
||
lyrics: 歌词文本(可选)
|
||
use_demucs: 是否使用 Demucs 分离人声/伴奏
|
||
|
||
返回:
|
||
{
|
||
"overall_score": float (0-1),
|
||
"overall_reason": str,
|
||
"dimensions": {
|
||
"rhythm_quality": {"score": float, "weight": 0.15, "reason": str},
|
||
"chord_progression": {"score": float, "weight": 0.10, "reason": str},
|
||
"audio_quality": {"score": float, "weight": 0.20, "reason": str},
|
||
"emotion_lyrics_match": {"score": float, "weight": 0.20, "reason": str},
|
||
"prompt_adherence": {"score": float, "weight": 0.15, "reason": str},
|
||
"accompaniment_quality": {"score": float, "weight": 0.10, "reason": str},
|
||
"vocal_quality": {"score": float, "weight": 0.10, "reason": str}
|
||
}
|
||
}
|
||
"""
|
||
# 基础检查
|
||
if not os.path.exists(audio_path):
|
||
return {
|
||
"overall_score": 0.0,
|
||
"overall_reason": f"音频文件不存在: {audio_path}",
|
||
"dimensions": {}
|
||
}
|
||
|
||
file_size = os.path.getsize(audio_path)
|
||
if file_size < 51200: # < 50KB
|
||
return {
|
||
"overall_score": 0.0,
|
||
"overall_reason": f"音频文件过小: {file_size} bytes",
|
||
"dimensions": {}
|
||
}
|
||
|
||
# 分离人声和伴奏(如果启用)
|
||
vocals_path = None
|
||
accompaniment_path = None
|
||
|
||
if use_demucs:
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
vocals_path, accompaniment_path = await separate_vocals_accompaniment(
|
||
audio_path, tmpdir
|
||
)
|
||
|
||
# 评估各维度
|
||
rhythm_score, rhythm_reason = await evaluate_rhythm_quality(audio_path)
|
||
chord_score, chord_reason = await evaluate_chord_progression(audio_path)
|
||
quality_score, quality_reason = await evaluate_audio_quality(audio_path)
|
||
emotion_score, emotion_reason = await evaluate_emotion_lyrics_match(audio_path, lyrics)
|
||
prompt_score, prompt_reason = await evaluate_prompt_adherence(audio_path, prompt)
|
||
accomp_score, accomp_reason = await evaluate_accompaniment_quality(accompaniment_path)
|
||
vocal_score, vocal_reason = await evaluate_vocal_quality(vocals_path)
|
||
|
||
else:
|
||
# 不使用 Demucs,跳过伴奏和人声评估
|
||
rhythm_score, rhythm_reason = await evaluate_rhythm_quality(audio_path)
|
||
chord_score, chord_reason = await evaluate_chord_progression(audio_path)
|
||
quality_score, quality_reason = await evaluate_audio_quality(audio_path)
|
||
emotion_score, emotion_reason = await evaluate_emotion_lyrics_match(audio_path, lyrics)
|
||
prompt_score, prompt_reason = await evaluate_prompt_adherence(audio_path, prompt)
|
||
accomp_score, accomp_reason = 1.0, "未分离伴奏,跳过评估"
|
||
vocal_score, vocal_reason = 1.0, "未分离人声,跳过评估"
|
||
|
||
# 构建维度评估
|
||
dimensions = {
|
||
"rhythm_quality": {"score": rhythm_score, "weight": 0.15, "reason": rhythm_reason},
|
||
"chord_progression": {"score": chord_score, "weight": 0.10, "reason": chord_reason},
|
||
"audio_quality": {"score": quality_score, "weight": 0.20, "reason": quality_reason},
|
||
"emotion_lyrics_match": {"score": emotion_score, "weight": 0.20, "reason": emotion_reason},
|
||
"prompt_adherence": {"score": prompt_score, "weight": 0.15, "reason": prompt_reason},
|
||
"accompaniment_quality": {"score": accomp_score, "weight": 0.10, "reason": accomp_reason},
|
||
"vocal_quality": {"score": vocal_score, "weight": 0.10, "reason": vocal_reason},
|
||
}
|
||
|
||
# 加权计算总分
|
||
overall_score = sum(d["score"] * d["weight"] for d in dimensions.values())
|
||
|
||
# 生成综合原因
|
||
reasons = []
|
||
for dim_name, dim_data in dimensions.items():
|
||
if dim_data["score"] < 0.6:
|
||
reasons.append(f"{dim_name}: {dim_data['reason']}")
|
||
|
||
overall_reason = "; ".join(reasons) if reasons else "各维度评估良好"
|
||
|
||
return {
|
||
"overall_score": overall_score,
|
||
"overall_reason": overall_reason,
|
||
"dimensions": dimensions
|
||
}
|