Add socio-economic data, performance mode, spells (full-state templates)

- world_bank.py: poverty, food security, fertility, life expectancy (World Bank)
  folded into the "shadow" / "bright" concept groups
- spells: save/load now captures the full live mix (gains/mutes/freezes), not
  just the wiring; TEMPLATES panel renamed SPELLS
- performance mode (press P): clean projection view + rotating "dispatches from
  the world" text readout for live shows

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
monsterrobotparty 2026-07-05 02:37:03 +10:00
parent a039ee7d7c
commit 60173d44cb
7 changed files with 185 additions and 15 deletions

View File

@ -82,17 +82,27 @@ their optional libs are installed), `--profile minimal` (just the simulator).
| Ephemeris / almanac | computed (Swiss Ephemeris) | planets, moon, planting calendar |
| National debt | US Treasury (no key) | slow dark ballast + daily churn |
| Inflation | US BLS CPI (no key) | grinding pressure |
| Human condition | World Bank (no key) | poverty, hunger, fertility, life expectancy |
The **dark half** (debt, inflation, pollution, quakes, the death rate) balances
the **bright** (births, moon, harmony, fertility, daylight) — group your sources
by *feeling*, not category: a "shadow" group, a "sad" group, whatever you blend.
Congress-trade and conflict feeds are possible too but need keyed/curated sources.
## Templates
## Spells
Save the whole wiring — routes, groups, orbits — as a named template from the ⚙
panel's TEMPLATES section (`save`, then `load` from the dropdown). They live in
`patches/*.json`. Build a set, name it, recall it.
Save a whole configuration — routes, groups, orbits, **and the live mix** (gains,
mutes, freezes) — as a named **spell** from the ⚙ panel's SPELLS section (`save`,
then `load` from the dropdown). Casting a saved spell restores the exact state.
They live in `patches/*.json`.
## Performance mode
Press **P** for a clean projection view: the editing chrome vanishes, the source
readouts declutter, and a rotating line of "dispatches from the world" (*bitcoin
at $62,902 · the moon is 81% lit · mercury is retrograde · M4.2 earthquake*)
appears for the audience. Open the console on a second monitor / projector and
hit P. (Curated selections + MIDI-mappable display controls are the next layer.)
## Strudel & MIDI voices

View File

@ -34,8 +34,8 @@
"human": {"label": "humanity", "orbit": "mars", "members": ["wiki.rate", "clock.popvel", "clock.births"]},
"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"]},
"light": {"label": "the bright", "members": ["clock.births", "astro.moon", "astro.harmony", "almanac.fertile", "sky.day"]}
"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"]}
},
"tweaks": {
@ -114,7 +114,11 @@
"debt.total": {"norm": {"lo": 38000000000000, "hi": 42000000000000}, "filter": {"min_cutoff": 0.08, "beta": 0.001}, "label": "US national debt"},
"debt.vel": {"norm": {"lo": 0, "hi": 50000000000}, "label": "debt daily churn"},
"econ.inflation": {"norm": {"lo": 0, "hi": 10}, "filter": {"min_cutoff": 0.1, "beta": 0.002}, "label": "US inflation (YoY %)"},
"econ.cpi_mom": {"norm": {"lo": -1, "hi": 2}, "label": "CPI month-over-month %"}
"econ.cpi_mom": {"norm": {"lo": -1, "hi": 2}, "label": "CPI month-over-month %"},
"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"}
},
"routes": [
@ -166,6 +170,9 @@
{"source": "debt.vel", "dest": "saturation", "amount": 0.4, "curve": "exp", "label": "the debt churn grinds the saturation"},
{"source": "debt.total", "dest": "drone.voices", "amount": 0.3, "curve": "lin", "label": "the weight of the debt as a drone"},
{"source": "econ.inflation", "dest": "tempo.nudge", "amount": 0.2, "curve": "scurve", "label": "inflation quietly presses the tempo"},
{"source": "econ.inflation", "dest": "master.space", "amount": 0.3, "curve": "lin", "label": "inflation erodes the space"}
{"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"}
]
}

8
hub.py
View File

@ -313,7 +313,9 @@ class Hub:
return
safe = "".join(c for c in name if c.isalnum() or c in "-_ ").strip() or "patch"
os.makedirs(self.patches_dir, exist_ok=True)
data = {"routes": self.matrix.routes, "groups": self.tweaks.dump_groups()}
data = {"routes": self.matrix.routes,
"groups": self.tweaks.dump_groups(),
"state": self.tweaks.dump_state()}
with open(os.path.join(self.patches_dir, safe + ".json"), "w") as f:
json.dump(data, f, indent=2)
self._scan_patches()
@ -330,7 +332,9 @@ class Hub:
self.matrix.routes = [dict(r) for r in data["routes"]]
if "groups" in data:
self.tweaks.load_groups(data["groups"])
print(f" loaded template '{name}'")
if "state" in data:
self.tweaks.load_state(data["state"])
print(f" loaded spell '{name}'")
def _handle_cmd(self, m: dict):
cmd = m.get("cmd")

1
run.py
View File

@ -41,6 +41,7 @@ WORLD = [
"workers/world_almanac.py",
"workers/world_debt.py",
"workers/world_econ.py",
"workers/world_bank.py",
]
# workers that retarget to the venue city

View File

@ -203,6 +203,25 @@ class TweakBank:
self.add_group(name, gd.get("members", []),
gd.get("label"), gd.get("orbit"))
def dump_state(self) -> dict:
"""Serialize live tweak settings (gains/mutes/freezes) — a spell's mix."""
return {
"groups": {g: dict(self.groups[g].p) for g in self.group_members},
"sources": {n: dict(t.p) for n, t in self.source_tweaks.items()
if t.active},
}
def load_state(self, state: dict):
for g, params in (state.get("groups") or {}).items():
tw = self.groups.get(g)
if tw:
for k, v in params.items():
tw.set(k, v)
for n, params in (state.get("sources") or {}).items():
tw = self.source_tweaks.setdefault(n, Tweak())
for k, v in params.items():
tw.set(k, v)
def macros(self) -> dict:
"""Summary of group tweak state for the UI."""
return {g: {"gain": round(t.p["gain"], 3),

View File

@ -529,6 +529,7 @@
for (const n of sources.values()) drawSourceNode(n, t);
for (const n of dests.values()) drawDestNode(n, t);
drawEditing(t);
if (performMode) drawPerform(t, dt);
drawHeader(t);
drawFooter(t);
if (!connected) drawConnecting(t);
@ -707,9 +708,11 @@
ctx.font = "600 13px 'Helvetica Neue', Arial, sans-serif";
ctx.fillStyle = rgba(rgbMix(col, 0.55), 0.95);
ctx.fillText(truncate(n.label, 22), x - NODE_R - 14, y - 1);
ctx.font = "500 10px 'Helvetica Neue', Arial, sans-serif";
ctx.fillStyle = "rgba(150,165,190,0.7)";
ctx.fillText(fmtRaw(n.rawDisp), x - NODE_R - 14, y + 13);
if (!performMode) {
ctx.font = "500 10px 'Helvetica Neue', Arial, sans-serif";
ctx.fillStyle = "rgba(150,165,190,0.7)";
ctx.fillText(fmtRaw(n.rawDisp), x - NODE_R - 14, y + 13);
}
// multi-select highlight ring
if (selection.has(n.key)) {
@ -1082,6 +1085,7 @@
let midiAccess = null, midiOut = null, midiIn = null;
let learnMode = false, learnTarget = null;
let patchList = [], templateSelect = null;
let performMode = false, dispatchIdx = 0, dispatchT = 0;
const midiBindings = loadBindings(); // "cc:17" / "note:36" -> {target,param,mode}
const lastCCSent = {}, lastNoteFor = {};
let outSel = null, inSel = null, midiStatusEl = null, learnBtn = null, bindCountEl = null;
@ -1158,6 +1162,52 @@
updateGroupBtn();
}
// performance mode — clean projection view for the audience (press P)
window.addEventListener("keydown", (ev) => {
const tag = (ev.target && ev.target.tagName) || "";
if (tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA") return;
if (ev.key === "p" || ev.key === "P") {
performMode = !performMode;
if (performMode) {
gearEl.style.display = "none"; panelEl.style.display = "none";
inspectEl.style.display = "none";
} else {
gearEl.style.display = ""; panelEl.style.display = ""; inspectEl.style.display = "";
}
}
});
// rotating "dispatches from the world" — data as readable poetry
function worldPhrases() {
const p = [], get = (k) => sources.get(k);
const moon = get("astro.moon"); if (moon) p.push("the moon is " + Math.round(moon.raw * 100) + "% lit");
const merc = get("astro.mercury_retro"); if (merc && merc.raw > 0.5) p.push("mercury is retrograde");
const btc = get("crypto.price"); if (btc && btc.raw > 0) p.push("bitcoin at $" + Math.round(btc.raw).toLocaleString());
const inf = get("econ.inflation"); if (inf && inf.raw > 0) p.push("inflation runs at " + inf.raw.toFixed(1) + "%");
const debt = get("debt.total"); if (debt && debt.raw > 0) p.push("$" + (debt.raw / 1e12).toFixed(1) + " trillion owed");
const pov = get("world.poverty"); if (pov && pov.raw > 0) p.push(pov.raw.toFixed(1) + "% of the world in extreme poverty");
const ev = eventTicker[0]; if (ev) p.push(ev.text.toLowerCase());
if (!p.length) p.push("the world is playing");
return p;
}
function drawPerform(t, dt) {
dispatchT += dt;
const phrases = worldPhrases();
if (dispatchT > 5.0) { dispatchT = 0; dispatchIdx++; }
const phrase = phrases[dispatchIdx % phrases.length];
let a = 1;
if (dispatchT < 0.6) a = dispatchT / 0.6;
else if (dispatchT > 4.4) a = (5.0 - dispatchT) / 0.6;
a = clamp(a, 0, 1);
ctx.save();
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.font = "300 30px 'Helvetica Neue', Arial, sans-serif";
ctx.fillStyle = "rgba(224,234,255," + (0.12 + a * 0.72).toFixed(3) + ")";
ctx.fillText(phrase, W / 2, H - 118);
ctx.restore();
}
function openInspector(node) {
inspectEl.innerHTML = "";
const close = document.createElement("span");
@ -1376,10 +1426,10 @@
panelEl.appendChild(clr);
// Templates — save / load named patches
const th = document.createElement("h3"); th.textContent = "TEMPLATES"; th.style.marginTop = "8px";
const th = document.createElement("h3"); th.textContent = "SPELLS"; th.style.marginTop = "8px";
panelEl.appendChild(th);
const nameRow = document.createElement("div"); nameRow.className = "row"; nameRow.style.margin = "4px 0";
const nameIn = document.createElement("input"); nameIn.type = "text"; nameIn.placeholder = "name…"; nameIn.className = "tplname";
const nameIn = document.createElement("input"); nameIn.type = "text"; nameIn.placeholder = "name your spell…"; nameIn.className = "tplname";
const saveBtn = document.createElement("button"); saveBtn.textContent = "save"; saveBtn.className = "midibtn";
saveBtn.onclick = () => { const n = nameIn.value.trim(); if (n) { sendCmd({ cmd: "save_patch", name: n }); nameIn.value = ""; } };
nameRow.appendChild(nameIn); nameRow.appendChild(saveBtn); panelEl.appendChild(nameRow);

79
workers/world_bank.py Normal file
View File

@ -0,0 +1,79 @@
"""
world_bank.py the human condition, slow and heavy.
Pulls World Bank development indicators (no key) for the whole world: extreme
poverty, undernourishment (food insecurity), fertility, life expectancy. These
are annual figures they barely move so they're near-DC ballast, a moral
weight under the set rather than a rhythm. (The API is annual and occasionally
throttles rapid requests, so this polls slowly and holds the last value.)
Emits:
/gs/world/poverty extreme poverty headcount, % of population
/gs/world/hunger prevalence of undernourishment, %
/gs/world/fertility births per woman
/gs/world/lifeexp life expectancy at birth, years
"""
from __future__ import annotations
import argparse
import json
import time
import urllib.request
from pythonosc.udp_client import SimpleUDPClient
BASE = "https://api.worldbank.org/v2/country/WLD/indicator/{ind}?format=json&mrnev=1"
UA = "Godstrument/1.0 (live instrument)"
INDICATORS = {
"SI.POV.DDAY": ("world/poverty", "extreme poverty %"),
"SN.ITK.DEFC.ZS": ("world/hunger", "undernourishment %"),
"SP.DYN.TFRT.IN": ("world/fertility", "fertility"),
"SP.DYN.LE00.IN": ("world/lifeexp", "life expectancy"),
}
def fetch(ind: str):
req = urllib.request.Request(BASE.format(ind=ind), headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=30) as r:
raw = r.read().decode("utf-8-sig").strip() # World Bank prepends a BOM
d = json.loads(raw)
if isinstance(d, list) and len(d) > 1 and d[1]:
row = d[1][0]
if row.get("value") is not None:
return float(row["value"]), row["date"]
return None, None
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=21600.0, help="re-query every Ns")
args = ap.parse_args()
client = SimpleUDPClient(args.host, args.port)
print("[worldbank] emitting /gs/world/poverty|hunger|fertility|lifeexp "
f"(re-query every {args.poll:g}s)")
values: dict[str, float] = {}
last_poll = 0.0
while True:
if time.time() - last_poll > args.poll or not values:
for ind, (addr, name) in INDICATORS.items():
try:
v, year = fetch(ind)
if v is not None:
values[addr] = v
print(f" 🌍 {name}: {v} ({year})")
time.sleep(1.0) # be gentle with the API
except Exception as e: # noqa
print(f"[worldbank] {ind} warn: {e}")
last_poll = time.time()
for addr, v in values.items():
client.send_message("/gs/" + addr, v)
time.sleep(10.0)
if __name__ == "__main__":
main()