@topy-ai/maggie 0.7.61 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-zh-TW.md +2 -2
- package/README.md +5 -5
- package/bundled-references/agent-goal-design.md +92 -0
- package/bundled-skills/catalog.json +4 -0
- package/bundled-skills/maggie-deployment/SKILL.md +21 -1
- package/bundled-skills/maggie-design/SKILL.md +29 -1
- package/bundled-skills/maggie-security/SKILL.md +10 -2
- package/bundled-skills/maggie-security/references/commands.md +10 -3
- package/bundled-skills/maggie-security/references/explaining-findings.md +7 -0
- package/bundled-skills/maggie-security/references/tools-and-licences.md +8 -0
- package/bundled-skills/maggie-security/registry/security-tools.registry.json +748 -652
- package/bundled-skills/maggie-spirit/SKILL.md +128 -0
- package/bundled-tools/clis/maggie_dash.py +12 -4
- package/bundled-tools/clis/maggie_security.py +1 -1
- package/bundled-tools/clis/maggie_security_code.py +49 -1
- package/bundled-tools/clis/maggie_security_deps.py +95 -0
- package/bundled-tools/clis/maggie_security_mcp.py +67 -1
- package/bundled-tools/clis/maggie_security_report.py +31 -2
- package/bundled-tools/clis/maggie_security_scan.py +168 -0
- package/bundled-tools/clis/maggie_security_web.py +45 -1
- package/bundled-tools/clis/maggie_spirit.py +143 -0
- package/bundled-tools/clis/security_data/tools.lock.json +36 -7
- package/bundled-tools/runtime/maggie_dash_pg.py +126 -0
- package/bundled-tools/runtime/maggie_dash_store.py +17 -5
- package/bundled-tools/runtime/spirit_agent.py +262 -0
- package/bundled-tools/runtime/spirit_base.py +256 -0
- package/bundled-tools/runtime/spirit_face.py +222 -0
- package/package.json +1 -1
- package/references/agent-goal-design.md +92 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Animate the Core-Agent faces through the 32-state system.
|
|
3
|
+
|
|
4
|
+
The agent's face (hair / eyes / prop) is the fixed identity; its ONEMAN O-rim
|
|
5
|
+
is the animatable ring — it reuses the exact motion vocabulary + palette +
|
|
6
|
+
badges from sprite_system.CATALOG (thinking sweeps, loading fills, critical
|
|
7
|
+
blinks, success turns green, awaiting shows the clock badge, …). `speaking`
|
|
8
|
+
also opens the mouth. Emits a PNG frame strip + still per state, a sprite sheet
|
|
9
|
+
and states.json per agent, into brand/<role>/.
|
|
10
|
+
"""
|
|
11
|
+
import sys, os, json, math
|
|
12
|
+
HERE=os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0,HERE)
|
|
13
|
+
OUT_BASE=HERE
|
|
14
|
+
import spirit_face as F
|
|
15
|
+
from spirit_base import CATALOG, GLYPH, palette, GROUPS
|
|
16
|
+
from PIL import Image
|
|
17
|
+
|
|
18
|
+
W,H,cx,cy=F.W,F.H,F.cx,F.cy
|
|
19
|
+
R_IN,R_OUT=F.R_IN,F.R_OUT
|
|
20
|
+
shade=F.shade; OUT=F.OUT
|
|
21
|
+
|
|
22
|
+
# rim cells (annulus) ordered by angle from top, clockwise
|
|
23
|
+
RIM=[]
|
|
24
|
+
for y in range(H):
|
|
25
|
+
for x in range(W):
|
|
26
|
+
d=math.hypot(x-cx,y-cy)
|
|
27
|
+
if R_IN-0.5<=d<=R_OUT: RIM.append((x,y))
|
|
28
|
+
def _ang(p): return (math.atan2(p[1]-cy,p[0]-cx)+math.pi/2)%(2*math.pi)
|
|
29
|
+
RIM.sort(key=_ang)
|
|
30
|
+
ANG={p:_ang(p) for p in RIM}
|
|
31
|
+
|
|
32
|
+
BX,BY=22,0
|
|
33
|
+
def put_badge(px,name,bg,fg):
|
|
34
|
+
for yy in range(0,6):
|
|
35
|
+
for xx in range(21,27): px[(xx,yy)]=None
|
|
36
|
+
g=GLYPH[name]
|
|
37
|
+
for r in range(5):
|
|
38
|
+
for c in range(5): px[(BX+c,BY+r)]=fg if g[r][c]=="1" else bg
|
|
39
|
+
def tri(t,nf): x=(2*t/nf); return x if x<=1 else 2-x
|
|
40
|
+
|
|
41
|
+
WHT=F.WHT; EYE=F.EYE
|
|
42
|
+
EYEPROPS={"glasses","shades","monocle"} # cover the eyes -> don't redraw eyes under them
|
|
43
|
+
MOUTHPROPS={"cigar"} # at the mouth -> keep base mouth
|
|
44
|
+
|
|
45
|
+
# ---- expression layer: per-state face animation (generic, reusable) ----
|
|
46
|
+
# state -> (eye-mode, mouth-mode). Blink is added automatically on longer loops.
|
|
47
|
+
EXPR={
|
|
48
|
+
"off":("closed","flat"),"booting":("wake","small"),"idle":("open","smile_s"),
|
|
49
|
+
"active":("open","smile_s"),"paused":("half","flat"),"offline":("closed","flat"),
|
|
50
|
+
"thinking":("up","small"),"loading":("open","flat"),"syncing":("dart","flat"),
|
|
51
|
+
"writing":("down","small"),"streaming":("open","talk"),"queued":("open","flat"),
|
|
52
|
+
"scanning":("dart","flat"),"deploying":("determined","flat"),
|
|
53
|
+
"speaking":("open","talk"),"listening":("side","small"),
|
|
54
|
+
"notification":("wide","small"),"mention":("side","smile"),"recording":("open","flat"),
|
|
55
|
+
"success":("happy","grin"),"warning":("worried","frown"),"critical":("wide","frown"),
|
|
56
|
+
"blocked":("side","flat"),"error":("x","frown"),"retrying":("determined","flat"),
|
|
57
|
+
"awaiting":("up","small"),"approved":("happy","smile"),"rejected":("worried","frown"),
|
|
58
|
+
"secure":("open","smile_s"),"vulnerable":("worried","frown"),"maintenance":("down","flat"),
|
|
59
|
+
"celebrating":("happy","grin"),
|
|
60
|
+
}
|
|
61
|
+
def _clear(px,x0,y0,x1,y1,SK):
|
|
62
|
+
for y in range(y0,y1+1):
|
|
63
|
+
for x in range(x0,x1+1): px[(x,y)]=SK
|
|
64
|
+
def draw_eyes_expr(px,mode,t,nf,SK,lash,blink):
|
|
65
|
+
for s in(-1,1):
|
|
66
|
+
_clear(px,cx+s*3 if s>0 else cx-6, cy-3, cx+6 if s>0 else cx-3, cy, SK)
|
|
67
|
+
if blink: mode="closed"
|
|
68
|
+
def whites(s):
|
|
69
|
+
for x in(cx+s*3,cx+s*4,cx+s*5): px[(x,cy-2)]=WHT; px[(x,cy-1)]=WHT
|
|
70
|
+
def lashmark(s):
|
|
71
|
+
if lash: px[(cx+s*6,cy-2)]=OUT; px[(cx+s*5,cy-3)]=OUT
|
|
72
|
+
for s in(-1,1):
|
|
73
|
+
if mode=="closed":
|
|
74
|
+
for x in(cx+s*3,cx+s*4,cx+s*5): px[(x,cy-1)]=OUT
|
|
75
|
+
lashmark(s); continue
|
|
76
|
+
if mode=="half":
|
|
77
|
+
for x in(cx+s*3,cx+s*4,cx+s*5): px[(x,cy-1)]=WHT
|
|
78
|
+
px[(cx+s*4,cy-1)]=EYE
|
|
79
|
+
for x in(cx+s*3,cx+s*4,cx+s*5): px[(x,cy-2)]=OUT
|
|
80
|
+
continue
|
|
81
|
+
if mode=="happy":
|
|
82
|
+
px[(cx+s*5,cy-1)]=OUT; px[(cx+s*4,cy-2)]=OUT; px[(cx+s*3,cy-1)]=OUT; continue
|
|
83
|
+
if mode=="x":
|
|
84
|
+
px[(cx+s*3,cy-2)]=OUT; px[(cx+s*4,cy-1)]=OUT; px[(cx+s*5,cy)]=OUT
|
|
85
|
+
px[(cx+s*5,cy-2)]=OUT; px[(cx+s*3,cy)]=OUT; continue
|
|
86
|
+
whites(s); lashmark(s)
|
|
87
|
+
pdx, pdy = 0, 0
|
|
88
|
+
if mode=="up": pdy=-1
|
|
89
|
+
elif mode=="down": pdy=0; px[(cx+s*3,cy-2)]=EYE # lowered lid feel
|
|
90
|
+
elif mode=="side": pdx=1
|
|
91
|
+
elif mode=="dart": pdx=1 if (t%2==0) else -1
|
|
92
|
+
elif mode=="wide": pass
|
|
93
|
+
elif mode=="worried": px[(cx+s*4 if s<0 else cx+s*3,cy-3)]=OUT # inner brow down
|
|
94
|
+
elif mode=="determined": px[(cx+s*5,cy-3)]=OUT # outer brow
|
|
95
|
+
px[(cx+s*(4+pdx), cy-1+pdy)]=EYE
|
|
96
|
+
if mode not in("wide","up"): px[(cx+s*4, cy-2)]=EYE if pdy==0 else WHT
|
|
97
|
+
if mode=="wake" and t< nf//2:
|
|
98
|
+
for x in(cx+s*3,cx+s*4,cx+s*5): px[(x,cy-1)]=OUT; px[(x,cy-2)]=SK
|
|
99
|
+
def draw_mouth_expr(px,mode,t,SK,mcol):
|
|
100
|
+
_clear(px,cx-2,cy+3,cx+2,cy+5,SK)
|
|
101
|
+
if mode=="smile" or mode=="smile_s":
|
|
102
|
+
px[(cx-2,cy+4)]=OUT;px[(cx-1,cy+5)]=OUT;px[(cx,cy+5)]=OUT;px[(cx+1,cy+5)]=OUT;px[(cx+2,cy+4)]=OUT
|
|
103
|
+
elif mode=="grin":
|
|
104
|
+
px[(cx-2,cy+4)]=OUT;px[(cx+2,cy+4)]=OUT
|
|
105
|
+
for x in range(cx-1,cx+2): px[(x,cy+4)]=OUT; px[(x,cy+5)]=WHT
|
|
106
|
+
elif mode=="flat":
|
|
107
|
+
for x in range(cx-2,cx+2): px[(x,cy+4)]=OUT
|
|
108
|
+
elif mode=="frown":
|
|
109
|
+
px[(cx-2,cy+5)]=OUT;px[(cx-1,cy+4)]=OUT;px[(cx,cy+4)]=OUT;px[(cx+1,cy+4)]=OUT;px[(cx+2,cy+5)]=OUT
|
|
110
|
+
elif mode=="small":
|
|
111
|
+
px[(cx,cy+4)]=OUT; px[(cx-1,cy+4)]=OUT
|
|
112
|
+
elif mode=="talk":
|
|
113
|
+
if t%2:
|
|
114
|
+
for x in range(cx-1,cx+2): px[(x,cy+4)]=OUT;px[(x,cy+5)]=OUT
|
|
115
|
+
px[(cx,cy+5)]=F._rgb('7a1a12')
|
|
116
|
+
else:
|
|
117
|
+
for x in range(cx-1,cx+2): px[(x,cy+4)]=OUT
|
|
118
|
+
if mcol and mode in("smile","smile_s","flat","grin"): # lipstick tint
|
|
119
|
+
for x in range(cx-2,cx+3):
|
|
120
|
+
if px.get((x,cy+4))==OUT: px[(x,cy+4)]=mcol
|
|
121
|
+
|
|
122
|
+
# ---- per-agent signature micro-animation on the prop (always-on identity) ----
|
|
123
|
+
def prop_anim(px,prop,t,nf,propcol):
|
|
124
|
+
ph=t%max(1,nf); frac=t/max(1,nf); rgb=F._rgb
|
|
125
|
+
if prop=="cigar":
|
|
126
|
+
for k in range(2): # smoke curl rising
|
|
127
|
+
yy=(cy+2)-((ph+k*2)%6); xx=cx+9+((ph+k)%2)
|
|
128
|
+
if yy>=cy-4: px[(xx,yy)]=rgb('9a9aa8') if k==0 else rgb('6a6a7a')
|
|
129
|
+
px[(cx+8,cy+4)]=rgb('ff4422') if ph%2==0 else rgb('ff7a44') # ember flicker
|
|
130
|
+
elif prop=="headset":
|
|
131
|
+
px[(cx-6,cy+4)]=rgb('ff4422') if ph%2==0 else rgb('5a1a10') # mic light blink
|
|
132
|
+
elif prop in("shades","glasses"):
|
|
133
|
+
gx=cx-5+int(frac*11) # glint sweeps both lenses
|
|
134
|
+
for lx0,lx1 in((cx-5,cx-1),(cx+2,cx+6)):
|
|
135
|
+
if lx0<=gx<=lx1: px[(gx,cy-1)]=WHT
|
|
136
|
+
elif prop=="monocle":
|
|
137
|
+
gx=cx+2+int(frac*4)
|
|
138
|
+
if cx+2<=gx<=cx+6: px[(gx,cy-1)]=WHT
|
|
139
|
+
px[(cx+4+(1 if ph%2 else 0),cy+2)]=rgb('d4af37') # chain sway
|
|
140
|
+
elif prop=="pen":
|
|
141
|
+
if ph%2==0: px[(cx+8,cy-1)]=rgb('f2c75a') # write spark at the nib
|
|
142
|
+
elif prop=="earbuds":
|
|
143
|
+
w=rgb('9fe8ff')
|
|
144
|
+
if ph%2==0: px[(cx-10,cy)]=w; px[(cx+10,cy)]=w # sound waves pulse out
|
|
145
|
+
else: px[(cx-11,cy-1)]=w; px[(cx+11,cy-1)]=w
|
|
146
|
+
elif prop=="bandana":
|
|
147
|
+
dy=1 if ph%2 else 0; BC=rgb(propcol or 'ef4444')
|
|
148
|
+
px[(cx-11,cy+dy)]=BC; px[(cx-11,cy+1+dy)]=shade(BC,-22) # tail flutter
|
|
149
|
+
|
|
150
|
+
def founder_anim(px,t,nf):
|
|
151
|
+
rgb=F._rgb; ph=t%max(1,nf); frac=t/max(1,nf)
|
|
152
|
+
gx=cx-5+int(frac*11) # glasses glint sweep
|
|
153
|
+
for lx0,lx1 in((cx-5,cx-3),(cx+3,cx+5)):
|
|
154
|
+
if lx0<=gx<=lx1: px[(gx,cy-1)]=WHT
|
|
155
|
+
GL = rgb('39d353') if ph%2==0 else rgb('1f7a3a') # green mask-vent pulse
|
|
156
|
+
for k in range(3): px[(cx+3,cy+4+k)]=GL
|
|
157
|
+
BC=rgb('e23b3b'); BCd=shade(BC,-24); w=1 if ph%2 else 0 # right-side tails ripple in the wind
|
|
158
|
+
for strand in(0,1):
|
|
159
|
+
for k in range(2,6):
|
|
160
|
+
xx=cx+8+k; yy=(cy-2-strand*2)-(k*2)//3-(w if k>=3 else 0)
|
|
161
|
+
if 0<=xx<W and 0<=yy<H: px[(xx,yy)]=BC if k<3 else BCd
|
|
162
|
+
|
|
163
|
+
NF={"breathe":10,"breathe_slow":10,"pulse":8,"blink":2,"arc":12,"scan":12,"spin":10,
|
|
164
|
+
"dualarc":12,"fill":12,"boot":10,"dots":6,"sparkout":4,"sparkin":4,"celebrate":8}
|
|
165
|
+
|
|
166
|
+
def frames(base, rim_live, state, P, ctx):
|
|
167
|
+
grp,role,motion,badge,fps=CATALOG[state]
|
|
168
|
+
col=P[role] if role in P else P["accent"]
|
|
169
|
+
nf=NF[motion]; out=[]
|
|
170
|
+
SK=F.SKINS[ctx["skin"]]; lash=(ctx["gender"]=="f")
|
|
171
|
+
eyemode,mouthmode=EXPR[state]
|
|
172
|
+
for t in range(nf):
|
|
173
|
+
px=dict(base)
|
|
174
|
+
def setrim(fn):
|
|
175
|
+
for i,p in enumerate(rim_live): px[p]=fn(i,p)
|
|
176
|
+
n=len(rim_live)
|
|
177
|
+
if motion in("breathe","breathe_slow"):
|
|
178
|
+
cc=shade(col,(10 if motion=="breathe_slow" else 16)*tri(t,nf)); setrim(lambda i,p:cc)
|
|
179
|
+
elif motion=="pulse":
|
|
180
|
+
cc=shade(col,42*tri(t,nf)); setrim(lambda i,p:cc)
|
|
181
|
+
elif motion=="blink":
|
|
182
|
+
cc=col if t%2==0 else shade(col,-60); setrim(lambda i,p:cc)
|
|
183
|
+
elif motion in("arc","scan","spin"):
|
|
184
|
+
win={"arc":80,"scan":34,"spin":54}[motion]; a0=(t/nf)*2*math.pi; w=math.radians(win)
|
|
185
|
+
setrim(lambda i,p:(P["acc_hot"] if min((ANG[p]-a0)%(2*math.pi),(a0-ANG[p])%(2*math.pi))<=w/2 else P["acc_dim"]))
|
|
186
|
+
elif motion in("fill","boot"):
|
|
187
|
+
prog=(t+1)/nf; setrim(lambda i,p:(col if (i/max(1,n))<=prog else P["acc_dim"]))
|
|
188
|
+
elif motion=="dualarc":
|
|
189
|
+
a0=(t/nf)*2*math.pi; w=math.radians(70)
|
|
190
|
+
def cm(i,p):
|
|
191
|
+
for b in(a0,a0+math.pi):
|
|
192
|
+
if min((ANG[p]-b)%(2*math.pi),(b-ANG[p])%(2*math.pi))<=w/2: return P["acc_hot"]
|
|
193
|
+
return P["acc_dim"]
|
|
194
|
+
setrim(cm)
|
|
195
|
+
elif motion=="dots":
|
|
196
|
+
cc=shade(col,14*tri(t,nf)); setrim(lambda i,p:cc)
|
|
197
|
+
elif motion=="sparkin":
|
|
198
|
+
cc=shade(col,16*tri(t,nf)); setrim(lambda i,p:cc)
|
|
199
|
+
elif motion=="celebrate":
|
|
200
|
+
cyc=[P["green"],P["amber"],P["accent"],P["acc_spark"]]; cc=cyc[t%len(cyc)]; setrim(lambda i,p:cc)
|
|
201
|
+
else: # sparkout + fallbacks: steady rim
|
|
202
|
+
setrim(lambda i,p:col)
|
|
203
|
+
# ---- personalized expression (eyes + mouth per state) ----
|
|
204
|
+
blink = (nf>=6 and t==nf-1) # a blink each loop on longer states
|
|
205
|
+
eyeCov=(ctx["prop"] in EYEPROPS) or ctx.get("specs")
|
|
206
|
+
mouthCov=(ctx["prop"] in MOUTHPROPS) or ctx.get("mask")
|
|
207
|
+
if not eyeCov:
|
|
208
|
+
draw_eyes_expr(px,eyemode,t,nf,SK,lash,blink)
|
|
209
|
+
if not mouthCov:
|
|
210
|
+
mcol=F._rgb('c0392b') if ctx.get("mouth")=="lips" else None
|
|
211
|
+
draw_mouth_expr(px,mouthmode,t,SK,mcol)
|
|
212
|
+
if ctx.get("founder"): founder_anim(px,t,nf) # founder signature anim
|
|
213
|
+
else: prop_anim(px, ctx["prop"], t, nf, ctx.get("propcol"))
|
|
214
|
+
if badge:
|
|
215
|
+
nm,bg,fg=badge; put_badge(px,nm,P[bg],P[fg])
|
|
216
|
+
out.append(px)
|
|
217
|
+
return out, fps
|
|
218
|
+
|
|
219
|
+
def gen_agent(role, accent, gender, params, scale=8):
|
|
220
|
+
P=palette(accent)
|
|
221
|
+
base=F.face(accent, gender=gender, **params)
|
|
222
|
+
rim_live=[p for p in RIM if base.get(p) in (F._rgb(accent), shade(F._rgb(accent),-30))]
|
|
223
|
+
ctx={"skin":params.get("skin","mid"),"gender":gender,"prop":params.get("prop"),
|
|
224
|
+
"mouth":params.get("mouth"),"propcol":params.get("propcol"),
|
|
225
|
+
"specs":params.get("specs"),"mask":params.get("mask"),"founder":(role=="founder")}
|
|
226
|
+
out=os.path.join(OUT_BASE, role); os.makedirs(out, exist_ok=True)
|
|
227
|
+
# wipe old O+1 agent files
|
|
228
|
+
for fn in os.listdir(out):
|
|
229
|
+
if fn.endswith(('.png','.svg','.json')): os.remove(os.path.join(out,fn))
|
|
230
|
+
meta={"title":role.upper(),"accent":accent,"gender":gender,"W":W,"H":H,"scale":scale,"groups":GROUPS,"states":{}}
|
|
231
|
+
def render(px): return F.render(px,scale)
|
|
232
|
+
for s in CATALOG:
|
|
233
|
+
frs,fps=frames(base,rim_live,s,P,ctx)
|
|
234
|
+
imgs=[render(px) for px in frs]
|
|
235
|
+
w=sum(i.width for i in imgs); strip=Image.new("RGBA",(w,imgs[0].height),(0,0,0,0)); x=0
|
|
236
|
+
for i in imgs: strip.paste(i,(x,0)); x+=i.width
|
|
237
|
+
strip.save(os.path.join(out,f"anim-{s}.png"))
|
|
238
|
+
render(frs[0]).save(os.path.join(out,f"still-{s}.png"))
|
|
239
|
+
grp,role_,motion,badge,_=CATALOG[s]
|
|
240
|
+
meta["states"][s]={"group":grp,"frames":len(frs),"fps":fps,"motion":motion,"badge":badge[0] if badge else None}
|
|
241
|
+
# sprite sheet (8x4, representative frame)
|
|
242
|
+
def rep(m,n): return {"blink":0,"fill":int(n*.62),"boot":int(n*.62),"arc":n//4,"scan":n//4,"spin":n//4,"dualarc":n//4,"dots":1}.get(m,n//2)
|
|
243
|
+
cols,rows,cell=8,4,6
|
|
244
|
+
tw=W*cell*0+W; # overview uses small renders
|
|
245
|
+
cellimg=lambda px: F.render(px, 5)
|
|
246
|
+
order=[s for g in GROUPS for s in CATALOG if CATALOG[s][0]==g]
|
|
247
|
+
tile=W*5; pad=6
|
|
248
|
+
sheet=Image.new("RGBA",(cols*tile+(cols+1)*pad, rows*(H*5)+(rows+1)*pad),(16,16,24,255))
|
|
249
|
+
for i,s in enumerate(order):
|
|
250
|
+
frs,_=frames(base,rim_live,s,P,ctx); im=cellimg(frs[rep(CATALOG[s][2],len(frs))])
|
|
251
|
+
X=pad+(i%cols)*(tile+pad); Y=pad+(i//cols)*(H*5+pad); sheet.paste(im,(X,Y),im)
|
|
252
|
+
sheet.save(os.path.join(out,"sprite-sheet.png"))
|
|
253
|
+
json.dump(meta, open(os.path.join(out,"states.json"),"w"), indent=1)
|
|
254
|
+
return meta
|
|
255
|
+
|
|
256
|
+
if __name__=="__main__":
|
|
257
|
+
only=sys.argv[1] if len(sys.argv)>1 else None
|
|
258
|
+
for role,acc,g,p in list(F.CAST)+[F.FOUNDER]:
|
|
259
|
+
r=role.lower()
|
|
260
|
+
if only and r!=only: continue
|
|
261
|
+
m=gen_agent(r,acc,g,p)
|
|
262
|
+
print(f"{role} {acc} {g}: {len(m['states'])} states -> brand/{r}/")
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ONEMAN sprite system — the reusable animated status-sprite basic set.
|
|
3
|
+
|
|
4
|
+
Every ONEMAN agent (Core or Service) shares ONE visual language: a 17x17 flat
|
|
5
|
+
pixel motif, a fixed semantic palette tinted by the agent's accent, a catalog of
|
|
6
|
+
32 common states, and a vocabulary of animation primitives. Give this generator
|
|
7
|
+
a Motif (the agent's mark) + an accent colour and it emits the whole set in a
|
|
8
|
+
consistent style: an animated PNG frame strip per state, a still SVG per state,
|
|
9
|
+
a combined overview sheet, and states.json (frames + fps) to drive CSS/canvas.
|
|
10
|
+
|
|
11
|
+
The ONEMAN mark (a blocky O ring + central 1 bar, accent #ff1212 = topy.ai) is
|
|
12
|
+
the default motif. A new agent reuses everything here and supplies only its own
|
|
13
|
+
motif grid + accent, so the whole roster animates and reads identically.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python3 sprite_system.py # regenerate the ONEMAN set
|
|
17
|
+
from sprite_system import Motif, generate # build another agent's set
|
|
18
|
+
generate(Motif.oneman(), accent="#ff1212", out="oneman", title="ONEMAN")
|
|
19
|
+
|
|
20
|
+
PIL only (no cairosvg).
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import json, math, os
|
|
24
|
+
from PIL import Image, ImageDraw
|
|
25
|
+
|
|
26
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
OUT_BASE = HERE
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------- palette ----
|
|
30
|
+
def _rgb(h): h=h.lstrip("#"); return (int(h[0:2],16),int(h[2:4],16),int(h[4:6],16))
|
|
31
|
+
def shade(c, amt):
|
|
32
|
+
"""Lighten (amt>0) or darken (amt<0) an rgb tuple toward white/black, in %."""
|
|
33
|
+
f=lambda v: max(0,min(255,round(v+(amt/100)*((255-v) if amt>0 else v))))
|
|
34
|
+
return (f(c[0]),f(c[1]),f(c[2]))
|
|
35
|
+
|
|
36
|
+
# fixed semantic colours shared by every agent (danger/success/etc are universal)
|
|
37
|
+
SEM = {"red":_rgb("ff1212"), "green":_rgb("36c76a"), "amber":_rgb("f2b01e"),
|
|
38
|
+
"blue":_rgb("3a9bff"), "grey":_rgb("4a4a58"), "ink":_rgb("14141e"),
|
|
39
|
+
"white":_rgb("f5f5fa")}
|
|
40
|
+
|
|
41
|
+
def palette(accent_hex):
|
|
42
|
+
a=_rgb(accent_hex)
|
|
43
|
+
p=dict(SEM)
|
|
44
|
+
p.update(accent=a, acc_dim=shade(a,-52), acc_deep=shade(a,-66),
|
|
45
|
+
acc_hot=shade(a,34), acc_spark=shade(a,72),
|
|
46
|
+
grey_d=shade(SEM["grey"],-42), green_d=shade(SEM["green"],-70),
|
|
47
|
+
amber_d=shade(SEM["amber"],-78), red_deep=shade(SEM["red"],-66))
|
|
48
|
+
return p
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------ motif ----
|
|
51
|
+
class Motif:
|
|
52
|
+
"""A 17x17 mark + the cell roles the animation vocabulary needs."""
|
|
53
|
+
def __init__(self, grid, core=None, dots=None, sparks=None):
|
|
54
|
+
self.grid=grid; self.N=len(grid); self.cx=self.cy=(self.N-1)/2
|
|
55
|
+
self.filled=[(x,y) for y in range(self.N) for x in range(self.N) if grid[y][x]]
|
|
56
|
+
self.core=core if core is not None else []
|
|
57
|
+
coreset=set(self.core)
|
|
58
|
+
ringc=[(x,y) for (x,y) in self.filled if (x,y) not in coreset]
|
|
59
|
+
# order ring cells by angle from top, clockwise — the animatable path
|
|
60
|
+
self.ring=sorted(ringc, key=lambda p:(math.atan2(p[1]-self.cy,p[0]-self.cx)+math.pi/2)%(2*math.pi))
|
|
61
|
+
self.dots=dots if dots is not None else []
|
|
62
|
+
self.sparks=sparks if sparks is not None else []
|
|
63
|
+
def ang(self,x,y): return (math.atan2(y-self.cy,x-self.cx))%(2*math.pi)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def oneman():
|
|
67
|
+
g=[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0],
|
|
68
|
+
[0,0,0,0,1,1,0,1,1,1,0,1,1,0,0,0,0],[0,0,0,1,1,1,0,1,1,1,0,1,1,1,0,0,0],
|
|
69
|
+
[0,0,1,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,1,1,1,1,0,0,1,1,1,0,0,1,1,1,1,0],
|
|
70
|
+
[0,1,1,1,0,0,0,1,1,1,0,0,0,1,1,1,0],[0,1,1,1,0,0,0,1,1,1,0,0,0,1,1,1,0],
|
|
71
|
+
[0,1,1,1,0,0,0,1,1,1,0,0,0,1,1,1,0],[0,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,0],
|
|
72
|
+
[0,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,0],[0,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,0],
|
|
73
|
+
[0,0,1,1,1,1,0,0,0,0,0,1,1,1,1,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0],
|
|
74
|
+
[0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0],
|
|
75
|
+
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]
|
|
76
|
+
core=[(x,y) for y in range(17) for x in range(17) if g[y][x] and x in (7,8,9) and y<=9]
|
|
77
|
+
dots=[(6,10),(8,10),(10,10)]
|
|
78
|
+
sparks_lv=[[],[(8,0),(1,5),(15,5),(0,8),(16,8)],
|
|
79
|
+
[(8,0),(7,0),(9,0),(1,4),(15,4),(0,8),(16,8),(1,12),(15,12)]]
|
|
80
|
+
m=Motif(g, core=core, dots=dots, sparks=sparks_lv); return m
|
|
81
|
+
|
|
82
|
+
# --------------------------------------------------------------- badges ------
|
|
83
|
+
GLYPH = {
|
|
84
|
+
"dot":["00000","00000","00100","00000","00000"],"check":["00000","00001","00010","10100","01000"],
|
|
85
|
+
"x":["10001","01010","00100","01010","10001"],"pause":["01010","01010","01010","01010","01010"],
|
|
86
|
+
"slash":["00001","00010","00100","01000","10000"],"clock":["01110","10101","10111","10001","01110"],
|
|
87
|
+
"lock":["01110","01010","11111","11011","11111"],"shield":["11111","11111","01110","01110","00100"],
|
|
88
|
+
"up":["00100","01110","11111","00100","00100"],"gear":["01010","11111","01110","11111","01010"],
|
|
89
|
+
}
|
|
90
|
+
def put_badge(px, name, bg, fg, N):
|
|
91
|
+
for yy in range(0,6):
|
|
92
|
+
for xx in range(11, min(17,N)): px[(xx,yy)]=None
|
|
93
|
+
g=GLYPH[name]
|
|
94
|
+
for r in range(5):
|
|
95
|
+
for c in range(5): px[(12+c,r)] = fg if g[r][c]=="1" else bg
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------- animation primitives ------
|
|
98
|
+
# Each returns {(x,y): rgb|None} for frame t of nf. `col` is the state's base.
|
|
99
|
+
def _fillall(m,col): return {p:col for p in m.filled}
|
|
100
|
+
def tri(t,nf): x=(2*t/nf); return x if x<=1 else 2-x # 0..1..0 triangle
|
|
101
|
+
|
|
102
|
+
def mo_breathe(m,col,t,nf,P,amt=16):
|
|
103
|
+
return _fillall(m, shade(col, amt*tri(t,nf)))
|
|
104
|
+
def mo_breathe_slow(m,col,t,nf,P): return mo_breathe(m,col,t,nf,P,amt=10)
|
|
105
|
+
def mo_pulse(m,col,t,nf,P):
|
|
106
|
+
k=tri(t,nf); return _fillall(m, tuple(round(col[i]+(P["acc_hot"][i] if col==P["accent"] else shade(col,55)[i]-col[i])*0 + (shade(col,42)[i]-col[i])*k) for i in range(3)))
|
|
107
|
+
def mo_blink(m,col,t,nf,P): return _fillall(m, col if t%2==0 else shade(col,-60))
|
|
108
|
+
def mo_arc(m,col,t,nf,P,win=80,hot=None):
|
|
109
|
+
px=_fillall(m,P["acc_dim"]); hot=hot or P["acc_hot"]; ang0=(t/nf)*2*math.pi; w=math.radians(win)
|
|
110
|
+
for (x,y) in m.ring:
|
|
111
|
+
a=m.ang(x,y); da=min((a-ang0)%(2*math.pi),(ang0-a)%(2*math.pi))
|
|
112
|
+
if da<=w/2: px[(x,y)]=hot
|
|
113
|
+
for p in m.core: px[p]=col
|
|
114
|
+
return px
|
|
115
|
+
def mo_scan(m,col,t,nf,P): return mo_arc(m,col,t,nf,P,win=34,hot=P["acc_spark"])
|
|
116
|
+
def mo_spin(m,col,t,nf,P): return mo_arc(m,col,t,nf,P,win=54)
|
|
117
|
+
def mo_dualarc(m,col,t,nf,P):
|
|
118
|
+
px=_fillall(m,P["acc_dim"]); ang0=(t/nf)*2*math.pi; w=math.radians(70)
|
|
119
|
+
for (x,y) in m.ring:
|
|
120
|
+
a=m.ang(x,y)
|
|
121
|
+
for b in (ang0,ang0+math.pi):
|
|
122
|
+
if min((a-b)%(2*math.pi),(b-a)%(2*math.pi))<=w/2: px[(x,y)]=P["acc_hot"]
|
|
123
|
+
for p in m.core: px[p]=col
|
|
124
|
+
return px
|
|
125
|
+
def mo_fill(m,col,t,nf,P,core_from=0.0):
|
|
126
|
+
px=_fillall(m,P["acc_dim"]); prog=(t+1)/nf
|
|
127
|
+
for (x,y) in m.ring:
|
|
128
|
+
a=(m.ang(x,y)+math.pi/2)%(2*math.pi)
|
|
129
|
+
if a/(2*math.pi)<=prog: px[(x,y)]=col
|
|
130
|
+
for p in m.core:
|
|
131
|
+
if prog>=core_from: px[p]=col
|
|
132
|
+
return px
|
|
133
|
+
def mo_boot(m,col,t,nf,P): return mo_fill(m,col,t,nf,P,core_from=0.7)
|
|
134
|
+
def mo_dots(m,col,t,nf,P):
|
|
135
|
+
px=_fillall(m,col); lit=t%max(1,len(m.dots))
|
|
136
|
+
for i,p in enumerate(m.dots): px[p]=P["acc_spark"] if i==lit else P["acc_deep"]
|
|
137
|
+
return px
|
|
138
|
+
def mo_sparkout(m,col,t,nf,P):
|
|
139
|
+
px=_fillall(m,col); step=[0,1,2,1][t%4]
|
|
140
|
+
for p in m.sparks[step]: px[p]=P["acc_spark"] if step==2 else P["acc_hot"]
|
|
141
|
+
return px
|
|
142
|
+
def mo_sparkin(m,col,t,nf,P):
|
|
143
|
+
px=_fillall(m,col); step=[2,1,0,1][t%4]
|
|
144
|
+
for p in m.sparks[step]: px[p]=P["acc_spark"]
|
|
145
|
+
return px
|
|
146
|
+
def mo_celebrate(m,col,t,nf,P):
|
|
147
|
+
cyc=[P["green"],P["amber"],P["accent"],P["acc_spark"]]
|
|
148
|
+
px=_fillall(m,cyc[t%len(cyc)]); step=[0,1,2,2][t%4]
|
|
149
|
+
for p in m.sparks[step]: px[p]=P["acc_spark"] if t%2 else P["white"]
|
|
150
|
+
return px
|
|
151
|
+
|
|
152
|
+
MOTION = {"breathe":mo_breathe,"breathe_slow":mo_breathe_slow,"pulse":mo_pulse,"blink":mo_blink,
|
|
153
|
+
"arc":mo_arc,"scan":mo_scan,"spin":mo_spin,"dualarc":mo_dualarc,"fill":mo_fill,"boot":mo_boot,
|
|
154
|
+
"dots":mo_dots,"sparkout":mo_sparkout,"sparkin":mo_sparkin,"celebrate":mo_celebrate}
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------- state catalog (32) --------
|
|
157
|
+
# state: (group, colour-role, motion, badge|None, fps)
|
|
158
|
+
A="accent"
|
|
159
|
+
CATALOG = {
|
|
160
|
+
# power / lifecycle
|
|
161
|
+
"off":("power","grey","breathe_slow",None,6), "booting":("power",A,"boot",None,8),
|
|
162
|
+
"idle":("power","acc_dim","breathe",None,6), "active":("power",A,"breathe",None,7),
|
|
163
|
+
"paused":("power","amber","breathe",("pause","amber_d","white"),6),
|
|
164
|
+
"offline":("power","grey","breathe_slow",("slash","grey_d","white"),4),
|
|
165
|
+
# work / process
|
|
166
|
+
"thinking":("work",A,"arc",None,11), "loading":("work",A,"fill",None,9),
|
|
167
|
+
"syncing":("work",A,"dualarc",None,10), "writing":("work",A,"dots",None,5),
|
|
168
|
+
"streaming":("work",A,"pulse",None,6), "queued":("work","amber","breathe",("clock","amber_d","white"),6),
|
|
169
|
+
"scanning":("work",A,"scan",None,16), "deploying":("work",A,"fill",("up","ink","white"),9),
|
|
170
|
+
# communication
|
|
171
|
+
"speaking":("comms",A,"sparkout",None,5), "listening":("comms","blue","sparkin",None,5),
|
|
172
|
+
"notification":("comms",A,"pulse",("dot","white","accent"),6),
|
|
173
|
+
"mention":("comms","blue","pulse",("dot","white","blue"),6),
|
|
174
|
+
"recording":("comms","red","pulse",("dot","white","red"),7),
|
|
175
|
+
# outcomes
|
|
176
|
+
"success":("outcome","green","breathe",None,7), "warning":("outcome","amber","pulse",None,6),
|
|
177
|
+
"critical":("outcome","red","blink",None,2), "blocked":("outcome","red","blink",("lock","ink","white"),2),
|
|
178
|
+
"error":("outcome","red","blink",("x","ink","white"),2),
|
|
179
|
+
"retrying":("outcome",A,"spin",None,12),
|
|
180
|
+
# governance / domain
|
|
181
|
+
"awaiting":("gov","amber","breathe",("clock","amber_d","white"),5),
|
|
182
|
+
"approved":("gov","green","breathe",("check","white","green"),7),
|
|
183
|
+
"rejected":("gov","red","breathe",("x","ink","white"),6),
|
|
184
|
+
"secure":("gov","green","breathe",("shield","green_d","white"),7),
|
|
185
|
+
"vulnerable":("gov","red","pulse",("shield","ink","white"),6),
|
|
186
|
+
"maintenance":("gov","grey","breathe",("gear","grey_d","white"),5),
|
|
187
|
+
"celebrating":("gov","green","celebrate",None,7),
|
|
188
|
+
}
|
|
189
|
+
GROUPS=["power","work","comms","outcome","gov"]
|
|
190
|
+
|
|
191
|
+
def _color(role,P): return P[role] if role in P else P.get(role, P["accent"])
|
|
192
|
+
|
|
193
|
+
def frames_for(m,state,P):
|
|
194
|
+
grp,role,motion,badge,fps=CATALOG[state]
|
|
195
|
+
col=_color(role,P); fn=MOTION[motion]
|
|
196
|
+
nf={"breathe":10,"breathe_slow":10,"pulse":8,"blink":2,"arc":12,"scan":12,"spin":10,
|
|
197
|
+
"dualarc":12,"fill":12,"boot":10,"dots":max(2,len(m.dots)*2),"sparkout":4,"sparkin":4,"celebrate":8}[motion]
|
|
198
|
+
out=[]
|
|
199
|
+
for t in range(nf):
|
|
200
|
+
px=fn(m,col,t,nf,P)
|
|
201
|
+
if badge:
|
|
202
|
+
nm,bg,fg=badge
|
|
203
|
+
put_badge(px, nm, P[bg], P[fg], m.N)
|
|
204
|
+
out.append(px)
|
|
205
|
+
return out, fps
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------- renderers -----
|
|
208
|
+
def render(px,N,scale):
|
|
209
|
+
img=Image.new("RGBA",(N*scale,N*scale),(0,0,0,0)); d=ImageDraw.Draw(img)
|
|
210
|
+
for (x,y),c in px.items():
|
|
211
|
+
if c is not None: d.rectangle([x*scale,y*scale,(x+1)*scale-1,(y+1)*scale-1],fill=c+(255,))
|
|
212
|
+
return img
|
|
213
|
+
def svg_of(px,N):
|
|
214
|
+
from collections import defaultdict
|
|
215
|
+
by=defaultdict(list)
|
|
216
|
+
for (x,y),c in px.items():
|
|
217
|
+
if c is not None: by[c].append((x,y))
|
|
218
|
+
parts=[]
|
|
219
|
+
for c,cells in by.items():
|
|
220
|
+
rs="".join(f'<rect x="{x}" y="{y}" width="1" height="1"/>' for x,y in sorted(cells))
|
|
221
|
+
parts.append(f'<g fill="#%02x%02x%02x">{rs}</g>'%c)
|
|
222
|
+
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {N} {N}" '
|
|
223
|
+
f'shape-rendering="crispEdges" role="img">{"".join(parts)}</svg>\n')
|
|
224
|
+
|
|
225
|
+
def generate(m:Motif, accent="#ff1212", out="oneman", title="ONEMAN", scale=9):
|
|
226
|
+
P=palette(accent); base=os.path.join(OUT_BASE, out); os.makedirs(base, exist_ok=True)
|
|
227
|
+
meta={"title":title,"accent":accent,"N":m.N,"scale":scale,"groups":GROUPS,"states":{}}
|
|
228
|
+
for s in CATALOG:
|
|
229
|
+
frames,fps=frames_for(m,s,P)
|
|
230
|
+
imgs=[render(px,m.N,scale) for px in frames]
|
|
231
|
+
w=sum(i.width for i in imgs); strip=Image.new("RGBA",(w,imgs[0].height),(0,0,0,0)); x=0
|
|
232
|
+
for i in imgs: strip.paste(i,(x,0)); x+=i.width
|
|
233
|
+
strip.save(os.path.join(base,f"anim-{s}.png"))
|
|
234
|
+
open(os.path.join(base,f"mark-{s}.svg"),"w").write(svg_of(frames[0],m.N)) # still fallback
|
|
235
|
+
grp,role,motion,badge,_=CATALOG[s]
|
|
236
|
+
meta["states"][s]={"group":grp,"frames":len(frames),"fps":fps,"motion":motion,
|
|
237
|
+
"badge":badge[0] if badge else None}
|
|
238
|
+
# overview sheet (representative frame 2 of each)
|
|
239
|
+
cols,rows,cell=8,4,7; tile=m.N*cell; pad=cell
|
|
240
|
+
sheet=Image.new("RGBA",(cols*tile+(cols+1)*pad,rows*tile+(rows+1)*pad),(16,16,24,255))
|
|
241
|
+
order=[s for g in GROUPS for s in CATALOG if CATALOG[s][0]==g]
|
|
242
|
+
def rep_idx(motion,n):
|
|
243
|
+
return {"blink":0,"fill":int(n*0.62),"boot":int(n*0.62),"arc":n//4,"scan":n//4,
|
|
244
|
+
"spin":n//4,"dualarc":n//4,"dots":1}.get(motion, n//2)
|
|
245
|
+
for i,s in enumerate(order):
|
|
246
|
+
fr,_=frames_for(m,s,P); im=render(fr[rep_idx(CATALOG[s][2],len(fr))],m.N,cell)
|
|
247
|
+
cx=pad+(i%cols)*(tile+pad); cy=pad+(i//cols)*(tile+pad); sheet.paste(im,(cx,cy),im)
|
|
248
|
+
sheet.save(os.path.join(base,"sprite-sheet.png"))
|
|
249
|
+
json.dump(meta, open(os.path.join(base,"states.json"),"w"), indent=1)
|
|
250
|
+
return meta
|
|
251
|
+
|
|
252
|
+
if __name__=="__main__":
|
|
253
|
+
meta=generate(Motif.oneman(), accent="#ff1212", out="oneman", title="ONEMAN")
|
|
254
|
+
print(f"{title if (title:=meta['title']) else ''}: {len(meta['states'])} states, all animated")
|
|
255
|
+
print("groups:", ", ".join(GROUPS))
|
|
256
|
+
print("states:", ", ".join(meta["states"]))
|