Files
sky-cam/test-sunrise.sh
T
Claude 84fa60b857 Add test-sunrise.sh pipeline smoke test
Runs the same 4-step pipeline as daily_sunrise_video.sh but uses the
most recent ~2 minutes of captured frames instead of the sunrise window,
grabs live RTSP audio for the audio mix step, and accepts an arbitrary
output path — so it can be run at any time of day to verify the full
pipeline (speed-adjust, audio mix, vertical time overlay) without
waiting for tomorrow's sunrise.

https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
2026-04-21 16:20:15 +00:00

157 lines
6.4 KiB
Bash
Executable File

#!/bin/bash
# test-sunrise.sh — smoke-test the sunrise pipeline using recent captured frames.
#
# Uses the same steps as daily_sunrise_video.sh:
# 1. raw video from the most recent ~2 min of JPEGs (12 frames @ 10s interval)
# 2. speed-adjust to SUNRISE_TARGET_SECS
# 3. grab 10s of live RTSP audio centred on the mid-point frame
# 4. mix audio + burn vertical time overlay
#
# Usage:
# ./test-sunrise.sh [output_path]
# ./test-sunrise.sh ~/drives/data2-main/test2.mp4
set -euo pipefail
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
source "$SCRIPT_DIR/sky-cam.conf"
source "$SCRIPT_DIR/.env" 2>/dev/null || true
export TZ="$TIMEZONE"
ENCODE_PRESET="${ENCODE_PRESET:-slow}"
CRF_SUNRISE="${CRF_SUNRISE:-28}"
SUNRISE_TARGET_SECS="${SUNRISE_TARGET_SECS:-10}"
SUNRISE_OVERLAY_OPACITY="${SUNRISE_OVERLAY_OPACITY:-0.45}"
OUTPUT="${1:-/tmp/sunrise-test.mp4}"
CAM="$SUNRISE_CAM"
TODAY=$(date +%Y-%m-%d)
image_dir="$BASE_DIR/$CAM/$TODAY"
NUM_FRAMES=12 # ~2 minutes at 10s interval
# ── Collect frames ────────────────────────────────────────────────────────────
if [ ! -d "$image_dir" ]; then
echo "ERROR: no frames today at $image_dir"
echo " Is capture.sh running? systemctl --user status sky-cam-capture-${CAM}.service"
exit 1
fi
mapfile -t frames < <(find "$image_dir" -name "*.jpg" | sort | tail -n "$NUM_FRAMES")
if [ "${#frames[@]}" -lt 1 ]; then
echo "ERROR: no frames found in $image_dir"
exit 1
fi
echo "Frames: ${#frames[@]} most-recent from $image_dir"
# ── Mid-point frame → overlay time ───────────────────────────────────────────
mid_idx=$(( ${#frames[@]} / 2 ))
mid_frame="${frames[$mid_idx]}"
mid_hms=$(basename "$mid_frame" .jpg) # HH-MM-SS
MID_TIME=$(echo "$mid_hms" | cut -c1-5 | tr '-' ':') # HH:MM
echo "Mid-point frame: $mid_hms → overlay time: $MID_TIME"
# Vertical text: each character on its own line (matches daily_sunrise_video.sh)
MID_VERT="${MID_TIME:0:1}\\n${MID_TIME:1:1}\\n${MID_TIME:2:1}\\n${MID_TIME:3:1}\\n${MID_TIME:4:1}"
# ── Font detection ────────────────────────────────────────────────────────────
FONT=""
if command -v fc-match &>/dev/null; then
FONT=$(fc-match "DejaVu Sans:style=Regular" --format="%{file}" 2>/dev/null || true)
fi
if [ -z "$FONT" ]; then
for f in /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf \
/usr/share/fonts/dejavu/DejaVuSans.ttf \
/usr/share/fonts/TTF/DejaVuSans.ttf \
/usr/share/fonts/truetype/freefont/FreeSans.ttf; do
[ -f "$f" ] && { FONT="$f"; break; }
done
fi
# ── Temp files ────────────────────────────────────────────────────────────────
TMP_LIST=$(mktemp --suffix=.txt)
TMP_RAW=$(mktemp --suffix=.mp4)
TMP_SPED=$(mktemp --suffix=.mp4)
TMP_AUDIO=$(mktemp --suffix=.m4a)
cleanup() { rm -f "$TMP_LIST" "$TMP_RAW" "$TMP_SPED" "$TMP_AUDIO" 2>/dev/null || true; }
trap cleanup EXIT
printf "file '%s'\n" "${frames[@]}" > "$TMP_LIST"
# ── Step 1: raw video from frames ─────────────────────────────────────────────
echo ""
echo "Step 1/4: encoding raw video from ${#frames[@]} frames..."
ffmpeg -loglevel warning \
-f concat -safe 0 -i "$TMP_LIST" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -vsync 2 -an \
-y "$TMP_RAW"
raw_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$TMP_RAW")
speed=$(echo "scale=3; $raw_dur / $SUNRISE_TARGET_SECS" | bc)
echo " Raw: ${raw_dur}s speed: ${speed}x"
# ── Step 2: speed-adjust to SUNRISE_TARGET_SECS ───────────────────────────────
echo "Step 2/4: speed-adjust to ${SUNRISE_TARGET_SECS}s..."
ffmpeg -loglevel warning \
-i "$TMP_RAW" \
-vf "setpts=PTS/$speed" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
-t "$SUNRISE_TARGET_SECS" \
-y "$TMP_SPED"
# ── Step 3: grab 10s of live audio centred on mid-point ──────────────────────
# Connects to the RTSP stream briefly — audio from "right now" stands in for
# the real sunrise-audio.m4a that capture.sh would record at sunrise.
echo "Step 3/4: recording ${SUNRISE_TARGET_SECS}s of live audio from camera..."
rtsp_var="CAM_RTSP_${CAM}"
RTSP_URL="${!rtsp_var:-}"
HAS_AUDIO=false
if [ -n "$RTSP_URL" ]; then
if ffmpeg -loglevel warning \
-rtsp_transport tcp \
-i "$RTSP_URL" \
-t "$SUNRISE_TARGET_SECS" \
-vn -c:a aac -b:a 64k \
-y "$TMP_AUDIO" 2>/dev/null; then
HAS_AUDIO=true
echo " Audio: captured OK"
else
echo " Audio: capture failed — continuing without audio"
fi
else
echo " Audio: CAM_RTSP_${CAM} not set — skipping"
fi
# ── Step 4: mix audio + burn overlay ─────────────────────────────────────────
echo "Step 4/4: mix audio + overlay → $OUTPUT"
mkdir -p "$(dirname "$OUTPUT")"
FADE_OUT=$(echo "scale=1; $SUNRISE_TARGET_SECS - 0.5" | bc)
DT="drawtext=fontfile='${FONT}'"
DT="${DT}:text='${MID_VERT}'"
DT="${DT}:fontcolor=white@${SUNRISE_OVERLAY_OPACITY}"
DT="${DT}:fontsize=h/22:line_spacing=4"
DT="${DT}:x=w-tw-18:y=(h-th)/2"
DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1"
if $HAS_AUDIO; then
ffmpeg -loglevel warning \
-i "$TMP_SPED" -i "$TMP_AUDIO" \
-filter_complex \
"[0:v]${DT}[vout];\
[1:a]afade=t=in:st=0:d=0.5,afade=t=out:st=${FADE_OUT}:d=0.5[aout]" \
-map "[vout]" -map "[aout]" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" \
-c:a aac -b:a 128k \
-y "$OUTPUT"
else
ffmpeg -loglevel warning \
-i "$TMP_SPED" \
-vf "$DT" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
-y "$OUTPUT"
fi
actual=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUTPUT")
echo ""
echo "Done: $OUTPUT (${actual}s, overlay: $MID_TIME)"