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:
parent
a039ee7d7c
commit
60173d44cb
18
README.md
18
README.md
@ -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
|
||||
|
||||
|
||||
15
config.json
15
config.json
@ -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
8
hub.py
@ -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
1
run.py
@ -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
|
||||
|
||||
19
transform.py
19
transform.py
@ -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),
|
||||
|
||||
@ -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
79
workers/world_bank.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user