← All ivy-nodes
IVYXSTUDIO · IVY NODE
D
Design: pack the files as deliverables
ivy.node.design-pack · v0.1.0
ivyx✓
Gathers the files the steps before it made into one answer of kind deliverables (request, client, service, brief, the spec, the files with their bytes). Nothing is written to disk: the receiver keeps the files, which is what IVYX Agency does with the run's deliverables.
#design#deliverables#agency
Inputs
| Field | Type | Description |
|---|---|---|
| files | array | The files made, each with its bytes as base64. |
| files_a | array | The files made, each with its bytes as base64. |
| files_b | array | The files made, each with its bytes as base64. |
| files_c | array | The files made, each with its bytes as base64. |
| files_d | array | The files made, each with its bytes as base64. |
| request_id | string | Not declared |
| client | string | Not declared |
| service | string | Not declared |
| brief | string | Not declared |
| spec | object | Not declared |
Outputs
| Field | Type | Description |
|---|---|---|
| kindrequired | string | Always deliverables. |
| request_id | string | Not declared |
| client | string | Not declared |
| service | string | Not declared |
| brief | string | Not declared |
| spec | object | Not declared |
| filesrequired | array | The files made, each with its bytes as base64. |
| countrequired | integer | Not declared |
Source
python
# Input preparation
inp = __ivy_ctx__["nodes"][__ivy_node_id__]["input"]
# Compute
# Shared by every design node (built into each by build.mjs): colours, text, files.
import base64, re, math
SAFE_PALETTES = [
{"bg": "#1E1B4B", "accent": "#F6A23B", "text": "#F8F7FF"},
{"bg": "#F4F2FF", "accent": "#6C3BFF", "text": "#1E1B4B"},
{"bg": "#0F3D3E", "accent": "#E8C547", "text": "#F3F7F6"},
{"bg": "#FFF4E6", "accent": "#D9480F", "text": "#2B1D12"},
{"bg": "#101418", "accent": "#3DDC97", "text": "#F2F4F6"},
]
def hex_ok(s):
return isinstance(s, str) and re.fullmatch(r"#[0-9a-fA-F]{6}", s.strip()) is not None
def palette_of(p, seed=0):
"""A palette with three valid hex colours; what the model got wrong is replaced from a safe pair."""
base = SAFE_PALETTES[abs(int(seed)) % len(SAFE_PALETTES)]
out = {}
p = p if isinstance(p, dict) else {}
for k in ("bg", "accent", "text"):
v = p.get(k)
out[k] = v.strip().upper() if hex_ok(v) else base[k]
if out["bg"].upper() == out["text"].upper():
out["text"] = base["text"] if base["text"].upper() != out["bg"].upper() else base["bg"]
return out
def rgb(h):
h = h.lstrip("#")
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
def mix(a, b, t):
return tuple(int(round(a[i] + (b[i] - a[i]) * t)) for i in range(3))
def to_hex(c):
return "#%02X%02X%02X" % c
def lum(h):
r, g, b = [x / 255 for x in rgb(h)]
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def esc(s):
return (str(s).replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """))
def text_of(v, limit, default=""):
s = " ".join(str(v).split()) if isinstance(v, (str, int, float)) else ""
if not s:
s = default
return s[:limit].rstrip() if len(s) > limit else s
def wrap(s, max_chars):
words = s.split()
lines, cur = [], ""
for w in words:
if cur and len(cur) + 1 + len(w) > max_chars:
lines.append(cur)
cur = w
else:
cur = (cur + " " + w).strip()
if cur:
lines.append(cur)
return lines or [""]
def b64(data):
return base64.b64encode(data).decode("ascii")
def file_entry(name, mime, data, kind, width=None, height=None, caption=None, variant=None, extra=None):
f = {"name": name, "mime": mime, "kind": kind, "bytes": len(data), "data": b64(data)}
if width:
f["width"] = int(width)
if height:
f["height"] = int(height)
if caption:
f["caption"] = caption
if variant:
f["variant"] = variant
if extra:
f.update(extra)
return f
def slug(s):
s = re.sub(r"[^a-z0-9]+", "-", str(s).lower()).strip("-")
return s[:40] or "design"
def font():
return "Inter, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif"
# design-pack: the files of the steps before it as one `kind: deliverables` answer. Nothing is written here: the files travel in the output (a write needs a person under this workspace's policy, and the receiver keeps them).
import time
class DesignPack:
"Design: pack the files as deliverables"
def run(self, inp):
out = {}
files = []
for key in ("files", "files_a", "files_b", "files_c", "files_d"):
v = inp.get(key)
if isinstance(v, list):
files.extend(f for f in v if isinstance(f, dict) and f.get("name") and f.get("data"))
seen = set()
unique = []
for f in files:
if f["name"] in seen:
continue
seen.add(f["name"])
unique.append(f)
rid = text_of(inp.get("request_id"), 60, "") or time.strftime("%Y%m%d-%H%M%S")
spec = inp.get("spec") if isinstance(inp.get("spec"), dict) else {}
out = {
"kind": "deliverables",
"request_id": rid,
"client": text_of(inp.get("client"), 80, ""),
"service": text_of(inp.get("service"), 80, ""),
"brief": text_of(inp.get("brief"), 2000, ""),
"spec": {k: v for k, v in spec.items() if isinstance(v, (str, int, float, bool, dict, list))},
"files": unique,
"count": len(unique),
}
return out
result = DesignPack().run(inp)
# Output collection (the runner reads it here)
out = __ivy_ctx__["nodes"][__ivy_node_id__]["output"]
out.update(result)Tests
Requires: python:3.9
- merge
Files from two steps are packed once each by name.
- empty
No files still answers the kind, so a receiver can tell an empty delivery from a broken one.