Godstrument/workers/world_quakes.py
type-two 510ddaaf8c 🔧 review fixes — the five-angle audit's whole findings list
Correctness (four were my own regressions from the REVELATION wave):
· ☸ WHEELS polymeter permanently detuned after any skipped frame —
  seqAbsStep advanced by ++ per transition, but one frame can cross two
  16ths (bpm ≳155, or the clamped dt on every tab refocus). Proven to
  drift 12 steps out at 174bpm; now advances by the true distance, so
  seqAbsStep % 16 === seqCurStep always holds.
· Sample & hold with exactly one lit step froze on its first sample
  forever (guard keyed on the wrapped step, which never changed) — now
  keyed on the absolute step.
· PSALM double-activation left the MICROPHONE HOT after "stop": the
  re-entrancy guard sat after the getUserMedia await, orphaning the
  first stream/interval/context. Guard is now synchronous, a stop
  during the prompt is honoured, and a second tap while starting
  cancels. Mic off means off — the promise the feature makes.
· Live-recording a drum tap into a short wheel wrote steps past wlen
  that never played; the tap now folds onto the lane's own wheel.
· Switching MIDI out port left notes droning on the old port (pending
  off-timers fired at the NEW one) — midiFlushOut() releases every held
  note + all-notes-off before the swap.
· world_quakes' seen-set grew unbounded on long-running rigs.

Security: stored XSS via the commune room name — the one untrusted
string reaching innerHTML unescaped (hub only lower-cased it). Escaped
client-side at both sinks, and the hub now strips markup chars too.

Robustness: world_ephemeris/world_almanac imported swisseph unguarded,
so a machine without pyswisseph crash-looped them every 10s forever
against the supervisor's exit-0 contract — both now retire quietly
(verified by simulating the missing dep), and pyswisseph is documented
in requirements.

Mobile: the top bar's five separately-pinned chips collided below
~500px — the first thing a phone visitor saw was a broken header. Now
a clean row with the account chip on its own line and the source
counter moved left.

Housekeeping: dead measureSpaced(), dead #account .code selector, and
an 888K stale worktree removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:43:20 +10:00

104 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Live global seismic activity from the USGS earthquake feed.
Every real earthquake on Earth in the last hour fires a percussive "event" whose
magnitude, depth, and location become musical: the planet playing itself as a drum.
"""
import argparse
import json
import sys
import time
import urllib.request
import urllib.error
from pythonosc.udp_client import SimpleUDPClient
NAME = "world_quakes"
FEED_URL = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson"
POLL_SECONDS = 60
USER_AGENT = "Godstrument/1.0 (live instrument; contact monsterrobotparty@gmail.com)"
def fetch_feed():
"""Fetch and parse the USGS GeoJSON feed. Returns the parsed dict or None on failure."""
req = urllib.request.Request(FEED_URL, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=30) as resp:
raw = resp.read()
return json.loads(raw)
def main():
parser = argparse.ArgumentParser(description="Emit live USGS earthquakes as OSC.")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=9000)
args = parser.parse_args()
client = SimpleUDPClient(args.host, args.port)
print(f"[{NAME}] emitting /gs/quake/... every {POLL_SECONDS}s")
seen = set()
first_poll = True
backoff = 5
while True:
try:
data = fetch_feed()
backoff = 5 # reset backoff after a good fetch
except (urllib.error.URLError, urllib.error.HTTPError, OSError, ValueError) as e:
print(f"[{NAME}] warning: fetch failed ({e}); retrying in {backoff}s")
time.sleep(backoff)
backoff = min(backoff * 2, POLL_SECONDS)
continue
features = data.get("features") or []
if first_poll:
# Record existing quakes but do NOT fire them (no startup burst).
for feat in features:
fid = feat.get("id")
if fid is not None:
seen.add(fid)
first_poll = False
else:
for feat in features:
fid = feat.get("id")
if fid is None or fid in seen:
continue
seen.add(fid)
if len(seen) > 4000: # the all_hour feed ages ids out, so an unbounded
seen.clear() # set is a slow leak on a long-running rig; a reset
seen.add(fid) # can at worst re-fire a quake still in the window
props = feat.get("properties") or {}
mag = props.get("mag")
if mag is None:
continue # skip quakes with no magnitude
place = props.get("place") or "unknown location"
geom = feat.get("geometry") or {}
coords = geom.get("coordinates") or [None, None, None]
lon = coords[0] if len(coords) > 0 else None
lat = coords[1] if len(coords) > 1 else None
depth = coords[2] if len(coords) > 2 else None
try:
client.send_message("/gs/quake/event", float(mag))
if depth is not None:
client.send_message("/gs/quake/depth", float(depth))
if lat is not None:
client.send_message("/gs/quake/lat", float(lat))
if lon is not None:
client.send_message("/gs/quake/lon", float(lon))
except (OSError, ValueError, TypeError) as e:
print(f"[{NAME}] warning: OSC send failed ({e})")
continue
depth_str = f"{depth:.0f}km" if depth is not None else "?km"
print(f" quake M{float(mag):.1f} {place} (depth {depth_str})")
time.sleep(POLL_SECONDS)
if __name__ == "__main__":
main()