✦ revelation: CANON — factory dimensions (Stage 2C)
Four shipped patches on a read-only shelf: ✦ the heartbeat (boom-bap, mpc 8-4, kick duck), ✦ the wheel within the wheel (WHEELS polymeter — kick 16 / hat 12 / clap 10), ✦ the squeeze (GODSQUASH — the market & quakes pump the master), ✦ quiet arrival (gentle earth-echo ambient, no drums). git-tracked in factory/, authored in the client's serializePatch shape. Course-correction from the brief's stale anchor: the template select reads /api/presets (account-based, auth.py), NOT the hub's legacy patchList — so the factory shelf is injected there. list_factory() + load_factory() prepend the ✦ patches for every signed-in player; PUT/DELETE on a ✦ name → 403 (read-only); a missing factory/ degrades to []. test_factory.py (deploy-excluded) validates shape. Verified: HTTP dispatch serves the 4 ✦ names leading the list, loads one, rejects writes (403), user saves still work, traversal/missing → None, zero index.html changes. Taste disclaimer: these are structural drafts — correct shape is the deliverable, beauty is the human's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
cccbce536c
commit
7e568d8323
46
auth.py
46
auth.py
@ -30,6 +30,8 @@ import time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
DEFAULT_DB = os.path.join(HERE, "godstrument_users.db")
|
||||
FACTORY_DIR = os.path.join(HERE, "factory") # ✦ CANON — git-tracked, read-only patches that ship with the instrument
|
||||
FACTORY_PREFIX = "✦ " # "✦ " — the mark of a factory dimension (also the read-only guard key)
|
||||
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||
USERNAME_RE = re.compile(r"^[A-Za-z0-9_.\-]{2,24}$")
|
||||
MIN_PW = 8
|
||||
@ -403,6 +405,42 @@ def delete_preset(user_id: int, name: str, db: str = DEFAULT_DB) -> bool:
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
# ---- ✦ CANON — factory dimensions (git-tracked, read-only, shared by everyone) ---
|
||||
def list_factory() -> list[dict]:
|
||||
"""The shipped ✦ patches, newest-name-order stable. Silent if factory/ is absent."""
|
||||
out = []
|
||||
try:
|
||||
for fn in sorted(os.listdir(FACTORY_DIR)):
|
||||
if not fn.endswith(".json"):
|
||||
continue
|
||||
try:
|
||||
with open(os.path.join(FACTORY_DIR, fn), encoding="utf-8") as f:
|
||||
name = (json.load(f).get("name") or "").strip()
|
||||
if name.startswith(FACTORY_PREFIX):
|
||||
out.append({"name": name, "updated": 0, "factory": True})
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
except OSError:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def load_factory(name: str):
|
||||
"""Load a factory patch by its ✦-prefixed name. Guards against path traversal
|
||||
(the name never touches the filesystem — we match on the JSON's own name field)."""
|
||||
for fn in os.listdir(FACTORY_DIR) if os.path.isdir(FACTORY_DIR) else []:
|
||||
if not fn.endswith(".json"):
|
||||
continue
|
||||
try:
|
||||
with open(os.path.join(FACTORY_DIR, fn), encoding="utf-8") as f:
|
||||
obj = json.load(f)
|
||||
if (obj.get("name") or "").strip() == name:
|
||||
return obj.get("data")
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
# ---- HTTP dispatch (called by hub.py's request handler) ---------------------
|
||||
COOKIE = "gs_session"
|
||||
|
||||
@ -498,13 +536,17 @@ def handle_api(method: str, path: str, body: bytes, cookie_header: str,
|
||||
return 404, {"error": "not found"}, None
|
||||
|
||||
if path == "/api/presets" and method == "GET":
|
||||
return 200, {"presets": list_presets(uid, db)}, None
|
||||
# ✦ CANON — the factory shelf first, then the player's own saved dimensions
|
||||
return 200, {"presets": list_factory() + list_presets(uid, db)}, None
|
||||
|
||||
if path.startswith("/api/presets/"):
|
||||
name = _unquote(path[len("/api/presets/"):])
|
||||
is_factory = name.startswith(FACTORY_PREFIX)
|
||||
if method == "GET":
|
||||
data = load_preset(uid, name, db)
|
||||
data = load_factory(name) if is_factory else load_preset(uid, name, db)
|
||||
return (200, {"name": name, "data": data}, None) if data is not None else (404, {"error": "no such preset"}, None)
|
||||
if is_factory: # ✦ factory dimensions are read-only — never write or delete them
|
||||
return 403, {"error": "factory dimensions are read-only — save it under a new name"}, None
|
||||
if method in ("PUT", "POST"):
|
||||
return (200, {"ok": True}, None) if save_preset(uid, name, payload.get("data"), db) else (400, {"error": "bad preset"}, None)
|
||||
if method == "DELETE":
|
||||
|
||||
57
factory/quiet_arrival.json
Normal file
57
factory/quiet_arrival.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "✦ quiet arrival",
|
||||
"data": {
|
||||
"routes": [
|
||||
{
|
||||
"source": "astro.moon",
|
||||
"dest": "echo.wet",
|
||||
"amount": 0.6,
|
||||
"curve": "lin",
|
||||
"gate": null,
|
||||
"quantize": null,
|
||||
"label": "the moon opens the echo"
|
||||
},
|
||||
{
|
||||
"source": "weather.wind",
|
||||
"dest": "echo.time",
|
||||
"amount": 0.5,
|
||||
"curve": "scurve",
|
||||
"gate": null,
|
||||
"quantize": null,
|
||||
"label": "the wind whips the tape speed"
|
||||
},
|
||||
{
|
||||
"source": "sky.elev",
|
||||
"dest": "pad.brightness",
|
||||
"amount": 0.7,
|
||||
"curve": "scurve",
|
||||
"gate": null,
|
||||
"quantize": null,
|
||||
"label": "the sun brightens the pads"
|
||||
},
|
||||
{
|
||||
"source": "sun.speed",
|
||||
"dest": "reverb.size",
|
||||
"amount": 0.5,
|
||||
"curve": "lin",
|
||||
"gate": null,
|
||||
"quantize": null,
|
||||
"label": "the solar wind sizes the room"
|
||||
}
|
||||
],
|
||||
"tweaks": {},
|
||||
"seqs": {},
|
||||
"bpm": 64,
|
||||
"groove": {
|
||||
"amount": 1,
|
||||
"preset": "straight",
|
||||
"swing": 0,
|
||||
"humanize": 0,
|
||||
"velo": 0
|
||||
},
|
||||
"scale": {
|
||||
"root": 5,
|
||||
"name": "major"
|
||||
}
|
||||
}
|
||||
}
|
||||
479
factory/the_heartbeat.json
Normal file
479
factory/the_heartbeat.json
Normal file
@ -0,0 +1,479 @@
|
||||
{
|
||||
"name": "✦ the heartbeat",
|
||||
"data": {
|
||||
"routes": [],
|
||||
"tweaks": {},
|
||||
"seqs": {
|
||||
"drum.kick": {
|
||||
"on": true,
|
||||
"steps": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"vel": [
|
||||
0.95,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.7,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.75,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8
|
||||
],
|
||||
"chance": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"rat": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"lock": [
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"len": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"wlen": 16
|
||||
},
|
||||
"drum.snare": {
|
||||
"on": true,
|
||||
"steps": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"vel": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8
|
||||
],
|
||||
"chance": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"rat": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"lock": [
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"len": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"wlen": 16
|
||||
},
|
||||
"drum.hat": {
|
||||
"on": true,
|
||||
"steps": [
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"vel": [
|
||||
0.7,
|
||||
0.4,
|
||||
0.6,
|
||||
0.4,
|
||||
0.7,
|
||||
0.4,
|
||||
0.6,
|
||||
0.4,
|
||||
0.7,
|
||||
0.4,
|
||||
0.6,
|
||||
0.4,
|
||||
0.7,
|
||||
0.4,
|
||||
0.6,
|
||||
0.5
|
||||
],
|
||||
"chance": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"rat": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"lock": [
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"len": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"wlen": 16
|
||||
},
|
||||
"drum.ohat": {
|
||||
"on": true,
|
||||
"steps": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"vel": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8
|
||||
],
|
||||
"chance": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"rat": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"lock": [
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"len": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"wlen": 16
|
||||
}
|
||||
},
|
||||
"bpm": 88,
|
||||
"groove": {
|
||||
"amount": 1,
|
||||
"preset": "mpc 8-4",
|
||||
"swing": 0.62,
|
||||
"humanize": 0.12,
|
||||
"velo": 0.18
|
||||
},
|
||||
"scale": {
|
||||
"root": 0,
|
||||
"name": "minor_pent"
|
||||
},
|
||||
"kit": {
|
||||
"kick": {
|
||||
"tune": 0.44,
|
||||
"punch": 0.6,
|
||||
"decay": 0.55,
|
||||
"duck": 0.5,
|
||||
"level": 0.9,
|
||||
"pan": 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
376
factory/the_squeeze.json
Normal file
376
factory/the_squeeze.json
Normal file
@ -0,0 +1,376 @@
|
||||
{
|
||||
"name": "✦ the squeeze",
|
||||
"data": {
|
||||
"routes": [
|
||||
{
|
||||
"source": "crypto.vel",
|
||||
"dest": "squash",
|
||||
"amount": 0.9,
|
||||
"curve": "lin",
|
||||
"gate": null,
|
||||
"quantize": null,
|
||||
"label": "the market pumps the master"
|
||||
},
|
||||
{
|
||||
"source": "quake.event",
|
||||
"dest": "squash",
|
||||
"amount": 0.7,
|
||||
"curve": "exp3",
|
||||
"gate": null,
|
||||
"quantize": null,
|
||||
"label": "a quake heaves the whole mix"
|
||||
}
|
||||
],
|
||||
"tweaks": {},
|
||||
"seqs": {
|
||||
"drum.kick": {
|
||||
"on": true,
|
||||
"steps": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"vel": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8
|
||||
],
|
||||
"chance": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"rat": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"lock": [
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"len": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"wlen": 16
|
||||
},
|
||||
"drum.hat": {
|
||||
"on": true,
|
||||
"steps": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"vel": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8
|
||||
],
|
||||
"chance": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"rat": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"lock": [
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"len": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"wlen": 16
|
||||
},
|
||||
"drum.snare": {
|
||||
"on": true,
|
||||
"steps": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"vel": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8
|
||||
],
|
||||
"chance": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"rat": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"lock": [
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"len": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"wlen": 16
|
||||
}
|
||||
},
|
||||
"bpm": 120,
|
||||
"groove": {
|
||||
"amount": 1,
|
||||
"preset": "straight",
|
||||
"swing": 0,
|
||||
"humanize": 0.03,
|
||||
"velo": 0.12
|
||||
},
|
||||
"scale": {
|
||||
"root": 0,
|
||||
"name": "minor_pent"
|
||||
}
|
||||
}
|
||||
}
|
||||
357
factory/the_wheel_within_the_wheel.json
Normal file
357
factory/the_wheel_within_the_wheel.json
Normal file
@ -0,0 +1,357 @@
|
||||
{
|
||||
"name": "✦ the wheel within the wheel",
|
||||
"data": {
|
||||
"routes": [],
|
||||
"tweaks": {},
|
||||
"seqs": {
|
||||
"drum.kick": {
|
||||
"on": true,
|
||||
"steps": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"vel": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8
|
||||
],
|
||||
"chance": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"rat": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"lock": [
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"len": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"wlen": 16
|
||||
},
|
||||
"drum.hat": {
|
||||
"on": true,
|
||||
"steps": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"vel": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8
|
||||
],
|
||||
"chance": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"rat": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"lock": [
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"len": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"wlen": 12
|
||||
},
|
||||
"drum.clap": {
|
||||
"on": true,
|
||||
"steps": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"vel": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8
|
||||
],
|
||||
"chance": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"rat": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"lock": [
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"len": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"wlen": 10
|
||||
}
|
||||
},
|
||||
"bpm": 96,
|
||||
"groove": {
|
||||
"amount": 1,
|
||||
"preset": "swing 16",
|
||||
"swing": 0.55,
|
||||
"humanize": 0.05,
|
||||
"velo": 0.1
|
||||
},
|
||||
"scale": {
|
||||
"root": 0,
|
||||
"name": "minor_pent"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
test_factory.py
Normal file
91
test_factory.py
Normal file
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""test_factory.py — validate the ✦ CANON factory patches' SHAPE.
|
||||
|
||||
Not a taste check (the human re-voices by ear) — this asserts every factory
|
||||
patch loads cleanly through the client's migrateSeq expectations: 16-step seq
|
||||
arrays, wlen in 1..16, sane bpm, valid route/groove/scale shape, and a
|
||||
✦-prefixed name. Stdlib only; excluded from the deploy by the test_*.py filter.
|
||||
|
||||
python3 test_factory.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
FACTORY = os.path.join(HERE, "factory")
|
||||
SCALES = {"chromatic", "major", "minor", "dorian", "phrygian", "lydian",
|
||||
"mixolydian", "pentatonic", "minor_pent", "wholetone", "hirajoshi"}
|
||||
SEQ_ARRAYS = ("steps", "vel", "chance", "rat", "lock", "len")
|
||||
|
||||
|
||||
def check_seq(key, s, errs):
|
||||
for a in SEQ_ARRAYS:
|
||||
if a in s:
|
||||
if not isinstance(s[a], list) or len(s[a]) != 16:
|
||||
errs.append(f"{key}.{a} must be a 16-length array")
|
||||
if "steps" not in s or not isinstance(s.get("steps"), list) or len(s["steps"]) != 16:
|
||||
errs.append(f"{key}.steps missing/not-16 (migrateSeq would reject → dropped)")
|
||||
if "wlen" in s and not (isinstance(s["wlen"], int) and 1 <= s["wlen"] <= 16):
|
||||
errs.append(f"{key}.wlen must be an int 1..16, got {s.get('wlen')!r}")
|
||||
if "on" in s and not isinstance(s["on"], (bool, int)):
|
||||
errs.append(f"{key}.on must be boolean-ish")
|
||||
|
||||
|
||||
def check_patch(name, p, errs):
|
||||
if not isinstance(p.get("bpm"), (int, float)) or not (40 <= p["bpm"] <= 180):
|
||||
errs.append(f"bpm must be 40..180, got {p.get('bpm')!r}")
|
||||
for r in p.get("routes", []):
|
||||
if not r.get("source") or not r.get("dest"):
|
||||
errs.append("route missing source/dest")
|
||||
if not isinstance(r.get("amount"), (int, float)):
|
||||
errs.append(f"route {r.get('source')}→{r.get('dest')} amount not numeric")
|
||||
for key, s in (p.get("seqs") or {}).items():
|
||||
check_seq(key, s, errs)
|
||||
sc = p.get("scale") or {}
|
||||
if sc and sc.get("name") not in SCALES:
|
||||
errs.append(f"scale.name {sc.get('name')!r} not a known scale")
|
||||
gr = p.get("groove") or {}
|
||||
for k in ("swing", "humanize", "velo"):
|
||||
if k in gr and not (0 <= gr[k] <= 1):
|
||||
errs.append(f"groove.{k} out of 0..1")
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.isdir(FACTORY):
|
||||
print("FAIL: no factory/ dir"); sys.exit(1)
|
||||
files = sorted(f for f in os.listdir(FACTORY) if f.endswith(".json"))
|
||||
if len(files) < 4:
|
||||
print(f"FAIL: expected ≥4 factory patches, found {len(files)}"); sys.exit(1)
|
||||
total_errs = 0
|
||||
names = []
|
||||
for fn in files:
|
||||
with open(os.path.join(FACTORY, fn), encoding="utf-8") as f:
|
||||
obj = json.load(f)
|
||||
name, data = obj.get("name", ""), obj.get("data")
|
||||
errs = []
|
||||
if not name.startswith("✦ "):
|
||||
errs.append(f"name {name!r} must start with '✦ '")
|
||||
if not isinstance(data, dict):
|
||||
errs.append("missing 'data' object")
|
||||
else:
|
||||
check_patch(name, data, errs)
|
||||
names.append(name)
|
||||
if errs:
|
||||
total_errs += len(errs)
|
||||
print(f" ✗ {fn}: " + "; ".join(errs))
|
||||
else:
|
||||
nseq = len(data.get("seqs") or {})
|
||||
nroute = len(data.get("routes") or [])
|
||||
print(f" ✓ {name} (bpm {data['bpm']}, {nseq} seqs, {nroute} routes)")
|
||||
# the four the brief names
|
||||
for want in ("✦ the heartbeat", "✦ the wheel within the wheel", "✦ the squeeze", "✦ quiet arrival"):
|
||||
if want not in names:
|
||||
print(f" ✗ missing canonical patch: {want}"); total_errs += 1
|
||||
if total_errs:
|
||||
print(f"\nFAIL: {total_errs} shape error(s)"); sys.exit(1)
|
||||
print(f"\nALL {len(files)} FACTORY PATCHES VALID (shape only — beauty is the human's)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user