Add mp3-to-mp4: convert MP3 to MP4 with static title card
Uses ffmpeg to generate an H.264/AAC video from an MP3 file with a centred title overlay. Supports optional background image (title at bottom) or solid colour (title centred). Includes spec, dryrun support, and 10 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
142
scripts/mp3-to-mp4
Executable file
142
scripts/mp3-to-mp4
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
# Defaults
|
||||
TITLE=""
|
||||
OUTPUT=""
|
||||
RESOLUTION="1920x1080"
|
||||
FONT_SIZE="72"
|
||||
FONT_COLOR="white"
|
||||
BG_COLOR="black"
|
||||
IMAGE=""
|
||||
DRYRUN=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT_NAME [OPTIONS] <input.mp3>
|
||||
|
||||
Convert an MP3 audio file into an MP4 video displaying a static title card.
|
||||
|
||||
Arguments:
|
||||
<input.mp3> Path to the MP3 file (required)
|
||||
|
||||
Options:
|
||||
-t, --title TEXT Title text to display (required)
|
||||
-o, --output PATH Output file path (default: input with .mp4 extension)
|
||||
-r, --resolution WxH Video resolution (default: 1920x1080)
|
||||
--font-size N Font size in pixels (default: 72)
|
||||
--font-color COLOR Font colour (default: white)
|
||||
--bg-color COLOR Background colour (default: black; ignored with --image)
|
||||
-i, --image PATH Background image (scaled to fill; title at bottom)
|
||||
-n, --dryrun Preview what would happen without creating files
|
||||
-h, --help Show this help message
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "Error: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
INPUT=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-t|--title) TITLE="$2"; shift 2 ;;
|
||||
-o|--output) OUTPUT="$2"; shift 2 ;;
|
||||
-r|--resolution) RESOLUTION="$2"; shift 2 ;;
|
||||
--font-size) FONT_SIZE="$2"; shift 2 ;;
|
||||
--font-color) FONT_COLOR="$2"; shift 2 ;;
|
||||
--bg-color) BG_COLOR="$2"; shift 2 ;;
|
||||
-i|--image) IMAGE="$2"; shift 2 ;;
|
||||
-n|--dryrun) DRYRUN=true; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-*) die "unknown option: $1" ;;
|
||||
*) INPUT="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate
|
||||
[[ -z "$INPUT" ]] && die "no input file specified. See --help."
|
||||
[[ -f "$INPUT" ]] || die "file not found: $INPUT"
|
||||
[[ -z "$TITLE" ]] && die "--title is required"
|
||||
command -v ffmpeg >/dev/null 2>&1 || die "ffmpeg not found. Install with: sudo apt install ffmpeg"
|
||||
command -v ffprobe >/dev/null 2>&1 || die "ffprobe not found. Install with: sudo apt install ffmpeg"
|
||||
[[ -n "$IMAGE" && ! -f "$IMAGE" ]] && die "image not found: $IMAGE"
|
||||
|
||||
# Default output path
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
OUTPUT="${INPUT%.*}.mp4"
|
||||
fi
|
||||
|
||||
# Get audio duration
|
||||
DURATION_SECS=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$INPUT" 2>/dev/null) \
|
||||
|| die "could not read audio duration from: $INPUT"
|
||||
|
||||
# Format duration for display
|
||||
format_duration() {
|
||||
local secs="${1%.*}"
|
||||
local mins=$((secs / 60))
|
||||
local rem=$((secs % 60))
|
||||
if [[ $mins -gt 0 ]]; then
|
||||
echo "${mins}m ${rem}s"
|
||||
else
|
||||
echo "${rem}s"
|
||||
fi
|
||||
}
|
||||
|
||||
DURATION_DISPLAY=$(format_duration "$DURATION_SECS")
|
||||
|
||||
# Get ffmpeg version
|
||||
FFMPEG_VERSION=$(ffmpeg -version | head -1 | awk '{print $3}')
|
||||
|
||||
if $DRYRUN; then
|
||||
echo "[dryrun] Input: $INPUT"
|
||||
echo "[dryrun] Output: $OUTPUT"
|
||||
echo "[dryrun] Title: $TITLE"
|
||||
echo "[dryrun] Duration: $DURATION_DISPLAY"
|
||||
echo "[dryrun] Resolution: $RESOLUTION"
|
||||
echo "[dryrun] Font size: $FONT_SIZE"
|
||||
echo "[dryrun] Font color: $FONT_COLOR"
|
||||
if [[ -n "$IMAGE" ]]; then
|
||||
echo "[dryrun] Background: $IMAGE (image)"
|
||||
else
|
||||
echo "[dryrun] Background: $BG_COLOR"
|
||||
fi
|
||||
echo "[dryrun] Dependencies OK: ffmpeg $FFMPEG_VERSION"
|
||||
echo "[dryrun] Would generate .mp4 video file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse resolution
|
||||
WIDTH="${RESOLUTION%x*}"
|
||||
HEIGHT="${RESOLUTION#*x}"
|
||||
|
||||
# Escape title for ffmpeg drawtext (colons and backslashes need escaping)
|
||||
ESCAPED_TITLE=$(echo "$TITLE" | sed "s/'/\\\\\\\\'/g" | sed 's/:/\\:/g')
|
||||
|
||||
if [[ -n "$IMAGE" ]]; then
|
||||
# Image background: scale+crop to fill, title centred at bottom
|
||||
ffmpeg -y \
|
||||
-loop 1 -i "$IMAGE" \
|
||||
-i "$INPUT" \
|
||||
-vf "scale=${WIDTH}:${HEIGHT}:force_original_aspect_ratio=increase,crop=${WIDTH}:${HEIGHT},drawtext=text='${ESCAPED_TITLE}':fontsize=${FONT_SIZE}:fontcolor=${FONT_COLOR}:x=(w-text_w)/2:y=h-text_h-40" \
|
||||
-c:v libx264 -tune stillimage -pix_fmt yuv420p \
|
||||
-c:a aac -b:a 192k \
|
||||
-shortest \
|
||||
"$OUTPUT"
|
||||
else
|
||||
# Solid colour background: title centred
|
||||
ffmpeg -y \
|
||||
-f lavfi -i "color=c=${BG_COLOR}:s=${RESOLUTION}:d=${DURATION_SECS}" \
|
||||
-i "$INPUT" \
|
||||
-vf "drawtext=text='${ESCAPED_TITLE}':fontsize=${FONT_SIZE}:fontcolor=${FONT_COLOR}:x=(w-text_w)/2:y=(h-text_h)/2" \
|
||||
-c:v libx264 -tune stillimage -pix_fmt yuv420p \
|
||||
-c:a aac -b:a 192k \
|
||||
-shortest \
|
||||
"$OUTPUT"
|
||||
fi
|
||||
|
||||
echo "$OUTPUT"
|
||||
Reference in New Issue
Block a user