""" 视频生成质量评估 - 多维度评估框架 评估维度: 1. 视觉质量 (25%) - 分辨率、清晰度、色彩 2. 运动流畅度 (20%) - 帧率稳定性、运动伪影 3. 语义一致性 (30%) - 视频内容与 prompt 描述的匹配度 4. 角色一致性 (15%) - Ref2V 生成时角色特征保持度 5. 时间连贯性 (10%) - 无闪烁、跳变 工具: - ffprobe: 视频元数据、帧率 - ffmpeg: 帧提取、质量分析 - LLM 多模态: 语义理解、角色匹配 """ import asyncio import json import logging import os import tempfile from typing import Dict, Tuple, Optional logger = logging.getLogger(__name__) async def analyze_video_metadata(video_path: str) -> Dict: """ 使用 ffprobe 提取视频元数据 返回: { "width": int, "height": int, "fps": float, "duration": float, "codec": str, "bitrate": int } """ try: proc = await asyncio.create_subprocess_exec( "ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,r_frame_rate,codec_name,bit_rate", "-show_entries", "format=duration", "-of", "json", video_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", {}) # 解析帧率 fps_str = stream.get("r_frame_rate", "30/1") if "/" in fps_str: num, den = fps_str.split("/") fps = float(num) / float(den) if float(den) > 0 else 30.0 else: fps = float(fps_str) return { "width": stream.get("width", 0), "height": stream.get("height", 0), "fps": fps, "duration": float(fmt.get("duration", 0)), "codec": stream.get("codec_name", "unknown"), "bitrate": int(stream.get("bit_rate", 0)) } except Exception as e: logger.error(f"Failed to analyze video metadata: {e}") return {} async def extract_key_frames(video_path: str, num_frames: int = 5) -> list: """ 从视频中提取关键帧 返回: 帧图片路径列表 """ try: # 获取视频时长 metadata = await analyze_video_metadata(video_path) duration = metadata.get("duration", 0) if duration <= 0: return [] # 均匀提取帧 interval = duration / (num_frames + 1) frames = [] with tempfile.TemporaryDirectory() as tmpdir: for i in range(num_frames): timestamp = interval * (i + 1) output_path = os.path.join(tmpdir, f"frame_{i:03d}.jpg") proc = await asyncio.create_subprocess_exec( "ffmpeg", "-ss", str(timestamp), "-i", video_path, "-vframes", "1", "-q:v", "2", output_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await proc.communicate() if os.path.exists(output_path): frames.append(output_path) # 复制到持久化位置(如果需要) return frames except Exception as e: logger.error(f"Failed to extract key frames: {e}") return [] async def evaluate_visual_quality(video_path: str) -> Tuple[float, str]: """ 评估视觉质量 检查项: - 分辨率 >= 720p (1280x720) - 码率合理性(避免过度压缩) - 帧率 >= 24fps 返回: (score 0-1, reason) """ metadata = await analyze_video_metadata(video_path) if not metadata: return 0.0, "无法读取视频元数据" width = metadata.get("width", 0) height = metadata.get("height", 0) fps = metadata.get("fps", 0) bitrate = metadata.get("bitrate", 0) score = 0.0 reasons = [] # 分辨率评分 pixels = width * height if pixels >= 1920 * 1080: # 1080p+ score += 0.4 reasons.append(f"高分辨率 {width}x{height}") elif pixels >= 1280 * 720: # 720p score += 0.3 reasons.append(f"标准分辨率 {width}x{height}") elif pixels >= 640 * 480: # 480p score += 0.2 reasons.append(f"较低分辨率 {width}x{height}") else: reasons.append(f"分辨率过低 {width}x{height}") # 帧率评分 if fps >= 29: score += 0.3 reasons.append(f"流畅帧率 {fps:.1f}fps") elif fps >= 24: score += 0.2 reasons.append(f"标准帧率 {fps:.1f}fps") else: reasons.append(f"帧率过低 {fps:.1f}fps") # 码率评分(经验值) if bitrate >= 2000000: # 2Mbps+ score += 0.3 reasons.append(f"码率充足 {bitrate//1000}kbps") elif bitrate >= 1000000: score += 0.2 reasons.append(f"码率一般 {bitrate//1000}kbps") else: reasons.append(f"码率过低 {bitrate//1000}kbps") return score, "; ".join(reasons) async def evaluate_motion_smoothness(video_path: str) -> Tuple[float, str]: """ 评估运动流畅度 检查项: - 帧率稳定性 - 运动伪影(通过帧差分析) 返回: (score 0-1, reason) """ metadata = await analyze_video_metadata(video_path) if not metadata: return 0.0, "无法读取视频元数据" fps = metadata.get("fps", 0) # 简单评估:帧率 >= 24fps 认为流畅 if fps >= 29: return 0.9, f"运动流畅 {fps:.1f}fps" elif fps >= 24: return 0.7, f"运动基本流畅 {fps:.1f}fps" elif fps >= 20: return 0.5, f"运动略显卡顿 {fps:.1f}fps" else: return 0.3, f"运动不流畅 {fps:.1f}fps" async def evaluate_semantic_consistency( video_path: str, prompt: str, frames: Optional[list] = None ) -> Tuple[float, str]: """ 评估语义一致性 - 视频内容与 prompt 描述的匹配度 使用 LLM 多模态能力: 1. 提取关键帧 2. 让 LLM 分析帧内容与 prompt 的匹配度 返回: (score 0-1, reason) """ if not prompt: return 0.5, "无 prompt,无法评估语义一致性" # 提取关键帧 if not frames: frames = await extract_key_frames(video_path, num_frames=3) if not frames: return 0.5, "无法提取视频帧,跳过语义评估" # 构建 LLM prompt llm_prompt = f"""分析以下视频帧与描述的匹配度: 描述:{prompt} 请评估: 1. 视频内容是否准确反映了描述中的场景、物体、动作 2. 氛围和情绪是否匹配 3. 整体视觉风格是否符合预期 返回 JSON 格式: {{ "match_score": 0-10, "reason": "简要说明匹配情况" }} """ try: # 调用 LLM 多模态(这里使用占位实现,实际应调用 vision_analyze 或类似工具) # 暂时返回中性分数 # TODO: 集成实际的 LLM 多模态调用 # 模拟 LLM 返回 return 0.8, "语义一致性评估(待集成 LLM 多模态)" except Exception as e: logger.warning(f"语义一致性评估失败: {e}") return 0.5, f"语义评估异常: {e}" async def evaluate_character_consistency( video_path: str, reference_image: Optional[str], frames: Optional[list] = None ) -> Tuple[float, str]: """ 评估角色一致性 - Ref2V 生成时角色特征保持度 使用 LLM 多模态: 1. 比较参考图和视频帧中的人物 2. 评估面部特征、服装、姿态的一致性 返回: (score 0-1, reason) """ if not reference_image: return 1.0, "非 Ref2V 模式,跳过角色一致性评估" if not os.path.exists(reference_image): return 0.5, f"参考图不存在: {reference_image}" # 提取关键帧 if not frames: frames = await extract_key_frames(video_path, num_frames=3) if not frames: return 0.5, "无法提取视频帧,跳过角色一致性评估" try: # 调用 LLM 多模态比较 # TODO: 集成实际的 LLM 多模态调用 # 模拟返回 return 0.85, "角色一致性评估(待集成 LLM 多模态)" except Exception as e: logger.warning(f"角色一致性评估失败: {e}") return 0.5, f"角色一致性评估异常: {e}" async def evaluate_temporal_coherence(video_path: str) -> Tuple[float, str]: """ 评估时间连贯性 - 检测闪烁、跳变、伪影 方法: 1. 分析相邻帧的差异 2. 检测异常跳变 返回: (score 0-1, reason) """ # 简单实现:假设大部分生成视频时间连贯性良好 # 实际可以通过 ffmpeg 的 frame diff 分析实现 metadata = await analyze_video_metadata(video_path) duration = metadata.get("duration", 0) # 视频时长越长,时间连贯性问题越容易暴露 if duration >= 5: return 0.8, "时间连贯性良好(基于时长推断)" else: return 0.7, "时间连贯性一般(视频较短)" async def evaluate_scene_video_quality( video_path: str, prompt: str, reference_image: Optional[str] = None ) -> Dict: """ 场景视频质量综合评估 参数: video_path: 视频文件路径 prompt: 生成时的描述 reference_image: Ref2V 的参考图(可选) 返回: { "overall_score": float (0-1), "overall_reason": str, "dimensions": { "visual_quality": {"score": float, "weight": 0.25, "reason": str}, "motion_smoothness": {"score": float, "weight": 0.20, "reason": str}, "semantic_consistency": {"score": float, "weight": 0.30, "reason": str}, "character_consistency": {"score": float, "weight": 0.15, "reason": str}, "temporal_coherence": {"score": float, "weight": 0.10, "reason": str} } } """ # 基础检查 if not os.path.exists(video_path): return { "overall_score": 0.0, "overall_reason": f"视频文件不存在: {video_path}", "dimensions": {} } file_size = os.path.getsize(video_path) if file_size < 10240: # < 10KB return { "overall_score": 0.0, "overall_reason": f"视频文件过小: {file_size} bytes", "dimensions": {} } # 提取关键帧(用于多个维度) frames = await extract_key_frames(video_path, num_frames=3) # 评估各维度 visual_score, visual_reason = await evaluate_visual_quality(video_path) motion_score, motion_reason = await evaluate_motion_smoothness(video_path) semantic_score, semantic_reason = await evaluate_semantic_consistency(video_path, prompt, frames) character_score, character_reason = await evaluate_character_consistency(video_path, reference_image, frames) temporal_score, temporal_reason = await evaluate_temporal_coherence(video_path) # 加权计算总分 dimensions = { "visual_quality": {"score": visual_score, "weight": 0.25, "reason": visual_reason}, "motion_smoothness": {"score": motion_score, "weight": 0.20, "reason": motion_reason}, "semantic_consistency": {"score": semantic_score, "weight": 0.30, "reason": semantic_reason}, "character_consistency": {"score": character_score, "weight": 0.15, "reason": character_reason}, "temporal_coherence": {"score": temporal_score, "weight": 0.10, "reason": temporal_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 }