summaryrefslogtreecommitdiff
path: root/synopticMorph.sh
blob: 0c22ca1adbe315b0760ba83ef4407afb0b38ddac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/bin/bashset -euo pipefailshopt -s nullglob# SJP 2026-06-10# Build a rolling morph timelapse of BOM synoptic charts (default 3-day window).# Cache structure: /mnt/enclave/synoptic/pairs/<chartA>__<chartB>/p_NNNN.png# On each call: identify rolling window, fill any missing pairs, evict# out-of-window pairs, stitch with linger/dwell, encode mp4.# Triggered via OnSuccess= on synoptic.service.ARCHIVE_DIR="${ARCHIVE_DIR:-/var/lib/synoptic/archive}"WORK_ROOT="${WORK_ROOT:-/mnt/enclave/synoptic}"PAIRS_DIR="$WORK_ROOT/pairs"OUT_DIR="$WORK_ROOT/out"PUBLISH_PATH="${PUBLISH_PATH:-/srv/www/pestrel/morph.mp4}"WINDOW_DAYS="${WINDOW_DAYS:-3}"WINDOW_CHARTS=$((WINDOW_DAYS * 4))MORPH_N="${MORPH_N:-23}"FPS="${FPS:-12}"LINGER="${LINGER:-8}"DWELL="${DWELL:-36}"FADE_IN="${FADE_IN:-1}"FADE_OUT="${FADE_OUT:-2}"require_cmd() {    command -v "$1" >/dev/null 2>&1 || { echo "Required: $1" >&2; exit 127; }}require_cmd magickrequire_cmd ffmpegrequire_cmd flockmkdir -p "$PAIRS_DIR" "$OUT_DIR"exec 9>"$WORK_ROOT/.lock"flock -n 9 || { echo "Morph already in progress; skipping."; exit 0; }# --- 1. Rolling window --------------------------------------------------mapfile -t window < <(ls -1 "$ARCHIVE_DIR"/[0-9]*.png 2>/dev/null | tail -n "$WINDOW_CHARTS")if [ "${#window[@]}" -lt 2 ]; then    echo "Need at least 2 charts in window (have ${#window[@]})." >&2    exit 1fifirst=$(basename "${window[0]}" .png)last=$(basename "${window[-1]}" .png)echo "Window: ${#window[@]} charts ($first$last)"# --- 2. Identify needed pair slugs --------------------------------------needed=()for i in $(seq 0 $((${#window[@]} - 2))); do    a=$(basename "${window[$i]}" .png)    b=$(basename "${window[$((i+1))]}" .png)    needed+=("${a}__${b}")done# --- 3. Fill any missing pairs ------------------------------------------SLOT=$((MORPH_N + 1))new_pairs=0for slug in "${needed[@]}"; do    dir="$PAIRS_DIR/$slug"    sentinel="$dir/p_$(printf '%04d' $SLOT).png"    if [ ! -f "$sentinel" ]; then        a="${slug%__*}"; b="${slug##*__}"        echo "  computing pair $a$b"        mkdir -p "$dir"        magick "$ARCHIVE_DIR/${a}.png" "$ARCHIVE_DIR/${b}.png" \            -morph "$MORPH_N" "$dir/p_%04d.png"        new_pairs=$((new_pairs + 1))    fidoneecho "Computed $new_pairs new pair(s)"# --- 4. Evict pairs outside the window ----------------------------------declare -A needed_setfor slug in "${needed[@]}"; do needed_set[$slug]=1; doneevicted=0for dir in "$PAIRS_DIR"/*/; do    slug=$(basename "$dir")    if [ -z "${needed_set[$slug]:-}" ]; then        rm -rf "$dir"        evicted=$((evicted + 1))    fidoneecho "Evicted $evicted out-of-window pair(s)"# --- 5. Assemble sequence with linger/dwell -----------------------------SEQ_DIR=$(mktemp -d "$WORK_ROOT/seq.XXXXXX")trap 'rm -rf "$SEQ_DIR"' EXITlast_pair_idx=$(( ${#needed[@]} - 1 ))out=0# Per pair: take frames 0..MORPH_N (24 frames), dropping the joint with next.# For the very last pair only, also take frame SLOT (the final chartB).# Insert LINGER after every j==0 (each source chart appears once, as next# pair's j==0). Insert DWELL after the final j==SLOT (the latest chart).for i in "${!needed[@]}"; do    dir="$PAIRS_DIR/${needed[$i]}"    last_frame=$MORPH_N    if [ "$i" -eq "$last_pair_idx" ]; then last_frame=$SLOT; fi    for j in $(seq 0 $last_frame); do        src="$dir/p_$(printf '%04d' $j).png"        if [ ! -f "$src" ]; then            echo "Missing pair frame: $src" >&2            exit 2        fi        ln -sf "$src" "$SEQ_DIR/seq_$(printf '%06d' $out).png"        out=$((out + 1))        if [ "$j" -eq 0 ]; then            for k in $(seq 1 $LINGER); do                ln -sf "$src" "$SEQ_DIR/seq_$(printf '%06d' $out).png"                out=$((out + 1))            done        elif [ "$i" -eq "$last_pair_idx" ] && [ "$j" -eq "$SLOT" ]; then            for k in $(seq 1 $DWELL); do                ln -sf "$src" "$SEQ_DIR/seq_$(printf '%06d' $out).png"                out=$((out + 1))            done        fi    donedonedur=$(awk "BEGIN{printf \"%.2f\", $out/$FPS}")fout=$(awk "BEGIN{printf \"%.3f\", $out/$FPS - $FADE_OUT}")echo "Sequence: $out frames = ${dur}s at ${FPS} fps; fade-out ${FADE_OUT}s from ${fout}s"# --- 6. Encode ----------------------------------------------------------TMP_MP4=$(mktemp "$OUT_DIR/morph.XXXXXX.mp4")ffmpeg -y -hide_banner -loglevel error -framerate "$FPS" \    -i "$SEQ_DIR/seq_%06d.png" \    -vf "fade=t=in:st=0:d=$FADE_IN,fade=t=out:st=$fout:d=$FADE_OUT" \    -c:v libx264 -pix_fmt yuv420p -movflags +faststart "$TMP_MP4"mkdir -p "$(dirname "$PUBLISH_PATH")"install -m 644 "$TMP_MP4" "$PUBLISH_PATH"rm -f "$TMP_MP4"echo "Published $PUBLISH_PATH ($(stat -c %s "$PUBLISH_PATH") bytes)"