Files
AmiReel/amigadb-short-fx-v4.sh
T
klevze 8a734d58d9 Add Shorts wizard and v4 video-style pipeline
Turn the Shorts tab into a step-by-step wizard and port
amigadb-short-fx-v4.sh: Video background style, auto/manual crop,
source fade-in, and blur/opacity/margin settings.
2026-08-15 17:59:18 +02:00

905 lines
35 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
# AmigaDB YouTube Shorts generator
# Creates a 1080x1920, 50 FPS vertical MP4 from an existing Amiga video.
# V4 expanded style pack for 4:3 Amiga footage, with demoscene-inspired FX and optional image/video backgrounds.
SCRIPT_NAME="$(basename "$0")"
SCRIPT_VERSION="4.2"
INPUT=""
OUTPUT=""
START="00:00:00"
DURATION="30"
TITLE=""
GROUP=""
YEAR=""
TYPE=""
HOOK="THIS RAN ON AN AMIGA"
WEBSITE="AMIGADB.NET"
FONT_FILE=""
STYLE="brand"
BACKGROUND_IMAGE="amigadb_short.png"
BACKGROUND_VIDEO=""
VIDEO_BLUR="0"
VIDEO_OPACITY="1.0"
SOURCE_CROP_MODE="auto"
CROP_LEFT="0"
CROP_RIGHT="0"
CROP_TOP="0"
CROP_BOTTOM="0"
SOURCE_MARGIN_X="40"
SOURCE_FADE_IN="1.5"
CRF="16"
PRESET="slow"
usage() {
cat <<USAGE
Usage:
$SCRIPT_NAME --input VIDEO --title TITLE [options]
Required:
--input FILE Source video
--title TEXT Production title
Production metadata:
--group TEXT Group / creator
--year YEAR Release year
--type TEXT Demo, Intro, Cracktro, Music Disk, ...
--hook TEXT Opening hook (default: THIS RAN ON AN AMIGA)
--website TEXT Closing website text (default: AMIGADB.NET)
Clip settings:
--start TIME Start position, e.g. 00:01:23.500 (default: 00:00:00)
--duration SECONDS Output duration in seconds (default: 30)
--output FILE Output MP4 path
Visual style:
--style NAME brand, pixel, mirror, crop, workbench, blur,
crt, copper, stars, grid, tiles, scan,
starfield, plasma, rasterbars, vhs,
monitor, split, spectrum, video
default: brand
--background-image Background image for brand style
default: amigadb_short.png
--background-video Background video file for video style
--video-blur NUMBER Background video blur sigma (default: 0 = no blur)
--video-opacity NUM Background video opacity 0.0-1.0 (default: 1.0)
--source-crop MODE Source crop for video style: auto or center (default: auto)
auto detects/removes black left/right borders
--crop-left PX Manually crop PX pixels from source left edge (default: 0)
--crop-right PX Manually crop PX pixels from source right edge (default: 0)
--crop-top PX Manually crop PX pixels from source top edge (default: 0)
--crop-bottom PX Manually crop PX pixels from source bottom edge (default: 0)
Any manual crop value overrides --source-crop auto/center
--source-margin-x PX Left/right margin around source video in final 1080px frame
(default: 40; use 0 for edge-to-edge)
--source-fade-in SEC Source overlay fade-in at start, in seconds
(default: 1.5; use 0 to disable)
Encoding:
--font FILE Bold TrueType/OpenType font file
--crf NUMBER H.264 quality; lower is better (default: 16)
--preset NAME x264 preset (default: slow)
Other:
-h, --help Show this help
--version Show script version
Examples:
$SCRIPT_NAME \
--input "ami-back.mp4" \
--start "00:00:34" \
--duration 30 \
--title "Ami-Back V1.04A" \
--group "Pirates" \
--year "1991" \
--type "Cracktro" \
--hook "A 1991 AMIGA CRACKTRO" \
--style brand \
--background-image "amigadb_short.png" \
--output "ami-back-brand-short.mp4"
$SCRIPT_NAME \
--input "demo.mp4" \
--title "Demo Title" \
--group "Demo Group" \
--year "1992" \
--type "Demo" \
--style crt
$SCRIPT_NAME \
--input "demo.mp4" \
--title "Demo Title" \
--style video \
--background-video "backgrounds/neon-tunnel.mp4" \
--video-blur 8 \
--video-opacity 0.65 \
--crop-left 120 \
--crop-right 120 \
--crop-top 0 \
--crop-bottom 0 \
--source-margin-x 40 \
--output "demo-video-short.mp4"
USAGE
}
fail() {
printf 'Error: %s\n' "$*" >&2
exit 1
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
find_default_font() {
local candidate=""
if command_exists fc-match; then
candidate="$(fc-match -f '%{file}\n' 'DejaVu Sans:style=Bold' 2>/dev/null | head -n 1 || true)"
if [[ -n "$candidate" && -f "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
fi
for candidate in \
/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf \
/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf \
/usr/share/fonts/opentype/noto/NotoSans-Bold.ttf; do
if [[ -f "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
while (($#)); do
case "$1" in
--input)
(($# >= 2)) || fail "Missing value after --input"
INPUT="$2"
shift 2
;;
--output)
(($# >= 2)) || fail "Missing value after --output"
OUTPUT="$2"
shift 2
;;
--start)
(($# >= 2)) || fail "Missing value after --start"
START="$2"
shift 2
;;
--duration)
(($# >= 2)) || fail "Missing value after --duration"
DURATION="$2"
shift 2
;;
--title)
(($# >= 2)) || fail "Missing value after --title"
TITLE="$2"
shift 2
;;
--group)
(($# >= 2)) || fail "Missing value after --group"
GROUP="$2"
shift 2
;;
--year)
(($# >= 2)) || fail "Missing value after --year"
YEAR="$2"
shift 2
;;
--type)
(($# >= 2)) || fail "Missing value after --type"
TYPE="$2"
shift 2
;;
--hook)
(($# >= 2)) || fail "Missing value after --hook"
HOOK="$2"
shift 2
;;
--website)
(($# >= 2)) || fail "Missing value after --website"
WEBSITE="$2"
shift 2
;;
--font)
(($# >= 2)) || fail "Missing value after --font"
FONT_FILE="$2"
shift 2
;;
--background-image|--background)
(($# >= 2)) || fail "Missing value after --background-image"
BACKGROUND_IMAGE="$2"
shift 2
;;
--background-video)
(($# >= 2)) || fail "Missing value after --background-video"
BACKGROUND_VIDEO="$2"
shift 2
;;
--video-blur|--background-video-blur)
(($# >= 2)) || fail "Missing value after --video-blur"
VIDEO_BLUR="$2"
shift 2
;;
--video-opacity|--background-video-opacity)
(($# >= 2)) || fail "Missing value after --video-opacity"
VIDEO_OPACITY="$2"
shift 2
;;
--source-crop)
(($# >= 2)) || fail "Missing value after --source-crop"
SOURCE_CROP_MODE="${2,,}"
shift 2
;;
--crop-left|--source-crop-left)
(($# >= 2)) || fail "Missing value after --crop-left"
CROP_LEFT="$2"
shift 2
;;
--crop-right|--source-crop-right)
(($# >= 2)) || fail "Missing value after --crop-right"
CROP_RIGHT="$2"
shift 2
;;
--crop-top|--source-crop-top)
(($# >= 2)) || fail "Missing value after --crop-top"
CROP_TOP="$2"
shift 2
;;
--crop-bottom|--source-crop-bottom)
(($# >= 2)) || fail "Missing value after --crop-bottom"
CROP_BOTTOM="$2"
shift 2
;;
--source-margin-x|--video-margin-x|--source-margin)
(($# >= 2)) || fail "Missing value after --source-margin-x"
SOURCE_MARGIN_X="$2"
shift 2
;;
--source-fade-in|--source-fade|--fade-in)
(($# >= 2)) || fail "Missing value after --source-fade-in"
SOURCE_FADE_IN="$2"
shift 2
;;
--style)
(($# >= 2)) || fail "Missing value after --style"
STYLE="${2,,}"
shift 2
;;
--crf)
(($# >= 2)) || fail "Missing value after --crf"
CRF="$2"
shift 2
;;
--preset)
(($# >= 2)) || fail "Missing value after --preset"
PRESET="$2"
shift 2
;;
--version)
printf "%s v%s\n" "$SCRIPT_NAME" "$SCRIPT_VERSION"
exit 0
;;
-h|--help)
usage
exit 0
;;
*)
fail "Unknown argument: $1"
;;
esac
done
command_exists ffmpeg || fail "ffmpeg is not installed or not in PATH"
[[ -n "$INPUT" ]] || fail "--input is required"
[[ -f "$INPUT" ]] || fail "Input file not found: $INPUT"
[[ -n "$TITLE" ]] || fail "--title is required"
[[ "$DURATION" =~ ^[0-9]+([.][0-9]+)?$ ]] || fail "--duration must be a positive number"
awk -v d="$DURATION" 'BEGIN { exit !(d > 0) }' || fail "--duration must be greater than zero"
[[ "$CRF" =~ ^[0-9]+$ ]] || fail "--crf must be an integer"
[[ "$VIDEO_BLUR" =~ ^[0-9]+([.][0-9]+)?$ ]] || fail "--video-blur must be zero or a positive number"
awk -v v="$VIDEO_BLUR" 'BEGIN { exit !(v >= 0) }' || fail "--video-blur must be zero or greater"
[[ "$VIDEO_OPACITY" =~ ^([0-9]+([.][0-9]+)?|[.][0-9]+)$ ]] || fail "--video-opacity must be a number between 0.0 and 1.0"
awk -v v="$VIDEO_OPACITY" 'BEGIN { exit !(v >= 0 && v <= 1) }' || fail "--video-opacity must be between 0.0 and 1.0"
case "$SOURCE_CROP_MODE" in
auto|center) ;;
*) fail "--source-crop must be 'auto' or 'center'" ;;
esac
for crop_value in "$CROP_LEFT" "$CROP_RIGHT" "$CROP_TOP" "$CROP_BOTTOM"; do
[[ "$crop_value" =~ ^[0-9]+$ ]] || fail "Manual crop values must be non-negative integer pixel values"
done
[[ "$SOURCE_MARGIN_X" =~ ^[0-9]+$ ]] || fail "--source-margin-x must be a non-negative integer pixel value"
(( SOURCE_MARGIN_X < 540 )) || fail "--source-margin-x must be less than 540"
SOURCE_TARGET_WIDTH=$((1080 - (SOURCE_MARGIN_X * 2)))
[[ "$SOURCE_FADE_IN" =~ ^([0-9]+([.][0-9]+)?|[.][0-9]+)$ ]] || fail "--source-fade-in must be zero or a positive number"
awk -v v="$SOURCE_FADE_IN" 'BEGIN { exit !(v >= 0) }' || fail "--source-fade-in must be zero or greater"
SOURCE_FADE_IN="$(awk -v v="$SOURCE_FADE_IN" -v d="$DURATION" 'BEGIN {
if (v > d) v = d;
printf "%.3f", v;
}')"
case "$STYLE" in
brand|pixel|mirror|crop|workbench|blur|crt|copper|stars|grid|tiles|scan|starfield|plasma|rasterbars|vhs|monitor|split|spectrum|video)
;;
*)
fail "Unknown style '$STYLE'. Use brand, pixel, mirror, crop, workbench, blur, crt, copper, stars, grid, tiles, scan, starfield, plasma, rasterbars, vhs, monitor, split, spectrum, or video"
;;
esac
if [[ -z "$FONT_FILE" ]]; then
FONT_FILE="$(find_default_font || true)"
fi
[[ -n "$FONT_FILE" && -f "$FONT_FILE" ]] || fail "No usable font found. Install fonts-dejavu-core or use --font FILE"
if [[ -z "$OUTPUT" ]]; then
base="$(basename "$INPUT")"
base="${base%.*}"
OUTPUT="${base}-${STYLE}-short.mp4"
fi
mkdir -p "$(dirname "$OUTPUT")"
TMP_DIR="$(mktemp -d -t amigadb-short.XXXXXX)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
printf '%s\n' "$HOOK" > "$TMP_DIR/hook.txt"
printf '%s\n' "$TITLE" > "$TMP_DIR/title.txt"
META_PARTS=()
[[ -n "$GROUP" ]] && META_PARTS+=("$GROUP")
[[ -n "$YEAR" ]] && META_PARTS+=("$YEAR")
[[ -n "$TYPE" ]] && META_PARTS+=("$TYPE")
META=""
if ((${#META_PARTS[@]})); then
for part in "${META_PARTS[@]}"; do
if [[ -n "$META" ]]; then
META+=" · "
fi
META+="$part"
done
fi
printf '%s\n' "$META" > "$TMP_DIR/meta.txt"
printf '%s\n' "$WEBSITE" > "$TMP_DIR/website.txt"
INFO_START="$(awk -v d="$DURATION" 'BEGIN { v=d-7; if (v<0) v=0; printf "%.3f", v }')"
CTA_START="$(awk -v d="$DURATION" 'BEGIN { v=d-3.5; if (v<0) v=0; printf "%.3f", v }')"
HOOK_END="$(awk -v d="$DURATION" 'BEGIN { v=2.7; if (d<v) v=d; printf "%.3f", v }')"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
resolve_path() {
local p="$1"
if [[ -f "$p" ]]; then
printf "%s\n" "$p"
return 0
fi
if [[ -f "$SCRIPT_DIR/$p" ]]; then
printf "%s\n" "$SCRIPT_DIR/$p"
return 0
fi
return 1
}
# Detect left/right black capture borders while deliberately ignoring the top
# and bottom 15% of the frame. This prevents full-width overscan/raster lines
# near the top/bottom from fooling cropdetect. Used by --style video.
detect_source_horizontal_crop() {
local detect_duration candidate crop_spec
detect_duration="$(awk -v d="$DURATION" 'BEGIN { v=(d<4?d:4); if (v<0.5) v=0.5; printf "%.3f", v }')"
# Inspect only the middle 70% vertically, then use cropdetect to locate the
# active horizontal picture. We only keep W and X; the source keeps its full
# height so this specifically removes left/right black borders.
candidate="$({
ffmpeg -hide_banner -ss "$START" -t "$detect_duration" -i "$INPUT" \
-an -vf "crop=iw:trunc(ih*0.70/2)*2:0:trunc(ih*0.15/2)*2,cropdetect=limit=28:round=2:reset=0" \
-f null - 2>&1 || true
} | grep -oE 'crop=[0-9]+:[0-9]+:[0-9]+:[0-9]+' \
| sort | uniq -c | sort -nr | head -n 1 | awk '{print $2}' || true)"
[[ "$candidate" =~ ^crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)$ ]] || return 1
local w="${BASH_REMATCH[1]}"
local x="${BASH_REMATCH[3]}"
# cropdetect can occasionally leave a few black pixels at each side. Trim a
# tiny extra safety margin (0.5% of detected width per side), still keeping
# even dimensions/offsets for H.264 compatibility.
crop_spec="$(awk -v w="$w" -v x="$x" 'BEGIN {
inset = int((w * 0.005) / 2) * 2;
nw = w - (2 * inset);
nx = x + inset;
nw = int(nw / 2) * 2;
nx = int(nx / 2) * 2;
if (nw < 16) exit 1;
printf "%d:%d", nw, nx;
}')" || return 1
[[ -n "$crop_spec" ]] || return 1
printf '%s\n' "$crop_spec"
}
if [[ "$STYLE" == "brand" ]]; then
BACKGROUND_IMAGE="$(resolve_path "$BACKGROUND_IMAGE" || true)"
[[ -n "$BACKGROUND_IMAGE" && -f "$BACKGROUND_IMAGE" ]] || fail "Brand style requires background image. Put amigadb_short.png next to the script or pass --background-image FILE"
fi
if [[ "$STYLE" == "video" ]]; then
[[ -n "$BACKGROUND_VIDEO" ]] || fail "Video style requires --background-video FILE"
BACKGROUND_VIDEO="$(resolve_path "$BACKGROUND_VIDEO" || true)"
[[ -n "$BACKGROUND_VIDEO" && -f "$BACKGROUND_VIDEO" ]] || fail "Background video not found. Pass a valid file with --background-video FILE"
fi
SOURCE_CROP_FILTER="crop='min(iw,ih*4/3)':'min(ih,iw*3/4)':'(iw-ow)/2':'(ih-oh)/2'"
SOURCE_SCALE_FILTER="scale=${SOURCE_TARGET_WIDTH}:-2:flags=lanczos"
SOURCE_CROP_SPEC="centered 4:3"
MANUAL_CROP=0
if (( CROP_LEFT > 0 || CROP_RIGHT > 0 || CROP_TOP > 0 || CROP_BOTTOM > 0 )); then
MANUAL_CROP=1
fi
if [[ "$STYLE" == "video" && "$MANUAL_CROP" -eq 1 ]]; then
# Manual values are pixels removed from each source edge. Manual crop takes
# precedence over automatic/center crop modes. Scale the remaining picture
# to the configured Short source width while preserving its aspect ratio.
SOURCE_CROP_FILTER="crop=iw-${CROP_LEFT}-${CROP_RIGHT}:ih-${CROP_TOP}-${CROP_BOTTOM}:${CROP_LEFT}:${CROP_TOP}"
SOURCE_SCALE_FILTER="scale=${SOURCE_TARGET_WIDTH}:-2:flags=lanczos"
SOURCE_CROP_SPEC="manual L=${CROP_LEFT} R=${CROP_RIGHT} T=${CROP_TOP} B=${CROP_BOTTOM}"
elif [[ "$STYLE" == "video" && "$SOURCE_CROP_MODE" == "auto" ]]; then
DETECTED_SOURCE_CROP="$(detect_source_horizontal_crop || true)"
if [[ -n "$DETECTED_SOURCE_CROP" ]]; then
DETECTED_W="${DETECTED_SOURCE_CROP%%:*}"
DETECTED_X="${DETECTED_SOURCE_CROP##*:}"
SOURCE_CROP_FILTER="crop=${DETECTED_W}:ih:${DETECTED_X}:0"
SOURCE_SCALE_FILTER="scale=${SOURCE_TARGET_WIDTH}:-2:flags=lanczos"
SOURCE_CROP_SPEC="w=${DETECTED_W}, x=${DETECTED_X} (auto left/right black crop)"
else
printf 'Warning: could not auto-detect left/right source black borders; using centered 4:3 crop.\n' >&2
fi
fi
# Escape paths for FFmpeg filter option parsing.
ffmpeg_escape_path() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//:/\\:}"
value="${value//\'/\\\'}"
printf '%s' "$value"
}
FONT_ESC="$(ffmpeg_escape_path "$FONT_FILE")"
HOOK_ESC="$(ffmpeg_escape_path "$TMP_DIR/hook.txt")"
TITLE_ESC="$(ffmpeg_escape_path "$TMP_DIR/title.txt")"
META_ESC="$(ffmpeg_escape_path "$TMP_DIR/meta.txt")"
WEBSITE_ESC="$(ffmpeg_escape_path "$TMP_DIR/website.txt")"
BACKGROUND_ESC=""
if [[ "$STYLE" == "brand" ]]; then
BACKGROUND_ESC="$(ffmpeg_escape_path "$BACKGROUND_IMAGE")"
fi
HOOK_BOX_Y=0
HOOK_BOX_H=245
HOOK_TEXT_Y=70
HOOK_FONT_SIZE=70
INFO_BOX_Y=1490
INFO_BOX_H=430
TITLE_TEXT_Y=1560
TITLE_FONT_SIZE=62
META_TEXT_Y=1655
META_FONT_SIZE=42
WEBSITE_TEXT_Y=1780
WEBSITE_FONT_SIZE=52
if [[ "$STYLE" == "brand" ]]; then
HOOK_BOX_Y=250
HOOK_BOX_H=140
HOOK_TEXT_Y=285
HOOK_FONT_SIZE=60
INFO_BOX_Y=1310
INFO_BOX_H=270
TITLE_TEXT_Y=1360
TITLE_FONT_SIZE=56
META_TEXT_Y=1435
META_FONT_SIZE=38
WEBSITE_TEXT_Y=1510
WEBSITE_FONT_SIZE=48
fi
HAS_AUDIO=0
if command_exists ffprobe && ffprobe -v error -select_streams a:0 -show_entries stream=index -of csv=p=0 "$INPUT" | grep -q .; then
HAS_AUDIO=1
fi
if [[ "$STYLE" == "spectrum" && "$HAS_AUDIO" -ne 1 ]]; then
fail "The spectrum style requires an audio stream in the source video"
fi
case "$STYLE" in
brand)
BASE_FILTER=$(cat <<'FILTER'
[1:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1[bg];
[0:v]scale=920:700:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0x07111f,setsar=1,eq=contrast=1.02:saturation=1.02[fg];
[bg]drawbox=x=65:y=505:w=950:h=730:[email protected]:t=fill[bg2];
[bg2][fg]overlay=x=(W-w)/2:y=520:format=auto[base]
FILTER
)
;;
pixel)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=270:480:force_original_aspect_ratio=increase,crop=270:480,scale=1080:1920:flags=neighbor,eq=brightness=-0.36:saturation=1.18,gblur=sigma=2,setsar=1[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0x55c8ff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=0:y='300+20*sin(t*1.70)':w=1080:h=8:[email protected]:t=fill,
drawbox=x=0:y='1610+18*sin(t*1.35)':w=1080:h=8:[email protected]:t=fill[base]
FILTER
)
;;
mirror)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=4[canvas_src][fg_src][top_src][bottom_src];
[canvas_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1,drawbox=x=0:y=0:w=1080:h=1920:color=0x010203:t=fill[canvas];
[fg_src]scale=940:760:force_original_aspect_ratio=decrease,setsar=1[fg];
[top_src]scale=1400:470:flags=lanczos,vflip,eq=brightness=-0.48:contrast=0.92:saturation=0.68,gblur=sigma=7,setsar=1,format=rgba,geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='255*0.62*(Y/H)'[top];
[bottom_src]scale=1400:470:flags=lanczos,vflip,eq=brightness=-0.50:contrast=0.90:saturation=0.64,gblur=sigma=7,setsar=1,format=rgba,geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='255*0.58*(1-Y/H)'[bottom];
[canvas][top]overlay=x=(W-w)/2:y=250:format=auto[tmp1];
[tmp1][bottom]overlay=x=(W-w)/2:y=1435:format=auto[tmp2];
[tmp2][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
crop)
BASE_FILTER=$(cat <<'FILTER'
[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1,eq=brightness=-0.04:saturation=1.05[base]
FILTER
)
;;
workbench)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[canvas_src][screen_src];
[canvas_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x090c20:t=fill,
drawbox=x=55:y=410:w=970:h=930:color=0xc4c8d0:t=fill,
drawbox=x=70:y=425:w=940:h=900:color=0x1b4f9c:t=fill,
drawbox=x=70:y=425:w=940:h=74:color=0xf28c28:t=fill,
drawbox=x=88:y=445:w=34:h=34:color=0x111111:t=fill[window];
[screen_src]scale=960:760:force_original_aspect_ratio=decrease,pad=iw+10:ih+10:5:5:color=0xffffff,setsar=1[screen];
[window][screen]overlay=x=(W-w)/2:y=525:format=auto[base]
FILTER
)
;;
blur)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=35,eq=brightness=-0.22:saturation=0.85,setsar=1[bg];
[fg_src]scale=1080:900:force_original_aspect_ratio=decrease,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
crt)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=28,eq=brightness=-0.45:saturation=0.55,setsar=1[bg];
[fg_src]scale=980:800:force_original_aspect_ratio=decrease,lenscorrection=k1=-0.060:k2=0.020,vignette=angle=PI/5,eq=contrast=1.08:saturation=1.10,setsar=1,
drawgrid=width=iw:height=4:thickness=1:[email protected][fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=50:y='220+mod(t*340,1480)':w=980:h=4:[email protected]:t=fill[base]
FILTER
)
;;
copper)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,eq=brightness=-0.50:saturation=0.80,gblur=sigma=18,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:[email protected]:t=fill,
drawbox=x=0:y='220+20*sin(t*1.00)':w=1080:h=140:[email protected]:t=fill,
drawbox=x=0:y='420+24*sin(t*1.20)':w=1080:h=100:[email protected]:t=fill,
drawbox=x=0:y='660+16*sin(t*1.50)':w=1080:h=120:[email protected]:t=fill,
drawbox=x=0:y='1110+22*sin(t*1.10)':w=1080:h=150:[email protected]:t=fill,
drawbox=x=0:y='1360+18*sin(t*1.30)':w=1080:h=110:[email protected]:t=fill,
drawbox=x=0:y='1660+15*sin(t*1.70)':w=1080:h=130:[email protected]:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
stars)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=30,eq=brightness=-0.72:saturation=0.28,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:[email protected]:t=fill,
drawbox=x='mod(100+80*t,1080)':y='mod(80+210*t,1920)':w=3:h=3:[email protected]:t=fill,
drawbox=x='mod(880-55*t,1080)':y='mod(220+170*t,1920)':w=2:h=2:[email protected]:t=fill,
drawbox=x='mod(300+120*t,1080)':y='mod(520+140*t,1920)':w=3:h=3:[email protected]:t=fill,
drawbox=x='mod(980-75*t,1080)':y='mod(710+160*t,1920)':w=2:h=2:[email protected]:t=fill,
drawbox=x='mod(420+65*t,1080)':y='mod(1040+200*t,1920)':w=4:h=4:[email protected]:t=fill,
drawbox=x='mod(750-90*t,1080)':y='mod(1280+110*t,1920)':w=2:h=2:[email protected]:t=fill,
drawbox=x='mod(210+75*t,1080)':y='mod(1440+180*t,1920)':w=3:h=3:[email protected]:t=fill,
drawbox=x='mod(1030-60*t,1080)':y='mod(1680+140*t,1920)':w=2:h=2:[email protected]:t=fill,
drawbox=x='mod(560+95*t,1080)':y='mod(260+260*t,1920)':w=2:h=2:[email protected]:t=fill,
drawbox=x='mod(60+110*t,1080)':y='mod(860+120*t,1920)':w=3:h=3:[email protected]:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+12:ih+12:6:6:color=0x55c8ff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
grid)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=24,eq=brightness=-0.62:saturation=0.36,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:[email protected]:t=fill,
drawgrid=width=90:height=90:thickness=2:[email protected],
drawbox=x=0:y=0:w=1080:h=1120:[email protected]:t=fill,
drawbox=x=0:y=1120:w=1080:h=4:[email protected]:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xff54d9,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
tiles)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=5[fg_src][a][b][c][d];
[a]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[ta];
[b]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,hflip,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[tb];
[c]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,vflip,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[tc];
[d]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,hflip,vflip,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[td];
[ta][tb]hstack=inputs=2[top];
[tc][td]hstack=inputs=2[bottom];
[top][bottom]vstack=inputs=2[bg];
[fg_src]scale=980:800:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
scan)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,scale=360:640:flags=neighbor,scale=1080:1920:flags=neighbor,eq=brightness=-0.44:saturation=1.05,gblur=sigma=2,setsar=1[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0x55c8ff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=40:y='220+mod(t*380,1480)':w=1000:h=8:[email protected]:t=fill,
drawbox=x=0:y='280+20*sin(t*1.40)':w=1080:h=6:[email protected]:t=fill,
drawbox=x=0:y='1640+15*sin(t*1.20)':w=1080:h=6:[email protected]:t=fill[base]
FILTER
)
;;
starfield)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=32,eq=brightness=-0.78:saturation=0.22,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:[email protected]:t=fill,
drawbox=x='mod(120+75*t,1080)':y='mod(80+520*t,1920)':w=3:h=18:[email protected]:t=fill,
drawbox=x='mod(970-55*t,1080)':y='mod(250+430*t,1920)':w=2:h=14:[email protected]:t=fill,
drawbox=x='mod(370+95*t,1080)':y='mod(430+600*t,1920)':w=4:h=24:[email protected]:t=fill,
drawbox=x='mod(810-80*t,1080)':y='mod(640+470*t,1920)':w=2:h=17:[email protected]:t=fill,
drawbox=x='mod(230+65*t,1080)':y='mod(900+540*t,1920)':w=3:h=20:[email protected]:t=fill,
drawbox=x='mod(1020-100*t,1080)':y='mod(1110+650*t,1920)':w=4:h=26:[email protected]:t=fill,
drawbox=x='mod(560+85*t,1080)':y='mod(1320+490*t,1920)':w=2:h=16:[email protected]:t=fill,
drawbox=x='mod(70+110*t,1080)':y='mod(1510+590*t,1920)':w=3:h=22:[email protected]:t=fill,
drawbox=x='mod(720-60*t,1080)':y='mod(1720+450*t,1920)':w=2:h=15:[email protected]:t=fill,
drawbox=x='mod(440+105*t,1080)':y='mod(180+700*t,1920)':w=3:h=28:[email protected]:t=fill,
drawbox=x='mod(900-72*t,1080)':y='mod(780+560*t,1920)':w=2:h=18:[email protected]:t=fill,
drawbox=x='mod(310+90*t,1080)':y='mod(1240+620*t,1920)':w=4:h=24:[email protected]:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0x70cfff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
plasma)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[dim_src][fg_src];
nullsrc=size=1080x1920:rate=50,format=rgb24,
geq=r='128+105*sin(X/68+T*1.9)+22*sin(Y/91-T*1.1)':g='128+96*sin(Y/75+T*1.5)+26*sin((X+Y)/120+T*1.3)':b='128+110*sin((X-Y)/82-T*1.7)',
gblur=sigma=18,eq=brightness=-0.16:saturation=1.22[plasma];
[dim_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=26,eq=brightness=-0.70:saturation=0.35,setsar=1[dim];
[plasma][dim]blend=all_mode=overlay:all_opacity=0.35[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
rasterbars)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=30,eq=brightness=-0.70:saturation=0.35,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:[email protected]:t=fill,
drawbox=x=0:y='250+90*sin(t*1.25)':w=1080:h=30:[email protected]:t=fill,
drawbox=x=0:y='300+90*sin(t*1.25)':w=1080:h=24:[email protected]:t=fill,
drawbox=x=0:y='345+90*sin(t*1.25)':w=1080:h=18:[email protected]:t=fill,
drawbox=x=0:y='1510+85*sin(t*1.10+1.2)':w=1080:h=18:[email protected]:t=fill,
drawbox=x=0:y='1550+85*sin(t*1.10+1.2)':w=1080:h=24:[email protected]:t=fill,
drawbox=x=0:y='1600+85*sin(t*1.10+1.2)':w=1080:h=30:[email protected]:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
vhs)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=26,eq=brightness=-0.44:saturation=0.55,noise=alls=10:allf=t+u,setsar=1[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,chromashift=cbh=2:crh=-2,noise=alls=5:allf=t+u,eq=contrast=1.08:saturation=0.92,vignette=angle=PI/4.5,setsar=1,
drawgrid=width=iw:height=5:thickness=1:[email protected][fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=0:y='300+mod(t*510,1320)':w=1080:h=10:[email protected]:t=fill,
drawbox=x=0:y='420+mod(t*320,1080)':w=1080:h=3:[email protected]:t=fill[base]
FILTER
)
;;
monitor)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][screen_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=34,eq=brightness=-0.68:saturation=0.32,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:[email protected]:t=fill,
drawbox=x=42:y=365:w=996:h=1000:color=0xaaa69a:t=fill,
drawbox=x=68:y=392:w=944:h=944:color=0x2a2926:t=fill,
drawbox=x=105:y=438:w=870:h=790:color=0x050505:t=fill,
drawbox=x=792:y=1260:w=44:h=18:color=0x6aff73:t=fill,
drawbox=x=852:y=1253:w=30:h=30:color=0x232323:t=fill,
drawbox=x=910:y=1253:w=30:h=30:color=0x232323:t=fill,
drawbox=x=380:y=1365:w=320:h=75:color=0x8f8b80:t=fill,
drawbox=x=290:y=1438:w=500:h=36:color=0x77736b:t=fill[monitor];
[screen_src]scale=840:750:force_original_aspect_ratio=decrease,lenscorrection=k1=-0.045:k2=0.012,vignette=angle=PI/5,eq=contrast=1.06:saturation=1.06,setsar=1[screen];
[monitor][screen]overlay=x=(W-w)/2:y=458:format=auto[base]
FILTER
)
;;
split)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=4[bg_src][main_src][top_src][bottom_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=34,eq=brightness=-0.55:saturation=0.55,setsar=1[bg];
[top_src]scale=1080:520:force_original_aspect_ratio=increase,crop=1080:360:y='(ih-360)/2',eq=brightness=-0.28:saturation=0.88,gblur=sigma=2,setsar=1[top];
[bottom_src]scale=1080:520:force_original_aspect_ratio=increase,crop=1080:360:y='(ih-360)/2',hflip,eq=brightness=-0.32:saturation=0.80,gblur=sigma=2,setsar=1[bottom];
[main_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xffffff,setsar=1[main];
[bg][top]overlay=x=0:y=275[tmp1];
[tmp1][bottom]overlay=x=0:y=1285[tmp2];
[tmp2][main]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
spectrum)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=30,eq=brightness=-0.62:saturation=0.40,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:[email protected]:t=fill[bg];
[fg_src]scale=1000:780:force_original_aspect_ratio=decrease,pad=iw+12:ih+12:6:6:color=0x55c8ff,setsar=1[fg];
[0:a]aformat=sample_fmts=fltp:channel_layouts=stereo,showspectrum=s=1000x250:mode=combined:color=rainbow:scale=sqrt:fscale=log:slide=scroll:win_func=hann:fps=50:opacity=0.85,format=rgba[spec];
[bg][spec]overlay=x=(W-w)/2:y=1360:format=auto[tmp];
[tmp][fg]overlay=x=(W-w)/2:y=480:format=auto[base]
FILTER
)
;;
video)
VIDEO_BLUR_FILTER=""
if awk -v v="$VIDEO_BLUR" 'BEGIN { exit !(v > 0) }'; then
VIDEO_BLUR_FILTER=",gblur=sigma=${VIDEO_BLUR}"
fi
BASE_FILTER=$(cat <<FILTER
color=c=black:s=1080x1920:r=50[black];
[1:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1${VIDEO_BLUR_FILTER},format=rgba,colorchannelmixer=aa=${VIDEO_OPACITY}[bgvideo];
[black][bgvideo]overlay=x=0:y=0:format=auto[bg];
[0:v]${SOURCE_CROP_FILTER},${SOURCE_SCALE_FILTER},setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
esac
SOURCE_FADE_PREFIX=""
if awk -v v="$SOURCE_FADE_IN" 'BEGIN { exit !(v > 0) }'; then
# Fade only the Amiga source stream. Background video/image stays fully
# visible from frame 0; the source overlay (and source-derived layers)
# ramp from transparent to opaque.
SOURCE_FADE_PREFIX="[0:v]format=rgba,fade=t=in:st=0:d=${SOURCE_FADE_IN}:alpha=1[srcfade];"
BASE_FILTER="${BASE_FILTER//\[0:v]/[srcfade]}"
fi
FILTER_COMPLEX=$(cat <<FILTER
${SOURCE_FADE_PREFIX}${BASE_FILTER};
[base]
drawbox=x=0:y=0:w=1080:h=245:color=[email protected]:t=fill:enable='between(t,0,${HOOK_END})',
drawtext=fontfile='${FONT_ESC}':textfile='${HOOK_ESC}':fontcolor=white:fontsize=70:line_spacing=10:x=(w-text_w)/2:y=70:fix_bounds=true:shadowcolor=[email protected]:shadowx=4:shadowy=4:enable='between(t,0,${HOOK_END})',
drawbox=x=0:y=1490:w=1080:h=430:color=[email protected]:t=fill:enable='gte(t,${INFO_START})',
drawtext=fontfile='${FONT_ESC}':textfile='${TITLE_ESC}':fontcolor=white:fontsize=62:line_spacing=8:x=(w-text_w)/2:y=1560:fix_bounds=true:shadowcolor=[email protected]:shadowx=4:shadowy=4:enable='gte(t,${INFO_START})',
drawtext=fontfile='${FONT_ESC}':textfile='${META_ESC}':fontcolor=[email protected]:fontsize=42:x=(w-text_w)/2:y=1655:fix_bounds=true:shadowcolor=[email protected]:shadowx=3:shadowy=3:enable='gte(t,${INFO_START})',
drawtext=fontfile='${FONT_ESC}':textfile='${WEBSITE_ESC}':fontcolor=0x55c8ff:fontsize=52:x=(w-text_w)/2:y=1780:fix_bounds=true:shadowcolor=[email protected]:shadowx=4:shadowy=4:enable='gte(t,${CTA_START})',
fps=50,format=yuv420p[vout]
FILTER
)
printf 'Creating AmigaDB YouTube Short...\n'
printf ' Input: %s\n' "$INPUT"
printf ' Segment: %s + %ss\n' "$START" "$DURATION"
printf ' Style: %s\n' "$STYLE"
printf ' Src fade: %ss\n' "$SOURCE_FADE_IN"
printf ' Output: %s\n' "$OUTPUT"
printf ' Font: %s\n' "$FONT_FILE"
if [[ "$STYLE" == "brand" ]]; then
printf ' Bg image: %s\n' "$BACKGROUND_IMAGE"
fi
if [[ "$STYLE" == "video" ]]; then
printf ' Bg video: %s\n' "$BACKGROUND_VIDEO"
printf ' Bg blur: %s\n' "$VIDEO_BLUR"
printf ' Bg alpha: %s\n' "$VIDEO_OPACITY"
printf ' Src crop: %s\n' "$SOURCE_CROP_SPEC"
printf ' Src margin:%s px each side (target width %s)\n' "$SOURCE_MARGIN_X" "$SOURCE_TARGET_WIDTH"
fi
FFMPEG_ARGS=(
-hide_banner -y
-ss "$START"
-t "$DURATION"
-i "$INPUT"
)
if [[ "$STYLE" == "brand" ]]; then
FFMPEG_ARGS+=( -loop 1 -i "$BACKGROUND_IMAGE" )
fi
if [[ "$STYLE" == "video" ]]; then
FFMPEG_ARGS+=( -stream_loop -1 -i "$BACKGROUND_VIDEO" )
fi
FFMPEG_ARGS+=(
-filter_complex "$FILTER_COMPLEX"
-map "[vout]"
-map 0:a?
-c:v libx264
-preset "$PRESET"
-crf "$CRF"
-profile:v high
-level:v 4.2
-pix_fmt yuv420p
-c:a aac
-b:a 320k
-ar 48000
-movflags +faststart
-t "$DURATION"
-shortest
"$OUTPUT"
)
ffmpeg "${FFMPEG_ARGS[@]}"
printf '\nDone: %s\n' "$OUTPUT"