Files
opencode-skill/skills/minimax-multimodal-toolkit/scripts/video/add_bgm.sh
Kunthawat Greethong 7edf5bc4d0 feat: Import 35+ skills, merge duplicates, add openclaw installer
Major updates:
- Added 35+ new skills from awesome-opencode-skills and antigravity repos
- Merged SEO skills into seo-master
- Merged architecture skills into architecture
- Merged security skills into security-auditor and security-coder
- Merged testing skills into testing-master and testing-patterns
- Merged pentesting skills into pentesting
- Renamed website-creator to thai-frontend-dev
- Replaced skill-creator with github version
- Removed Chutes references (use MiniMax API instead)
- Added install-openclaw-skills.sh for cross-platform installation
- Updated .env.example with MiniMax API credentials
2026-03-26 11:37:39 +07:00

222 lines
7.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Add background music to a video file (pure bash)
#
# Usage:
# bash scripts/video/add_bgm.sh --video input.mp4 --audio bgm.mp3 -o output.mp4
# bash scripts/video/add_bgm.sh --video input.mp4 --generate-bgm --music-prompt "upbeat pop" -o output.mp4
# bash scripts/video/add_bgm.sh --video input.mp4 --audio bgm.mp3 --replace-audio -o output.mp4
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
MUSIC_API_URL="${MINIMAX_API_HOST:-https://api.minimaxi.com}/v1/music_generation"
# ============================================================================
# Common functions
# ============================================================================
load_env() {
local env_file
for env_file in "$PROJECT_ROOT/.env" "$(pwd)/.env"; do
if [[ -f "$env_file" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}"; line="$(echo "$line" | xargs)"
[[ -z "$line" || "$line" != *=* ]] && continue
local key="${line%%=*}" val="${line#*=}"
key="$(echo "$key" | xargs)"; val="$(echo "$val" | xargs)"
if [[ ${#val} -ge 2 ]]; then
case "$val" in \"*\") val="${val:1:${#val}-2}" ;; \'*\') val="${val:1:${#val}-2}" ;; esac
fi
[[ -z "${!key:-}" ]] && export "$key=$val"
done < "$env_file"
fi
done
}
get_video_duration() {
ffprobe -v error -show_entries format=duration -of json "$1" 2>/dev/null | jq -r '.format.duration'
}
video_has_audio() {
local out
out="$(ffprobe -v error -select_streams a -show_entries stream=codec_type -of csv=p=0 "$1" 2>/dev/null)"
[[ "$out" == *audio* ]]
}
generate_music() {
local prompt="$1" output_path="$2" instrumental="${3:-false}"
local payload
local effective_prompt="${prompt:-background music, cinematic, ambient}"
if [[ "$instrumental" == "true" ]]; then
payload=$(jq -n \
--arg p "$effective_prompt. pure music, no lyrics" \
'{model: "music-2.5", prompt: $p, lyrics: "[intro] [outro]", output_format: "url"}')
else
payload=$(jq -n \
--arg p "$effective_prompt" \
'{model: "music-2.5", prompt: $p, lyrics: "[Intro]\nla da da\nla la la", output_format: "url"}')
fi
echo "Generating ${instrumental:+instrumental }music..."
echo " Prompt: $prompt"
local raw http_code response
raw="$(curl -s -w "\n%{http_code}" -X POST "$MUSIC_API_URL" \
-H "Authorization: Bearer ${MINIMAX_API_KEY}" \
-H "Content-Type: application/json" \
--max-time 300 -d "$payload")"
http_code="${raw##*$'\n'}"; response="${raw%$'\n'*}"
[[ "$http_code" -ge 400 ]] 2>/dev/null && { echo "Error: Music API HTTP $http_code" >&2; return 1; }
local sc
sc="$(echo "$response" | jq -r '.base_resp.status_code // 0')" 2>/dev/null || true
[[ "$sc" != "0" && -n "$sc" ]] && { echo "Error: Music API error: $(echo "$response" | jq '.base_resp')" >&2; return 1; }
local audio_url
audio_url="$(echo "$response" | jq -r '.data.audio_url // .data.audio // .data.audio_file.download_url // empty')"
[[ -z "$audio_url" ]] && { echo "Error: No audio URL in music response" >&2; return 1; }
mkdir -p "$(dirname "$output_path")"
# Download with retry
local attempt
for attempt in 1 2 3; do
if curl -s -o "$output_path" --max-time 120 "$audio_url" 2>/dev/null; then
local size; size="$(wc -c < "$output_path" | tr -d ' ')"
echo " Downloaded: $output_path ($size bytes)"
return 0
fi
if [[ $attempt -lt 3 ]]; then
local wait=$((2 ** attempt))
echo " Download attempt $attempt failed. Retrying in ${wait}s..."
sleep "$wait"
fi
done
echo "Error: Download failed after 3 attempts" >&2
return 1
}
# ============================================================================
# Main
# ============================================================================
main() {
load_env
local video="" audio="" output=""
local generate_bgm=false instrumental=false music_prompt=""
local bgm_volume=0.3 fade_in=0 fade_out=0 replace_audio=false
while [[ $# -gt 0 ]]; do
case "$1" in
--video) video="$2"; shift 2 ;;
--audio) audio="$2"; shift 2 ;;
--generate-bgm) generate_bgm=true; shift ;;
--instrumental) instrumental=true; shift ;;
--music-prompt) music_prompt="$2"; shift 2 ;;
--bgm-volume) bgm_volume="$2"; shift 2 ;;
--fade-in) fade_in="$2"; shift 2 ;;
--fade-out) fade_out="$2"; shift 2 ;;
--replace-audio) replace_audio=true; shift ;;
-o|--output) output="$2"; shift 2 ;;
-h|--help)
cat <<'USAGE'
Add Background Music to Video
Usage:
add_bgm.sh --video INPUT --audio BGM -o OUTPUT
add_bgm.sh --video INPUT --generate-bgm --music-prompt "style" -o OUTPUT
Options:
--video FILE Input video file (required)
--audio FILE Background music audio file
--generate-bgm Generate BGM via MiniMax API
--instrumental Make generated BGM instrumental
--music-prompt TEXT Prompt for BGM generation
--bgm-volume FLOAT BGM volume level (default: 0.3)
--fade-in SECS BGM fade-in duration
--fade-out SECS BGM fade-out duration
--replace-audio Replace original audio instead of mixing
-o, --output FILE Output video file (required)
Examples:
add_bgm.sh --video input.mp4 --audio bgm.mp3 -o output.mp4
add_bgm.sh --video input.mp4 --generate-bgm --music-prompt "upbeat pop" -o output.mp4
add_bgm.sh --video input.mp4 --audio bgm.mp3 --replace-audio -o output.mp4
USAGE
exit 0
;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
if [[ -z "$video" || ! -f "$video" ]]; then
echo "Error: Video file not found: ${video:-<none>}" >&2; exit 1
fi
if [[ -z "$audio" && "$generate_bgm" != "true" ]]; then
echo "Error: Provide --audio or --generate-bgm" >&2; exit 1
fi
if [[ -z "$output" ]]; then
echo "Error: --output / -o is required" >&2; exit 1
fi
local audio_path="$audio"
if $generate_bgm; then
if [[ -z "${MINIMAX_API_KEY:-}" ]]; then
echo "Error: MINIMAX_API_KEY not set." >&2; exit 1
fi
audio_path="${output%.*}_bgm.mp3"
generate_music "$music_prompt" "$audio_path" "$instrumental" || exit 1
fi
if [[ ! -f "$audio_path" ]]; then
echo "Error: Audio file not found: $audio_path" >&2; exit 1
fi
local duration
duration="$(get_video_duration "$video")"
echo "Video duration: $(printf '%.1f' "$duration")s"
mkdir -p "$(dirname "$output")"
local has_audio=false
video_has_audio "$video" && has_audio=true
local bgm_filter="[1:a]volume=${bgm_volume}"
[[ "$(echo "$fade_in > 0" | bc -l)" == "1" ]] && bgm_filter+=",afade=t=in:d=${fade_in}"
if [[ "$(echo "$fade_out > 0" | bc -l)" == "1" ]]; then
local fo_start
fo_start="$(echo "$duration - $fade_out" | bc -l)"
[[ "$(echo "$fo_start < 0" | bc -l)" == "1" ]] && fo_start=0
bgm_filter+=",afade=t=out:st=${fo_start}:d=${fade_out}"
fi
if $has_audio && ! $replace_audio; then
bgm_filter+="[bgm];[0:a][bgm]amix=inputs=2:duration=first:dropout_transition=2[aout]"
echo "Merging video + audio (mixing with original, bgm_volume=${bgm_volume})..."
ffmpeg -y \
-i "$video" -i "$audio_path" \
-filter_complex "$bgm_filter" \
-map 0:v -map "[aout]" \
-c:v copy -c:a aac -shortest "$output" 2>/dev/null
else
bgm_filter+="[bgm]"
echo "Merging video + audio (${replace_audio:+replacing original}${replace_audio:-no original audio}, bgm_volume=${bgm_volume})..."
ffmpeg -y \
-i "$video" -i "$audio_path" \
-filter_complex "$bgm_filter" \
-map 0:v -map "[bgm]" \
-c:v copy -c:a aac -shortest "$output" 2>/dev/null
fi
echo "Output saved: $output"
echo "Done!"
}
main "$@"