A 13-agent adversarial whole-app sweep (client controls / runtime / server-auth /
new features / feed-config-honesty, each finding reproduced-or-refuted) found ZERO
broken functions — verdict healthy. Fixed the reachable low-severity edges it did
confirm:
- viz/index.html:1804 — arranger 'grooves' tab threw TypeError (nSel.isNote) when
dests is empty in the cold-connect window (post-sign-in, pre-WS-hello). Guard:
dests.get(arrSelKey) || {isNote:false} (the 'clips' tab already tolerates empty).
- viz/index.html — picking a skin from the sky menu / F4 next-skin / F4 prev while
in ZERO mode force-enabled an invisible, un-toggleable chakraMode (every clear
path is zeroMode-guarded). Added the same !zeroMode guard to all three sites
(byte-identical outside zero — no regression).
- hub.py:117 — GODSTRUMENT_READONLY parse treated any non-empty non-'0' value as
true (so 'false'/'no'/'off' enabled read-only). Now an explicit truthy set
{1,true,yes,on}; prod uses '1' → stays read-only (verified before deploy).
- viz/index.html:2996 — removed a dead else branch in zero 'save as vibe' (its
'vibes land in the next update' placeholder was unreachable; saveVibe is a real
sibling method) → call this.saveVibe(gk) directly.
- transform.py:40 — collapsed a no-op ternary (both branches float(v)).
- viz/manual.html — regenerated (was stale vs build_manual.py; picked up the
admin-panel CSS added in 764e28b; grimoire prose was already in sync).
Left as documented cosmetic: sanitizeVibe caps emoji at 2 code points (can split a
ZWJ/flag glyph on a shared vibe — display only, no XSS). Verified: node --check,
all .py compile, auth --selftest green, app boots into zero clean, console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
246 lines
9.7 KiB
Python
246 lines
9.7 KiB
Python
"""
|
|
transform.py — the performance layer. The human hands on the machine.
|
|
|
|
Raw sources (planet, room, markets) are honest but mechanical. This layer lets
|
|
you *play* them live: bend, offset, freeze, or smooth a signal on the fly — the
|
|
way you'd nudge a CDJ platter or lean on a pitch-bend wheel. And you can bind one
|
|
control to a whole GROUP of sources, so a single knob leans on four data streams
|
|
at once.
|
|
|
|
A Tweak sits between a source's normalized 0..1 value and the modulation matrix:
|
|
|
|
value ── group tweak ── source tweak ──► matrix
|
|
|
|
Parameters (each live-settable by a MIDI knob, a hand sensor, or the UI):
|
|
gain multiply (0..~2) — depth / "how much this source matters now"
|
|
offset add (-1..1) — bias / bend / lean
|
|
invert flip (0/1) — mirror the signal
|
|
freeze hold current value (0/1) — "stop the planet here" (CDJ pause)
|
|
smooth 0..1 extra lag — make it liquid, or snap it tight
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
def _clamp01(x: float) -> float:
|
|
return 0.0 if x < 0.0 else 1.0 if x > 1.0 else x
|
|
|
|
|
|
class Tweak:
|
|
"""A live-adjustable transform on one signal (or a whole group)."""
|
|
|
|
DEFAULTS = {"gain": 1.0, "offset": 0.0, "invert": 0.0,
|
|
"freeze": 0.0, "smooth": 0.0, "mute": 0.0}
|
|
|
|
def __init__(self, spec: dict | None = None):
|
|
self.p = dict(self.DEFAULTS)
|
|
if spec:
|
|
for k, v in spec.items():
|
|
if k in self.p:
|
|
self.p[k] = float(v)
|
|
self._held: dict[str, float] = {} # freeze holds, keyed by signal name
|
|
self._lag: dict[str, float] = {} # smoothing state, keyed by signal name
|
|
self.orbit_lfo = 1.0 # external planetary-orbit gain (hub-set)
|
|
|
|
def set(self, param: str, value: float):
|
|
if param in self.p:
|
|
self.p[param] = float(value)
|
|
|
|
@property
|
|
def active(self) -> bool:
|
|
p = self.p
|
|
return (p["gain"] != 1.0 or p["offset"] != 0.0 or p["invert"] >= 0.5
|
|
or p["freeze"] >= 0.5 or p["smooth"] > 0.0 or p["mute"] >= 0.5)
|
|
|
|
def apply(self, x: float, key: str = "") -> float:
|
|
# `key` is the signal name: a group Tweak is applied to every member, so
|
|
# freeze/smooth state must be kept per-signal, not once for the group.
|
|
p = self.p
|
|
# mute: silence this source/group entirely
|
|
if p["mute"] >= 0.5:
|
|
return 0.0
|
|
# freeze: hold each signal's own last value (a CDJ pause on the data)
|
|
if p["freeze"] >= 0.5:
|
|
h = self._held.get(key)
|
|
if h is None:
|
|
h = self._lag.get(key, x)
|
|
self._held[key] = h
|
|
return h
|
|
self._held.pop(key, None)
|
|
|
|
v = (1.0 - x) if p["invert"] >= 0.5 else x
|
|
v = v * p["gain"] * self.orbit_lfo + p["offset"]
|
|
v = _clamp01(v)
|
|
|
|
s = p["smooth"]
|
|
if s > 0.0:
|
|
alpha = 1.0 - min(0.985, s * 0.985) # smooth=0 -> 1 (pass), 1 -> ~0.015
|
|
prev = self._lag.get(key)
|
|
self._lag[key] = v if prev is None else prev + alpha * (v - prev)
|
|
v = self._lag[key]
|
|
else:
|
|
self._lag[key] = v
|
|
return _clamp01(v)
|
|
|
|
|
|
class TweakBank:
|
|
"""Owns every group + per-source tweak and resolves live control bindings.
|
|
|
|
config sections:
|
|
groups: { "planet": {"members": ["weather.temp", ...], "label": "..."} }
|
|
tweaks: { "crypto.vel": {gain,offset,...}, "@planet": {gain,...} }
|
|
controls: [ { "src": "hand.open", "target": "@planet",
|
|
"param": "gain", "min": 0.2, "max": 1.3 }, ... ]
|
|
(src is any signal name — a MIDI CC like "midi.cc.1", a sensor
|
|
like "hand.open", or another data source. target "@name" = group.)
|
|
"""
|
|
|
|
def __init__(self, cfg: dict):
|
|
self.groups: dict[str, Tweak] = {}
|
|
self.group_members: dict[str, list[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
|
|
self.group_label: dict[str, str] = {} # group -> display label
|
|
|
|
tw = cfg.get("tweaks", {})
|
|
for gname, g in cfg.get("groups", {}).items():
|
|
members = g.get("members", [])
|
|
self.group_members[gname] = members
|
|
self.groups[gname] = Tweak(tw.get("@" + gname))
|
|
self.group_label[gname] = g.get("label", gname)
|
|
if g.get("orbit"):
|
|
self.group_orbit[gname] = g["orbit"]
|
|
for m in members:
|
|
self.member_groups.setdefault(m, []).append(gname)
|
|
for name, spec in tw.items():
|
|
if not name.startswith("@"):
|
|
self.source_tweaks[name] = Tweak(spec)
|
|
|
|
# ensure a tweak object exists for every control target
|
|
for b in self.controls:
|
|
self._resolve(b.get("target", ""))
|
|
|
|
def _resolve(self, target: str) -> Tweak | None:
|
|
if not target:
|
|
return None
|
|
if target.startswith("@"):
|
|
name = target[1:]
|
|
return self.groups.setdefault(name, Tweak())
|
|
return self.source_tweaks.setdefault(target, Tweak())
|
|
|
|
def apply_controls(self, sources: dict[str, float]):
|
|
"""Read live control signals and push them onto tweak params."""
|
|
for b in self.controls:
|
|
cv = sources.get(b.get("src"))
|
|
if cv is None:
|
|
continue
|
|
lo, hi = b.get("min", 0.0), b.get("max", 1.0)
|
|
mapped = lo + (hi - lo) * cv
|
|
tw = self._resolve(b.get("target", ""))
|
|
if tw:
|
|
tw.set(b.get("param", "gain"), mapped)
|
|
|
|
def transform(self, sources: dict[str, float]) -> dict[str, float]:
|
|
out: dict[str, float] = {}
|
|
for name, val in sources.items():
|
|
v = val
|
|
for g in self.member_groups.get(name, ()): # stack every concept-group
|
|
v = self.groups[g].apply(v, name)
|
|
st = self.source_tweaks.get(name)
|
|
if st:
|
|
v = st.apply(v, name)
|
|
out[name] = v
|
|
return out
|
|
|
|
# ---- live control from the UI (browser -> hub) --------------------
|
|
def set_param(self, target: str, param: str, value: float):
|
|
tw = self._resolve(target)
|
|
if tw:
|
|
tw.set(param, value)
|
|
|
|
def toggle(self, target: str, param: str = "mute") -> float:
|
|
tw = self._resolve(target)
|
|
if not tw:
|
|
return 0.0
|
|
cur = tw.p.get(param, 0.0)
|
|
newv = 0.0 if cur >= 0.5 else 1.0
|
|
tw.set(param, newv)
|
|
return newv
|
|
|
|
def is_muted(self, name: str) -> bool:
|
|
st = self.source_tweaks.get(name)
|
|
if st and st.p.get("mute", 0.0) >= 0.5:
|
|
return True
|
|
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)
|
|
if tw:
|
|
tw.orbit_lfo = v
|
|
|
|
def add_group(self, name: str, members: list, label: str = None,
|
|
orbit: str = None):
|
|
"""Create/replace a group at runtime (browser multi-select -> group)."""
|
|
if not name or not members:
|
|
return
|
|
# drop stale memberships from a previous definition so removed sources
|
|
# don't keep getting muted/tweaked by this group forever
|
|
for m in self.group_members.get(name, []):
|
|
gl = self.member_groups.get(m)
|
|
if gl and name in gl:
|
|
gl.remove(name)
|
|
self.group_members[name] = list(members)
|
|
self.groups.setdefault(name, Tweak())
|
|
self.group_label[name] = label or name
|
|
if orbit:
|
|
self.group_orbit[name] = orbit
|
|
for m in members:
|
|
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."""
|
|
return {g: {"members": self.group_members[g],
|
|
"label": self.group_label.get(g, g),
|
|
"orbit": self.group_orbit.get(g)}
|
|
for g in self.group_members}
|
|
|
|
def load_groups(self, groups: dict):
|
|
"""Rebuild groups from a saved template."""
|
|
for name, gd in (groups or {}).items():
|
|
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),
|
|
"offset": round(t.p["offset"], 3),
|
|
"freeze": t.p["freeze"] >= 0.5,
|
|
"mute": t.p["mute"] >= 0.5,
|
|
"orbit": self.group_orbit.get(g),
|
|
"lfo": round(t.orbit_lfo, 3)}
|
|
for g, t in self.groups.items() if g in self.group_members}
|