scripts/finalize_delivery.sh
scripts/finalize_delivery.shBrowse 9 files
2,242 tokens
6,900 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env bash2 3set -euo pipefail4 5usage() {6 printf 'Usage: %s rendered.mp4 output-dir output-stem\n' "$(basename "$0")" >&27}8 9die() {10 printf 'ERROR: %s\n' "$*" >&211 exit 112}13 14[[ "$#" -eq 3 ]] || { usage; exit 64; }15 16INPUT="$1"17OUTPUT_DIR="$2"18STEM="$3"19 20[[ -f "$INPUT" && -s "$INPUT" ]] || die "input render is missing or empty: $INPUT"21[[ "$STEM" =~ ^[A-Za-z0-9._-]+$ ]] || die "output stem contains unsupported characters: $STEM"22 23for command in ffmpeg ffprobe jq mktemp awk sed; do24 command -v "$command" >/dev/null 2>&1 || die "required command unavailable: $command"25done26 27mkdir -p "$OUTPUT_DIR"28OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd -P)"29INPUT="$(cd "$(dirname "$INPUT")" && pwd -P)/$(basename "$INPUT")"30 31MASTER="$OUTPUT_DIR/${STEM}-master.mp4"32SHARE="$OUTPUT_DIR/${STEM}-share.mp4"33REPORT="$OUTPUT_DIR/${STEM}-delivery-report.json"34CONTACT="$OUTPUT_DIR/${STEM}-contact-sheet.png"35 36# Keep shared reports portable and avoid exposing the developer machine path.37INPUT_REPORT="$(basename "$INPUT")"38MASTER_REPORT="$(basename "$MASTER")"39SHARE_REPORT="$(basename "$SHARE")"40CONTACT_REPORT="$(basename "$CONTACT")"41 42for output in "$MASTER" "$SHARE" "$REPORT" "$CONTACT"; do43 [[ ! -e "$output" ]] || die "refusing to overwrite existing output: $output"44done45 46TMP_ROOT="${TMPDIR:-/tmp}"47TMP_DIR="$(mktemp -d "${TMP_ROOT%/}/presenter-finalize.XXXXXX")"48trap 'rm -rf -- "$TMP_DIR"' EXIT INT TERM49 50STREAM_JSON="$TMP_DIR/source-probe.json"51ffprobe -v error -show_streams -show_format -of json "$INPUT" >"$STREAM_JSON"52jq -e '.streams | any(.codec_type == "video") and any(.codec_type == "audio")' "$STREAM_JSON" >/dev/null \53 || die "input must contain decodable video and audio streams"54 55WIDTH="$(jq -r '[.streams[] | select(.codec_type == "video")][0].width' "$STREAM_JSON")"56HEIGHT="$(jq -r '[.streams[] | select(.codec_type == "video")][0].height' "$STREAM_JSON")"57FPS="$(jq -r '[.streams[] | select(.codec_type == "video")][0].avg_frame_rate' "$STREAM_JSON")"58[[ "$FPS" != "0/0" && -n "$FPS" ]] || FPS="30"59 60MEASURE_LOG="$TMP_DIR/loudnorm-pass1.log"61ffmpeg -hide_banner -nostdin -nostats -i "$INPUT" \62 -map 0:a:0 -vn \63 -af 'loudnorm=I=-16:TP=-1.5:LRA=9:print_format=json' \64 -f null - >/dev/null 2>"$MEASURE_LOG"65 66MEASURE_JSON="$TMP_DIR/measure.json"67sed -n '/^{/,/^}/p' "$MEASURE_LOG" >"$MEASURE_JSON"68jq -e . "$MEASURE_JSON" >/dev/null || die "could not parse loudness measurement"69 70MEASURED_I="$(jq -r .input_i "$MEASURE_JSON")"71MEASURED_TP="$(jq -r .input_tp "$MEASURE_JSON")"72MEASURED_LRA="$(jq -r .input_lra "$MEASURE_JSON")"73MEASURED_THRESH="$(jq -r .input_thresh "$MEASURE_JSON")"74OFFSET="$(jq -r .target_offset "$MEASURE_JSON")"75 76LOUDNORM="loudnorm=I=-16:TP=-1.5:LRA=9:measured_I=${MEASURED_I}:measured_TP=${MEASURED_TP}:measured_LRA=${MEASURED_LRA}:measured_thresh=${MEASURED_THRESH}:offset=${OFFSET}:linear=true:print_format=summary"77VIDEO_FILTER="scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos,setsar=1,fps=${FPS},format=yuv420p"78 79encode() {80 local crf="$1"81 local preset="$2"82 local audio_bitrate="$3"83 local destination="$4"84 local temporary="$5"85 86 ffmpeg -hide_banner -nostdin -loglevel warning -stats -i "$INPUT" \87 -map 0:v:0 -map 0:a:0 -sn -dn \88 -vf "$VIDEO_FILTER" -af "$LOUDNORM" \89 -c:v libx264 -preset "$preset" -crf "$crf" -profile:v high \90 -pix_fmt yuv420p -tag:v avc1 -fps_mode cfr \91 -color_primaries bt709 -color_trc bt709 -colorspace bt709 \92 -c:a aac -b:a "$audio_bitrate" -ar 48000 -ac 2 \93 -map_metadata -1 -map_chapters -1 -movflags +faststart \94 "$temporary"95 [[ -s "$temporary" ]] || die "encoder produced an empty output"96 mv -- "$temporary" "$destination"97}98 99encode 16 slow 256k "$MASTER" "$TMP_DIR/master.mp4"100encode 24 medium 160k "$SHARE" "$TMP_DIR/share.mp4"101 102for file in "$MASTER" "$SHARE"; do103 ffmpeg -hide_banner -nostdin -v error -xerror -i "$file" \104 -map 0:v:0 -map 0:a:0 -f null - >/dev/null105done106 107ffprobe -v error -show_streams -show_format -of json "$MASTER" >"$TMP_DIR/master-probe.json"108ffprobe -v error -show_streams -show_format -of json "$SHARE" >"$TMP_DIR/share-probe.json"109 110for probe_file in "$STREAM_JSON" "$TMP_DIR/master-probe.json" "$TMP_DIR/share-probe.json"; do111 jq 'if .format.filename? then .format.filename = (.format.filename | split("/") | last) else . end' \112 "$probe_file" >"${probe_file}.portable"113 mv -- "${probe_file}.portable" "$probe_file"114done115 116ffmpeg -hide_banner -nostdin -i "$MASTER" \117 -vf 'blackdetect=d=0.10:pix_th=0.02' -an -f null - \118 >/dev/null 2>"$TMP_DIR/blackdetect.log"119BLACK_EVENTS="$(grep -c 'black_start:' "$TMP_DIR/blackdetect.log" || true)"120 121DURATION="$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$MASTER")"122awk -v duration="$DURATION" 'BEGIN {123 p[1]=0.2; p[2]=duration*0.125; p[3]=duration*0.25; p[4]=duration*0.375;124 p[5]=duration*0.5; p[6]=duration*0.625; p[7]=duration*0.75;125 p[8]=duration*0.875; p[9]=duration-0.2;126 for(i=1;i<=9;i++) printf "%.6f\n", p[i]127}' >"$TMP_DIR/timestamps.txt"128 129if (( WIDTH > HEIGHT )); then130 CONTACT_FRAME_FILTER='scale=480:270:force_original_aspect_ratio=decrease:flags=lanczos,pad=480:270:(ow-iw)/2:(oh-ih)/2:color=black'131elif (( HEIGHT > WIDTH )); then132 CONTACT_FRAME_FILTER='scale=270:480:force_original_aspect_ratio=decrease:flags=lanczos,pad=270:480:(ow-iw)/2:(oh-ih)/2:color=black'133else134 CONTACT_FRAME_FILTER='scale=360:360:force_original_aspect_ratio=decrease:flags=lanczos,pad=360:360:(ow-iw)/2:(oh-ih)/2:color=black'135fi136 137index=0138while IFS= read -r timestamp; do139 index=$((index + 1))140 printf -v frame '%s/frame-%02d.png' "$TMP_DIR" "$index"141 ffmpeg -hide_banner -nostdin -loglevel error -ss "$timestamp" -i "$MASTER" \142 -frames:v 1 -vf "$CONTACT_FRAME_FILTER" -update 1 "$frame"143done <"$TMP_DIR/timestamps.txt"144 145ffmpeg -hide_banner -nostdin -loglevel error -framerate 1 -start_number 1 \146 -i "$TMP_DIR/frame-%02d.png" -frames:v 1 \147 -vf 'tile=3x3:padding=12:margin=12:color=0x101218' -update 1 "$TMP_DIR/contact.png"148mv -- "$TMP_DIR/contact.png" "$CONTACT"149 150jq -n \151 --arg status verified \152 --arg input "$INPUT_REPORT" \153 --arg master "$MASTER_REPORT" \154 --arg share "$SHARE_REPORT" \155 --arg contact "$CONTACT_REPORT" \156 --arg duration "$DURATION" \157 --argjson black_events "$BLACK_EVENTS" \158 --slurpfile measured "$MEASURE_JSON" \159 --slurpfile source_probe "$STREAM_JSON" \160 --slurpfile master_probe "$TMP_DIR/master-probe.json" \161 --slurpfile share_probe "$TMP_DIR/share-probe.json" \162 '{status:$status,input:$input,master:$master,share:$share,contact_sheet:$contact,duration_s:($duration|tonumber),source_loudness:$measured[0],source_probe:$source_probe[0],master_probe:$master_probe[0],share_probe:$share_probe[0],full_decode_passed:true,black_frame_events:$black_events}' \163 >"$TMP_DIR/report.json"164mv -- "$TMP_DIR/report.json" "$REPORT"165 166printf 'Master: %s\nShare: %s\nReport: %s\nContact: %s\n' "$MASTER" "$SHARE" "$REPORT" "$CONTACT"167