verify-video.sh 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. #!/bin/bash
  2. # verify-video.sh — 渲染产物侧硬校验(PASS/FAIL,不靠agent目测)
  3. #
  4. # 检查项:分辨率/fps、时长误差、audio stream存在性、首尾黑帧、LUFS响度、体积
  5. # 合成侧的校验(lint/layout/motion/contrast)由 hyperframes check 负责,此脚本只管产物。
  6. #
  7. # Usage:
  8. # bash verify-video.sh video.mp4 [--duration=10] [--fps=60] [--width=1920] [--height=1080]
  9. # [--no-audio] # 明确无音频的中间产物,跳过audio+响度检查
  10. # [--allow-black-open] # 片头刻意黑场开场时跳过片头黑帧检查
  11. #
  12. # Exit code: 0 = 全PASS;1 = 有FAIL
  13. set -u
  14. FILE="${1:-}"
  15. if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
  16. echo "Usage: bash verify-video.sh video.mp4 [--duration=N] [--fps=N] [--width=N] [--height=N] [--no-audio] [--allow-black-open]"
  17. exit 1
  18. fi
  19. shift || true
  20. EXP_DURATION=""; EXP_FPS=""; EXP_W=""; EXP_H=""; NO_AUDIO=0; ALLOW_BLACK_OPEN=0
  21. for a in "$@"; do
  22. case "$a" in
  23. --duration=*) EXP_DURATION="${a#*=}" ;;
  24. --fps=*) EXP_FPS="${a#*=}" ;;
  25. --width=*) EXP_W="${a#*=}" ;;
  26. --height=*) EXP_H="${a#*=}" ;;
  27. --no-audio) NO_AUDIO=1 ;;
  28. --allow-black-open) ALLOW_BLACK_OPEN=1 ;;
  29. esac
  30. done
  31. FAILS=0
  32. pass() { echo " ✓ PASS $1"; }
  33. fail() { echo " ✗ FAIL $1"; FAILS=$((FAILS+1)); }
  34. warn() { echo " ⚠ WARN $1"; }
  35. echo "▸ verify-video: $FILE"
  36. # ---------- 基本流信息 ----------
  37. INFO=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height,avg_frame_rate -show_entries format=duration,size -of default=noprint_wrappers=1 "$FILE" 2>/dev/null)
  38. W=$(echo "$INFO" | grep '^width=' | cut -d= -f2)
  39. H=$(echo "$INFO" | grep '^height=' | cut -d= -f2)
  40. FPS_RAW=$(echo "$INFO" | grep '^avg_frame_rate=' | cut -d= -f2)
  41. DUR=$(echo "$INFO" | grep '^duration=' | cut -d= -f2)
  42. SIZE=$(echo "$INFO" | grep '^size=' | cut -d= -f2)
  43. FPS=$(python3 -c "print(round(eval('${FPS_RAW:-0}' if '${FPS_RAW:-0}'!='0/0' else '0'),2))" 2>/dev/null || echo "?")
  44. [ -z "$W" ] && { fail "无法读取视频流(文件损坏或非视频)"; echo "✗ 1项FAIL"; exit 1; }
  45. echo " info: ${W}x${H} · ${FPS}fps · ${DUR%.*}s · $((SIZE/1024))KB"
  46. # ---------- 分辨率 / fps ----------
  47. if [ -n "$EXP_W" ]; then
  48. [ "$W" = "$EXP_W" ] && [ "$H" = "$EXP_H" ] && pass "分辨率 ${W}x${H}" || fail "分辨率 ${W}x${H},期望 ${EXP_W}x${EXP_H}"
  49. fi
  50. if [ -n "$EXP_FPS" ]; then
  51. python3 -c "exit(0 if abs($FPS-$EXP_FPS)<=0.5 else 1)" 2>/dev/null && pass "帧率 ${FPS}fps" || fail "帧率 ${FPS}fps,期望 ${EXP_FPS}fps"
  52. fi
  53. # ---------- 时长误差(±2% 或 ±0.2s 取大者)----------
  54. if [ -n "$EXP_DURATION" ]; then
  55. python3 -c "
  56. d=float('$DUR'); e=float('$EXP_DURATION')
  57. tol=max(e*0.02,0.2)
  58. exit(0 if abs(d-e)<=tol else 1)" 2>/dev/null && pass "时长 ${DUR%.*}s(期望 ${EXP_DURATION}s)" || fail "时长 ${DUR}s,期望 ${EXP_DURATION}s(容差2%)"
  59. fi
  60. # ---------- audio stream ----------
  61. HAS_AUDIO=$(ffprobe -v error -select_streams a -show_entries stream=codec_type -of csv=p=0 "$FILE" 2>/dev/null | head -1)
  62. if [ "$NO_AUDIO" = "1" ]; then
  63. [ -z "$HAS_AUDIO" ] && pass "无音轨(--no-audio 中间产物)" || warn "声明--no-audio但存在音轨"
  64. else
  65. if [ -n "$HAS_AUDIO" ]; then
  66. pass "audio stream 存在"
  67. # ---------- LUFS 响度(成品参考 -14 LUFS ±4)----------
  68. LUFS=$(ffmpeg -i "$FILE" -af loudnorm=print_format=summary -f null - 2>&1 | grep 'Input Integrated' | grep -oE '\-?[0-9]+\.?[0-9]*')
  69. if [ -n "$LUFS" ]; then
  70. python3 -c "exit(0 if -18<=float('$LUFS')<=-10 else 1)" 2>/dev/null \
  71. && pass "响度 ${LUFS} LUFS(目标区间 -18~-10)" \
  72. || warn "响度 ${LUFS} LUFS 偏离 -14±4 区间,检查混音增益"
  73. fi
  74. else
  75. fail "无 audio stream——skill铁律:动画默认交付形态是带SFX+BGM的MP4,无声=半成品"
  76. fi
  77. fi
  78. # ---------- 首尾黑帧 ----------
  79. BLACK=$(ffmpeg -i "$FILE" -vf "blackdetect=d=0.1:pix_th=0.10" -an -f null - 2>&1 | grep -oE 'black_start:[0-9.]+ black_end:[0-9.]+' )
  80. if [ -n "$BLACK" ]; then
  81. HEAD_BLACK=$(echo "$BLACK" | awk -F'[: ]' '$2<0.3{print}' | head -1)
  82. TOTAL=${DUR%.*}
  83. TAIL_BLACK=$(echo "$BLACK" | awk -F'[: ]' -v t="$TOTAL" '$4>t-0.3{print}' | head -1)
  84. if [ -n "$HEAD_BLACK" ] && [ "$ALLOW_BLACK_OPEN" = "0" ]; then
  85. fail "片头黑帧($HEAD_BLACK)——录制起点偏移的典型症状;刻意黑场开场用 --allow-black-open"
  86. else
  87. [ -n "$HEAD_BLACK" ] && pass "片头黑场(--allow-black-open 已声明)"
  88. fi
  89. [ -n "$TAIL_BLACK" ] && fail "片尾黑帧($TAIL_BLACK)——loop回跳或时长超录的典型症状"
  90. [ -z "$HEAD_BLACK" ] && [ -z "$TAIL_BLACK" ] && warn "片中存在黑帧段(如是刻意转场可忽略):$(echo "$BLACK" | head -2 | tr '\n' ' ')"
  91. else
  92. pass "无黑帧"
  93. fi
  94. # ---------- 汇总 ----------
  95. echo ""
  96. if [ "$FAILS" = "0" ]; then
  97. echo "◇ verify-video: 全部PASS"
  98. exit 0
  99. else
  100. echo "✗ verify-video: ${FAILS}项FAIL"
  101. exit 1
  102. fi