#!/usr/bin/env python3 """Every human edit to Wikipedia on Earth, right now, as one live pulse. The Wikimedia recentchange SSE firehose is a chaotic global stream of edits; each edit's byte-delta becomes a percussive glitch and the edits/sec is a breathing tempo — the collective heartbeat of humanity writing itself down. """ import argparse import json import time import urllib.request from collections import deque from pythonosc.udp_client import SimpleUDPClient STREAM_URL = "https://stream.wikimedia.org/v2/stream/recentchange" USER_AGENT = "Godstrument/1.0 (live instrument; monsterrobotparty@gmail.com)" MAX_EVENTS_PER_SEC = 30 # throttle emitted /event messages RATE_EMIT_INTERVAL = 1.0 # emit /gs/wiki/rate about once per second RATE_WINDOW = 5.0 # rolling window (s) for edits-per-second def run(host, port): client = SimpleUDPClient(host, port) print(f"[world_wiki] emitting /gs/wiki/... every ~1s", flush=True) backoff = 1.0 # rolling timestamps of ALL counted edits (for edits-per-second) edit_times = deque() # timestamps of emitted /event messages within the current second (throttle) emit_window = deque() last_rate_emit = time.monotonic() while True: try: req = urllib.request.Request(STREAM_URL, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=60) as resp: backoff = 1.0 # connected cleanly, reset backoff for raw in resp: line = raw.decode("utf-8", "replace").rstrip("\n") # SSE: a "data: " line carries the JSON payload. if line.startswith("data: "): payload = line[6:] try: change = json.loads(payload) except (ValueError, TypeError): continue if change.get("type") in ("edit", "new"): length = change.get("length") or {} new = length.get("new") old = length.get("old") if new is not None and old is not None: edit_size = abs(int(new) - int(old)) else: edit_size = 0 now = time.monotonic() edit_times.append(now) # Throttle emitted events to ~MAX_EVENTS_PER_SEC. while emit_window and now - emit_window[0] >= 1.0: emit_window.popleft() if len(emit_window) < MAX_EVENTS_PER_SEC: try: client.send_message("/gs/wiki/edit/event", float(edit_size)) emit_window.append(now) except OSError as exc: print(f"[world_wiki] OSC send failed: {exc}", flush=True) # A blank line ends an SSE event; use this beat to # trim the rolling window and emit the rate. now = time.monotonic() while edit_times and now - edit_times[0] >= RATE_WINDOW: edit_times.popleft() if now - last_rate_emit >= RATE_EMIT_INTERVAL: span = min(RATE_WINDOW, max(now - last_rate_emit, 1e-6)) rate = len(edit_times) / span if edit_times else 0.0 try: client.send_message("/gs/wiki/rate", float(rate)) except OSError as exc: print(f"[world_wiki] OSC send failed: {exc}", flush=True) last_rate_emit = now # Stream ended without error; loop to reconnect. print("[world_wiki] stream closed, reconnecting...", flush=True) except Exception as exc: # noqa: BLE401 — never die on a transient error print(f"[world_wiki] stream error: {exc}; retrying in {backoff:.0f}s", flush=True) time.sleep(backoff) backoff = min(backoff * 2, 30.0) def main(): parser = argparse.ArgumentParser(description="Wikipedia recentchange SSE -> OSC") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=9000) args = parser.parse_args() run(args.host, args.port) if __name__ == "__main__": main()