随机
Enter 搜索 ↑↓ 切换 Esc 清空

fan-video_highlight

脚本

精彩片段智能生成器

一键脚本

bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/fan-video_highlight.sh) copy 3
bash <(curl -sL ../sh/fan-video_highlight.sh) copy 2

效果预览

执行脚本效果预览

补充说明

该脚本用于批量处理目录下的 MP4 视频,自动生成「高光时刻」:默认 OUTPUT=timeline 不切割视频,只按时间线生成 sidecar JSON 元数据 + 缩略图(最省空间,播放时 UI 定位原片时间线即可);设置 OUTPUT=clip 则切出独立 mp4 片段。启动时自动静默检查/安装 ffmpeg,支持按 MAXDEPTH 递归遍历子目录,并以 .highlight.stop 停止标记随时中断。适合粉丝视频、直播回放等长视频的本地「高光时刻」整理场景。

功能特点

参数说明

变量 默认值 说明
MODE copy 位置参数:copy=流复制快速 / exact=帧级精确重编码(libx264 crf23 + aac128k)
MAXDEPTH 1 位置参数(正整数):递归遍历子目录深度,默认只处理当前目录
OUTPUT timeline timeline=只生成 sidecar JSON + 缩略图 / clip=切割独立 mp4 片段
PURGE 0 仅 timeline 下发用:1=删除旧的切割片段文件回收磁盘
MAX_CLIPS 8 单视频最多切割片段数
SEC_PER_CLIP 30 目标每段时长(秒),短视频自动缩减
FORCE 0 1=强制重切全部片段
THUMB webp 缩略图格式:webp / jpg / both / 0(0=不生成)
THUMB_WIDTH 720 宽度,高度等比缩放(scale=720:-2)
THUMB_QUALITY 80 仅对 webp 生效,libwebp 质量
THUMB_TIME 3 截图时间点(秒);片段短于该值时取中点
THUMB_FORCE 0 1=强制重截缩略图

用法示例

# 默认 timeline:只生成 sidecar JSON + 缩略图,不切割视频
bash fan-video_highlight.sh copy 2

# 切独立 mp4 片段(仅需离线片段/分享时使用)
OUTPUT=clip bash fan-video_highlight.sh exact 3

# 参数顺序不敏感,与上面等价
bash fan-video_highlight.sh 2 copy

# 只刷新缩略图(片段不动)
THUMB_FORCE=1 bash fan-video_highlight.sh

# 全部重做(重切片段 + 重截缩略图)
FORCE=1 THUMB_FORCE=1 OUTPUT=clip bash fan-video_highlight.sh

# 换格式/尺寸/质量
THUMB=both THUMB_WIDTH=480 THUMB_QUALITY=70 THUMB_FORCE=1 bash fan-video_highlight.sh

# 跳过依赖安装检查
SKIP_DEPS=1 bash fan-video_highlight.sh copy

输出说明

内容 说明
.highlights/ 输出目录,自动创建
<名>_NN_<标题>.json sidecar 元数据(title / start_time / end_time / score),timeline 模式的核心输出
<名>_NN_<标题>.mp4 片段文件(OUTPUT=clip 时生成),NN 为两位序号(如 long_01_前期精彩.mp4)
<名>_NN_<标题>.thumb.webp/.jpg 对应片段的缩略图,与片段同名

注意事项

高级使用方法

遍历“/vol2/1000/Video”下的子目录,直接使用脚本内置的深度参数,无需再手动定义循环(模式与深度顺序可互换):

cd /vol2/1000/Video
bash fan-video_highlight.sh copy 3
# 等价:bash fan-video_highlight.sh 3 copy
  1. 启动脚本:正常运行

  2. 想要强制停止,新开一个终端,在相同目录执行:

touch .highlight.stop

脚本会在处理完当前这个目录后,自动退出,不再进入下一个文件夹。

脚本源码

#!/bin/bash
set -u
shopt -s nullglob

install_deps() {
  if [[ "${SKIP_DEPS:-0}" == "1" ]]; then
    echo "SKIP_DEPS=1,跳过依赖安装检查"
    return 0
  fi
  local need_install=0
  if ! command -v ffmpeg &/dev/null; then
    need_install=1
  fi
  if ! command -v ffprobe &/dev/null; then
    need_install=1
  fi
  if [[ $need_install -eq 0 ]]; then
    echo "✅ ffmpeg / ffprobe 已安装,无需处理"
    return 0
  fi
  echo "⚠️ 检测缺失 ffmpeg/ffprobe,开始静默安装..."
  if command -v apt &/dev/null; then
    apt update -qq >/dev/null 2>&1
    apt install -y -qq ffmpeg >/dev/null 2>&1
  elif command -v dnf &/dev/null; then
    dnf install -y ffmpeg >/dev/null 2>&1
  elif command -v yum &/dev/null; then
    yum install -y ffmpeg >/dev/null 2>&1
  elif command -v pacman &/dev/null; then
    pacman -S --noconfirm ffmpeg >/dev/null 2>&1
  else
    echo "❌ 不支持当前系统包管理器,请手动安装 ffmpeg"
    exit 1
  fi
  if ! command -v ffmpeg &/dev/null || ! command -v ffprobe &/dev/null; then
    echo "❌ 依赖安装失败,请手动安装 ffmpeg"
    exit 1
  fi
  echo "✅ ffmpeg 安装完成"
}

MODE="copy"
MAXDEPTH=1
STOP_FILE="./.highlight.stop"
OUTPUT="${OUTPUT:-timeline}"
MAX_CLIPS="${MAX_CLIPS:-8}"
SEC_PER_CLIP="${SEC_PER_CLIP:-30}"
FORCE="${FORCE:-0}"
THUMB="${THUMB:-webp}"
THUMB_WIDTH="${THUMB_WIDTH:-720}"
THUMB_QUALITY="${THUMB_QUALITY:-80}"
THUMB_TIME="${THUMB_TIME:-3}"
THUMB_FORCE="${THUMB_FORCE:-0}"
PURGE="${PURGE:-0}"

parse_args() {
  for arg in "$@"; do
    if [[ "$arg" =~ ^[0-9]+$ ]]; then
      MAXDEPTH="$arg"
    elif [[ "$arg" == "copy" || "$arg" == "exact" ]]; then
      MODE="$arg"
    fi
  done
}

process_single_dir() {
  local dir="$1"
  echo -e "\n===== 进入目录:$dir ====="
  (
    cd "$dir" || exit 1
    echo "== 配置: OUTPUT=$OUTPUT MODE=$MODE THUMB=$THUMB FORCE=$FORCE THUMB_FORCE=$THUMB_FORCE =="
    mkdir -p .highlights

    # 检测webp支持
    if [ "$THUMB" = "webp" ] || [ "$THUMB" = "both" ]; then
      if ! ffmpeg -hide_banner -encoders 2>/dev/null | grep -q libwebp; then
        echo "警告: ffmpeg 不支持 libwebp,改用 jpg"
        THUMB="jpg"
      fi
    fi

    ok=0; fail=0; skip=0
    thumb_ok=0; thumb_fail=0; thumb_skip=0

    for video in *.mp4; do
      [ -f "$video" ] || continue
      stem="${video%.*}"
      dur_s=$(ffprobe -v error -show_entries format=duration \
        -of default=noprint_wrappers=1:nokey=1 "$video" 2>/dev/null)
      case "$dur_s" in ''|*[!0-9.]*) echo "跳过 ${video}"; continue;; esac
      dur=${dur_s%%.*}

      if [ "$dur" -lt 15 ]; then
        n=1; clip="$dur_s"
      else
        n=$(( dur / SEC_PER_CLIP + 1 ))
        [ "$n" -gt "$MAX_CLIPS" ] && n="$MAX_CLIPS"
        clip=$(( dur / n ))
        [ "$clip" -gt "$SEC_PER_CLIP" ] && clip="$SEC_PER_CLIP"
      fi

      echo "${stem}: 时长 ${dur}s → ${n} 段 × ${clip}s"
      i=0
      while [ "$i" -lt "$n" ]; do
        idx=$(printf "%02d" $((i+1)))
        if [ "$dur" -lt 15 ]; then
          start=0; title="完整片段"
        else
          start=$(awk -v i="$i" -v d="$dur_s" -v nn="$n" -v cl="$clip" \
            'BEGIN{s=(i+0.5)/nn*d-cl/2;if(s<0)s=0;m=d-cl;if(s>m)s=(m>0?m:0);printf "%.0f",s}')
          title=$(awk -v i="$i" -v nn="$n" 'BEGIN{
            r=(i+0.5)*100/nn
            if(r<12)t="开场高能"; else if(r>85)t="结局高潮";
            else if(r>62)t="后期转折"; else if(r>=50)t="中点高潮";
            else if(r<32)t="前期精彩"; else t="精彩片段"; print t}')
        fi

        base=".highlights/${stem}_${idx}_${title}"
        out="${base}.mp4"
        rebuilt=0

        if [ "$OUTPUT" = "clip" ]; then
          if [ -e "$out" ] && [ "$FORCE" != 1 ]; then
            echo "  跳过 $(basename "$out")"
            skip=$((skip+1))
          else
            if [ "$MODE" = "exact" ]; then
              ffmpeg -hide_banner -loglevel error -y -ss "$start" -i "$video" -t "$clip" \
                -c:v libx264 -preset veryfast -crf 23 -c:a aac -b:a 128k \
                -avoid_negative_ts make_zero -movflags +faststart "$out" \
                && { ok=$((ok+1)); rebuilt=1; echo "  ↳ $(basename "$out") @${start}s"; } \
                || { fail=$((fail+1)); echo "  失败: $out"; }
            else
              ffmpeg -hide_banner -loglevel error -ss "$start" -i "$video" -t "$clip" -c copy \
                -avoid_negative_ts make_zero -movflags +faststart -y "$out" \
                && { ok=$((ok+1)); rebuilt=1; echo "  ↳ $(basename "$out") @${start}s"; } \
                || { fail=$((fail+1)); echo "  失败: $out"; }
            fi
          fi
        fi

        st=$start
        cdur=""
        if [ "$OUTPUT" = "clip" ] && [ -e "$out" ]; then
          cdur=$(ffprobe -v error -show_entries format=duration \
            -of default=noprint_wrappers=1:nokey=1 "$out" 2>/dev/null)
          case "$cdur" in ''|*[!0-9.]*) cdur="";; esac
        fi
        if [ -n "$cdur" ]; then
          endt=$(awk -v s="$st" -v d="$cdur" 'BEGIN{printf "%.3f", s+d}')
        else
          endt=$(awk -v s="$st" -v cl="$clip" 'BEGIN{printf "%.3f", s+cl}')
        fi

        # 生成JSON元数据
        if [ ! -e "$base.json" ] || [ "$rebuilt" = 1 ]; then
          cat > "$base.json" <<EOF
{
  "title": "$title",
  "start_time": $st,
  "end_time": $endt,
  "score": 8.5
}
EOF
          echo "  元数据 $(basename "$base.json") ($st → ${endt}s)"
          [ "$OUTPUT" = "timeline" ] && ok=$((ok+1))
        else
          [ "$OUTPUT" = "timeline" ] && skip=$((skip+1))
        fi

        if [ "$OUTPUT" = "timeline" ] && [ "$PURGE" = 1 ] && [ -e "$out" ]; then
          rm -f "$out"
          echo "  清理 $(basename "$out")"
        fi

        if [ "$THUMB" != "0" ]; then
          need=0
          if [ "$rebuilt" = 1 ] || [ "$THUMB_FORCE" = 1 ]; then
            need=1
          else
            case "$THUMB" in
              webp) [ ! -e "${base}.thumb.webp" ] && need=1 ;;
              jpg)  [ ! -e "${base}.thumb.jpg" ]  && need=1 ;;
              both) { [ ! -e "${base}.thumb.webp" ] || [ ! -e "${base}.thumb.jpg" ]; } && need=1 ;;
            esac
          fi

          if [ "$need" = 1 ]; then
            if [ "$OUTPUT" = "clip" ]; then
              thumb_src="$out"
              if [ -n "$cdur" ] && awk -v d="$cdur" -v t="$THUMB_TIME" \
                   'BEGIN{exit !(d+0>=t+0)}'; then
                ss="$THUMB_TIME"
              else
                ss=$(awk -v d="${cdur:-0}" 'BEGIN{printf "%.0f",d/2}')
              fi
            else
              thumb_src="$video"
              ss=$(awk -v s="$st" -v t="$THUMB_TIME" -v d="$dur_s" \
                'BEGIN{x=s+t;if(x>=d-1)x=(d-1>0?d-1:0);printf "%.0f",x}')
            fi

            if [ "$THUMB" = "webp" ] || [ "$THUMB" = "both" ]; then
              ffmpeg -hide_banner -loglevel error -ss "$ss" -i "$thumb_src" -frames:v 1 \
                -vf "scale=${THUMB_WIDTH}:-2" -c:v libwebp -quality "$THUMB_QUALITY" \
                -y "${base}.thumb.webp" \
                && { thumb_ok=$((thumb_ok+1)); echo "    ✓ $(basename "$base").thumb.webp"; } \
                || { thumb_fail=$((thumb_fail+1)); echo "    ✗ webp 失败"; }
            fi
            
            if [ "$THUMB" = "jpg" ] || [ "$THUMB" = "both" ]; then
              ffmpeg -hide_banner -loglevel error -ss "$ss" -i "$thumb_src" -frames:v 1 \
                -vf "scale=${THUMB_WIDTH}:-2" -q:v 3 -y "${base}.thumb.jpg" \
                && { thumb_ok=$((thumb_ok+1)); echo "    ✓ $(basename "$base").thumb.jpg"; } \
                || { thumb_fail=$((thumb_fail+1)); echo "    ✗ jpg 失败"; }
            fi
          else
            thumb_skip=$((thumb_skip+1))
            case "$THUMB" in
              webp) echo "    - 缩略图已存在" ;;
              jpg)  echo "    - 缩略图已存在" ;;
              both) echo "    - 缩略图已存在" ;;
            esac
          fi
        fi
        i=$((i+1))
      done
    done

    echo
    echo "完成:$([ "$OUTPUT" = "timeline" ] && echo "成功 ${ok}(时间线片段),跳过 ${skip}" || echo "成功 ${ok},失败 ${fail},跳过 ${skip}")"
    [ "$THUMB" != "0" ] && \
      echo "缩略图:成功 ${thumb_ok},失败 ${thumb_fail},跳过 ${thumb_skip}"
  )
}

main() {
  install_deps
  parse_args "$@"
  echo "===== 全局配置:MODE=$MODE MAXDEPTH=$MAXDEPTH ====="

  if compgen -G "./*.mp4" >/dev/null; then
    process_single_dir "."
  fi

  find . -mindepth 1 -maxdepth "$MAXDEPTH" -type d -print0 | while IFS= read -r -d '' dir; do
    if [[ -f "$STOP_FILE" ]]; then
      echo -e "\n【检测到停止标记,终止任务】"
      rm -f "$STOP_FILE"
      return 0
    fi
    compgen -G "$dir/*.mp4" >/dev/null || continue
    process_single_dir "$dir"
  done
}

main "$@"

创建本地脚本

new_script="new_test.sh"

cat > "$new_script" << 'EOF'
#!/bin/bash

# 粘贴脚本源码

EOF

# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"

相关命令