Chinese zodiac + seasons + live wildfires; overlapping concept groups
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
60173d44cb
commit
7f4b4bcabe
24
config.json
24
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"}
|
||||
]
|
||||
}
|
||||
|
||||
2
run.py
2
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
|
||||
|
||||
15
transform.py
15
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."""
|
||||
|
||||
70
workers/world_fire.py
Normal file
70
workers/world_fire.py
Normal file
@ -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()
|
||||
70
workers/world_zodiac.py
Normal file
70
workers/world_zodiac.py
Normal file
@ -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()
|
||||
Loading…
Reference in New Issue
Block a user