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
222 lines
7.7 KiB
Bash
Executable File
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 "$@"
|