Godstrument/workers/replay_weather.py
monsterrobotparty 8deb65b4fe Godstrument: a multi-modal sensor-fusion instrument
Everything — your hand, the room, a satellite, the markets, the sky — becomes an
OSC control signal fused through a modulation matrix. Includes:
- hub + normalize (One-Euro) + matrix (curves/gates/quantize) + transform (tweaks/groups)
- 20+ workers: sim sensors, 8 keyless world feeds, ephemeris/almanac/clock (computed),
  time-warp replay (quakes/weather/db), and the dealgod market warehouse feed
- planetary orbital LFOs, SQLite recorder, city targeting
- live browser console: mute, group macros, drag-to-patch, inspector, Web MIDI

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 02:13:05 +10:00

117 lines
4.0 KiB
Python

"""
replay_weather.py — days of sky, sweeping past in minutes.
Fetches hourly weather history for the venue from Open-Meteo's archive, then
replays it compressed onto a loop (default: 7 days -> 4 minutes) so slow weather
finally moves fast enough to shape a track. Jog-scrubbable like the quake replay.
Drop-in replacement for world_weather.py — emits the same /gs/weather/* addresses
(temp, wind, precip, pressure, humidity) as smoothly interpolated sweeps.
"""
from __future__ import annotations
import argparse
import datetime
import json
import threading
import time
import urllib.request
from pythonosc.udp_client import SimpleUDPClient
from pythonosc.dispatcher import Dispatcher
from pythonosc.osc_server import ThreadingOSCUDPServer
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from timewarp import ReplayBuffer
UA = "Godstrument/1.0 (live instrument)"
FIELDS = {
"temperature_2m": "temp",
"wind_speed_10m": "wind",
"precipitation": "precip",
"surface_pressure": "pressure",
"relative_humidity_2m": "humidity",
}
class Jog:
def __init__(self, rate_min=0.25, rate_max=4.0):
self.rate_min, self.rate_max = rate_min, rate_max
self.rate_mult = 1.0
self.nudge = 0.0
def on_rate(self, addr, *a):
if a:
v = max(0.0, min(1.0, float(a[0])))
self.rate_mult = self.rate_min + (self.rate_max - self.rate_min) * v
def on_nudge(self, addr, *a):
if a:
self.nudge = (max(0.0, min(1.0, float(a[0]))) - 0.5) * 2.0
def fetch(lat: float, lon: float, days: int):
end = datetime.datetime.now(datetime.timezone.utc).date()
start = end - datetime.timedelta(days=days)
hourly = ",".join(FIELDS.keys())
url = ("https://archive-api.open-meteo.com/v1/archive"
f"?latitude={lat}&longitude={lon}"
f"&start_date={start}&end_date={end}&hourly={hourly}")
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=30) as r:
data = json.load(r)
h = data["hourly"]
n = len(h["time"])
times = list(range(n)) # uniform hourly index is fine for phase mapping
return times, {out: h.get(api, []) for api, out in FIELDS.items()}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--jog-port", type=int, default=9102)
ap.add_argument("--lat", type=float, default=35.68)
ap.add_argument("--lon", type=float, default=139.69)
ap.add_argument("--days", type=int, default=7)
ap.add_argument("--into", type=float, default=240.0, help="loop length (s)")
args = ap.parse_args()
client = SimpleUDPClient(args.host, args.port)
jog = Jog()
bufs: dict[str, ReplayBuffer] = {}
while not bufs:
try:
times, series = fetch(args.lat, args.lon, args.days)
for name, vals in series.items():
b = ReplayBuffer(mode="continuous", into_seconds=args.into)
b.load_continuous(times, vals)
if b.loaded:
bufs[name] = b
print(f"[replay_weather] {len(times)}h of weather ({args.lat},{args.lon}) "
f"-> looping every {args.into:g}s (jog udp/{args.jog_port})")
except Exception as e: # noqa
print(f"[replay_weather] fetch failed ({e}); retrying in 10s")
time.sleep(10)
disp = Dispatcher()
disp.map("/jog/rate", jog.on_rate)
disp.map("/jog/nudge", jog.on_nudge)
srv = ThreadingOSCUDPServer(("127.0.0.1", args.jog_port), disp)
threading.Thread(target=srv.serve_forever, daemon=True).start()
period = 1.0 / 30.0
while True:
for name, b in bufs.items():
b.tick(period, jog.rate_mult, jog.nudge)
client.send_message(f"/gs/weather/{name}", float(b.current()))
time.sleep(period)
if __name__ == "__main__":
main()