From 7f4b4bcabe97458c651fc88dc33b97738f3ad96c Mon Sep 17 00:00:00 2001 From: monsterrobotparty Date: Sun, 5 Jul 2026 02:48:50 +1000 Subject: [PATCH] Chinese zodiac + seasons + live wildfires; overlapping concept groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - world_zodiac.py: Chinese zodiac year/element + 2-hour animal, N/S summer (computed) - world_fire.py: live wildfires from NASA EONET (the fire element) - overlapping groups: a source can now belong to many concept groups at once (elements fire/earth/air/water/spirit, plus wealth + summer vibes) โ€” they stack - transform.py: member_groups (1:many) replaces 1:1 grouping Co-Authored-By: Claude Opus 4.8 --- config.json | 24 ++++++++++++-- run.py | 2 ++ transform.py | 15 ++++----- workers/world_fire.py | 70 +++++++++++++++++++++++++++++++++++++++++ workers/world_zodiac.py | 70 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 workers/world_fire.py create mode 100644 workers/world_zodiac.py diff --git a/config.json b/config.json index 0e4edf0..2f3fd83 100644 --- a/config.json +++ b/config.json @@ -35,7 +35,14 @@ "market": {"label": "your market", "orbit": "mercury", "members": ["crypto.vel", "market.velocity", "market.turnover"]}, "heavens": {"label": "the sky", "orbit": "neptune", "members": ["astro.moon", "astro.tension", "astro.harmony", "astro.saturn", "astro.mars"]}, "dark": {"label": "the shadow", "members": ["debt.vel", "econ.inflation", "air.pm25", "quake.event", "clock.deaths", "astro.tension", "world.poverty", "world.hunger"]}, - "light": {"label": "the bright", "members": ["clock.births", "astro.moon", "astro.harmony", "almanac.fertile", "sky.day", "world.lifeexp", "world.fertility"]} + "light": {"label": "the bright", "members": ["clock.births", "astro.moon", "astro.harmony", "almanac.fertile", "sky.day", "world.lifeexp", "world.fertility"]}, + "fire": {"label": "๐Ÿ”ฅ fire", "members": ["fire.count", "fire.event", "astro.mars", "sun.speed", "crypto.vel"]}, + "earth": {"label": "โ›ฐ earth", "members": ["quake.depth", "debt.total", "world.poverty", "astro.saturn"]}, + "air": {"label": "๐Ÿ’จ air", "members": ["weather.wind", "air.pm25", "planes.count", "wiki.rate"]}, + "water": {"label": "๐Ÿ’ง water", "members": ["weather.precip", "weather.humidity", "astro.moon", "season.south"]}, + "spirit": {"label": "โœจ spirit", "members": ["astro.harmony", "almanac.fertile", "zodiac.hour", "clock.births"]}, + "wealth": {"label": "๐Ÿ’ฐ wealth", "members": ["crypto.price", "market.turnover", "market.velocity", "debt.total"]}, + "summer": {"label": "โ˜€ summer", "members": ["season.north", "season.south", "weather.temp", "sky.day"]} }, "tweaks": { @@ -118,7 +125,14 @@ "world.poverty": {"norm": {"lo": 0, "hi": 20}, "filter": {"min_cutoff": 0.05, "beta": 0.001}, "label": "extreme poverty (world %)"}, "world.hunger": {"norm": {"lo": 0, "hi": 15}, "filter": {"min_cutoff": 0.05, "beta": 0.001}, "label": "undernourishment (world %)"}, "world.fertility": {"norm": {"lo": 0, "hi": 4}, "label": "world fertility rate"}, - "world.lifeexp": {"norm": {"lo": 50, "hi": 85}, "label": "world life expectancy"} + "world.lifeexp": {"norm": {"lo": 50, "hi": 85}, "label": "world life expectancy"}, + "zodiac.year": {"norm": {"lo": 0, "hi": 1}, "label": "zodiac year animal"}, + "zodiac.element": {"norm": {"lo": 0, "hi": 1}, "label": "zodiac year element"}, + "zodiac.hour": {"norm": {"lo": 0, "hi": 1}, "filter": {"min_cutoff": 0.1, "beta": 0.001}, "label": "hour animal (2h cycle)"}, + "season.north": {"norm": {"lo": 0, "hi": 1}, "filter": {"min_cutoff": 0.08, "beta": 0.001}, "label": "northern summer"}, + "season.south": {"norm": {"lo": 0, "hi": 1}, "filter": {"min_cutoff": 0.08, "beta": 0.001}, "label": "southern summer"}, + "fire.count": {"norm": {"lo": 0, "hi": 600}, "filter": {"min_cutoff": 0.2, "beta": 0.002}, "label": "wildfires worldwide"}, + "fire.event": {"type": "event", "attack": 0.01, "decay": 0.5, "label": "new wildfire"} }, "routes": [ @@ -173,6 +187,10 @@ {"source": "econ.inflation", "dest": "master.space", "amount": 0.3, "curve": "lin", "label": "inflation erodes the space"}, {"source": "world.poverty", "dest": "drone.voices", "amount": 0.35, "curve": "lin", "label": "the world's poverty weights the drone"}, {"source": "world.hunger", "dest": "saturation", "amount": 0.25, "curve": "lin", "label": "hunger grinds under everything"}, - {"source": "world.lifeexp", "dest": "pad.brightness", "amount": 0.2, "curve": "lin", "label": "longer lives, brighter pads"} + {"source": "world.lifeexp", "dest": "pad.brightness", "amount": 0.2, "curve": "lin", "label": "longer lives, brighter pads"}, + {"source": "fire.count", "dest": "saturation", "amount": 0.4, "curve": "exp", "label": "the world's wildfires burn the saturation"}, + {"source": "fire.event", "dest": "glitch", "amount": 0.8, "curve": "lin", "label": "each new fire flares a glitch"}, + {"source": "season.north", "dest": "pad.brightness", "amount": 0.3, "curve": "scurve", "label": "northern summer brightens"}, + {"source": "zodiac.hour", "dest": "wavetable.morph", "amount": 0.3, "curve": "lin", "label": "the hour animal turns the timbre"} ] } diff --git a/run.py b/run.py index d0515e6..630d443 100644 --- a/run.py +++ b/run.py @@ -42,6 +42,8 @@ WORLD = [ "workers/world_debt.py", "workers/world_econ.py", "workers/world_bank.py", + "workers/world_zodiac.py", + "workers/world_fire.py", ] # workers that retarget to the venue city diff --git a/transform.py b/transform.py index c38b108..68ca296 100644 --- a/transform.py +++ b/transform.py @@ -93,7 +93,7 @@ class TweakBank: def __init__(self, cfg: dict): self.groups: dict[str, Tweak] = {} self.group_members: dict[str, list[str]] = {} - self.member_group: dict[str, str] = {} + self.member_groups: dict[str, list] = {} # a source can be in many groups self.source_tweaks: dict[str, Tweak] = {} self.controls: list[dict] = list(cfg.get("controls", [])) self.group_orbit: dict[str, str] = {} # group -> planet name @@ -108,7 +108,7 @@ class TweakBank: if g.get("orbit"): self.group_orbit[gname] = g["orbit"] for m in members: - self.member_group.setdefault(m, gname) + self.member_groups.setdefault(m, []).append(gname) for name, spec in tw.items(): if not name.startswith("@"): self.source_tweaks[name] = Tweak(spec) @@ -141,8 +141,7 @@ class TweakBank: out: dict[str, float] = {} for name, val in sources.items(): v = val - g = self.member_group.get(name) - if g: + for g in self.member_groups.get(name, ()): # stack every concept-group v = self.groups[g].apply(v) st = self.source_tweaks.get(name) if st: @@ -169,8 +168,8 @@ class TweakBank: st = self.source_tweaks.get(name) if st and st.p.get("mute", 0.0) >= 0.5: return True - g = self.member_group.get(name) - return bool(g and self.groups[g].p.get("mute", 0.0) >= 0.5) + return any(self.groups[g].p.get("mute", 0.0) >= 0.5 + for g in self.member_groups.get(name, ())) def set_orbit_lfo(self, gname: str, v: float): tw = self.groups.get(gname) @@ -188,7 +187,9 @@ class TweakBank: if orbit: self.group_orbit[name] = orbit for m in members: - self.member_group[m] = name + gl = self.member_groups.setdefault(m, []) + if name not in gl: + gl.append(name) def dump_groups(self) -> dict: """Serialize current groups for saving a template.""" diff --git a/workers/world_fire.py b/workers/world_fire.py new file mode 100644 index 0000000..cf78ed2 --- /dev/null +++ b/workers/world_fire.py @@ -0,0 +1,70 @@ +""" +world_fire.py โ€” the world on fire. Live wildfires. + +Pulls open wildfire events worldwide from NASA's EONET (Earth Observatory +Natural Event Tracker โ€” free, no key). The count is a slow burn; each newly +detected fire is a flare-up you can trigger on. The elemental fire of the set โ€” +bushfires, forest fires, the literal heat of the planet. + +Emits: + /gs/fire/count number of open wildfire events worldwide + /gs/fire/event fired when a new wildfire is detected (magnitude 1) +""" + +from __future__ import annotations +import argparse +import json +import time +import urllib.request + +from pythonosc.udp_client import SimpleUDPClient + +URL = ("https://eonet.gsfc.nasa.gov/api/v3/events" + "?category=wildfires&status=open&limit=500") +UA = "Godstrument/1.0 (live instrument)" + + +def fetch(): + req = urllib.request.Request(URL, headers={"User-Agent": UA}) + with urllib.request.urlopen(req, timeout=30) as r: + return json.load(r).get("events", []) + + +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("--poll", type=float, default=600.0) + args = ap.parse_args() + + client = SimpleUDPClient(args.host, args.port) + print(f"[fire] emitting /gs/fire/count|event (NASA EONET wildfires) " + f"(re-query every {args.poll:g}s)") + + seen: set[str] = set() + count = 0 + first = True + last_poll = 0.0 + while True: + try: + if time.time() - last_poll > args.poll or first: + events = fetch() + count = len(events) + for e in events: + eid = e.get("id") + if eid and eid not in seen: + seen.add(eid) + if not first: # no burst on the first load + client.send_message("/gs/fire/event", 1.0) + print(f" ๐Ÿ”ฅ {e.get('title', 'wildfire')}") + print(f" {count} wildfires burning worldwide") + first = False + last_poll = time.time() + client.send_message("/gs/fire/count", float(count)) + except Exception as e: # noqa + print(f"[fire] warn: {e}") + time.sleep(10.0) + + +if __name__ == "__main__": + main() diff --git a/workers/world_zodiac.py b/workers/world_zodiac.py new file mode 100644 index 0000000..676fdc7 --- /dev/null +++ b/workers/world_zodiac.py @@ -0,0 +1,70 @@ +""" +world_zodiac.py โ€” the Chinese zodiac + the turning seasons. Computed, no API. + +The year's animal and element (a 12- and 60-year wheel), plus the "hour animal" +that turns every two hours โ€” a slow calendar sequencer the day walks through. +And the seasons: northern vs southern summer intensity from the day of the year, +so "all data that happens in summer" has a signal to gate against, both +hemispheres. + +Emits: + /gs/zodiac/year the year's animal (0..1 over Rat..Pig) + /gs/zodiac/element the year's element (0..1 over Wood/Fire/Earth/Metal/Water) + /gs/zodiac/hour the current 2-hour animal (0..1) โ€” a 12-step day cycle + /gs/season/north northern-hemisphere summer intensity (0..1) + /gs/season/south southern-hemisphere summer intensity (0..1) +""" + +from __future__ import annotations +import argparse +import datetime +import math +import time + +from pythonosc.udp_client import SimpleUDPClient + +ANIMALS = ["Rat", "Ox", "Tiger", "Rabbit", "Dragon", "Snake", + "Horse", "Goat", "Monkey", "Rooster", "Dog", "Pig"] +ELEMENTS = ["Wood", "Fire", "Earth", "Metal", "Water"] + + +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("--interval", type=float, default=30.0) + args = ap.parse_args() + + client = SimpleUDPClient(args.host, args.port) + print("[zodiac] emitting /gs/zodiac/* and /gs/season/* (computed, no API)") + + last_hour = None + while True: + try: + utc = datetime.datetime.now(datetime.timezone.utc) + y = utc.year + year_animal = (y - 4) % 12 + year_element = ((y - 4) % 10) // 2 + h = datetime.datetime.now().hour # local hour + hour_animal = ((h + 1) // 2) % 12 + doy = utc.timetuple().tm_yday + north = 0.5 + 0.5 * math.cos(2 * math.pi * (doy - 172) / 365.25) + south = 1.0 - north + + client.send_message("/gs/zodiac/year", year_animal / 11.0) + client.send_message("/gs/zodiac/element", year_element / 4.0) + client.send_message("/gs/zodiac/hour", hour_animal / 11.0) + client.send_message("/gs/season/north", north) + client.send_message("/gs/season/south", south) + + if hour_animal != last_hour: + print(f" โ˜ฏ year of the {ELEMENTS[year_element]} " + f"{ANIMALS[year_animal]} ยท hour of the {ANIMALS[hour_animal]}") + last_hour = hour_animal + except Exception as e: # noqa + print(f"[zodiac] warn: {e}") + time.sleep(args.interval) + + +if __name__ == "__main__": + main()