[FEAT] collage: empty nest illustration when no birds have been heard (#25)

This commit is contained in:
Teddy Warner
2026-07-06 16:53:12 -07:00
committed by GitHub
parent 2d481d8e33
commit 54d241e184
5 changed files with 39 additions and 25 deletions
+19 -6
View File
@@ -167,7 +167,7 @@ def shoot(url, out, *, title=None, subtitle=None, vw=600, vh=800, dsf=2,
mat=0.04, collage_vh=52, cluster_xbias=1.0, cluster_ybias=1.2,
count_exp=0.4, cluster_pad=1, small_floor=0.04, window_hours=None,
timeout_ms=45000, user=None, password=None, species=None, cutout_base=None,
cutout_local=None):
cutout_local=None, empty_text="listening for birds…"):
pad_side, pad_top, pad_bottom = int(vw * mat), int(vh * mat * 0.92), int(vh * mat)
auth = "Basic " + base64.b64encode(f"{user}:{password or ''}".encode()).decode() if user else None
@@ -206,6 +206,16 @@ def shoot(url, out, *, title=None, subtitle=None, vw=600, vh=800, dsf=2,
timeout=timeout_ms)
except PWTimeout:
print("some illustrations did not finish loading; capturing anyway", file=sys.stderr)
elif page.query_selector(".nest-img") is not None:
# Birdless empty state: wait for the nest illustration to load so
# the frame never captures a blank collage area.
try:
page.wait_for_function(
"() => { const n=document.querySelector('.nest-img');"
" return n && n.complete && n.naturalWidth>0; }",
timeout=timeout_ms)
except PWTimeout:
print("nest illustration did not finish loading; capturing anyway", file=sys.stderr)
if misses:
raise RuntimeError(f"apt.js tunables not found ({len(misses)}); refusing to ship a half-tuned frame")
@@ -213,10 +223,11 @@ def shoot(url, out, *, title=None, subtitle=None, vw=600, vh=800, dsf=2,
page.evaluate("t=>{const e=document.querySelector('.static-head .pre'); if(e)e.textContent=t;}", title)
if subtitle is not None:
page.evaluate("s=>{const e=document.querySelector('.static-head h1'); if(e)e.textContent=s;}", subtitle)
# Soften the empty-state line for a fresh frame whose mic hasn't
# heard a bird yet, and darken it so it survives the e-ink dither and
# the matting step's ink detection (a no-op once there are birds).
page.evaluate("() => { const e = document.querySelector('.empty'); if (e) { e.textContent = 'listening for birds…'; e.style.color = '#555'; } }")
# Set the empty-state line for a birdless frame (the mic hasn't heard
# anything yet, or BirdWeather has no recent detections nearby) and
# darken it so it survives the e-ink dither and the matting step's ink
# detection (a no-op once there are birds).
page.evaluate("(t) => { const e = document.querySelector('.empty'); if (e) { e.textContent = t; e.style.color = '#555'; } }", empty_text)
page.wait_for_timeout(250)
# clip is CSS px; device_scale_factor scales the PNG to vw*dsf by vh*dsf = 1200x1600
page.screenshot(path=out, clip={"x": 0, "y": 0, "width": vw, "height": vh})
@@ -241,7 +252,9 @@ def shoot_birdweather(out, species, *, title=None, subtitle=None, timeout_ms=450
cutout_local = os.path.join(here, "..", "avian", "assets", "illustrations")
# BirdWeather's flat 7-day counts need a steeper exponent for the same hero
# hierarchy; the slightly smaller titles match the mic frame's optical weight.
for k, v in (("count_exp", 1.0), ("headline_px", 39), ("eyebrow_px", 17)):
# A birdless BirdWeather frame says "no recent detections nearby", not "listening".
for k, v in (("count_exp", 1.0), ("headline_px", 39), ("eyebrow_px", 17),
("empty_text", "no recent detections nearby")):
look.setdefault(k, v)
return shoot(f"http://127.0.0.1:{port}/", out,
title=title or "Avian Visitors", subtitle=subtitle or "Heard Today",