mix-voiceover.sh 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. #!/usr/bin/env bash
  2. # mix-voiceover.sh · Mix voiceover (人声主轨) + optional BGM into an MP4
  3. #
  4. # Usage:
  5. # bash mix-voiceover.sh <video.mp4> --voiceover=<voice.mp3> [options]
  6. #
  7. # Required:
  8. # --voiceover=<path> Path to voiceover mp3 (人声主轨, 来自 narrate-pipeline.mjs)
  9. #
  10. # Optional:
  11. # --bgm=<path> BGM mp3 path (overrides --bgm-mood)
  12. # --bgm-mood=<name> Pick a preset BGM from assets/ (educational / tech / tutorial / ...)
  13. # --bgm-volume=<0-1> BGM 静态音量, 默认 0.18 (相对人声)
  14. # --no-ducking 关闭 sidechain ducking(默认开启:人声响时 BGM 自动让路)
  15. # --voice-volume=<0-2> 人声音量倍率, 默认 1.0
  16. # --out=<path> 输出路径, 默认 <input>-voiced.mp4
  17. #
  18. # Behavior:
  19. # - 视频流 stream copy(不重编码,快)
  20. # - 人声始终是主轨,必带;BGM 可选
  21. # - 默认开 ducking:人声响时 BGM 压到约 -10dB,人声停时回升
  22. # - 输出长度 = 视频长度(人声/BGM 较短就尾静音;较长就截断)
  23. #
  24. # Examples:
  25. # bash mix-voiceover.sh anim.mp4 --voiceover=narration/voiceover.mp3
  26. # bash mix-voiceover.sh anim.mp4 --voiceover=v.mp3 --bgm-mood=educational
  27. # bash mix-voiceover.sh anim.mp4 --voiceover=v.mp3 --bgm=~/Music/song.mp3 --bgm-volume=0.12
  28. # bash mix-voiceover.sh anim.mp4 --voiceover=v.mp3 --bgm-mood=tech --no-ducking
  29. #
  30. set -e
  31. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  32. ASSETS_DIR="$SCRIPT_DIR/../assets"
  33. INPUT=""
  34. VOICEOVER=""
  35. BGM=""
  36. BGM_MOOD=""
  37. BGM_VOLUME="0.18"
  38. VOICE_VOLUME="1.0"
  39. DUCKING="1"
  40. OUTPUT=""
  41. for arg in "$@"; do
  42. case "$arg" in
  43. --voiceover=*) VOICEOVER="${arg#*=}" ;;
  44. --bgm=*) BGM="${arg#*=}" ;;
  45. --bgm-mood=*) BGM_MOOD="${arg#*=}" ;;
  46. --bgm-volume=*) BGM_VOLUME="${arg#*=}" ;;
  47. --voice-volume=*) VOICE_VOLUME="${arg#*=}" ;;
  48. --no-ducking) DUCKING="0" ;;
  49. --out=*) OUTPUT="${arg#*=}" ;;
  50. -*) echo "未知参数:$arg" >&2; exit 1 ;;
  51. *) INPUT="$arg" ;;
  52. esac
  53. done
  54. if [ -z "$INPUT" ] || [ ! -f "$INPUT" ]; then
  55. echo "Usage: bash mix-voiceover.sh <video.mp4> --voiceover=<v.mp3> [--bgm=<b.mp3> | --bgm-mood=<name>]" >&2
  56. exit 1
  57. fi
  58. if [ -z "$VOICEOVER" ] || [ ! -f "$VOICEOVER" ]; then
  59. echo "✗ 缺 --voiceover=<path>" >&2
  60. exit 1
  61. fi
  62. # 解析 BGM 来源
  63. if [ -z "$BGM" ] && [ -n "$BGM_MOOD" ]; then
  64. BGM="$ASSETS_DIR/bgm-${BGM_MOOD}.mp3"
  65. fi
  66. if [ -n "$BGM" ] && [ ! -f "$BGM" ]; then
  67. echo "✗ BGM 文件不存在: $BGM" >&2
  68. echo " 可用 mood: $(ls "$ASSETS_DIR" 2>/dev/null | grep -E '^bgm-.*\.mp3$' | sed 's/^bgm-//;s/\.mp3$//' | tr '\n' ' ')" >&2
  69. exit 1
  70. fi
  71. # 输出路径
  72. if [ -z "$OUTPUT" ]; then
  73. base="${INPUT%.*}"
  74. OUTPUT="${base}-voiced.mp4"
  75. fi
  76. echo "─ mix-voiceover ──────────────"
  77. echo " 视频: $INPUT"
  78. echo " 人声: $VOICEOVER (vol=$VOICE_VOLUME)"
  79. if [ -n "$BGM" ]; then
  80. echo " BGM: $BGM (vol=$BGM_VOLUME, ducking=$DUCKING)"
  81. else
  82. echo " BGM: (无)"
  83. fi
  84. echo " 输出: $OUTPUT"
  85. echo "──────────────────────────────"
  86. # ── ffmpeg filter graph ─────────────────────────────────────
  87. if [ -z "$BGM" ]; then
  88. # 仅人声
  89. ffmpeg -y -i "$INPUT" -i "$VOICEOVER" \
  90. -filter_complex "[1:a]volume=${VOICE_VOLUME}[a]" \
  91. -map 0:v -map "[a]" \
  92. -c:v copy -c:a aac -b:a 192k -shortest \
  93. "$OUTPUT"
  94. elif [ "$DUCKING" = "1" ]; then
  95. # 人声 + BGM + sidechain ducking
  96. ffmpeg -y -i "$INPUT" -i "$VOICEOVER" -i "$BGM" \
  97. -filter_complex "
  98. [1:a]volume=${VOICE_VOLUME}[voice];
  99. [2:a]volume=${BGM_VOLUME},aloop=loop=-1:size=2e9[bgm_lo];
  100. [bgm_lo][voice]sidechaincompress=threshold=0.04:ratio=8:attack=5:release=300:makeup=1[bgm_ducked];
  101. [voice][bgm_ducked]amix=inputs=2:duration=first:dropout_transition=0,afade=t=out:st=0:d=0.5:curve=tri[a]
  102. " \
  103. -map 0:v -map "[a]" \
  104. -c:v copy -c:a aac -b:a 192k -shortest \
  105. "$OUTPUT"
  106. else
  107. # 人声 + BGM 静态混合
  108. ffmpeg -y -i "$INPUT" -i "$VOICEOVER" -i "$BGM" \
  109. -filter_complex "
  110. [1:a]volume=${VOICE_VOLUME}[voice];
  111. [2:a]volume=${BGM_VOLUME},aloop=loop=-1:size=2e9[bgm];
  112. [voice][bgm]amix=inputs=2:duration=first:dropout_transition=0[a]
  113. " \
  114. -map 0:v -map "[a]" \
  115. -c:v copy -c:a aac -b:a 192k -shortest \
  116. "$OUTPUT"
  117. fi
  118. echo "✓ 完成:$OUTPUT"