From cba5b5b1e89d9625fd9801619ad5ec9b9613df2b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 18:20:20 +0000 Subject: [PATCH 1/3] Document drawtext overlay settings for easy customization Adds a quick-edit cheatsheet covering font, color, size, position, and shadow/border for the sunrise-time overlay so the layout can be tweaked without grokking ffmpeg's drawtext syntax. --- daily_sunrise_video.sh | 93 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/daily_sunrise_video.sh b/daily_sunrise_video.sh index 69b7983..167b0aa 100755 --- a/daily_sunrise_video.sh +++ b/daily_sunrise_video.sh @@ -140,6 +140,20 @@ if $TEST_MODE; then fi # ── Font detection ──────────────────────────────────────────────────────────── +# Picks a TrueType font file for ffmpeg's drawtext filter. drawtext needs an +# absolute path to a .ttf/.otf file — it cannot use a font *name* like Arial. +# +# To use a different font: +# 1. Find the .ttf path on this machine, e.g.: +# fc-match "Liberation Mono" --format='%{file}\n' +# find /usr/share/fonts -name '*.ttf' | grep -i mono +# 2. Either (a) hard-code it below by replacing the fc-match line with: +# FONT="/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf" +# or (b) change the fc-match query string ("DejaVu Sans:style=Regular") +# to your preferred face, e.g. "Liberation Mono:style=Bold". +# 3. Add the chosen path to the fallback list below so the script still works +# if fc-match is missing. +# Bold faces (e.g. DejaVuSans-Bold.ttf) read better at small sizes against sky. FONT="" if command -v fc-match &>/dev/null; then FONT=$(fc-match "DejaVu Sans:style=Regular" --format="%{file}" 2>/dev/null || true) @@ -198,20 +212,99 @@ $TEST_MODE || echo "Speed-adjusted: ${sped_dur}s (saved — overlay still pendi $TEST_MODE && echo "Speed-adjusted: ${sped_dur}s" # ── Build overlay filter ────────────────────────────────────────────────────── +# Burns the sunrise time onto the video using ffmpeg's drawtext filter. +# Layout is VERTICAL — each character of "HH:MM" is on its own line, stacked +# top-to-bottom in the lower-right corner. This is achieved by writing each +# char to a separate line of $temp_text, then drawtext renders that file +# verbatim (newlines = line breaks). +# +# To switch to a HORIZONTAL "HH:MM" overlay, replace the printf below with: +# printf '%s' "$SR_TIME" > "$temp_text" +# and bump fontsize (e.g. h/18) since horizontal text needs less vertical room. +# +# ── Quick-edit cheatsheet for every drawtext parameter ────────────────────── +# +# FONT FILE (set above in the "Font detection" block, not here). +# drawtext requires a .ttf path, not a font name. +# +# fontcolor Text color. Accepts named colors (yellow, white, red, cyan, +# orange, lime, magenta, gray) or hex (0xFFCC00, 0xFF8800). +# The "@N" suffix is opacity 0.0–1.0; comes from +# SUNRISE_OVERLAY_OPACITY in sky-cam.conf. Examples: +# fontcolor=white@0.7 # softer white, 70% opaque +# fontcolor=0xFF8800@0.9 # warm orange, 90% opaque +# +# fontsize Pixel height of glyphs. "h/22" = video-height / 22, so it +# scales with resolution (1080p → ~49px, 720p → ~33px). Use a +# smaller divisor for BIGGER text: +# fontsize=h/30 → small fontsize=h/22 → current +# fontsize=h/18 → medium fontsize=h/14 → large +# Or set an absolute pixel size: fontsize=48 +# +# line_spacing Pixels of gap between the stacked characters. Increase for +# a more spaced look (e.g. 8 or 12); 0 packs them tight. +# +# x , y Position of the text box. ffmpeg exposes: +# w = video width h = video height +# tw = text width th = text height +# Current values place the box in the BOTTOM-RIGHT with an +# 18px right margin and a bottom margin of 18px + 5% of height. +# Recipes for the other corners (keep ~18px breathing room): +# Top-left: x=18 y=18 +# Top-right: x=w-tw-18 y=18 +# Bottom-left: x=18 y=h-th-18-h*0.05 +# Bottom-right: x=w-tw-18 y=h-th-18-h*0.05 (current) +# Centered: x=(w-tw)/2 y=(h-th)/2 +# Centered top: x=(w-tw)/2 y=24 +# To nudge the current spot, change the "18" margins or the +# "h*0.05" lift (5% above the bottom edge — raise to 0.08 to +# clear a status bar, lower to 0.0 to sit flush at the bottom). +# +# shadowcolor / shadowx / shadowy +# A 1-pixel black drop-shadow @55% opacity for legibility against +# bright sky. Set shadowx=shadowy=0 to disable. For a thicker +# outline, swap the shadow* lines for: +# :borderw=2:bordercolor=black@0.7 +# (borderw = outline thickness in pixels.) +# +# Other useful drawtext options you can append with ":name=value": +# box=1:boxcolor=black@0.4:boxborderw=6 # solid background pill +# alpha='if(lt(t,1),t,1)' # 1-second fade-in +# enable='between(t,2,8)' # only show 2s–8s +# +# Full reference: https://ffmpeg.org/ffmpeg-filters.html#drawtext DT="" if [ "${SUNRISE_OVERLAY_ENABLED:-true}" = "true" ]; then + # Vertical layout: write each char of "HH:MM" on its own line. + # SR_TIME is exactly 5 chars (e.g. "06:42"); indices 0..4 below. + # Switch to horizontal by replacing this printf with: printf '%s' "$SR_TIME" > "$temp_text" temp_text=$(mktemp --suffix=.txt) printf '%s\n%s\n%s\n%s\n%s' \ "${SR_TIME:0:1}" "${SR_TIME:1:1}" "${SR_TIME:2:1}" "${SR_TIME:3:1}" "${SR_TIME:4:1}" \ > "$temp_text" + + # Source the glyph file + the text. fontfile is omitted if FONT detection + # failed above, in which case ffmpeg falls back to its built-in font. if [ -n "$FONT" ]; then DT="drawtext=fontfile='${FONT}':textfile='${temp_text}'" else DT="drawtext=textfile='${temp_text}'" fi + + # Color + opacity. Change "yellow" to any color name or 0xRRGGBB. + # Opacity comes from SUNRISE_OVERLAY_OPACITY in sky-cam.conf. DT="${DT}:fontcolor=yellow@${SUNRISE_OVERLAY_OPACITY}" + + # Size + spacing. fontsize is relative to video height (h/22). DT="${DT}:fontsize=h/22:line_spacing=4" + + # Position — bottom-right corner. See the corner recipes in the comment + # block above. The "18" values are pixel margins; "h*0.05" lifts the box + # 5% of the height off the bottom so it doesn't kiss the frame edge. DT="${DT}:x=w-tw-18:y=h-th-18-h*0.05" + + # Drop-shadow for legibility. Set shadowx=shadowy=0 to disable, or swap + # for :borderw=2:bordercolor=black@0.7 for a thicker outline. DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1" fi From 7c5a7032222d0c1c59c464090bad255ac8e0f692 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 18:33:03 +0000 Subject: [PATCH 2/3] Fix sky-cam-audio-capture.service spurious failure on no-op retention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retention block ended with `[ test ] && echo`, which under `set -e` returns exit 1 when `deleted` is 0 — the normal case on most days. As the script's last command, that bash exit propagated to systemd, which marked the unit failed and fired the OnFailure ntfy even though the audio file was written successfully. Add `|| true` and an explicit `exit 0` so a no-op retention pass is treated as success. --- sunrise-audio-capture.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sunrise-audio-capture.sh b/sunrise-audio-capture.sh index 3fdc129..610d605 100755 --- a/sunrise-audio-capture.sh +++ b/sunrise-audio-capture.sh @@ -90,5 +90,7 @@ retain="${SUNRISE_AUDIO_RETENTION_DAYS:-7}" if [ "$retain" -gt 0 ]; then audio_root="${AUDIO_DIR:-$BASE_DIR/audio}/sunrise/$CAM" deleted=$(find "$audio_root" -name "sunrise-audio.m4a" -mtime +"$retain" -print -delete 2>/dev/null | wc -l) - [ "$deleted" -gt 0 ] && echo "Retention: deleted $deleted sunrise audio clip(s) older than ${retain}d" + [ "$deleted" -gt 0 ] && echo "Retention: deleted $deleted sunrise audio clip(s) older than ${retain}d" || true fi + +exit 0 From 9e1087f875e3effa599face85079ee762dd4bf68 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 18:40:27 +0000 Subject: [PATCH 3/3] Tag ambient audio chunks with current OpenWeather conditions Adds weather-tag.sh, a small failure-safe helper that queries OpenWeather's free "Current weather" endpoint and prints one tag from a fixed vocabulary (thunderstorm, rain, rainshower, snow, snowstorm, fog, windy, cloudy, clear) or nothing on any failure. ambient-record.sh calls it just before each 30-minute chunk so the filename reflects conditions at the moment of capture, e.g. east-08-30-00-thunderstorm.m4a east-12-00-00-clear.m4a A foggy dawn no longer mislabels the sunny afternoon files. Off by default. Enable with WEATHER_ENABLED=true and an OPENWEATHER_API_KEY entry in .env; missing key / API error / disabled toggle all fall through to the un-tagged filename, never blocking the recorder. --- ambient-record.sh | 12 ++++++- sky-cam.conf | 32 +++++++++++++++++ weather-tag.sh | 87 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100755 weather-tag.sh diff --git a/ambient-record.sh b/ambient-record.sh index 6e296b6..31c5626 100755 --- a/ambient-record.sh +++ b/ambient-record.sh @@ -50,7 +50,17 @@ while true; do mkdir -p "$dir" timestamp=$(date +%H-%M-%S) - outfile="$dir/${CAM}-${timestamp}.m4a" + + # Weather tag for THIS chunk — queried right before recording so the + # filename reflects conditions at the time of capture, not earlier in the + # day. weather-tag.sh prints empty on disabled / missing key / API error, + # so the un-tagged filename is the safe fallback. + weather_tag=$("$SCRIPT_DIR/weather-tag.sh" 2>/dev/null || true) + if [ -n "$weather_tag" ]; then + outfile="$dir/${CAM}-${timestamp}-${weather_tag}.m4a" + else + outfile="$dir/${CAM}-${timestamp}.m4a" + fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] Recording → $outfile (${CHUNK}s)" diff --git a/sky-cam.conf b/sky-cam.conf index f6b0819..550b21c 100644 --- a/sky-cam.conf +++ b/sky-cam.conf @@ -174,6 +174,38 @@ LATITUDE="${LATITUDE:-0.0000}" LONGITUDE="${LONGITUDE:-0.0000}" TIMEZONE="${TIMEZONE:-America/New_York}" +# ── Weather tagging (OpenWeather) ───────────────────────────────────────────── +# When enabled, ambient-record.sh queries OpenWeather's "Current weather" +# endpoint just before each 30-minute audio chunk and appends a one-word +# weather tag to the filename, e.g.: +# east-08-30-00-thunderstorm.m4a +# east-09-00-00-rain.m4a +# east-09-30-00-cloudy.m4a +# That way each chunk is labelled with what was happening DURING that chunk — +# a foggy dawn won't mislabel the sunny afternoon files. +# +# Tag vocabulary (fixed set — anything else → no tag): +# thunderstorm rain rainshower snow snowstorm +# fog windy cloudy clear +# +# Setup: +# 1. Get a free API key: https://openweathermap.org/api +# (the free "Current weather" tier — 60 calls/min — is plenty) +# 2. Add to .env: OPENWEATHER_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx +# 3. Confirm LATITUDE / LONGITUDE are set in .env (already needed by sunrise.py) +# 4. Flip WEATHER_ENABLED=true below +# +# The query uses the same LATITUDE / LONGITUDE configured above — one location +# for the whole installation; per-camera coordinates are not supported. +# +# Failure-safe: if the toggle is off, the key is missing, the network is down, +# or the API errors out, weather-tag.sh prints nothing and the recorder falls +# back to the un-tagged filename. ambient-record.sh never blocks on this. +# +# Manual test: ./weather-tag.sh (prints e.g. "cloudy" or nothing) +WEATHER_ENABLED=false +OPENWEATHER_API_KEY="${OPENWEATHER_API_KEY:-}" + # ── Cameras ─────────────────────────────────────────────────────────────────── # List every camera name here (space-separated inside the parentheses). # Each name becomes a subfolder under BASE_DIR (images) and BASE_DIR/movies. diff --git a/weather-tag.sh b/weather-tag.sh new file mode 100755 index 0000000..e113459 --- /dev/null +++ b/weather-tag.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# weather-tag.sh — print a one-word weather tag for the current moment at +# (LATITUDE, LONGITUDE), using the OpenWeather "Current weather" endpoint. +# +# Output: a single lowercase word from this fixed vocabulary, or empty: +# thunderstorm rain rainshower snow snowstorm +# fog windy cloudy clear +# +# Used by ambient-record.sh to suffix each 30-minute chunk's filename so the +# file collection self-documents what the audio actually captured. +# +# Failure-safe: if WEATHER_ENABLED≠true, the API key is missing, the network +# is down, the API returns an error, or the response can't be parsed, this +# script prints NOTHING and exits 0. Callers MUST treat empty output as +# "no tag — fall back to the un-tagged filename". Never blocks the recorder. +# +# Required setup: +# 1. Sign up at https://openweathermap.org/api → free "Current weather" plan +# 2. Put the key in .env: OPENWEATHER_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx +# 3. Confirm LATITUDE / LONGITUDE are set in .env (already needed by sunrise.py) +# 4. Flip WEATHER_ENABLED=true in sky-cam.conf +# +# Manual test: ./weather-tag.sh (prints e.g. "cloudy" or nothing) + +set -uo pipefail # NOT -e: we want to swallow every failure into "no tag" + +SCRIPT_DIR="$(dirname "$(realpath "$0")")" +source "$SCRIPT_DIR/sky-cam.conf" 2>/dev/null || exit 0 +source "$SCRIPT_DIR/.env" 2>/dev/null || true + +[ "${WEATHER_ENABLED:-false}" = "true" ] || exit 0 +[ -n "${OPENWEATHER_API_KEY:-}" ] || exit 0 +[ -n "${LATITUDE:-}" ] || exit 0 +[ -n "${LONGITUDE:-}" ] || exit 0 + +# ── Fetch current conditions ───────────────────────────────────────────────── +# 5s connect / 8s total — must never delay the recorder noticeably. +url="https://api.openweathermap.org/data/2.5/weather?lat=${LATITUDE}&lon=${LONGITUDE}&appid=${OPENWEATHER_API_KEY}&units=metric" +resp=$(curl -s --connect-timeout 5 --max-time 8 "$url" 2>/dev/null) || exit 0 +[ -n "$resp" ] || exit 0 + +# ── Parse condition ID + wind speed ────────────────────────────────────────── +# Condition codes: https://openweathermap.org/weather-conditions +# OpenWeather may return multiple entries in weather[]; the first is primary. +if command -v jq >/dev/null 2>&1; then + code=$(echo "$resp" | jq -r '.weather[0].id // empty' 2>/dev/null) + wind=$(echo "$resp" | jq -r '.wind.speed // 0' 2>/dev/null) +else + # Fallback parser if jq is missing — grabs the first "id":NNN it sees. + code=$(echo "$resp" | grep -oE '"id":[0-9]+' | head -n1 | grep -oE '[0-9]+') + wind=$(echo "$resp" | grep -oE '"speed":[0-9.]+' | head -n1 | grep -oE '[0-9.]+') +fi + +[ -n "${code:-}" ] || exit 0 +wind="${wind:-0}" + +# ── Map condition code → tag (vocabulary documented at top of file) ────────── +# Group ranges follow OpenWeather's published condition-code table: +# 2xx thunder / 3xx drizzle / 5xx rain / 6xx snow / 7xx atmosphere / 800 clear / 80x clouds +tag="" +case "$code" in + 2*) tag="thunderstorm" ;; + 3*) tag="rainshower" ;; # drizzle = light shower + 500|520|521|522|531) tag="rainshower" ;; # light / shower variants + 501|502|503|504|511) tag="rain" ;; # moderate → extreme + freezing + 600|601|612|613|615|616|620|621) tag="snow" ;; + 602|622) tag="snowstorm" ;; # heavy snow / heavy shower snow + 611) tag="snow" ;; # sleet + 701|711|721|741|762) tag="fog" ;; # mist / smoke / haze / fog / ash + 731|751|761|771) tag="windy" ;; # dust whirls / sand / dust / squall + 781) tag="thunderstorm" ;; # tornado → reuse strongest tag + 800) tag="clear" ;; + 801) tag="clear" ;; # few clouds — still mostly clear + 802|803|804) tag="cloudy" ;; + *) tag="" ;; # unknown code → no tag +esac + +# Wind override: gusty conditions drown out other audio cues. If the base +# tag is clear/cloudy and wind ≥ 8 m/s (~18 mph), promote to "windy". +if [ "$tag" = "clear" ] || [ "$tag" = "cloudy" ]; then + # bash can't compare floats — strip the decimal for an integer check. + wind_int="${wind%.*}" + [ "${wind_int:-0}" -ge 8 ] 2>/dev/null && tag="windy" +fi + +[ -n "$tag" ] && printf '%s' "$tag" +exit 0