diff options
| author | St33v <github@f3rr3t.com> | 2026-06-10 20:26:30 +1000 |
|---|---|---|
| committer | St33v <github@f3rr3t.com> | 2026-06-10 20:26:30 +1000 |
| commit | ac348c4279104aa15c9fbabf58ac423f218f7491 (patch) | |
| tree | 23773d6c969bab894e16da89242e5cf63fd33314 | |
| parent | e433a99ceddf7168d23377ffc8d585fc80ba8fb2 (diff) | |
PoC arc (three iterations), the cremonde storage reckoning that
forced a workspace migration to /mnt/enclave, and the production
pipeline shape — content-addressed pair cache, rolling 30-day
window, OnSuccess= chain off synoptic.service.
Side-quest captured: a small "latest transition" mp4, deferred.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| -rw-r--r-- | doc/synoptic-morph-buildout.md | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/doc/synoptic-morph-buildout.md b/doc/synoptic-morph-buildout.md new file mode 100644 index 0000000..77a6756 --- /dev/null +++ b/doc/synoptic-morph-buildout.md @@ -0,0 +1,152 @@ +--- +title: "Synoptic morph timelapse — PoC to production" +date-created: "2026-06-10" +type: "memoir" +status: "complete" +author: + - "st33v" + - "claude-opus-4-7" +model: + - "claude-opus-4-7" +tldr: "st33v's long-held fantasy of a slowly-morphing synoptic-chart timelapse went from idea to a wired-up production pipeline in one session. Three PoC iterations tuned framerate, dwell, fade, and pause controls; storage reality on cremonde forced a workspace migration to /mnt/enclave; final shape is a rolling 30-day cache of pair morphs at /mnt/enclave/synoptic/pairs/, triggered automatically off synoptic.service via OnSuccess=, published at pestrel.com/morph.html." +chat-url: "https://claude.ai/chat/..." +session-kind: "creative" +side-quests: 1 +reader-targets: + - "st33v" + - "claude-code" + - "scrivener" +related: + entities: + - "cremonde" + - "stan" + - "pestrel.com" + concepts: + - "rolling pair-morph cache" + - "fade-in across the loop boundary" + songs: [] +tags: + - "bom" + - "synoptic" + - "morph" + - "imagemagick" + - "ffmpeg" + - "systemd" +provenance: + - "doc/radar-four-cities-and-nav.md (earlier today)" + - "doc/bom-radar-rollout.md (previous day)" +--- + +# Synoptic morph timelapse — PoC to production + +## TL;DR + +A "just one more thing" at the end of the radar follow-up session turned into the most aesthetic piece of work in this whole arc. st33v has fantasised for years about a slowly-morphing animation of the synoptic charts; ImageMagick's `-morph` plus ffmpeg's `apng`/h264 made it almost trivial once we stopped hitting resource limits and started caching the pair morphs. The output now lives at pestrel.com/morph.html, regenerates automatically after each new chart, and uses ~1 minute of CPU per 6-hour cycle in steady state. + +## Context + +Followed straight on from the radar-four-cities-and-nav session. st33v asked, almost as an afterthought: are we discarding the synoptic charts after refresh? We weren't — 304 archived PNGs going back 75 days, plus the source PDFs. He surfaced the long-held vision: a beautiful slowly-morphing animation, mostly static (continent outline, lat/lon grid) but with fronts and pressure systems sliding across the chart between 6-hour observations. + +## The PoC arc + +Three iterations, each driven by st33v's feedback after watching the previous one. + +### PoC 1 — proving the technique + +8 charts (2 days), `-morph 11`, 12 fps. ImageMagick wrote it cleanly in 79 seconds. 7-second output. Confirmed the aesthetic instinct: continent and grid sit still, isobars slide convincingly, H/L labels smear a bit but in a way that reads as aesthetic rather than broken. st33v's "transformative work" framing dispatched the §10 copyright concern from the original radar spec — derivative enough, less worrying. + +### PoC 2 — slowing down + +> "it's hard to take it in" — st33v + +Halved the framerate to 6 fps and added a 4-frame linger on each source chart so the "real" frames dwell ≈0.8 s before morphing. Confirmed working, length doubled to ~19 s for 2 days. Added an autoplay-muted-loop HTML wrapper at `/morph-poc.html`. + +### PoC 3 — three days, smoother, fades + +st33v's framing: *"the three-day duration is quite good for putting the current frame into context."* + +Bumped to 12 fps with `-morph 23` for smoother motion within the same wall-clock duration. Hit ImageMagick's resource policy: `magick *.png -morph 23` on 12 inputs aborted with SIGABRT. Fix was structural — process pair-by-pair (`magick chartN.png chartN+1.png -morph 23 …` per gap), then concat. Same output, well under per-process limits, and *naturally cacheable* since each pair is independent. That accidental discovery shaped the production design. + +Added: 1-second fade-in from black at the start, 2-second fade-out at the end, 3-second dwell on the latest chart. The fade-in deliberately re-applies at the loop boundary so each cycle "resets" rather than jump-cuts. + +### PoC controls + +- **Pause/resume**: mouse-click on the video works; Shift-key handler is there but appears to be eaten by i3wm or Firefox in st33v's setup. Click is the reliable path; ⏸ overlay confirms the paused state. +- **Cross-page buttons**: Radars (top: 60%) and Static Chart (top: 75%) on the morph page; Radars and Monthly history on the synoptic page. All bottom-left-edge, dark translucent, dashed inner border for visual lightness. + +## The storage reckoning + +st33v mentioned "I have 34GB free at present" while we discussed PDF retention. `df` told a different story: cremonde root was at **977 MB free** (90% full), `/tmp` was tmpfs at 88% with 1.7 GB of PoC frames. The 34 GB was actually on `/mnt/enclave/` — a separate 65 GB ext4 partition, root-owned (`drwxr-xr-x 6 root root`). + +This was a tractable kind of confusion, but a useful one: storage planning for the production morph (≈7 GB of pair cache for 30 days) had to land on enclave, not root. We had st33v sudo-create `/mnt/enclave/synoptic/{pairs,frames,out}` owned by him, then moved the cached pairs out of tmpfs: + +``` +mv /tmp/morph-poc/pairs/* /mnt/enclave/synoptic/pairs/ +rm -rf /tmp/morph-poc +``` + +`/tmp` recovered to 9 MB used. The bubble was diagnosed as one-off — production writes to `/mnt/enclave/synoptic/` natively, so tmpfs won't get filled again. + +## Production shape + +Written and shipped as `e433a99`: + +- **`synopticMorph.sh`** — content-addressed pair cache at `pairs/<chartA>__<chartB>/p_NNNN.png`. Each run picks the last 120 charts, fills any missing pair (steady-state: one), evicts any pair whose slug isn't in the current window, symlinks frames into a temp `seq/` with linger/dwell, encodes via ffmpeg. `flock` guards against concurrent runs. +- **`systemd/synoptic-morph.service`** — oneshot, `TimeoutStartSec=2h` to cover the ~50-minute bootstrap, `Nice=10` and `IOSchedulingClass=idle` so it doesn't fight the radar timer or anything else for resources. +- **`synoptic.service`** gains `OnSuccess=synoptic-morph.service` — so every successful chart fetch triggers a morph re-render, asynchronously. +- **`morph.html`** at pestrel.com/morph.html → /morph.mp4; **index.html** "Monthly history" button at `top: 75%`, "Radars" at `top: 60%`. + +Bootstrap kicked off manually at end of session. ~50 min expected. + +## Resolutions + +- **Per-pair morph over bulk morph.** Forced by the policy abort, but kept because it naturally enables caching. Decisive structural win. +- **Rolling cache, not delete-after-encode.** Only the oldest pair needs to be evicted each cycle; the rest stays. Trades ~7 GB of disk on enclave for ~24 min/cycle of CPU. +- **OnSuccess= chain, not separate timer.** Morph runs strictly after a successful chart, never on a stale archive. +- **APNG/mp4 split.** Radar loops use APNG (small files, no audio track). Synoptic morph uses mp4/h264 (variable-rate would have ballooned with 4000-frame APNG; h264 keeps it small and works with native `<video>` controls). +- **Click-to-pause as the primary control.** Shift handler retained as best-effort; window manager interception is treated as the user's responsibility, not the page's. +- **`/mnt/enclave/synoptic/`** as the canonical workspace. `/tmp` reverts to tmpfs scratch. + +## Cheat sheet — debugging a future morph cycle + +```bash +# Bootstrap or refresh manually +sudo systemctl start --no-block synoptic-morph.service +journalctl -u synoptic-morph.service -f + +# Inspect cache state +ls /mnt/enclave/synoptic/pairs/ | wc -l # ~119 dirs in steady state +du -sh /mnt/enclave/synoptic/pairs/ # ~7 GB +ls /mnt/enclave/synoptic/pairs/ | sort | tail -3 # newest pairs + +# Verify output +ffprobe /srv/www/pestrel/morph.mp4 2>&1 | grep -E "Duration|Stream" + +# Tune +sudo systemctl edit synoptic-morph.service # add Environment=FPS=8 etc +``` + +The script reads `WINDOW_DAYS`, `MORPH_N`, `FPS`, `LINGER`, `DWELL`, `FADE_IN`, `FADE_OUT`, `PUBLISH_PATH` from the environment, so tuning doesn't require editing the script. + +## Operational learnings + +- **`-morph N` on a sequence has a hidden resource ceiling.** Arch's `/etc/ImageMagick-7/policy.xml` caps in-memory pixel area; pair-by-pair invocation dodges it cleanly. The error mode is SIGABRT with a one-line abort message, not a graceful failure — easy to miss in journal output. +- **The pair-morph idiom is the right primitive for caching.** Once we had it, the rolling-window design followed naturally; eviction is just "remove dirs whose slug isn't in needed_set". +- **`OnSuccess=` for chaining oneshots is the underused systemd primitive.** Synoptic→morph is a perfect fit: morph is *strictly downstream* of synoptic, runs on different timing semantics (CPU-bound rather than network-bound), and should never run on a failed chart fetch. +- **`Nice=10` + `IOSchedulingClass=idle` for low-priority background work.** Doesn't slow the visible work; only uses cycles that would otherwise sit idle. +- **Fade-in re-applies at the loop boundary in browsers.** This is exactly what we want for an autoplay-muted-loop video — gives each cycle a "breath" rather than a jump-cut. Worth designing for explicitly. +- **tmpfs is the right default for `/tmp`.** Anything that wants to persist or grow large should write to a real filesystem; the PoC putting 1.7 GB in tmpfs was diagnosed and fixed by relocating the workspace, not by changing tmpfs. + +## Appendix A — side-quest: a "latest transition" mp4 + +*Surfaced from st33v's "Also can you add the fade-in/fade out to the raw a-b morph" remark, then deferred under "leave the monthly stitch and controls for now".* + +The cleanest read: produce a small (~3 s) standalone mp4 of just the most recent 6-hour transition, fades baked in, served alongside `morph.mp4`. Cheap to add — one extra pair-dir → seq → encode pass on the most recent pair only, using the existing cached PNGs. + +Useful for: +- a "what just changed in the last 6h" view on the synoptic page, in-line below the static chart; +- embedding in social-media-style cards; +- generating the music-bed cue points later, since each transition has a stable duration. + +Don't build unless a use case crystallises. The infrastructure (cached per-pair PNGs) supports it for free. + |
