solfaces 1.0.1 → 2.0.0
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.md +359 -87
- package/dist/agent/index.cjs +14 -14
- package/dist/agent/index.js +4 -4
- package/dist/agent/mcp-server.cjs +716 -277
- package/dist/{chunk-VMNATBH3.cjs → chunk-23XJ5VDX.cjs} +37 -27
- package/dist/chunk-23XJ5VDX.cjs.map +1 -0
- package/dist/chunk-46ZEFA6R.cjs +243 -0
- package/dist/chunk-46ZEFA6R.cjs.map +1 -0
- package/dist/{chunk-A6N3RPEA.cjs → chunk-546TBMAR.cjs} +6 -6
- package/dist/{chunk-A6N3RPEA.cjs.map → chunk-546TBMAR.cjs.map} +1 -1
- package/dist/chunk-6QRDULAO.cjs +191 -0
- package/dist/chunk-6QRDULAO.cjs.map +1 -0
- package/dist/chunk-6UWILY7E.cjs +647 -0
- package/dist/chunk-6UWILY7E.cjs.map +1 -0
- package/dist/chunk-DRUSCLEF.js +177 -0
- package/dist/chunk-DRUSCLEF.js.map +1 -0
- package/dist/chunk-HCEE4K4T.js +625 -0
- package/dist/chunk-HCEE4K4T.js.map +1 -0
- package/dist/chunk-JS527VKL.js +238 -0
- package/dist/chunk-JS527VKL.js.map +1 -0
- package/dist/{chunk-SNJABBAT.js → chunk-LRHYF5QN.js} +3 -3
- package/dist/{chunk-SNJABBAT.js.map → chunk-LRHYF5QN.js.map} +1 -1
- package/dist/{chunk-RX6D5FGH.js → chunk-TTGJZEPV.js} +30 -20
- package/dist/chunk-TTGJZEPV.js.map +1 -0
- package/dist/core/index.cjs +69 -29
- package/dist/core/index.d.cts +29 -47
- package/dist/core/index.d.ts +29 -47
- package/dist/core/index.js +3 -3
- package/dist/index.cjs +75 -35
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/react/index.cjs +431 -397
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +3 -2
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +427 -393
- package/dist/react/index.js.map +1 -1
- package/dist/solfaces.cdn.global.js +2 -2
- package/dist/solfaces.cdn.global.js.map +1 -1
- package/dist/themes/index.cjs +29 -17
- package/dist/themes/index.d.cts +10 -7
- package/dist/themes/index.d.ts +10 -7
- package/dist/themes/index.js +1 -1
- package/dist/{traits-DAFZnXeS.d.cts → traits-QlWuxZDD.d.cts} +45 -1
- package/dist/{traits-DAFZnXeS.d.ts → traits-QlWuxZDD.d.ts} +45 -1
- package/dist/vanilla/index.cjs +20 -8
- package/dist/vanilla/index.cjs.map +1 -1
- package/dist/vanilla/index.d.cts +1 -1
- package/dist/vanilla/index.d.ts +1 -1
- package/dist/vanilla/index.js +17 -5
- package/dist/vanilla/index.js.map +1 -1
- package/package.json +1 -2
- package/python/solfaces.py +557 -235
- package/skill.md +210 -65
- package/dist/chunk-2DIKGLXZ.cjs +0 -126
- package/dist/chunk-2DIKGLXZ.cjs.map +0 -1
- package/dist/chunk-CVFO7YHY.cjs +0 -97
- package/dist/chunk-CVFO7YHY.cjs.map +0 -1
- package/dist/chunk-H3SK3MNX.cjs +0 -409
- package/dist/chunk-H3SK3MNX.cjs.map +0 -1
- package/dist/chunk-KSGFMW33.js +0 -401
- package/dist/chunk-KSGFMW33.js.map +0 -1
- package/dist/chunk-LQWJRHGC.js +0 -86
- package/dist/chunk-LQWJRHGC.js.map +0 -1
- package/dist/chunk-RX6D5FGH.js.map +0 -1
- package/dist/chunk-VMNATBH3.cjs.map +0 -1
- package/dist/chunk-WURY4QGH.js +0 -117
- package/dist/chunk-WURY4QGH.js.map +0 -1
package/python/solfaces.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
SOLFACES — Python Port
|
|
2
|
+
SOLFACES v2 — Python Port
|
|
3
3
|
Deterministic wallet avatar generation for Python backends, bots, and scripts.
|
|
4
|
-
Zero dependencies. Generates identical traits to the
|
|
4
|
+
Zero dependencies. Generates identical traits and SVG output to the TypeScript version.
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
7
|
from solfaces import generate_traits, render_svg, describe_appearance
|
|
@@ -13,25 +13,26 @@ Usage:
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
from dataclasses import dataclass
|
|
16
|
-
from typing import Optional, Dict
|
|
16
|
+
from typing import Optional, Dict
|
|
17
17
|
import ctypes
|
|
18
|
+
import math
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
# ─── Types ────────────────────────────────────────────────────
|
|
21
22
|
|
|
22
23
|
@dataclass
|
|
23
24
|
class SolFaceTraits:
|
|
24
|
-
face_shape: int # 0-3
|
|
25
|
-
skin_color: int # 0-
|
|
25
|
+
face_shape: int # 0-3 (consumed for PRNG ordering, all render as squircle)
|
|
26
|
+
skin_color: int # 0-9
|
|
26
27
|
eye_style: int # 0-7
|
|
27
28
|
eye_color: int # 0-4
|
|
28
29
|
eyebrows: int # 0-4
|
|
29
30
|
nose: int # 0-3
|
|
30
|
-
mouth: int # 0-
|
|
31
|
-
hair_style: int # 0-
|
|
32
|
-
hair_color: int # 0-
|
|
33
|
-
accessory: int # 0-
|
|
34
|
-
bg_color: int # 0-
|
|
31
|
+
mouth: int # 0-7
|
|
32
|
+
hair_style: int # 0-9
|
|
33
|
+
hair_color: int # 0-9
|
|
34
|
+
accessory: int # 0-9
|
|
35
|
+
bg_color: int # 0-9
|
|
35
36
|
|
|
36
37
|
def to_dict(self) -> Dict[str, int]:
|
|
37
38
|
return {
|
|
@@ -51,46 +52,142 @@ class SolFaceTraits:
|
|
|
51
52
|
|
|
52
53
|
# ─── Color Palettes ──────────────────────────────────────────
|
|
53
54
|
|
|
54
|
-
SKIN_COLORS = [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
SKIN_COLORS = [
|
|
56
|
+
"#faeae5", "#efd6c8", "#e4c5aa", "#d5b590", "#c59e77",
|
|
57
|
+
"#b4875f", "#9d6d4d", "#805742", "#654134", "#4b2d25",
|
|
58
|
+
]
|
|
59
|
+
EYE_COLORS = ["#382414", "#3868A8", "#38784C", "#808838", "#586878"]
|
|
60
|
+
HAIR_COLORS = [
|
|
61
|
+
"#1A1A24", "#4C3428", "#887058", "#D4B868", "#A84830",
|
|
62
|
+
"#C0C0CC", "#484858", "#783850", "#D8B0A0", "#C08048",
|
|
63
|
+
]
|
|
64
|
+
BG_COLORS = [
|
|
65
|
+
"#b98387", "#a9a360", "#9eb785", "#69ab79", "#81bbb0",
|
|
66
|
+
"#6499af", "#7f8bbd", "#8869ab", "#b785b3", "#ab6984",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ─── Color Math ──────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
def hex_to_rgb(h: str) -> tuple[int, int, int]:
|
|
73
|
+
h = h.lstrip("#")
|
|
74
|
+
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def rgb_to_hex(r: float, g: float, b: float) -> str:
|
|
78
|
+
return "#{:02x}{:02x}{:02x}".format(
|
|
79
|
+
max(0, min(255, round(r))),
|
|
80
|
+
max(0, min(255, round(g))),
|
|
81
|
+
max(0, min(255, round(b))),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def darken(h: str, pct: float = 0.12) -> str:
|
|
86
|
+
r, g, b = hex_to_rgb(h)
|
|
87
|
+
return rgb_to_hex(r * (1 - pct), g * (1 - pct), b * (1 - pct))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def lighten(h: str, pct: float = 0.15) -> str:
|
|
91
|
+
r, g, b = hex_to_rgb(h)
|
|
92
|
+
return rgb_to_hex(r + (255 - r) * pct, g + (255 - g) * pct, b + (255 - b) * pct)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def blend(a: str, b: str, t: float = 0.5) -> str:
|
|
96
|
+
r1, g1, b1 = hex_to_rgb(a)
|
|
97
|
+
r2, g2, b2 = hex_to_rgb(b)
|
|
98
|
+
return rgb_to_hex(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def luminance(h: str) -> float:
|
|
102
|
+
r, g, b = hex_to_rgb(h)
|
|
103
|
+
return (r + g + b) / 3
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def derive_skin_colors(skin: str) -> dict:
|
|
107
|
+
sL = luminance(skin)
|
|
108
|
+
is_dark = sL < 100
|
|
109
|
+
|
|
110
|
+
skin_hi = lighten(skin, 0.10)
|
|
111
|
+
skin_lo = darken(skin, 0.22)
|
|
112
|
+
skin_mid = darken(skin, 0.05)
|
|
113
|
+
|
|
114
|
+
sr, sg, sb = hex_to_rgb(skin)
|
|
115
|
+
if sL > 120:
|
|
116
|
+
r_b = 0.03 if sL > 180 else 0.06
|
|
117
|
+
g_d = 0.30 if sL > 180 else 0.28
|
|
118
|
+
b_d = 0.25 if sL > 180 else 0.22
|
|
119
|
+
cheek_color = rgb_to_hex(
|
|
120
|
+
min(255, sr + sr * r_b),
|
|
121
|
+
max(0, sg - sg * g_d),
|
|
122
|
+
max(0, sb - sb * b_d),
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
cheek_color = rgb_to_hex(min(255, sr + 50), max(0, sg - 10), max(0, sb - 5))
|
|
126
|
+
|
|
127
|
+
cheek_opacity = 0.15 + 0.18 * (1 - min(1, sL / 240))
|
|
128
|
+
|
|
129
|
+
lip_t = max(0, min(1, (sL - 60) / 180))
|
|
130
|
+
lip_base = blend("#D89090", "#A83848", lip_t)
|
|
131
|
+
mid_boost = 1 - abs(sL - 140) / 80
|
|
132
|
+
lip_blend = (0.70 if is_dark else 0.62) + max(0, mid_boost) * 0.12
|
|
133
|
+
lip_raw = blend(skin, lip_base, min(0.82, lip_blend))
|
|
134
|
+
lr, lg, lb = hex_to_rgb(lip_raw)
|
|
135
|
+
lip_d = abs(sr - lr) + abs(sg - lg) + abs(sb - lb)
|
|
136
|
+
lip_color = blend(skin, lip_base, 0.78) if lip_d < 60 else lip_raw
|
|
137
|
+
|
|
138
|
+
brow_color = lighten(skin, 0.35 if sL < 80 else 0.25) if is_dark else darken(skin, 0.55)
|
|
139
|
+
nose_fill = lighten(skin, 0.20) if is_dark else darken(skin, 0.20)
|
|
140
|
+
|
|
141
|
+
ear_t = max(0, min(1, (sL - 100) / 60))
|
|
142
|
+
ear_fill = blend(lighten(skin, 0.08), skin_mid, ear_t)
|
|
143
|
+
ear_shadow = darken(skin, 0.10 + 0.06 * (1 - min(1, sL / 160)))
|
|
144
|
+
lid_color = lighten(skin, 0.18) if is_dark else darken(skin, 0.15)
|
|
145
|
+
|
|
146
|
+
ew_t = max(0, min(1, (sL - 60) / 180))
|
|
147
|
+
eye_white = blend("#EDE8E0", "#FBF8F2", ew_t)
|
|
148
|
+
|
|
149
|
+
warmth = 0.3 if sL > 140 else (0.5 if sL > 100 else 0.7)
|
|
150
|
+
acc_color = blend("#808890", blend(skin, "#B0A898", 0.3), warmth)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"skin_hi": skin_hi, "skin_lo": skin_lo, "skin_mid": skin_mid,
|
|
154
|
+
"is_dark": is_dark, "cheek_color": cheek_color, "cheek_opacity": cheek_opacity,
|
|
155
|
+
"lip_color": lip_color, "nose_fill": nose_fill, "brow_color": brow_color,
|
|
156
|
+
"ear_fill": ear_fill, "ear_shadow": ear_shadow,
|
|
157
|
+
"eye_white": eye_white, "lid_color": lid_color, "acc_color": acc_color,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def buzz_opacity(hair_col: str, skin: str) -> float:
|
|
162
|
+
hr, hg, hb = hex_to_rgb(hair_col)
|
|
163
|
+
sr, sg, sb = hex_to_rgb(skin)
|
|
164
|
+
return 0.70 if abs(hr - sr) + abs(hg - sg) + abs(hb - sb) < 80 else 0.50
|
|
58
165
|
|
|
59
166
|
|
|
60
167
|
# ─── Hashing (djb2) — exact JS parity ────────────────────────
|
|
61
168
|
|
|
62
169
|
def _djb2(s: str) -> int:
|
|
63
|
-
"""DJB2 hash — matches JavaScript implementation exactly."""
|
|
64
170
|
hash_val = 5381
|
|
65
171
|
for ch in s:
|
|
66
|
-
# Replicate JS: ((hash << 5) + hash + charCode) | 0
|
|
67
172
|
hash_val = ctypes.c_int32((hash_val << 5) + hash_val + ord(ch)).value
|
|
68
|
-
# Return unsigned 32-bit (>>> 0 in JS)
|
|
69
173
|
return hash_val & 0xFFFFFFFF
|
|
70
174
|
|
|
71
175
|
|
|
72
176
|
# ─── PRNG (mulberry32) — exact JS parity ─────────────────────
|
|
73
177
|
|
|
74
178
|
def _mulberry32(seed: int):
|
|
75
|
-
"""Mulberry32 PRNG — matches JavaScript implementation exactly."""
|
|
76
179
|
s = ctypes.c_int32(seed).value
|
|
77
180
|
|
|
78
181
|
def next_val() -> float:
|
|
79
182
|
nonlocal s
|
|
80
183
|
s = ctypes.c_int32(s + 0x6D2B79F5).value
|
|
81
|
-
|
|
82
|
-
# Math.imul(s ^ (s >>> 15), 1 | s)
|
|
83
184
|
a = (s ^ ((s & 0xFFFFFFFF) >> 15)) & 0xFFFFFFFF
|
|
84
185
|
b = (1 | s) & 0xFFFFFFFF
|
|
85
186
|
t = ctypes.c_int32(_imul(a, b)).value
|
|
86
|
-
|
|
87
|
-
# (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
88
187
|
c = (t ^ ((t & 0xFFFFFFFF) >> 7)) & 0xFFFFFFFF
|
|
89
188
|
d = (61 | t) & 0xFFFFFFFF
|
|
90
189
|
old_t = t
|
|
91
190
|
t = (ctypes.c_int32(old_t + _imul(c, d)).value) ^ old_t
|
|
92
|
-
|
|
93
|
-
# ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
94
191
|
result = ((t ^ ((t & 0xFFFFFFFF) >> 14)) & 0xFFFFFFFF)
|
|
95
192
|
return result / 4294967296
|
|
96
193
|
|
|
@@ -98,7 +195,6 @@ def _mulberry32(seed: int):
|
|
|
98
195
|
|
|
99
196
|
|
|
100
197
|
def _imul(a: int, b: int) -> int:
|
|
101
|
-
"""Emulate Math.imul — 32-bit integer multiply."""
|
|
102
198
|
a = a & 0xFFFFFFFF
|
|
103
199
|
b = b & 0xFFFFFFFF
|
|
104
200
|
result = (a * b) & 0xFFFFFFFF
|
|
@@ -110,197 +206,383 @@ def _imul(a: int, b: int) -> int:
|
|
|
110
206
|
# ─── Trait Generation ─────────────────────────────────────────
|
|
111
207
|
|
|
112
208
|
def generate_traits(wallet_address: str) -> SolFaceTraits:
|
|
113
|
-
"""
|
|
114
|
-
Generate deterministic avatar traits from a Solana wallet address.
|
|
115
|
-
Produces identical output to the JavaScript version.
|
|
116
|
-
"""
|
|
117
209
|
seed = _djb2(wallet_address)
|
|
118
210
|
rand = _mulberry32(seed)
|
|
119
211
|
|
|
212
|
+
# IMPORTANT: Order must NEVER change — shifts all downstream values.
|
|
120
213
|
return SolFaceTraits(
|
|
121
214
|
face_shape=int(rand() * 4),
|
|
122
|
-
skin_color=int(rand() *
|
|
215
|
+
skin_color=int(rand() * 10),
|
|
123
216
|
eye_style=int(rand() * 8),
|
|
124
217
|
eye_color=int(rand() * 5),
|
|
125
218
|
eyebrows=int(rand() * 5),
|
|
126
219
|
nose=int(rand() * 4),
|
|
127
|
-
mouth=int(rand() *
|
|
128
|
-
hair_style=int(rand() *
|
|
129
|
-
hair_color=int(rand() *
|
|
130
|
-
accessory=int(rand() *
|
|
131
|
-
bg_color=int(rand() *
|
|
220
|
+
mouth=int(rand() * 8),
|
|
221
|
+
hair_style=int(rand() * 10),
|
|
222
|
+
hair_color=int(rand() * 10),
|
|
223
|
+
accessory=int(rand() * 10),
|
|
224
|
+
bg_color=int(rand() * 10),
|
|
132
225
|
)
|
|
133
226
|
|
|
134
227
|
|
|
228
|
+
def effective_accessory(t: SolFaceTraits) -> int:
|
|
229
|
+
ai = t.accessory % 10
|
|
230
|
+
hi = t.hair_style % 10
|
|
231
|
+
if (ai == 4 or ai == 7) and (hi == 5 or hi == 6):
|
|
232
|
+
return 0
|
|
233
|
+
return ai
|
|
234
|
+
|
|
235
|
+
|
|
135
236
|
def trait_hash(wallet_address: str) -> str:
|
|
136
|
-
"""Return 8-char hex hash for cache keys."""
|
|
137
237
|
return f"{_djb2(wallet_address):08x}"
|
|
138
238
|
|
|
139
239
|
|
|
140
240
|
# ─── Trait Labels ─────────────────────────────────────────────
|
|
141
241
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
242
|
+
SKIN_LABELS = ["Porcelain", "Ivory", "Fair", "Light", "Sand",
|
|
243
|
+
"Golden", "Warm", "Caramel", "Brown", "Deep"]
|
|
244
|
+
EYE_STYLE_LABELS = ["Round", "Minimal", "Almond", "Wide", "Relaxed", "Joyful", "Bright", "Gentle"]
|
|
245
|
+
EYE_COLOR_LABELS = ["Chocolate", "Sky", "Emerald", "Hazel", "Storm"]
|
|
246
|
+
BROW_LABELS = ["Wispy", "Straight", "Natural", "Arched", "Angled"]
|
|
247
|
+
NOSE_LABELS = ["Shadow", "Button", "Soft", "Nostrils"]
|
|
248
|
+
MOUTH_LABELS = ["Smile", "Calm", "Happy", "Oh", "Smirk", "Grin", "Flat", "Pout"]
|
|
249
|
+
HAIR_STYLE_LABELS = ["Bald", "Short", "Curly", "Side Sweep", "Puff",
|
|
250
|
+
"Long", "Bob", "Buzz", "Wavy", "Topknot"]
|
|
251
|
+
HAIR_COLOR_LABELS = ["Black", "Espresso", "Walnut", "Honey", "Copper",
|
|
252
|
+
"Silver", "Charcoal", "Burgundy", "Strawberry", "Ginger"]
|
|
253
|
+
ACC_LABELS = ["None", "Beauty Mark", "Round Glasses", "Rect Glasses", "Earring",
|
|
254
|
+
"Headband", "Freckles", "Stud Earrings", "Aviators", "Band-Aid"]
|
|
255
|
+
BG_COLOR_LABELS = ["Rose", "Olive", "Sage", "Fern", "Mint",
|
|
256
|
+
"Ocean", "Sky", "Lavender", "Orchid", "Blush"]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_trait_labels(t: SolFaceTraits) -> Dict[str, str]:
|
|
260
|
+
ai = effective_accessory(t)
|
|
156
261
|
return {
|
|
157
|
-
"faceShape":
|
|
158
|
-
"skinColor": SKIN_LABELS[
|
|
159
|
-
"eyeStyle":
|
|
160
|
-
"eyeColor": EYE_COLOR_LABELS[
|
|
161
|
-
"eyebrows": BROW_LABELS[
|
|
162
|
-
"nose": NOSE_LABELS[
|
|
163
|
-
"mouth": MOUTH_LABELS[
|
|
164
|
-
"hairStyle":
|
|
165
|
-
"hairColor": HAIR_COLOR_LABELS[
|
|
166
|
-
"accessory": ACC_LABELS[
|
|
167
|
-
"bgColor": BG_COLOR_LABELS[
|
|
262
|
+
"faceShape": "Squircle",
|
|
263
|
+
"skinColor": SKIN_LABELS[t.skin_color] if t.skin_color < len(SKIN_LABELS) else "Fair",
|
|
264
|
+
"eyeStyle": EYE_STYLE_LABELS[t.eye_style] if t.eye_style < len(EYE_STYLE_LABELS) else "Round",
|
|
265
|
+
"eyeColor": EYE_COLOR_LABELS[t.eye_color] if t.eye_color < len(EYE_COLOR_LABELS) else "Chocolate",
|
|
266
|
+
"eyebrows": BROW_LABELS[t.eyebrows] if t.eyebrows < len(BROW_LABELS) else "Wispy",
|
|
267
|
+
"nose": NOSE_LABELS[t.nose] if t.nose < len(NOSE_LABELS) else "Shadow",
|
|
268
|
+
"mouth": MOUTH_LABELS[t.mouth] if t.mouth < len(MOUTH_LABELS) else "Smile",
|
|
269
|
+
"hairStyle": HAIR_STYLE_LABELS[t.hair_style] if t.hair_style < len(HAIR_STYLE_LABELS) else "Bald",
|
|
270
|
+
"hairColor": HAIR_COLOR_LABELS[t.hair_color] if t.hair_color < len(HAIR_COLOR_LABELS) else "Black",
|
|
271
|
+
"accessory": ACC_LABELS[ai] if ai < len(ACC_LABELS) else "None",
|
|
272
|
+
"bgColor": BG_COLOR_LABELS[t.bg_color] if t.bg_color < len(BG_COLOR_LABELS) else "Rose",
|
|
168
273
|
}
|
|
169
274
|
|
|
170
275
|
|
|
171
276
|
# ─── SVG Rendering ────────────────────────────────────────────
|
|
172
277
|
|
|
173
|
-
def
|
|
174
|
-
if
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
278
|
+
def _to_base36(n: int) -> str:
|
|
279
|
+
if n == 0:
|
|
280
|
+
return "0"
|
|
281
|
+
chars = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
282
|
+
result = ""
|
|
283
|
+
while n > 0:
|
|
284
|
+
result = chars[n % 36] + result
|
|
285
|
+
n //= 36
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _build_defs(gid: str, skin: str, skin_hi: str, skin_lo: str,
|
|
290
|
+
hair_col: str, bg_col: str, cheek_color: str,
|
|
291
|
+
cheek_opacity: float, flat: bool, full: bool) -> str:
|
|
292
|
+
if flat:
|
|
293
|
+
return ""
|
|
294
|
+
d = "<defs>"
|
|
295
|
+
d += f'<linearGradient id="{gid}sg" x1="0" y1="0" x2="0" y2="1">'
|
|
296
|
+
d += f'<stop offset="0%" stop-color="{skin_hi}"/>'
|
|
297
|
+
d += f'<stop offset="100%" stop-color="{skin_lo}"/>'
|
|
298
|
+
d += "</linearGradient>"
|
|
299
|
+
d += f'<linearGradient id="{gid}hg" x1="0" y1="0" x2="0" y2="1">'
|
|
300
|
+
d += f'<stop offset="0%" stop-color="{lighten(hair_col, 0.15)}"/>'
|
|
301
|
+
d += f'<stop offset="100%" stop-color="{darken(hair_col, 0.15)}"/>'
|
|
302
|
+
d += "</linearGradient>"
|
|
303
|
+
d += f'<linearGradient id="{gid}bg" x1="0" y1="0" x2="1" y2="1">'
|
|
304
|
+
d += f'<stop offset="0%" stop-color="{lighten(bg_col, 0.12)}"/>'
|
|
305
|
+
d += f'<stop offset="100%" stop-color="{darken(bg_col, 0.12)}"/>'
|
|
306
|
+
d += "</linearGradient>"
|
|
307
|
+
if full:
|
|
308
|
+
d += f'<radialGradient id="{gid}glow" cx="0.5" cy="0.28" r="0.45">'
|
|
309
|
+
d += '<stop offset="0%" stop-color="#ffffff" stop-opacity="0.10"/>'
|
|
310
|
+
d += '<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>'
|
|
311
|
+
d += "</radialGradient>"
|
|
312
|
+
d += f'<radialGradient id="{gid}chin" cx="0.5" cy="0.85" r="0.35">'
|
|
313
|
+
d += f'<stop offset="0%" stop-color="{skin_lo}" stop-opacity="0.30"/>'
|
|
314
|
+
d += f'<stop offset="100%" stop-color="{skin_lo}" stop-opacity="0"/>'
|
|
315
|
+
d += "</radialGradient>"
|
|
316
|
+
d += f'<radialGradient id="{gid}cL" cx="0.5" cy="0.5" r="0.5">'
|
|
317
|
+
d += f'<stop offset="0%" stop-color="{cheek_color}" stop-opacity="{cheek_opacity:.2f}"/>'
|
|
318
|
+
d += f'<stop offset="100%" stop-color="{cheek_color}" stop-opacity="0"/>'
|
|
319
|
+
d += "</radialGradient>"
|
|
320
|
+
d += f'<radialGradient id="{gid}cR" cx="0.5" cy="0.5" r="0.5">'
|
|
321
|
+
d += f'<stop offset="0%" stop-color="{cheek_color}" stop-opacity="{cheek_opacity:.2f}"/>'
|
|
322
|
+
d += f'<stop offset="100%" stop-color="{cheek_color}" stop-opacity="0"/>'
|
|
323
|
+
d += "</radialGradient>"
|
|
324
|
+
d += "</defs>"
|
|
325
|
+
return d
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _render_hair_back(hi: int, gid: str, flat: bool) -> str:
|
|
329
|
+
fill = "currentColor" if flat else f"url(#{gid}hg)"
|
|
330
|
+
if hi == 5: return f'<rect x="10" y="14" width="44" height="42" rx="6" fill="{fill}"/>'
|
|
331
|
+
if hi == 6: return f'<rect x="12" y="14" width="40" height="32" rx="8" fill="{fill}"/>'
|
|
332
|
+
if hi == 8: return f'<rect x="11" y="14" width="42" height="38" rx="8" fill="{fill}"/>'
|
|
209
333
|
return ""
|
|
210
334
|
|
|
211
335
|
|
|
212
|
-
def
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
336
|
+
def _render_ears(ear_fill: str, ear_shadow: str) -> str:
|
|
337
|
+
return (
|
|
338
|
+
f'<ellipse cx="11" cy="34" rx="4" ry="5" fill="{ear_fill}"/>'
|
|
339
|
+
f'<ellipse cx="11" cy="34" rx="2.5" ry="3.5" fill="{ear_shadow}" opacity="0.3"/>'
|
|
340
|
+
f'<ellipse cx="53" cy="34" rx="4" ry="5" fill="{ear_fill}"/>'
|
|
341
|
+
f'<ellipse cx="53" cy="34" rx="2.5" ry="3.5" fill="{ear_shadow}" opacity="0.3"/>'
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _render_face(gid: str, skin: str, flat: bool) -> str:
|
|
346
|
+
fill = skin if flat else f"url(#{gid}sg)"
|
|
347
|
+
return f'<rect x="14" y="16" width="36" height="38" rx="12" ry="12" fill="{fill}"/>'
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _render_face_overlays(gid: str) -> str:
|
|
351
|
+
return (
|
|
352
|
+
f'<rect x="14" y="16" width="36" height="38" rx="12" ry="12" fill="url(#{gid}glow)"/>'
|
|
353
|
+
f'<rect x="14" y="16" width="36" height="38" rx="12" ry="12" fill="url(#{gid}chin)"/>'
|
|
354
|
+
f'<ellipse cx="22" cy="42" rx="5" ry="3.5" fill="url(#{gid}cL)"/>'
|
|
355
|
+
f'<ellipse cx="42" cy="42" rx="5" ry="3.5" fill="url(#{gid}cR)"/>'
|
|
356
|
+
'<line x1="20" y1="50" x2="44" y2="50" stroke="currentColor" stroke-width="0.3" opacity="0.08" stroke-linecap="round"/>'
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _render_hair_front(hi: int, gid: str, hair_col: str, skin: str, flat: bool) -> str:
|
|
361
|
+
fill = hair_col if flat else f"url(#{gid}hg)"
|
|
362
|
+
if hi == 0: return ""
|
|
363
|
+
if hi == 1: return f'<path d="M14 28 Q14 14 32 12 Q50 14 50 28 L50 22 Q50 12 32 10 Q14 12 14 22 Z" fill="{fill}"/>'
|
|
364
|
+
if hi == 2:
|
|
365
|
+
return (f'<g fill="{fill}">'
|
|
366
|
+
'<circle cx="20" cy="14" r="5"/><circle cx="28" cy="11" r="5.5"/>'
|
|
367
|
+
'<circle cx="36" cy="11" r="5.5"/><circle cx="44" cy="14" r="5"/>'
|
|
368
|
+
'<circle cx="16" cy="20" r="4"/><circle cx="48" cy="20" r="4"/></g>')
|
|
369
|
+
if hi == 3:
|
|
370
|
+
return (f'<path d="M14 26 Q14 12 32 10 Q50 12 50 26 L50 20 Q50 10 32 8 Q14 10 14 20 Z" fill="{fill}"/>'
|
|
371
|
+
f'<path d="M14 20 Q8 16 10 8 Q14 10 20 16 Z" fill="{fill}"/>')
|
|
372
|
+
if hi == 4: return f'<ellipse cx="32" cy="10" rx="14" ry="8" fill="{fill}"/>'
|
|
373
|
+
if hi == 5: return f'<path d="M14 28 Q14 12 32 10 Q50 12 50 28 L50 20 Q50 10 32 8 Q14 10 14 20 Z" fill="{fill}"/>'
|
|
374
|
+
if hi == 6:
|
|
375
|
+
return (f'<path d="M14 28 Q14 12 32 10 Q50 12 50 28 L50 20 Q50 10 32 8 Q14 10 14 20 Z" fill="{fill}"/>'
|
|
376
|
+
f'<rect x="10" y="28" width="8" height="14" rx="4" fill="{fill}"/>'
|
|
377
|
+
f'<rect x="46" y="28" width="8" height="14" rx="4" fill="{fill}"/>')
|
|
378
|
+
if hi == 7:
|
|
379
|
+
bop = buzz_opacity(hair_col, skin)
|
|
380
|
+
return f'<rect x="15" y="13" width="34" height="16" rx="10" ry="8" fill="{hair_col}" opacity="{bop:.2f}"/>'
|
|
381
|
+
if hi == 8:
|
|
382
|
+
return (f'<path d="M14 28 Q14 12 32 10 Q50 12 50 28 L50 20 Q50 10 32 8 Q14 10 14 20 Z" fill="{fill}"/>'
|
|
383
|
+
f'<path d="M12 30 Q10 20 14 16" fill="none" stroke="{fill}" stroke-width="4" stroke-linecap="round"/>'
|
|
384
|
+
f'<path d="M52 30 Q54 20 50 16" fill="none" stroke="{fill}" stroke-width="4" stroke-linecap="round"/>')
|
|
385
|
+
if hi == 9:
|
|
386
|
+
return (f'<path d="M14 28 Q14 14 32 12 Q50 14 50 28 L50 22 Q50 12 32 10 Q14 12 14 22 Z" fill="{fill}"/>'
|
|
387
|
+
f'<ellipse cx="32" cy="6" rx="6" ry="5" fill="{fill}"/>')
|
|
219
388
|
return ""
|
|
220
389
|
|
|
221
390
|
|
|
222
|
-
def
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
391
|
+
def _render_eyes(ei: int, eye_col: str, eye_white: str, lid_color: str, full: bool) -> str:
|
|
392
|
+
lx, rx, y = 25, 39, 33
|
|
393
|
+
s = ""
|
|
394
|
+
if ei == 0:
|
|
395
|
+
s += f'<circle cx="{lx}" cy="{y}" r="4" fill="{eye_white}"/><circle cx="{lx+0.8}" cy="{y}" r="2.2" fill="{eye_col}"/>'
|
|
396
|
+
if full: s += f'<circle cx="{lx+1.5}" cy="{y-1}" r="0.7" fill="white" opacity="0.8"/>'
|
|
397
|
+
s += f'<circle cx="{rx}" cy="{y}" r="4" fill="{eye_white}"/><circle cx="{rx+0.8}" cy="{y}" r="2.2" fill="{eye_col}"/>'
|
|
398
|
+
if full: s += f'<circle cx="{rx+1.5}" cy="{y-1}" r="0.7" fill="white" opacity="0.8"/>'
|
|
399
|
+
elif ei == 1:
|
|
400
|
+
s += f'<circle cx="{lx}" cy="{y}" r="2" fill="{eye_col}"/><circle cx="{rx}" cy="{y}" r="2" fill="{eye_col}"/>'
|
|
401
|
+
elif ei == 2:
|
|
402
|
+
s += f'<ellipse cx="{lx}" cy="{y}" rx="4.5" ry="2.8" fill="{eye_white}"/><circle cx="{lx+0.5}" cy="{y}" r="1.8" fill="{eye_col}"/>'
|
|
403
|
+
if full: s += f'<circle cx="{lx+1.2}" cy="{y-0.8}" r="0.6" fill="white" opacity="0.7"/>'
|
|
404
|
+
s += f'<ellipse cx="{rx}" cy="{y}" rx="4.5" ry="2.8" fill="{eye_white}"/><circle cx="{rx+0.5}" cy="{y}" r="1.8" fill="{eye_col}"/>'
|
|
405
|
+
if full: s += f'<circle cx="{rx+1.2}" cy="{y-0.8}" r="0.6" fill="white" opacity="0.7"/>'
|
|
406
|
+
elif ei == 3:
|
|
407
|
+
s += f'<circle cx="{lx}" cy="{y}" r="5" fill="{eye_white}"/><circle cx="{lx}" cy="{y+0.5}" r="2.8" fill="{eye_col}"/>'
|
|
408
|
+
if full: s += f'<circle cx="{lx+1.5}" cy="{y-1}" r="0.8" fill="white" opacity="0.8"/>'
|
|
409
|
+
s += f'<circle cx="{rx}" cy="{y}" r="5" fill="{eye_white}"/><circle cx="{rx}" cy="{y+0.5}" r="2.8" fill="{eye_col}"/>'
|
|
410
|
+
if full: s += f'<circle cx="{rx+1.5}" cy="{y-1}" r="0.8" fill="white" opacity="0.8"/>'
|
|
411
|
+
elif ei == 4:
|
|
412
|
+
s += f'<ellipse cx="{lx}" cy="{y+1}" rx="4" ry="2.2" fill="{eye_white}"/><circle cx="{lx}" cy="{y+1}" r="1.5" fill="{eye_col}"/>'
|
|
413
|
+
if full: s += f'<line x1="{lx-4.5}" y1="{y-0.5}" x2="{lx+4.5}" y2="{y-0.5}" stroke="{lid_color}" stroke-width="0.8" stroke-linecap="round"/>'
|
|
414
|
+
s += f'<ellipse cx="{rx}" cy="{y+1}" rx="4" ry="2.2" fill="{eye_white}"/><circle cx="{rx}" cy="{y+1}" r="1.5" fill="{eye_col}"/>'
|
|
415
|
+
if full: s += f'<line x1="{rx-4.5}" y1="{y-0.5}" x2="{rx+4.5}" y2="{y-0.5}" stroke="{lid_color}" stroke-width="0.8" stroke-linecap="round"/>'
|
|
416
|
+
elif ei == 5:
|
|
417
|
+
s += f'<path d="M{lx-4} {y} Q{lx} {y+4} {lx+4} {y}" fill="none" stroke="{eye_col}" stroke-width="1.8" stroke-linecap="round"/>'
|
|
418
|
+
s += f'<path d="M{rx-4} {y} Q{rx} {y+4} {rx+4} {y}" fill="none" stroke="{eye_col}" stroke-width="1.8" stroke-linecap="round"/>'
|
|
419
|
+
elif ei == 6:
|
|
420
|
+
s += f'<circle cx="{lx}" cy="{y}" r="3.5" fill="{eye_white}"/><circle cx="{lx+0.5}" cy="{y}" r="2" fill="{eye_col}"/>'
|
|
421
|
+
s += f'<circle cx="{lx+1.5}" cy="{y-1}" r="1" fill="white" opacity="0.9"/>'
|
|
422
|
+
if full:
|
|
423
|
+
s += f'<line x1="{lx+2.5}" y1="{y-3.5}" x2="{lx+4}" y2="{y-5}" stroke="{eye_col}" stroke-width="0.8" stroke-linecap="round"/>'
|
|
424
|
+
s += f'<line x1="{lx+3.5}" y1="{y-2.5}" x2="{lx+5}" y2="{y-3.5}" stroke="{eye_col}" stroke-width="0.8" stroke-linecap="round"/>'
|
|
425
|
+
s += f'<circle cx="{rx}" cy="{y}" r="3.5" fill="{eye_white}"/><circle cx="{rx+0.5}" cy="{y}" r="2" fill="{eye_col}"/>'
|
|
426
|
+
s += f'<circle cx="{rx+1.5}" cy="{y-1}" r="1" fill="white" opacity="0.9"/>'
|
|
427
|
+
if full:
|
|
428
|
+
s += f'<line x1="{rx+2.5}" y1="{y-3.5}" x2="{rx+4}" y2="{y-5}" stroke="{eye_col}" stroke-width="0.8" stroke-linecap="round"/>'
|
|
429
|
+
s += f'<line x1="{rx+3.5}" y1="{y-2.5}" x2="{rx+5}" y2="{y-3.5}" stroke="{eye_col}" stroke-width="0.8" stroke-linecap="round"/>'
|
|
430
|
+
elif ei == 7:
|
|
431
|
+
s += f'<ellipse cx="{lx}" cy="{y}" rx="4.5" ry="1.5" fill="{eye_white}"/><ellipse cx="{lx+0.5}" cy="{y}" rx="2.2" ry="1.2" fill="{eye_col}"/>'
|
|
432
|
+
s += f'<ellipse cx="{rx}" cy="{y}" rx="4.5" ry="1.5" fill="{eye_white}"/><ellipse cx="{rx+0.5}" cy="{y}" rx="2.2" ry="1.2" fill="{eye_col}"/>'
|
|
433
|
+
else:
|
|
434
|
+
s += f'<circle cx="{lx}" cy="{y}" r="3.5" fill="{eye_white}"/><circle cx="{lx+0.8}" cy="{y}" r="2" fill="{eye_col}"/>'
|
|
435
|
+
s += f'<circle cx="{rx}" cy="{y}" r="3.5" fill="{eye_white}"/><circle cx="{rx+0.8}" cy="{y}" r="2" fill="{eye_col}"/>'
|
|
436
|
+
|
|
437
|
+
if full and ei not in (1, 5):
|
|
438
|
+
s += f'<path d="M{lx-4} {y-1.5} Q{lx} {y-4} {lx+4} {y-1.5}" fill="none" stroke="{lid_color}" stroke-width="0.5" opacity="0.4"/>'
|
|
439
|
+
s += f'<path d="M{rx-4} {y-1.5} Q{rx} {y-4} {rx+4} {y-1.5}" fill="none" stroke="{lid_color}" stroke-width="0.5" opacity="0.4"/>'
|
|
440
|
+
return s
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _render_eyebrows(bi: int, brow_color: str) -> str:
|
|
444
|
+
lx, rx, y = 25, 39, 27
|
|
445
|
+
if bi == 0:
|
|
446
|
+
return (f'<line x1="{lx-3}" y1="{y}" x2="{lx+3}" y2="{y-0.5}" stroke="{brow_color}" stroke-width="0.7" stroke-linecap="round"/>'
|
|
447
|
+
f'<line x1="{rx-3}" y1="{y-0.5}" x2="{rx+3}" y2="{y}" stroke="{brow_color}" stroke-width="0.7" stroke-linecap="round"/>')
|
|
448
|
+
if bi == 1:
|
|
449
|
+
return (f'<line x1="{lx-3.5}" y1="{y}" x2="{lx+3.5}" y2="{y}" stroke="{brow_color}" stroke-width="1.2" stroke-linecap="round"/>'
|
|
450
|
+
f'<line x1="{rx-3.5}" y1="{y}" x2="{rx+3.5}" y2="{y}" stroke="{brow_color}" stroke-width="1.2" stroke-linecap="round"/>')
|
|
451
|
+
if bi == 2:
|
|
452
|
+
return (f'<path d="M{lx-3.5} {y+0.5} Q{lx} {y-1.5} {lx+3.5} {y+0.5}" fill="none" stroke="{brow_color}" stroke-width="1.2" stroke-linecap="round"/>'
|
|
453
|
+
f'<path d="M{rx-3.5} {y+0.5} Q{rx} {y-1.5} {rx+3.5} {y+0.5}" fill="none" stroke="{brow_color}" stroke-width="1.2" stroke-linecap="round"/>')
|
|
454
|
+
if bi == 3:
|
|
455
|
+
return (f'<path d="M{lx-4} {y+1} Q{lx} {y-3} {lx+4} {y+1}" fill="none" stroke="{brow_color}" stroke-width="1" stroke-linecap="round"/>'
|
|
456
|
+
f'<path d="M{rx-4} {y+1} Q{rx} {y-3} {rx+4} {y+1}" fill="none" stroke="{brow_color}" stroke-width="1" stroke-linecap="round"/>')
|
|
457
|
+
if bi == 4:
|
|
458
|
+
return (f'<polyline points="{lx-3},{y+1} {lx},{y-2} {lx+3},{y}" fill="none" stroke="{brow_color}" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
459
|
+
f'<polyline points="{rx-3},{y} {rx},{y-2} {rx+3},{y+1}" fill="none" stroke="{brow_color}" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>')
|
|
242
460
|
return ""
|
|
243
461
|
|
|
244
462
|
|
|
245
|
-
def
|
|
246
|
-
|
|
247
|
-
if
|
|
248
|
-
if
|
|
249
|
-
if
|
|
250
|
-
if
|
|
463
|
+
def _render_nose(ni: int, nose_fill: str) -> str:
|
|
464
|
+
cx, y = 32, 39
|
|
465
|
+
if ni == 0: return f'<ellipse cx="{cx}" cy="{y}" rx="2" ry="1.2" fill="{nose_fill}" opacity="0.35"/>'
|
|
466
|
+
if ni == 1: return f'<circle cx="{cx}" cy="{y}" r="1.8" fill="{nose_fill}" opacity="0.5"/>'
|
|
467
|
+
if ni == 2: return f'<path d="M{cx-2} {y+1} Q{cx} {y-2} {cx+2} {y+1}" fill="none" stroke="{nose_fill}" stroke-width="1" stroke-linecap="round" opacity="0.5"/>'
|
|
468
|
+
if ni == 3: return f'<circle cx="{cx-1.8}" cy="{y}" r="1.2" fill="{nose_fill}" opacity="0.4"/><circle cx="{cx+1.8}" cy="{y}" r="1.2" fill="{nose_fill}" opacity="0.4"/>'
|
|
469
|
+
return f'<ellipse cx="{cx}" cy="{y}" rx="2" ry="1.2" fill="{nose_fill}" opacity="0.35"/>'
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _render_mouth(mi: int, lip_color: str, is_dark: bool) -> str:
|
|
473
|
+
cx, y = 32, 45
|
|
474
|
+
tc = "#e8e0d8" if is_dark else "#ffffff"
|
|
475
|
+
if mi == 0: return f'<path d="M{cx-4} {y} Q{cx} {y+4} {cx+4} {y}" fill="none" stroke="{lip_color}" stroke-width="1.4" stroke-linecap="round"/>'
|
|
476
|
+
if mi == 1: return f'<line x1="{cx-3}" y1="{y+1}" x2="{cx+3}" y2="{y+1}" stroke="{lip_color}" stroke-width="1.2" stroke-linecap="round"/>'
|
|
477
|
+
if mi == 2: return f'<path d="M{cx-5} {y} Q{cx} {y+5} {cx+5} {y}" fill="none" stroke="{lip_color}" stroke-width="1.5" stroke-linecap="round"/>'
|
|
478
|
+
if mi == 3: return f'<ellipse cx="{cx}" cy="{y+1}" rx="2.5" ry="3" fill="{lip_color}" opacity="0.7"/>'
|
|
479
|
+
if mi == 4: return f'<path d="M{cx-4} {y+1} Q{cx+1} {y+1} {cx+4} {y-1.5}" fill="none" stroke="{lip_color}" stroke-width="1.3" stroke-linecap="round"/>'
|
|
480
|
+
if mi == 5:
|
|
481
|
+
return (f'<path d="M{cx-5} {y} Q{cx} {y+6} {cx+5} {y}" fill="{tc}" stroke="{lip_color}" stroke-width="1"/>'
|
|
482
|
+
f'<line x1="{cx-4}" y1="{y+1.5}" x2="{cx+4}" y2="{y+1.5}" stroke="{lip_color}" stroke-width="0.3" opacity="0.3"/>')
|
|
483
|
+
if mi == 6: return f'<line x1="{cx-4}" y1="{y+1}" x2="{cx+4}" y2="{y+1}" stroke="{lip_color}" stroke-width="1.5" stroke-linecap="round"/>'
|
|
484
|
+
if mi == 7:
|
|
485
|
+
return (f'<ellipse cx="{cx}" cy="{y+1}" rx="3.5" ry="2" fill="{lip_color}" opacity="0.25"/>'
|
|
486
|
+
f'<path d="M{cx-3} {y} Q{cx} {y+2.5} {cx+3} {y}" fill="none" stroke="{lip_color}" stroke-width="1.2" stroke-linecap="round"/>')
|
|
487
|
+
return f'<path d="M{cx-4} {y} Q{cx} {y+4} {cx+4} {y}" fill="none" stroke="{lip_color}" stroke-width="1.4" stroke-linecap="round"/>'
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _render_accessory(ai: int, acc_color: str, glasses_color: str, earring_color: str, headband_color: str) -> str:
|
|
491
|
+
if ai == 0: return ""
|
|
492
|
+
if ai == 1: return '<circle cx="40" cy="44" r="0.8" fill="#3a2a2a"/>'
|
|
493
|
+
if ai == 2:
|
|
494
|
+
return (f'<g fill="none" stroke="{glasses_color}" stroke-width="1">'
|
|
495
|
+
'<circle cx="25" cy="33" r="5.5"/><circle cx="39" cy="33" r="5.5"/>'
|
|
496
|
+
'<line x1="30.5" y1="33" x2="33.5" y2="33"/>'
|
|
497
|
+
'<line x1="19.5" y1="33" x2="14" y2="31"/>'
|
|
498
|
+
'<line x1="44.5" y1="33" x2="50" y2="31"/></g>')
|
|
499
|
+
if ai == 3:
|
|
500
|
+
return (f'<g fill="none" stroke="{glasses_color}" stroke-width="1">'
|
|
501
|
+
'<rect x="19" y="29" width="12" height="8" rx="1.5"/>'
|
|
502
|
+
'<rect x="33" y="29" width="12" height="8" rx="1.5"/>'
|
|
503
|
+
'<line x1="31" y1="33" x2="33" y2="33"/>'
|
|
504
|
+
'<line x1="19" y1="33" x2="14" y2="31"/>'
|
|
505
|
+
'<line x1="45" y1="33" x2="50" y2="31"/></g>')
|
|
506
|
+
if ai == 4:
|
|
507
|
+
return (f'<circle cx="10" cy="38" r="1.5" fill="{earring_color}"/>'
|
|
508
|
+
f'<circle cx="10" cy="41" r="2" fill="{earring_color}" opacity="0.8"/>')
|
|
509
|
+
if ai == 5:
|
|
510
|
+
return f'<rect x="13" y="20" width="38" height="3.5" rx="1.5" fill="{headband_color}" opacity="0.85"/>'
|
|
511
|
+
if ai == 6:
|
|
512
|
+
return ('<g fill="#a0785a" opacity="0.35">'
|
|
513
|
+
'<circle cx="21" cy="40" r="0.6"/><circle cx="23" cy="42" r="0.5"/>'
|
|
514
|
+
'<circle cx="19" cy="41.5" r="0.5"/><circle cx="43" cy="40" r="0.6"/>'
|
|
515
|
+
'<circle cx="41" cy="42" r="0.5"/><circle cx="45" cy="41.5" r="0.5"/></g>')
|
|
516
|
+
if ai == 7:
|
|
517
|
+
return (f'<circle cx="10" cy="37" r="1.2" fill="{earring_color}"/>'
|
|
518
|
+
f'<circle cx="54" cy="37" r="1.2" fill="{earring_color}"/>')
|
|
519
|
+
if ai == 8:
|
|
520
|
+
return (f'<g fill="none" stroke="{glasses_color}" stroke-width="1.2">'
|
|
521
|
+
f'<path d="M19 30 Q19 28 25 28 Q31 28 31 33 Q31 38 25 38 Q19 38 19 33 Z" fill="{glasses_color}" fill-opacity="0.15"/>'
|
|
522
|
+
f'<path d="M33 30 Q33 28 39 28 Q45 28 45 33 Q45 38 39 38 Q33 38 33 33 Z" fill="{glasses_color}" fill-opacity="0.15"/>'
|
|
523
|
+
'<line x1="31" y1="32" x2="33" y2="32"/>'
|
|
524
|
+
'<line x1="19" y1="31" x2="14" y2="29"/>'
|
|
525
|
+
'<line x1="45" y1="31" x2="50" y2="29"/></g>')
|
|
526
|
+
if ai == 9:
|
|
527
|
+
return ('<g>'
|
|
528
|
+
'<rect x="38" y="38" width="8" height="4" rx="1" fill="#f0d0a0" transform="rotate(-15 42 40)"/>'
|
|
529
|
+
'<line x1="40" y1="39" x2="40" y2="41" stroke="#c0a080" stroke-width="0.4" transform="rotate(-15 42 40)"/>'
|
|
530
|
+
'<line x1="42" y1="39" x2="42" y2="41" stroke="#c0a080" stroke-width="0.4" transform="rotate(-15 42 40)"/>'
|
|
531
|
+
'<line x1="44" y1="39" x2="44" y2="41" stroke="#c0a080" stroke-width="0.4" transform="rotate(-15 42 40)"/></g>')
|
|
251
532
|
return ""
|
|
252
533
|
|
|
253
534
|
|
|
254
535
|
def render_svg(
|
|
255
536
|
wallet_address: str,
|
|
256
537
|
size: int = 64,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
mouth_color: str = "#c05050",
|
|
260
|
-
eyebrow_color: str = "#2a2020",
|
|
261
|
-
accessory_color: str = "#444",
|
|
262
|
-
eye_white_color: str = "white",
|
|
263
|
-
nose_color: Optional[str] = None,
|
|
264
|
-
color_overrides: Optional[Dict[str, str]] = None,
|
|
538
|
+
flat: bool = False,
|
|
539
|
+
detail: str = "auto",
|
|
265
540
|
) -> str:
|
|
266
|
-
"""
|
|
267
|
-
Render a SolFace as an SVG string.
|
|
268
|
-
Produces identical output to the JavaScript version.
|
|
269
|
-
|
|
270
|
-
Args:
|
|
271
|
-
color_overrides: Optional dict with keys: skin, eyes, hair, bg,
|
|
272
|
-
mouth, eyebrow, accessory, nose, eye_white
|
|
273
|
-
"""
|
|
274
541
|
t = generate_traits(wallet_address)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
skin =
|
|
278
|
-
eye_col =
|
|
279
|
-
hair_col =
|
|
280
|
-
bg_col =
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
542
|
+
|
|
543
|
+
full = detail == "full" or (detail == "auto" and size >= 48)
|
|
544
|
+
skin = SKIN_COLORS[t.skin_color % len(SKIN_COLORS)]
|
|
545
|
+
eye_col = EYE_COLORS[t.eye_color % len(EYE_COLORS)]
|
|
546
|
+
hair_col = HAIR_COLORS[t.hair_color % len(HAIR_COLORS)]
|
|
547
|
+
bg_col = BG_COLORS[t.bg_color % len(BG_COLORS)]
|
|
548
|
+
|
|
549
|
+
derived = derive_skin_colors(skin)
|
|
550
|
+
gid = "sf" + _to_base36(_djb2(wallet_address))
|
|
551
|
+
|
|
552
|
+
hi = t.hair_style % 10
|
|
553
|
+
ai = effective_accessory(t)
|
|
554
|
+
|
|
555
|
+
glasses_color = "#4a4a5a"
|
|
556
|
+
earring_color = blend(skin, "#d4a840", 0.4)
|
|
557
|
+
headband_color = blend(hair_col, "#c04040", 0.5)
|
|
558
|
+
|
|
559
|
+
bg_fill = bg_col if flat else f"url(#{gid}bg)"
|
|
560
|
+
|
|
561
|
+
parts = []
|
|
562
|
+
parts.append(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="{size}" height="{size}">')
|
|
563
|
+
parts.append(_build_defs(gid, skin, derived["skin_hi"], derived["skin_lo"],
|
|
564
|
+
hair_col, bg_col, derived["cheek_color"],
|
|
565
|
+
derived["cheek_opacity"], flat, full))
|
|
566
|
+
parts.append(f'<rect x="0" y="0" width="64" height="64" fill="{bg_fill}" opacity="1" rx="4"/>')
|
|
567
|
+
parts.append(_render_hair_back(hi, gid, flat))
|
|
568
|
+
parts.append(_render_ears(derived["ear_fill"], derived["ear_shadow"]))
|
|
569
|
+
parts.append(_render_face(gid, skin, flat))
|
|
570
|
+
if full:
|
|
571
|
+
parts.append(_render_face_overlays(gid))
|
|
572
|
+
if ai == 5:
|
|
573
|
+
parts.append(_render_accessory(5, derived["acc_color"], glasses_color, earring_color, headband_color))
|
|
574
|
+
parts.append(_render_hair_front(hi, gid, hair_col, skin, flat))
|
|
575
|
+
parts.append(_render_eyes(t.eye_style % 8, eye_col, derived["eye_white"], derived["lid_color"], full))
|
|
576
|
+
parts.append(_render_eyebrows(t.eyebrows % 5, derived["brow_color"]))
|
|
577
|
+
parts.append(_render_nose(t.nose % 4, derived["nose_fill"]))
|
|
578
|
+
parts.append(_render_mouth(t.mouth % 8, derived["lip_color"], derived["is_dark"]))
|
|
579
|
+
if ai != 0 and ai != 5:
|
|
580
|
+
parts.append(_render_accessory(ai, derived["acc_color"], glasses_color, earring_color, headband_color))
|
|
581
|
+
parts.append("</svg>")
|
|
299
582
|
return "".join(p for p in parts if p)
|
|
300
583
|
|
|
301
584
|
|
|
302
585
|
def render_data_uri(wallet_address: str, **kwargs) -> str:
|
|
303
|
-
"""Render as a data URI for use in <img> tags or HTML emails."""
|
|
304
586
|
from urllib.parse import quote
|
|
305
587
|
svg = render_svg(wallet_address, **kwargs)
|
|
306
588
|
return f"data:image/svg+xml;charset=utf-8,{quote(svg)}"
|
|
@@ -308,40 +590,38 @@ def render_data_uri(wallet_address: str, **kwargs) -> str:
|
|
|
308
590
|
|
|
309
591
|
# ─── Description ──────────────────────────────────────────────
|
|
310
592
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
_EYE_STYLE_DESC = {0: "round, wide-open", 1: "small and
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
593
|
+
_SKIN_DESC = {0: "porcelain", 1: "ivory", 2: "fair", 3: "light", 4: "sand",
|
|
594
|
+
5: "golden", 6: "warm", 7: "caramel", 8: "brown", 9: "deep"}
|
|
595
|
+
_EYE_STYLE_DESC = {0: "round, wide-open", 1: "small and minimal", 2: "almond-shaped",
|
|
596
|
+
3: "wide and expressive", 4: "relaxed, half-lidded", 5: "joyful, crescent-shaped",
|
|
597
|
+
6: "bright and sparkling", 7: "gentle and narrow"}
|
|
598
|
+
_EYE_COLOR_DESC = {0: "dark brown", 1: "blue", 2: "green", 3: "hazel", 4: "gray"}
|
|
599
|
+
_BROW_DESC = {0: "wispy", 1: "straight", 2: "natural", 3: "elegantly arched", 4: "sharply angled"}
|
|
600
|
+
_NOSE_DESC = {0: "a subtle shadow nose", 1: "a small button nose", 2: "a soft curved nose",
|
|
601
|
+
3: "a button nose with visible nostrils"}
|
|
602
|
+
_MOUTH_DESC = {0: "a gentle smile", 1: "a calm, neutral expression", 2: "a happy grin",
|
|
603
|
+
3: "a surprised O-shaped mouth", 4: "a confident smirk", 5: "a wide, toothy grin",
|
|
604
|
+
6: "a flat, straight expression", 7: "a soft pout"}
|
|
605
|
+
_HAIR_STYLE_DESC = {0: "bald, with no hair", 1: "short, neatly cropped hair", 2: "bouncy, curly hair",
|
|
606
|
+
3: "side-swept hair", 4: "a voluminous puff",
|
|
607
|
+
5: "long hair that falls past the shoulders", 6: "a clean bob cut",
|
|
608
|
+
7: "a close buzz cut", 8: "flowing, wavy hair", 9: "a neat topknot"}
|
|
609
|
+
_HAIR_COLOR_DESC = {0: "jet black", 1: "espresso brown", 2: "walnut", 3: "honey blonde",
|
|
610
|
+
4: "copper red", 5: "silver", 6: "charcoal", 7: "burgundy",
|
|
611
|
+
8: "strawberry", 9: "ginger"}
|
|
612
|
+
_ACC_DESC = {0: "", 1: "a beauty mark", 2: "round glasses", 3: "rectangular glasses",
|
|
613
|
+
4: "a dangling earring", 5: "a headband", 6: "freckles", 7: "stud earrings",
|
|
614
|
+
8: "aviator sunglasses", 9: "a band-aid"}
|
|
615
|
+
_BG_DESC = {0: "rose", 1: "olive", 2: "sage", 3: "fern", 4: "mint",
|
|
616
|
+
5: "ocean", 6: "sky", 7: "lavender", 8: "orchid", 9: "blush"}
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _build_paragraph(
|
|
620
|
+
t, ai: int,
|
|
326
621
|
perspective: str = "third",
|
|
327
622
|
name: Optional[str] = None,
|
|
328
623
|
include_background: bool = True,
|
|
329
624
|
) -> str:
|
|
330
|
-
"""
|
|
331
|
-
Generate natural language description of a SolFace.
|
|
332
|
-
|
|
333
|
-
Args:
|
|
334
|
-
wallet_address: Solana wallet address
|
|
335
|
-
perspective: "first" for agent self-description, "third" for external
|
|
336
|
-
name: Optional name (e.g., agent name)
|
|
337
|
-
include_background: Include background color in description
|
|
338
|
-
|
|
339
|
-
Returns:
|
|
340
|
-
Human-readable appearance description
|
|
341
|
-
"""
|
|
342
|
-
t = generate_traits(wallet_address)
|
|
343
|
-
|
|
344
|
-
# Subject intro
|
|
345
625
|
if perspective == "first":
|
|
346
626
|
subj = f"I'm {name}. I have" if name else "I have"
|
|
347
627
|
im = "I'm"
|
|
@@ -349,25 +629,17 @@ def describe_appearance(
|
|
|
349
629
|
subj = f"{name} has" if name else "This SolFace has"
|
|
350
630
|
im = "They're"
|
|
351
631
|
|
|
352
|
-
# Build parts list
|
|
353
632
|
parts = []
|
|
633
|
+
parts.append(f"{subj} a squircle face with {_SKIN_DESC.get(t.skin_color, 'warm')} skin")
|
|
354
634
|
|
|
355
|
-
# Face + skin
|
|
356
|
-
face = _FACE_DESC.get(t.face_shape, "round")
|
|
357
|
-
skin = _SKIN_DESC.get(t.skin_color, "warm")
|
|
358
|
-
parts.append(f"{subj} a {face} face with {skin} skin")
|
|
359
|
-
|
|
360
|
-
# Eyes
|
|
361
635
|
eye_s = _EYE_STYLE_DESC.get(t.eye_style, "round")
|
|
362
636
|
eye_c = _EYE_COLOR_DESC.get(t.eye_color, "dark")
|
|
363
637
|
parts.append(f"{eye_s} {eye_c} eyes")
|
|
364
638
|
|
|
365
|
-
# Eyebrows
|
|
366
639
|
brows = _BROW_DESC.get(t.eyebrows, "")
|
|
367
640
|
if brows:
|
|
368
641
|
parts.append(f"{brows} eyebrows")
|
|
369
642
|
|
|
370
|
-
# Hair
|
|
371
643
|
if t.hair_style == 0:
|
|
372
644
|
parts.append("and is bald")
|
|
373
645
|
else:
|
|
@@ -378,7 +650,6 @@ def describe_appearance(
|
|
|
378
650
|
else:
|
|
379
651
|
parts.append(f"and {hc} {hs}")
|
|
380
652
|
|
|
381
|
-
# Assemble main sentence
|
|
382
653
|
desc = parts[0]
|
|
383
654
|
if len(parts) > 2:
|
|
384
655
|
desc += ", " + ", ".join(parts[1:-1]) + ", " + parts[-1]
|
|
@@ -386,29 +657,19 @@ def describe_appearance(
|
|
|
386
657
|
desc += " and " + parts[1]
|
|
387
658
|
desc += "."
|
|
388
659
|
|
|
389
|
-
# Nose
|
|
390
660
|
nose = _NOSE_DESC.get(t.nose, "")
|
|
391
661
|
if nose:
|
|
392
|
-
if perspective == "first"
|
|
393
|
-
nose_subj = "I have"
|
|
394
|
-
else:
|
|
395
|
-
nose_subj = f"{name} has" if name else "They have"
|
|
662
|
+
nose_subj = "I have" if perspective == "first" else (f"{name} has" if name else "They have")
|
|
396
663
|
desc += f" {nose_subj} {nose}."
|
|
397
664
|
|
|
398
|
-
|
|
399
|
-
acc = _ACC_DESC.get(t.accessory, "")
|
|
665
|
+
acc = _ACC_DESC.get(ai, "")
|
|
400
666
|
if acc:
|
|
401
667
|
desc += f" {im} wearing {acc}."
|
|
402
668
|
|
|
403
|
-
# Mouth
|
|
404
669
|
mouth = _MOUTH_DESC.get(t.mouth, "a smile")
|
|
405
|
-
if perspective == "first"
|
|
406
|
-
mouth_subj = "I have"
|
|
407
|
-
else:
|
|
408
|
-
mouth_subj = f"{name} has" if name else "They have"
|
|
670
|
+
mouth_subj = "I have" if perspective == "first" else (f"{name} has" if name else "They have")
|
|
409
671
|
desc += f" {mouth_subj} {mouth}."
|
|
410
672
|
|
|
411
|
-
# Background
|
|
412
673
|
if include_background:
|
|
413
674
|
bg = _BG_DESC.get(t.bg_color, "colorful")
|
|
414
675
|
desc += f" The background is {bg}."
|
|
@@ -416,14 +677,73 @@ def describe_appearance(
|
|
|
416
677
|
return desc
|
|
417
678
|
|
|
418
679
|
|
|
419
|
-
def
|
|
420
|
-
|
|
421
|
-
|
|
680
|
+
def _build_structured(t, ai: int, include_background: bool = True) -> str:
|
|
681
|
+
lines = [
|
|
682
|
+
"Face: squircle",
|
|
683
|
+
f"Skin: {_SKIN_DESC.get(t.skin_color, 'warm')}",
|
|
684
|
+
f"Eyes: {_EYE_STYLE_DESC.get(t.eye_style, 'round')}, {_EYE_COLOR_DESC.get(t.eye_color, 'dark')}",
|
|
685
|
+
f"Eyebrows: {_BROW_DESC.get(t.eyebrows, 'wispy')}",
|
|
686
|
+
]
|
|
687
|
+
nose = _NOSE_DESC.get(t.nose, "")
|
|
688
|
+
if nose:
|
|
689
|
+
lines.append(f"Nose: {nose.lstrip('a ')}")
|
|
690
|
+
lines.append(f"Mouth: {_MOUTH_DESC.get(t.mouth, 'smile')}")
|
|
691
|
+
if t.hair_style == 0:
|
|
692
|
+
lines.append("Hair: bald")
|
|
693
|
+
else:
|
|
694
|
+
hs = _HAIR_STYLE_DESC.get(t.hair_style, "")
|
|
695
|
+
hc = _HAIR_COLOR_DESC.get(t.hair_color, "")
|
|
696
|
+
lines.append(f"Hair: {hc} {hs[2:] if hs.startswith('a ') else hs}")
|
|
697
|
+
acc = _ACC_DESC.get(ai, "")
|
|
698
|
+
if acc:
|
|
699
|
+
lines.append(f"Accessory: {acc}")
|
|
700
|
+
if include_background:
|
|
701
|
+
lines.append(f"Background: {_BG_DESC.get(t.bg_color, 'colorful')}")
|
|
702
|
+
return "\n".join(lines)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _build_compact(t, ai: int) -> str:
|
|
706
|
+
parts = []
|
|
707
|
+
parts.append("squircle face")
|
|
708
|
+
parts.append(f"{_SKIN_DESC.get(t.skin_color, 'warm')} skin")
|
|
709
|
+
parts.append(f"{_EYE_COLOR_DESC.get(t.eye_color, 'dark')} {_EYE_STYLE_DESC.get(t.eye_style, 'round')} eyes")
|
|
710
|
+
if t.hair_style == 0:
|
|
711
|
+
parts.append("bald")
|
|
712
|
+
else:
|
|
713
|
+
hs = _HAIR_STYLE_DESC.get(t.hair_style, "hair").split(",")[0]
|
|
714
|
+
hc = _HAIR_COLOR_DESC.get(t.hair_color, "")
|
|
715
|
+
parts.append(f"{hc} {hs[2:] if hs.startswith('a ') else hs}")
|
|
716
|
+
acc = _ACC_DESC.get(ai, "")
|
|
717
|
+
if acc:
|
|
718
|
+
parts.append(acc)
|
|
719
|
+
mouth = _MOUTH_DESC.get(t.mouth, "smiling")
|
|
720
|
+
if mouth.startswith("a "):
|
|
721
|
+
mouth = mouth[2:]
|
|
722
|
+
parts.append(mouth)
|
|
723
|
+
return ", ".join(parts)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def describe_appearance(
|
|
727
|
+
wallet_address: str,
|
|
728
|
+
format: str = "paragraph",
|
|
729
|
+
perspective: str = "third",
|
|
730
|
+
name: Optional[str] = None,
|
|
731
|
+
include_background: bool = True,
|
|
732
|
+
) -> str:
|
|
733
|
+
t = generate_traits(wallet_address)
|
|
734
|
+
ai = effective_accessory(t)
|
|
735
|
+
if format == "structured":
|
|
736
|
+
return _build_structured(t, ai, include_background)
|
|
737
|
+
if format == "compact":
|
|
738
|
+
return _build_compact(t, ai)
|
|
739
|
+
return _build_paragraph(t, ai, perspective, name, include_background)
|
|
422
740
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
741
|
+
|
|
742
|
+
def sol_face_alt_text(wallet_address: str) -> str:
|
|
743
|
+
return f"SolFace avatar: {describe_appearance(wallet_address, format='compact', include_background=False)}"
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def agent_appearance_prompt(wallet_address: str, agent_name: Optional[str] = None) -> str:
|
|
427
747
|
desc = describe_appearance(wallet_address, perspective="first", name=agent_name, include_background=False)
|
|
428
748
|
h = trait_hash(wallet_address)
|
|
429
749
|
return f"My visual identity is a SolFace avatar (ID: {h}) derived from my wallet address. {desc} This appearance is deterministic — anyone who looks up my wallet will see the same face."
|
|
@@ -435,7 +755,7 @@ if __name__ == "__main__":
|
|
|
435
755
|
import sys
|
|
436
756
|
|
|
437
757
|
if len(sys.argv) < 2:
|
|
438
|
-
print("Usage: python solfaces.py <wallet_address> [--svg] [--json] [--describe] [--size N]")
|
|
758
|
+
print("Usage: python solfaces.py <wallet_address> [--svg] [--json] [--describe] [--size N] [--flat]")
|
|
439
759
|
sys.exit(1)
|
|
440
760
|
|
|
441
761
|
wallet = sys.argv[1]
|
|
@@ -449,8 +769,10 @@ if __name__ == "__main__":
|
|
|
449
769
|
sys.exit(1)
|
|
450
770
|
size = int(sys.argv[idx + 1])
|
|
451
771
|
|
|
772
|
+
use_flat = "--flat" in args
|
|
773
|
+
|
|
452
774
|
if "--svg" in args:
|
|
453
|
-
print(render_svg(wallet, size=size))
|
|
775
|
+
print(render_svg(wallet, size=size, flat=use_flat))
|
|
454
776
|
elif "--json" in args:
|
|
455
777
|
import json
|
|
456
778
|
t = generate_traits(wallet)
|
|
@@ -468,8 +790,8 @@ if __name__ == "__main__":
|
|
|
468
790
|
labels = get_trait_labels(t)
|
|
469
791
|
print(f"SolFace for {wallet[:8]}...{wallet[-4:]}")
|
|
470
792
|
print(f"Hash: {trait_hash(wallet)}")
|
|
471
|
-
print(
|
|
793
|
+
print("---")
|
|
472
794
|
for k, v in labels.items():
|
|
473
795
|
print(f" {k}: {v}")
|
|
474
|
-
print(
|
|
796
|
+
print("---")
|
|
475
797
|
print(describe_appearance(wallet))
|