[FEAT] frame: BirdWeather refreshes every 15 min, only when the birds change
This commit is contained in:
+29
-3
@@ -23,6 +23,7 @@ EBIRD = "https://api.ebird.org/v2/data/obs/geo/recent"
|
|||||||
APT_JS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "avian", "frontend", "apt.js")
|
APT_JS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "avian", "frontend", "apt.js")
|
||||||
|
|
||||||
_drawable = None
|
_drawable = None
|
||||||
|
_GEO_CACHE = os.path.expanduser("~/.birdframe/geocode.json")
|
||||||
|
|
||||||
|
|
||||||
def _graphql(query, timeout):
|
def _graphql(query, timeout):
|
||||||
@@ -39,12 +40,37 @@ def _graphql(query, timeout):
|
|||||||
|
|
||||||
|
|
||||||
def geocode(zip_code, country="us", timeout=20):
|
def geocode(zip_code, country="us", timeout=20):
|
||||||
"""ZIP / postal code to (lat, lon) via the keyless zippopotam.us gazetteer."""
|
"""ZIP / postal code to (lat, lon) via the keyless zippopotam.us gazetteer.
|
||||||
url = f"{GEOCODER}/{country}/{urllib.parse.quote(zip_code.strip())}"
|
A ZIP's coordinates never move, so the result is cached on disk and the
|
||||||
|
frame geocodes once rather than on every 15-minute refresh."""
|
||||||
|
zip_code = zip_code.strip()
|
||||||
|
key = f"{country}/{zip_code}".lower()
|
||||||
|
store = {}
|
||||||
|
try:
|
||||||
|
with open(_GEO_CACHE) as f:
|
||||||
|
loaded = json.load(f)
|
||||||
|
if isinstance(loaded, dict):
|
||||||
|
store = loaded
|
||||||
|
hit = store.get(key)
|
||||||
|
if isinstance(hit, list) and len(hit) == 2: # shape-guard a hand-edited / stale entry
|
||||||
|
return float(hit[0]), float(hit[1])
|
||||||
|
except Exception:
|
||||||
|
store = {}
|
||||||
|
url = f"{GEOCODER}/{country}/{urllib.parse.quote(zip_code)}"
|
||||||
req = urllib.request.Request(url, headers={"User-Agent": "AvianVisitors-frame/1.0"})
|
req = urllib.request.Request(url, headers={"User-Agent": "AvianVisitors-frame/1.0"})
|
||||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
place = json.loads(r.read(200_000))["places"][0]
|
place = json.loads(r.read(200_000))["places"][0]
|
||||||
return float(place["latitude"]), float(place["longitude"])
|
lat, lon = float(place["latitude"]), float(place["longitude"])
|
||||||
|
store[key] = [lat, lon]
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(_GEO_CACHE), exist_ok=True)
|
||||||
|
tmp = _GEO_CACHE + ".tmp"
|
||||||
|
with open(tmp, "w") as f:
|
||||||
|
json.dump(store, f)
|
||||||
|
os.replace(tmp, _GEO_CACHE) # atomic, like display.py's state write
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return lat, lon
|
||||||
|
|
||||||
|
|
||||||
def bbox(lat, lon, miles):
|
def bbox(lat, lon, miles):
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ shoot = true
|
|||||||
# Or read a PNG a sibling process drops next to us:
|
# Or read a PNG a sibling process drops next to us:
|
||||||
# image = "~/.birdframe/frame.png"
|
# image = "~/.birdframe/frame.png"
|
||||||
|
|
||||||
|
# Or render from BirdWeather for any ZIP, no mic (what install.sh --bird-weather
|
||||||
|
# writes). The panel still refreshes only when the local top birds change:
|
||||||
|
# species_source = "birdweather"
|
||||||
|
# zip = "94107"
|
||||||
|
# bw_days = 7 # BirdWeather lookback window, in days
|
||||||
|
# bw_country = "us" # geocoder country for the ZIP
|
||||||
|
# shoot = true # this Pi renders the collage
|
||||||
|
|
||||||
hours = 24 # window shown on the frame
|
hours = 24 # window shown on the frame
|
||||||
|
|
||||||
# --- Titles and look (used when this Pi renders, i.e. shoot = true) ----------
|
# --- Titles and look (used when this Pi renders, i.e. shoot = true) ----------
|
||||||
|
|||||||
+34
-9
@@ -40,6 +40,10 @@ SPECTRA6 = [(236, 234, 223), (26, 26, 28), (165, 60, 56),
|
|||||||
|
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
"base_url": "http://birdnet.local",
|
"base_url": "http://birdnet.local",
|
||||||
|
"species_source": "", # "" = the recent API at base_url; "birdweather" = BirdWeather near a ZIP
|
||||||
|
"zip": "", # BirdWeather ZIP / postal code (with species_source = "birdweather")
|
||||||
|
"bw_days": 7, # BirdWeather lookback window, in days
|
||||||
|
"bw_country": "us", # geocoder country for the ZIP
|
||||||
"hours": 24,
|
"hours": 24,
|
||||||
"image": "", # local PNG written by the shooter
|
"image": "", # local PNG written by the shooter
|
||||||
"image_url": "", # or a published screenshot URL
|
"image_url": "", # or a published screenshot URL
|
||||||
@@ -93,6 +97,16 @@ def signature(species):
|
|||||||
return hashlib.sha256(json.dumps(items).encode()).hexdigest()[:16]
|
return hashlib.sha256(json.dumps(items).encode()).hexdigest()[:16]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_species(cfg, auth=None):
|
||||||
|
"""The species list the signature is built from: the BirdNET-Pi recent API
|
||||||
|
by default, or BirdWeather's recent detections near a ZIP when
|
||||||
|
species_source = "birdweather"."""
|
||||||
|
if cfg.get("species_source") == "birdweather":
|
||||||
|
import birdweather
|
||||||
|
return birdweather.species_for_zip(cfg["zip"], country=cfg["bw_country"], days=cfg["bw_days"])
|
||||||
|
return fetch_recent(cfg["base_url"], cfg["hours"], cfg["timeout"], auth)
|
||||||
|
|
||||||
|
|
||||||
# --- image ------------------------------------------------------------------
|
# --- image ------------------------------------------------------------------
|
||||||
def get_image(src, timeout, auth=None):
|
def get_image(src, timeout, auth=None):
|
||||||
if re.match(r"^https?://", src):
|
if re.match(r"^https?://", src):
|
||||||
@@ -190,17 +204,19 @@ def mat_and_center(img, mat, empty=False):
|
|||||||
tb = _region_bbox(img, paper, top, split[0]) if split else None
|
tb = _region_bbox(img, paper, top, split[0]) if split else None
|
||||||
cb = _region_bbox(img, paper, split[1], bot + 1) if split else None
|
cb = _region_bbox(img, paper, split[1], bot + 1) if split else None
|
||||||
box_w, box_h = A5_W * (1 - mat), A5_H * (1 - mat)
|
box_w, box_h = A5_W * (1 - mat), A5_H * (1 - mat)
|
||||||
# No birds: the content under the title is just the one-line empty-state
|
# No birds yet: hold the title where a full collage would put it and float
|
||||||
# note. Render a calm title card (a modest title with the small note
|
# the one-line note where the birds would be, so a birdless frame reads like
|
||||||
# below) rather than blowing the lone title up to fill the opening.
|
# a full one with the collage removed, not a title card drifted to the middle.
|
||||||
if empty and tb and cb:
|
if empty and tb and cb:
|
||||||
title = _scale_h(img.crop(tb), box_h * TITLE_H_FRAC)
|
title = _scale_h(img.crop(tb), box_h * TITLE_H_FRAC)
|
||||||
note = _scale_w(img.crop(cb), box_w * 0.30)
|
note = _scale_w(img.crop(cb), box_w * 0.30)
|
||||||
gap = round(box_h * 0.05)
|
gap = round(box_h * GAP_FRAC)
|
||||||
|
region = box_w * COLLAGE_FRAC # stand in for a usual-size collage
|
||||||
cw = max(title.width, note.width)
|
cw = max(title.width, note.width)
|
||||||
comp = Image.new("RGB", (cw, title.height + gap + note.height), paper)
|
comp = Image.new("RGB", (cw, round(title.height + gap + region)), paper)
|
||||||
comp.paste(title, ((cw - title.width) // 2, 0))
|
comp.paste(title, ((cw - title.width) // 2, 0))
|
||||||
comp.paste(note, ((cw - note.width) // 2, title.height + gap))
|
ny = title.height + gap + max(0, (round(region) - note.height) // 2)
|
||||||
|
comp.paste(note, ((cw - note.width) // 2, ny))
|
||||||
canvas = Image.new("RGB", (PANEL_W, PANEL_H), paper)
|
canvas = Image.new("RGB", (PANEL_W, PANEL_H), paper)
|
||||||
canvas.paste(comp, ((PANEL_W - comp.width) // 2, (PANEL_H - comp.height) // 2))
|
canvas.paste(comp, ((PANEL_W - comp.width) // 2, (PANEL_H - comp.height) // 2))
|
||||||
return canvas
|
return canvas
|
||||||
@@ -298,7 +314,16 @@ def in_quiet_hours(cfg, hour):
|
|||||||
|
|
||||||
|
|
||||||
# --- run --------------------------------------------------------------------
|
# --- run --------------------------------------------------------------------
|
||||||
def obtain_image(cfg):
|
def obtain_image(cfg, species=None):
|
||||||
|
if cfg.get("species_source") == "birdweather":
|
||||||
|
from shoot import shoot_birdweather
|
||||||
|
if species is None: # gate skipped (--no-signature): fetch the list to render
|
||||||
|
species = fetch_species(cfg, _auth(cfg))
|
||||||
|
out = os.path.join(os.path.expanduser(cfg["cache"]), "frame.png")
|
||||||
|
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||||
|
shoot_birdweather(out, species, title=cfg["shoot_title"], subtitle=cfg["shoot_subtitle"],
|
||||||
|
timeout_ms=cfg["timeout"] * 1000)
|
||||||
|
return Image.open(out).convert("RGB")
|
||||||
if cfg["shoot"]:
|
if cfg["shoot"]:
|
||||||
from shoot import shoot
|
from shoot import shoot
|
||||||
out = os.path.join(os.path.expanduser(cfg["cache"]), "shot.png")
|
out = os.path.join(os.path.expanduser(cfg["cache"]), "shot.png")
|
||||||
@@ -322,7 +347,7 @@ def run(cfg, preview=None, force=False, use_signature=True, mat_box=False):
|
|||||||
species = None
|
species = None
|
||||||
if use_signature:
|
if use_signature:
|
||||||
try:
|
try:
|
||||||
species = fetch_recent(cfg["base_url"], cfg["hours"], cfg["timeout"], _auth(cfg))
|
species = fetch_species(cfg, _auth(cfg))
|
||||||
sig = signature(species)
|
sig = signature(species)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"signature fetch failed: {e}", file=sys.stderr) # treat as no change
|
print(f"signature fetch failed: {e}", file=sys.stderr) # treat as no change
|
||||||
@@ -338,7 +363,7 @@ def run(cfg, preview=None, force=False, use_signature=True, mat_box=False):
|
|||||||
print("refresh:", "changed" if changed else "heal")
|
print("refresh:", "changed" if changed else "heal")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
img = fit_panel(obtain_image(cfg))
|
img = fit_panel(obtain_image(cfg, species))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"could not get image: {e}", file=sys.stderr) # keep last panel image
|
print(f"could not get image: {e}", file=sys.stderr) # keep last panel image
|
||||||
return
|
return
|
||||||
|
|||||||
+24
-32
@@ -141,42 +141,34 @@ elif [ "$MODE" = image ]; then
|
|||||||
printf '%s\n' 'saturation = 0.6'
|
printf '%s\n' 'saturation = 0.6'
|
||||||
} > "$CONFIG"
|
} > "$CONFIG"
|
||||||
else
|
else
|
||||||
{ printf '%s\n' '# birdframe-mode: birdweather'; cat config.example.toml; } > "$CONFIG"
|
# birdweather: this Pi renders from BirdWeather near $ZIP, gated on the same
|
||||||
|
# signature as the other modes - it only redraws when the local top birds change.
|
||||||
|
{
|
||||||
|
printf '%s\n' '# birdframe-mode: birdweather'
|
||||||
|
printf '%s\n' '# AvianVisitors frame, BirdWeather mode: renders the top birds near a ZIP.'
|
||||||
|
printf '%s\n' 'species_source = "birdweather"'
|
||||||
|
printf 'zip = "%s"\n' "$ZIP"
|
||||||
|
printf '%s\n' 'bw_days = 7 # BirdWeather lookback window, in days'
|
||||||
|
printf '%s\n' 'bw_country = "us" # geocoder country for the ZIP'
|
||||||
|
printf '%s\n' 'shoot = true # this Pi renders the collage'
|
||||||
|
printf '%s\n' 'shoot_title = "Avian Visitors"'
|
||||||
|
printf '%s\n' 'shoot_subtitle = "Heard Today"'
|
||||||
|
printf '%s\n' 'rotate = 90 # flip to 270 if the frame hangs the other way up'
|
||||||
|
printf '%s\n' 'saturation = 0.6'
|
||||||
|
} > "$CONFIG"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "5/5 Installing systemd service + timer..."
|
echo "5/5 Installing systemd service + timer..."
|
||||||
if [ "$MODE" = birdweather ]; then
|
# Every mode runs display.py against the config on the standard 15-minute timer;
|
||||||
PY="$FRAME/.venv/bin/python"
|
# only the config differs. display.py renders inline for local + birdweather and
|
||||||
PNG="$HOME/.birdframe/frame.png"
|
# pushes to the panel only when the birds change.
|
||||||
sudo tee /etc/systemd/system/birdframe.service >/dev/null <<SERVICE
|
sed "s|/home/monalisa/AvianVisitors/frame|$FRAME|g; s|/home/monalisa|$HOME|g; s|User=monalisa|User=$USER|" \
|
||||||
[Unit]
|
|
||||||
Description=AvianVisitors frame, BirdWeather mode (ZIP $ZIP)
|
|
||||||
Documentation=https://github.com/Twarner491/AvianVisitors
|
|
||||||
Wants=network-online.target
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=$USER
|
|
||||||
WorkingDirectory=$FRAME
|
|
||||||
ExecStart=/bin/sh -c '$PY $FRAME/shoot.py --bird-weather --zip "$ZIP" --out $PNG && $PY $FRAME/display.py --config $HOME/.birdframe/config.toml --image-url $PNG --no-signature --force'
|
|
||||||
Environment=PYTHONUNBUFFERED=1
|
|
||||||
Nice=10
|
|
||||||
TimeoutStartSec=300
|
|
||||||
SERVICE
|
|
||||||
# Remote ZIPs with no nearby station fall back to eBird, which needs a key.
|
|
||||||
if [ -n "$EBIRD_KEY" ]; then
|
|
||||||
echo "Environment=EBIRD_API_KEY=$EBIRD_KEY" | sudo tee -a /etc/systemd/system/birdframe.service >/dev/null
|
|
||||||
fi
|
|
||||||
# BirdWeather's recent-species list drifts slowly, so refresh a few times a day.
|
|
||||||
sed 's|OnUnitActiveSec=.*|OnUnitActiveSec=6h|' systemd/birdframe.timer \
|
|
||||||
| sudo tee /etc/systemd/system/birdframe.timer >/dev/null
|
|
||||||
else
|
|
||||||
# local + image both run display.py against the config; only the config differs.
|
|
||||||
sed "s|/home/monalisa/AvianVisitors/frame|$FRAME|g; s|/home/monalisa|$HOME|g; s|User=monalisa|User=$USER|" \
|
|
||||||
systemd/birdframe.service | sudo tee /etc/systemd/system/birdframe.service >/dev/null
|
systemd/birdframe.service | sudo tee /etc/systemd/system/birdframe.service >/dev/null
|
||||||
sudo cp systemd/birdframe.timer /etc/systemd/system/birdframe.timer
|
# BirdWeather's remote-ZIP eBird fallback reads its key from the unit environment.
|
||||||
|
if [ "$MODE" = birdweather ] && [ -n "$EBIRD_KEY" ]; then
|
||||||
|
echo "Environment=EBIRD_API_KEY=$EBIRD_KEY" | sudo tee -a /etc/systemd/system/birdframe.service >/dev/null
|
||||||
fi
|
fi
|
||||||
|
sudo cp systemd/birdframe.timer /etc/systemd/system/birdframe.timer
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable --now birdframe.timer # --now starts it immediately, not only on the next boot
|
sudo systemctl enable --now birdframe.timer # --now starts it immediately, not only on the next boot
|
||||||
|
|
||||||
@@ -202,7 +194,7 @@ DONE
|
|||||||
cat <<DONE
|
cat <<DONE
|
||||||
|
|
||||||
Installed in BirdWeather mode for ZIP $ZIP. The frame renders the top birds near
|
Installed in BirdWeather mode for ZIP $ZIP. The frame renders the top birds near
|
||||||
you on the Pi and refreshes every 6 hours.
|
you on the Pi and refreshes every 15 min, only when the local top birds change.
|
||||||
DONE
|
DONE
|
||||||
# The bundled illustrations center on the western U.S. If birds near this ZIP
|
# The bundled illustrations center on the western U.S. If birds near this ZIP
|
||||||
# aren't in the cloned set the frame quietly skips them, which has tripped
|
# aren't in the cloned set the frame quietly skips them, which has tripped
|
||||||
|
|||||||
+46
-18
@@ -225,6 +225,30 @@ def shoot(url, out, *, title=None, subtitle=None, vw=600, vh=800, dsf=2,
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def shoot_birdweather(out, species, *, title=None, subtitle=None, timeout_ms=45000, **look):
|
||||||
|
"""Render `species` ([{sci,com,n}]) as the BirdWeather collage into `out`.
|
||||||
|
|
||||||
|
The mic path screenshots a live site; this builds the same page from a
|
||||||
|
species list instead. It serves the bundled frontend on localhost, feeds it
|
||||||
|
the species, and routes cutouts to the local clone first then GitHub, so the
|
||||||
|
--bird-weather CLI and display.py's inline render share one setup. An empty
|
||||||
|
list renders the page's empty-state card, the same as the mic mode. `look`
|
||||||
|
overrides any shoot() tunable (the CLI passes its flags through)."""
|
||||||
|
if species is None:
|
||||||
|
raise RuntimeError("shoot_birdweather needs a species list")
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_httpd, port = _serve_frontend(os.path.join(here, "..", "avian", "frontend"))
|
||||||
|
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)):
|
||||||
|
look.setdefault(k, v)
|
||||||
|
return shoot(f"http://127.0.0.1:{port}/", out,
|
||||||
|
title=title or "Avian Visitors", subtitle=subtitle or "Heard Today",
|
||||||
|
species=species, cutout_base=RAW_ILLUSTRATIONS, cutout_local=cutout_local,
|
||||||
|
timeout_ms=timeout_ms, **look)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
ap = argparse.ArgumentParser(description="Screenshot the AvianVisitors collage for the e-ink frame.")
|
ap = argparse.ArgumentParser(description="Screenshot the AvianVisitors collage for the e-ink frame.")
|
||||||
ap.add_argument("--url", default="http://birdnet.local")
|
ap.add_argument("--url", default="http://birdnet.local")
|
||||||
@@ -257,7 +281,6 @@ def main():
|
|||||||
ap.add_argument("--password")
|
ap.add_argument("--password")
|
||||||
ap.add_argument("--timeout", type=int, default=45000)
|
ap.add_argument("--timeout", type=int, default=45000)
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
url, title, subtitle, species, cutout_base, cutout_local = a.url, a.title, a.subtitle, None, None, None
|
|
||||||
if a.bird_weather:
|
if a.bird_weather:
|
||||||
if not a.zip:
|
if not a.zip:
|
||||||
print("--bird-weather needs --zip", file=sys.stderr)
|
print("--bird-weather needs --zip", file=sys.stderr)
|
||||||
@@ -268,28 +291,33 @@ def main():
|
|||||||
if not species:
|
if not species:
|
||||||
print(f"no drawable birds near {a.zip}; nothing to render", file=sys.stderr)
|
print(f"no drawable birds near {a.zip}; nothing to render", file=sys.stderr)
|
||||||
sys.exit(3)
|
sys.exit(3)
|
||||||
front = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "avian", "frontend")
|
# Pass the CLI's look flags through; shoot_birdweather fills the bird-weather
|
||||||
_httpd, port = _serve_frontend(front)
|
# defaults (steeper count exponent, smaller titles) for anything left unset.
|
||||||
url = f"http://127.0.0.1:{port}/"
|
look = {k: v for k, v in (("count_exp", a.count_exp), ("headline_px", a.headline_px),
|
||||||
cutout_base = RAW_ILLUSTRATIONS
|
("eyebrow_px", a.eyebrow_px)) if v is not None}
|
||||||
# Locally generated cutouts may not be on GitHub yet; serve the clone first.
|
look.update(vw=a.width, vh=a.height, dsf=a.dsf, mat=a.mat, collage_vh=a.collage_vh,
|
||||||
cutout_local = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
cluster_xbias=a.cluster_xbias, cluster_ybias=a.cluster_ybias,
|
||||||
"..", "avian", "assets", "illustrations")
|
cluster_pad=a.cluster_pad, small_floor=a.small_floor, lowercase=a.lowercase,
|
||||||
title = title or "Avian Visitors"
|
window_hours=a.window_hours, user=a.user, password=a.password)
|
||||||
subtitle = subtitle or "Heard Today"
|
|
||||||
# BirdWeather's 7-day counts are flatter than a mic's, so they need a steeper
|
|
||||||
# exponent to get the same hero-bird hierarchy and collage shape.
|
|
||||||
count_exp = a.count_exp if a.count_exp is not None else (1.0 if a.bird_weather else 0.4)
|
|
||||||
headline_px = a.headline_px if a.headline_px is not None else (39 if a.bird_weather else 42)
|
|
||||||
eyebrow_px = a.eyebrow_px if a.eyebrow_px is not None else (17 if a.bird_weather else 18)
|
|
||||||
try:
|
try:
|
||||||
shoot(url, a.out, title=title, subtitle=subtitle, vw=a.width, vh=a.height, dsf=a.dsf,
|
shoot_birdweather(a.out, species, title=a.title, subtitle=a.subtitle,
|
||||||
|
timeout_ms=a.timeout, **look)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"shoot failed: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"wrote {a.out}")
|
||||||
|
return
|
||||||
|
# Mic path: screenshot the live AvianVisitors site at --url.
|
||||||
|
count_exp = a.count_exp if a.count_exp is not None else 0.4
|
||||||
|
headline_px = a.headline_px if a.headline_px is not None else 42
|
||||||
|
eyebrow_px = a.eyebrow_px if a.eyebrow_px is not None else 18
|
||||||
|
try:
|
||||||
|
shoot(a.url, a.out, title=a.title, subtitle=a.subtitle, vw=a.width, vh=a.height, dsf=a.dsf,
|
||||||
headline_px=headline_px, eyebrow_px=eyebrow_px, lowercase=a.lowercase,
|
headline_px=headline_px, eyebrow_px=eyebrow_px, lowercase=a.lowercase,
|
||||||
mat=a.mat, collage_vh=a.collage_vh, cluster_xbias=a.cluster_xbias,
|
mat=a.mat, collage_vh=a.collage_vh, cluster_xbias=a.cluster_xbias,
|
||||||
cluster_ybias=a.cluster_ybias, count_exp=count_exp, cluster_pad=a.cluster_pad,
|
cluster_ybias=a.cluster_ybias, count_exp=count_exp, cluster_pad=a.cluster_pad,
|
||||||
small_floor=a.small_floor,
|
small_floor=a.small_floor,
|
||||||
window_hours=a.window_hours, timeout_ms=a.timeout, user=a.user, password=a.password,
|
window_hours=a.window_hours, timeout_ms=a.timeout, user=a.user, password=a.password)
|
||||||
species=species, cutout_base=cutout_base, cutout_local=cutout_local)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"shoot failed: {e}", file=sys.stderr)
|
print(f"shoot failed: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ WorkingDirectory=/home/monalisa/AvianVisitors/frame
|
|||||||
ExecStart=/home/monalisa/AvianVisitors/frame/.venv/bin/python /home/monalisa/AvianVisitors/frame/display.py --config /home/monalisa/.birdframe/config.toml
|
ExecStart=/home/monalisa/AvianVisitors/frame/.venv/bin/python /home/monalisa/AvianVisitors/frame/display.py --config /home/monalisa/.birdframe/config.toml
|
||||||
Environment=PYTHONUNBUFFERED=1
|
Environment=PYTHONUNBUFFERED=1
|
||||||
Nice=10
|
Nice=10
|
||||||
TimeoutStartSec=180
|
TimeoutStartSec=300
|
||||||
|
|||||||
Reference in New Issue
Block a user