solfaces 1.0.2 → 2.1.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 +489 -97
- package/SKILL.md +171 -0
- package/dist/agent/index.cjs +15 -14
- package/dist/agent/index.js +5 -4
- package/dist/agent/mcp-server.cjs +2956 -287
- package/dist/chunk-6QRDULAO.cjs +191 -0
- package/dist/chunk-6QRDULAO.cjs.map +1 -0
- package/dist/{chunk-RX6D5FGH.js → chunk-77SPWQU5.js} +69 -28
- package/dist/chunk-77SPWQU5.js.map +1 -0
- package/dist/chunk-CQWXUU7P.js +239 -0
- package/dist/chunk-CQWXUU7P.js.map +1 -0
- package/dist/chunk-CXRVPOTI.cjs +244 -0
- package/dist/chunk-CXRVPOTI.cjs.map +1 -0
- package/dist/chunk-DRUSCLEF.js +177 -0
- package/dist/chunk-DRUSCLEF.js.map +1 -0
- package/dist/{chunk-VMNATBH3.cjs → chunk-F244Q4KC.cjs} +74 -33
- package/dist/chunk-F244Q4KC.cjs.map +1 -0
- package/dist/chunk-HVPGR6G5.cjs +647 -0
- package/dist/chunk-HVPGR6G5.cjs.map +1 -0
- package/dist/{chunk-SNJABBAT.js → chunk-MGP7F65H.js} +3 -3
- package/dist/{chunk-SNJABBAT.js.map → chunk-MGP7F65H.js.map} +1 -1
- package/dist/chunk-R3MC2AJZ.cjs +2247 -0
- package/dist/chunk-R3MC2AJZ.cjs.map +1 -0
- package/dist/chunk-SWML743U.js +625 -0
- package/dist/chunk-SWML743U.js.map +1 -0
- package/dist/chunk-SX3FQDKM.js +2238 -0
- package/dist/chunk-SX3FQDKM.js.map +1 -0
- package/dist/{chunk-A6N3RPEA.cjs → chunk-WTCXTXTV.cjs} +6 -6
- package/dist/{chunk-A6N3RPEA.cjs.map → chunk-WTCXTXTV.cjs.map} +1 -1
- package/dist/constants-Bi5uTRp5.d.cts +17 -0
- package/dist/constants-Bi5uTRp5.d.ts +17 -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 +104 -35
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -5
- package/dist/names/index.cjs +40 -0
- package/dist/names/index.cjs.map +1 -0
- package/dist/names/index.d.cts +36 -0
- package/dist/names/index.d.ts +36 -0
- package/dist/names/index.js +3 -0
- package/dist/names/index.js.map +1 -0
- package/dist/react/index.cjs +454 -397
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +17 -3
- package/dist/react/index.d.ts +17 -3
- package/dist/react/index.js +450 -394
- 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 +22 -5
- package/python/solfaces.py +830 -237
- package/reference/integrations.md +166 -0
- package/reference/react.md +108 -0
- package/reference/themes.md +124 -0
- 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/skill.md +0 -463
package/python/solfaces.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
|
-
from solfaces import generate_traits, render_svg, describe_appearance
|
|
7
|
+
from solfaces import generate_traits, render_svg, describe_appearance, derive_name
|
|
8
8
|
|
|
9
9
|
traits = generate_traits("7xKXq...")
|
|
10
10
|
svg = render_svg("7xKXq...", size=256)
|
|
@@ -13,25 +13,28 @@ 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 hashlib
|
|
19
|
+
import math
|
|
20
|
+
import struct
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
# ─── Types ────────────────────────────────────────────────────
|
|
21
24
|
|
|
22
25
|
@dataclass
|
|
23
26
|
class SolFaceTraits:
|
|
24
|
-
face_shape: int # 0-3
|
|
25
|
-
skin_color: int # 0-
|
|
27
|
+
face_shape: int # 0-3 (consumed for PRNG ordering, all render as squircle)
|
|
28
|
+
skin_color: int # 0-9
|
|
26
29
|
eye_style: int # 0-7
|
|
27
30
|
eye_color: int # 0-4
|
|
28
31
|
eyebrows: int # 0-4
|
|
29
32
|
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-
|
|
33
|
+
mouth: int # 0-7
|
|
34
|
+
hair_style: int # 0-9
|
|
35
|
+
hair_color: int # 0-9
|
|
36
|
+
accessory: int # 0-9
|
|
37
|
+
bg_color: int # 0-9
|
|
35
38
|
|
|
36
39
|
def to_dict(self) -> Dict[str, int]:
|
|
37
40
|
return {
|
|
@@ -51,46 +54,142 @@ class SolFaceTraits:
|
|
|
51
54
|
|
|
52
55
|
# ─── Color Palettes ──────────────────────────────────────────
|
|
53
56
|
|
|
54
|
-
SKIN_COLORS = [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
SKIN_COLORS = [
|
|
58
|
+
"#faeae5", "#efd6c8", "#e4c5aa", "#d5b590", "#c59e77",
|
|
59
|
+
"#b4875f", "#9d6d4d", "#805742", "#654134", "#4b2d25",
|
|
60
|
+
]
|
|
61
|
+
EYE_COLORS = ["#382414", "#3868A8", "#38784C", "#808838", "#586878"]
|
|
62
|
+
HAIR_COLORS = [
|
|
63
|
+
"#1A1A24", "#4C3428", "#887058", "#D4B868", "#A84830",
|
|
64
|
+
"#C0C0CC", "#484858", "#783850", "#D8B0A0", "#C08048",
|
|
65
|
+
]
|
|
66
|
+
BG_COLORS = [
|
|
67
|
+
"#b98387", "#a9a360", "#9eb785", "#69ab79", "#81bbb0",
|
|
68
|
+
"#6499af", "#7f8bbd", "#8869ab", "#b785b3", "#ab6984",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ─── Color Math ──────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def hex_to_rgb(h: str) -> tuple[int, int, int]:
|
|
75
|
+
h = h.lstrip("#")
|
|
76
|
+
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rgb_to_hex(r: float, g: float, b: float) -> str:
|
|
80
|
+
return "#{:02x}{:02x}{:02x}".format(
|
|
81
|
+
max(0, min(255, round(r))),
|
|
82
|
+
max(0, min(255, round(g))),
|
|
83
|
+
max(0, min(255, round(b))),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def darken(h: str, pct: float = 0.12) -> str:
|
|
88
|
+
r, g, b = hex_to_rgb(h)
|
|
89
|
+
return rgb_to_hex(r * (1 - pct), g * (1 - pct), b * (1 - pct))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def lighten(h: str, pct: float = 0.15) -> str:
|
|
93
|
+
r, g, b = hex_to_rgb(h)
|
|
94
|
+
return rgb_to_hex(r + (255 - r) * pct, g + (255 - g) * pct, b + (255 - b) * pct)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def blend(a: str, b: str, t: float = 0.5) -> str:
|
|
98
|
+
r1, g1, b1 = hex_to_rgb(a)
|
|
99
|
+
r2, g2, b2 = hex_to_rgb(b)
|
|
100
|
+
return rgb_to_hex(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def luminance(h: str) -> float:
|
|
104
|
+
r, g, b = hex_to_rgb(h)
|
|
105
|
+
return (r + g + b) / 3
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def derive_skin_colors(skin: str) -> dict:
|
|
109
|
+
sL = luminance(skin)
|
|
110
|
+
is_dark = sL < 100
|
|
111
|
+
|
|
112
|
+
skin_hi = lighten(skin, 0.10)
|
|
113
|
+
skin_lo = darken(skin, 0.22)
|
|
114
|
+
skin_mid = darken(skin, 0.05)
|
|
115
|
+
|
|
116
|
+
sr, sg, sb = hex_to_rgb(skin)
|
|
117
|
+
if sL > 120:
|
|
118
|
+
r_b = 0.03 if sL > 180 else 0.06
|
|
119
|
+
g_d = 0.30 if sL > 180 else 0.28
|
|
120
|
+
b_d = 0.25 if sL > 180 else 0.22
|
|
121
|
+
cheek_color = rgb_to_hex(
|
|
122
|
+
min(255, sr + sr * r_b),
|
|
123
|
+
max(0, sg - sg * g_d),
|
|
124
|
+
max(0, sb - sb * b_d),
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
cheek_color = rgb_to_hex(min(255, sr + 50), max(0, sg - 10), max(0, sb - 5))
|
|
128
|
+
|
|
129
|
+
cheek_opacity = 0.15 + 0.18 * (1 - min(1, sL / 240))
|
|
130
|
+
|
|
131
|
+
lip_t = max(0, min(1, (sL - 60) / 180))
|
|
132
|
+
lip_base = blend("#D89090", "#A83848", lip_t)
|
|
133
|
+
mid_boost = 1 - abs(sL - 140) / 80
|
|
134
|
+
lip_blend = (0.70 if is_dark else 0.62) + max(0, mid_boost) * 0.12
|
|
135
|
+
lip_raw = blend(skin, lip_base, min(0.82, lip_blend))
|
|
136
|
+
lr, lg, lb = hex_to_rgb(lip_raw)
|
|
137
|
+
lip_d = abs(sr - lr) + abs(sg - lg) + abs(sb - lb)
|
|
138
|
+
lip_color = blend(skin, lip_base, 0.78) if lip_d < 60 else lip_raw
|
|
139
|
+
|
|
140
|
+
brow_color = lighten(skin, 0.35 if sL < 80 else 0.25) if is_dark else darken(skin, 0.55)
|
|
141
|
+
nose_fill = lighten(skin, 0.20) if is_dark else darken(skin, 0.20)
|
|
142
|
+
|
|
143
|
+
ear_t = max(0, min(1, (sL - 100) / 60))
|
|
144
|
+
ear_fill = blend(lighten(skin, 0.08), skin_mid, ear_t)
|
|
145
|
+
ear_shadow = darken(skin, 0.10 + 0.06 * (1 - min(1, sL / 160)))
|
|
146
|
+
lid_color = lighten(skin, 0.18) if is_dark else darken(skin, 0.15)
|
|
147
|
+
|
|
148
|
+
ew_t = max(0, min(1, (sL - 60) / 180))
|
|
149
|
+
eye_white = blend("#EDE8E0", "#FBF8F2", ew_t)
|
|
150
|
+
|
|
151
|
+
warmth = 0.3 if sL > 140 else (0.5 if sL > 100 else 0.7)
|
|
152
|
+
acc_color = blend("#808890", blend(skin, "#B0A898", 0.3), warmth)
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"skin_hi": skin_hi, "skin_lo": skin_lo, "skin_mid": skin_mid,
|
|
156
|
+
"is_dark": is_dark, "cheek_color": cheek_color, "cheek_opacity": cheek_opacity,
|
|
157
|
+
"lip_color": lip_color, "nose_fill": nose_fill, "brow_color": brow_color,
|
|
158
|
+
"ear_fill": ear_fill, "ear_shadow": ear_shadow,
|
|
159
|
+
"eye_white": eye_white, "lid_color": lid_color, "acc_color": acc_color,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def buzz_opacity(hair_col: str, skin: str) -> float:
|
|
164
|
+
hr, hg, hb = hex_to_rgb(hair_col)
|
|
165
|
+
sr, sg, sb = hex_to_rgb(skin)
|
|
166
|
+
return 0.70 if abs(hr - sr) + abs(hg - sg) + abs(hb - sb) < 80 else 0.50
|
|
58
167
|
|
|
59
168
|
|
|
60
169
|
# ─── Hashing (djb2) — exact JS parity ────────────────────────
|
|
61
170
|
|
|
62
171
|
def _djb2(s: str) -> int:
|
|
63
|
-
"""DJB2 hash — matches JavaScript implementation exactly."""
|
|
64
172
|
hash_val = 5381
|
|
65
173
|
for ch in s:
|
|
66
|
-
# Replicate JS: ((hash << 5) + hash + charCode) | 0
|
|
67
174
|
hash_val = ctypes.c_int32((hash_val << 5) + hash_val + ord(ch)).value
|
|
68
|
-
# Return unsigned 32-bit (>>> 0 in JS)
|
|
69
175
|
return hash_val & 0xFFFFFFFF
|
|
70
176
|
|
|
71
177
|
|
|
72
178
|
# ─── PRNG (mulberry32) — exact JS parity ─────────────────────
|
|
73
179
|
|
|
74
180
|
def _mulberry32(seed: int):
|
|
75
|
-
"""Mulberry32 PRNG — matches JavaScript implementation exactly."""
|
|
76
181
|
s = ctypes.c_int32(seed).value
|
|
77
182
|
|
|
78
183
|
def next_val() -> float:
|
|
79
184
|
nonlocal s
|
|
80
185
|
s = ctypes.c_int32(s + 0x6D2B79F5).value
|
|
81
|
-
|
|
82
|
-
# Math.imul(s ^ (s >>> 15), 1 | s)
|
|
83
186
|
a = (s ^ ((s & 0xFFFFFFFF) >> 15)) & 0xFFFFFFFF
|
|
84
187
|
b = (1 | s) & 0xFFFFFFFF
|
|
85
188
|
t = ctypes.c_int32(_imul(a, b)).value
|
|
86
|
-
|
|
87
|
-
# (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
88
189
|
c = (t ^ ((t & 0xFFFFFFFF) >> 7)) & 0xFFFFFFFF
|
|
89
190
|
d = (61 | t) & 0xFFFFFFFF
|
|
90
191
|
old_t = t
|
|
91
192
|
t = (ctypes.c_int32(old_t + _imul(c, d)).value) ^ old_t
|
|
92
|
-
|
|
93
|
-
# ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
94
193
|
result = ((t ^ ((t & 0xFFFFFFFF) >> 14)) & 0xFFFFFFFF)
|
|
95
194
|
return result / 4294967296
|
|
96
195
|
|
|
@@ -98,7 +197,6 @@ def _mulberry32(seed: int):
|
|
|
98
197
|
|
|
99
198
|
|
|
100
199
|
def _imul(a: int, b: int) -> int:
|
|
101
|
-
"""Emulate Math.imul — 32-bit integer multiply."""
|
|
102
200
|
a = a & 0xFFFFFFFF
|
|
103
201
|
b = b & 0xFFFFFFFF
|
|
104
202
|
result = (a * b) & 0xFFFFFFFF
|
|
@@ -110,197 +208,383 @@ def _imul(a: int, b: int) -> int:
|
|
|
110
208
|
# ─── Trait Generation ─────────────────────────────────────────
|
|
111
209
|
|
|
112
210
|
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
211
|
seed = _djb2(wallet_address)
|
|
118
212
|
rand = _mulberry32(seed)
|
|
119
213
|
|
|
214
|
+
# IMPORTANT: Order must NEVER change — shifts all downstream values.
|
|
120
215
|
return SolFaceTraits(
|
|
121
216
|
face_shape=int(rand() * 4),
|
|
122
|
-
skin_color=int(rand() *
|
|
217
|
+
skin_color=int(rand() * 10),
|
|
123
218
|
eye_style=int(rand() * 8),
|
|
124
219
|
eye_color=int(rand() * 5),
|
|
125
220
|
eyebrows=int(rand() * 5),
|
|
126
221
|
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() *
|
|
222
|
+
mouth=int(rand() * 8),
|
|
223
|
+
hair_style=int(rand() * 10),
|
|
224
|
+
hair_color=int(rand() * 10),
|
|
225
|
+
accessory=int(rand() * 10),
|
|
226
|
+
bg_color=int(rand() * 10),
|
|
132
227
|
)
|
|
133
228
|
|
|
134
229
|
|
|
230
|
+
def effective_accessory(t: SolFaceTraits) -> int:
|
|
231
|
+
ai = t.accessory % 10
|
|
232
|
+
hi = t.hair_style % 10
|
|
233
|
+
if (ai == 4 or ai == 7) and (hi == 5 or hi == 6):
|
|
234
|
+
return 0
|
|
235
|
+
return ai
|
|
236
|
+
|
|
237
|
+
|
|
135
238
|
def trait_hash(wallet_address: str) -> str:
|
|
136
|
-
"""Return 8-char hex hash for cache keys."""
|
|
137
239
|
return f"{_djb2(wallet_address):08x}"
|
|
138
240
|
|
|
139
241
|
|
|
140
242
|
# ─── Trait Labels ─────────────────────────────────────────────
|
|
141
243
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
244
|
+
SKIN_LABELS = ["Porcelain", "Ivory", "Fair", "Light", "Sand",
|
|
245
|
+
"Golden", "Warm", "Caramel", "Brown", "Deep"]
|
|
246
|
+
EYE_STYLE_LABELS = ["Round", "Minimal", "Almond", "Wide", "Relaxed", "Joyful", "Bright", "Gentle"]
|
|
247
|
+
EYE_COLOR_LABELS = ["Chocolate", "Sky", "Emerald", "Hazel", "Storm"]
|
|
248
|
+
BROW_LABELS = ["Wispy", "Straight", "Natural", "Arched", "Angled"]
|
|
249
|
+
NOSE_LABELS = ["Shadow", "Button", "Soft", "Nostrils"]
|
|
250
|
+
MOUTH_LABELS = ["Smile", "Calm", "Happy", "Oh", "Smirk", "Grin", "Flat", "Pout"]
|
|
251
|
+
HAIR_STYLE_LABELS = ["Bald", "Short", "Curly", "Side Sweep", "Puff",
|
|
252
|
+
"Long", "Bob", "Buzz", "Wavy", "Topknot"]
|
|
253
|
+
HAIR_COLOR_LABELS = ["Black", "Espresso", "Walnut", "Honey", "Copper",
|
|
254
|
+
"Silver", "Charcoal", "Burgundy", "Strawberry", "Ginger"]
|
|
255
|
+
ACC_LABELS = ["None", "Beauty Mark", "Round Glasses", "Rect Glasses", "Earring",
|
|
256
|
+
"Headband", "Freckles", "Stud Earrings", "Aviators", "Band-Aid"]
|
|
257
|
+
BG_COLOR_LABELS = ["Rose", "Olive", "Sage", "Fern", "Mint",
|
|
258
|
+
"Ocean", "Sky", "Lavender", "Orchid", "Blush"]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_trait_labels(t: SolFaceTraits) -> Dict[str, str]:
|
|
262
|
+
ai = effective_accessory(t)
|
|
156
263
|
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[
|
|
264
|
+
"faceShape": "Squircle",
|
|
265
|
+
"skinColor": SKIN_LABELS[t.skin_color] if t.skin_color < len(SKIN_LABELS) else "Fair",
|
|
266
|
+
"eyeStyle": EYE_STYLE_LABELS[t.eye_style] if t.eye_style < len(EYE_STYLE_LABELS) else "Round",
|
|
267
|
+
"eyeColor": EYE_COLOR_LABELS[t.eye_color] if t.eye_color < len(EYE_COLOR_LABELS) else "Chocolate",
|
|
268
|
+
"eyebrows": BROW_LABELS[t.eyebrows] if t.eyebrows < len(BROW_LABELS) else "Wispy",
|
|
269
|
+
"nose": NOSE_LABELS[t.nose] if t.nose < len(NOSE_LABELS) else "Shadow",
|
|
270
|
+
"mouth": MOUTH_LABELS[t.mouth] if t.mouth < len(MOUTH_LABELS) else "Smile",
|
|
271
|
+
"hairStyle": HAIR_STYLE_LABELS[t.hair_style] if t.hair_style < len(HAIR_STYLE_LABELS) else "Bald",
|
|
272
|
+
"hairColor": HAIR_COLOR_LABELS[t.hair_color] if t.hair_color < len(HAIR_COLOR_LABELS) else "Black",
|
|
273
|
+
"accessory": ACC_LABELS[ai] if ai < len(ACC_LABELS) else "None",
|
|
274
|
+
"bgColor": BG_COLOR_LABELS[t.bg_color] if t.bg_color < len(BG_COLOR_LABELS) else "Rose",
|
|
168
275
|
}
|
|
169
276
|
|
|
170
277
|
|
|
171
278
|
# ─── SVG Rendering ────────────────────────────────────────────
|
|
172
279
|
|
|
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
|
-
|
|
280
|
+
def _to_base36(n: int) -> str:
|
|
281
|
+
if n == 0:
|
|
282
|
+
return "0"
|
|
283
|
+
chars = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
284
|
+
result = ""
|
|
285
|
+
while n > 0:
|
|
286
|
+
result = chars[n % 36] + result
|
|
287
|
+
n //= 36
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _build_defs(gid: str, skin: str, skin_hi: str, skin_lo: str,
|
|
292
|
+
hair_col: str, bg_col: str, cheek_color: str,
|
|
293
|
+
cheek_opacity: float, flat: bool, full: bool) -> str:
|
|
294
|
+
if flat:
|
|
295
|
+
return ""
|
|
296
|
+
d = "<defs>"
|
|
297
|
+
d += f'<linearGradient id="{gid}sg" x1="0" y1="0" x2="0" y2="1">'
|
|
298
|
+
d += f'<stop offset="0%" stop-color="{skin_hi}"/>'
|
|
299
|
+
d += f'<stop offset="100%" stop-color="{skin_lo}"/>'
|
|
300
|
+
d += "</linearGradient>"
|
|
301
|
+
d += f'<linearGradient id="{gid}hg" x1="0" y1="0" x2="0" y2="1">'
|
|
302
|
+
d += f'<stop offset="0%" stop-color="{lighten(hair_col, 0.15)}"/>'
|
|
303
|
+
d += f'<stop offset="100%" stop-color="{darken(hair_col, 0.15)}"/>'
|
|
304
|
+
d += "</linearGradient>"
|
|
305
|
+
d += f'<linearGradient id="{gid}bg" x1="0" y1="0" x2="1" y2="1">'
|
|
306
|
+
d += f'<stop offset="0%" stop-color="{lighten(bg_col, 0.12)}"/>'
|
|
307
|
+
d += f'<stop offset="100%" stop-color="{darken(bg_col, 0.12)}"/>'
|
|
308
|
+
d += "</linearGradient>"
|
|
309
|
+
if full:
|
|
310
|
+
d += f'<radialGradient id="{gid}glow" cx="0.5" cy="0.28" r="0.45">'
|
|
311
|
+
d += '<stop offset="0%" stop-color="#ffffff" stop-opacity="0.10"/>'
|
|
312
|
+
d += '<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>'
|
|
313
|
+
d += "</radialGradient>"
|
|
314
|
+
d += f'<radialGradient id="{gid}chin" cx="0.5" cy="0.85" r="0.35">'
|
|
315
|
+
d += f'<stop offset="0%" stop-color="{skin_lo}" stop-opacity="0.30"/>'
|
|
316
|
+
d += f'<stop offset="100%" stop-color="{skin_lo}" stop-opacity="0"/>'
|
|
317
|
+
d += "</radialGradient>"
|
|
318
|
+
d += f'<radialGradient id="{gid}cL" cx="0.5" cy="0.5" r="0.5">'
|
|
319
|
+
d += f'<stop offset="0%" stop-color="{cheek_color}" stop-opacity="{cheek_opacity:.2f}"/>'
|
|
320
|
+
d += f'<stop offset="100%" stop-color="{cheek_color}" stop-opacity="0"/>'
|
|
321
|
+
d += "</radialGradient>"
|
|
322
|
+
d += f'<radialGradient id="{gid}cR" cx="0.5" cy="0.5" r="0.5">'
|
|
323
|
+
d += f'<stop offset="0%" stop-color="{cheek_color}" stop-opacity="{cheek_opacity:.2f}"/>'
|
|
324
|
+
d += f'<stop offset="100%" stop-color="{cheek_color}" stop-opacity="0"/>'
|
|
325
|
+
d += "</radialGradient>"
|
|
326
|
+
d += "</defs>"
|
|
327
|
+
return d
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _render_hair_back(hi: int, gid: str, flat: bool) -> str:
|
|
331
|
+
fill = "currentColor" if flat else f"url(#{gid}hg)"
|
|
332
|
+
if hi == 5: return f'<rect x="10" y="14" width="44" height="42" rx="6" fill="{fill}"/>'
|
|
333
|
+
if hi == 6: return f'<rect x="12" y="14" width="40" height="32" rx="8" fill="{fill}"/>'
|
|
334
|
+
if hi == 8: return f'<rect x="11" y="14" width="42" height="38" rx="8" fill="{fill}"/>'
|
|
209
335
|
return ""
|
|
210
336
|
|
|
211
337
|
|
|
212
|
-
def
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
338
|
+
def _render_ears(ear_fill: str, ear_shadow: str) -> str:
|
|
339
|
+
return (
|
|
340
|
+
f'<ellipse cx="11" cy="34" rx="4" ry="5" fill="{ear_fill}"/>'
|
|
341
|
+
f'<ellipse cx="11" cy="34" rx="2.5" ry="3.5" fill="{ear_shadow}" opacity="0.3"/>'
|
|
342
|
+
f'<ellipse cx="53" cy="34" rx="4" ry="5" fill="{ear_fill}"/>'
|
|
343
|
+
f'<ellipse cx="53" cy="34" rx="2.5" ry="3.5" fill="{ear_shadow}" opacity="0.3"/>'
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _render_face(gid: str, skin: str, flat: bool) -> str:
|
|
348
|
+
fill = skin if flat else f"url(#{gid}sg)"
|
|
349
|
+
return f'<rect x="14" y="16" width="36" height="38" rx="12" ry="12" fill="{fill}"/>'
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _render_face_overlays(gid: str) -> str:
|
|
353
|
+
return (
|
|
354
|
+
f'<rect x="14" y="16" width="36" height="38" rx="12" ry="12" fill="url(#{gid}glow)"/>'
|
|
355
|
+
f'<rect x="14" y="16" width="36" height="38" rx="12" ry="12" fill="url(#{gid}chin)"/>'
|
|
356
|
+
f'<ellipse cx="22" cy="42" rx="5" ry="3.5" fill="url(#{gid}cL)"/>'
|
|
357
|
+
f'<ellipse cx="42" cy="42" rx="5" ry="3.5" fill="url(#{gid}cR)"/>'
|
|
358
|
+
'<line x1="20" y1="50" x2="44" y2="50" stroke="currentColor" stroke-width="0.3" opacity="0.08" stroke-linecap="round"/>'
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _render_hair_front(hi: int, gid: str, hair_col: str, skin: str, flat: bool) -> str:
|
|
363
|
+
fill = hair_col if flat else f"url(#{gid}hg)"
|
|
364
|
+
if hi == 0: return ""
|
|
365
|
+
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}"/>'
|
|
366
|
+
if hi == 2:
|
|
367
|
+
return (f'<g fill="{fill}">'
|
|
368
|
+
'<circle cx="20" cy="14" r="5"/><circle cx="28" cy="11" r="5.5"/>'
|
|
369
|
+
'<circle cx="36" cy="11" r="5.5"/><circle cx="44" cy="14" r="5"/>'
|
|
370
|
+
'<circle cx="16" cy="20" r="4"/><circle cx="48" cy="20" r="4"/></g>')
|
|
371
|
+
if hi == 3:
|
|
372
|
+
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}"/>'
|
|
373
|
+
f'<path d="M14 20 Q8 16 10 8 Q14 10 20 16 Z" fill="{fill}"/>')
|
|
374
|
+
if hi == 4: return f'<ellipse cx="32" cy="10" rx="14" ry="8" fill="{fill}"/>'
|
|
375
|
+
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}"/>'
|
|
376
|
+
if hi == 6:
|
|
377
|
+
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}"/>'
|
|
378
|
+
f'<rect x="10" y="28" width="8" height="14" rx="4" fill="{fill}"/>'
|
|
379
|
+
f'<rect x="46" y="28" width="8" height="14" rx="4" fill="{fill}"/>')
|
|
380
|
+
if hi == 7:
|
|
381
|
+
bop = buzz_opacity(hair_col, skin)
|
|
382
|
+
return f'<rect x="15" y="13" width="34" height="16" rx="10" ry="8" fill="{hair_col}" opacity="{bop:.2f}"/>'
|
|
383
|
+
if hi == 8:
|
|
384
|
+
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}"/>'
|
|
385
|
+
f'<path d="M12 30 Q10 20 14 16" fill="none" stroke="{fill}" stroke-width="4" stroke-linecap="round"/>'
|
|
386
|
+
f'<path d="M52 30 Q54 20 50 16" fill="none" stroke="{fill}" stroke-width="4" stroke-linecap="round"/>')
|
|
387
|
+
if hi == 9:
|
|
388
|
+
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}"/>'
|
|
389
|
+
f'<ellipse cx="32" cy="6" rx="6" ry="5" fill="{fill}"/>')
|
|
219
390
|
return ""
|
|
220
391
|
|
|
221
392
|
|
|
222
|
-
def
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
393
|
+
def _render_eyes(ei: int, eye_col: str, eye_white: str, lid_color: str, full: bool) -> str:
|
|
394
|
+
lx, rx, y = 25, 39, 33
|
|
395
|
+
s = ""
|
|
396
|
+
if ei == 0:
|
|
397
|
+
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}"/>'
|
|
398
|
+
if full: s += f'<circle cx="{lx+1.5}" cy="{y-1}" r="0.7" fill="white" opacity="0.8"/>'
|
|
399
|
+
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}"/>'
|
|
400
|
+
if full: s += f'<circle cx="{rx+1.5}" cy="{y-1}" r="0.7" fill="white" opacity="0.8"/>'
|
|
401
|
+
elif ei == 1:
|
|
402
|
+
s += f'<circle cx="{lx}" cy="{y}" r="2" fill="{eye_col}"/><circle cx="{rx}" cy="{y}" r="2" fill="{eye_col}"/>'
|
|
403
|
+
elif ei == 2:
|
|
404
|
+
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}"/>'
|
|
405
|
+
if full: s += f'<circle cx="{lx+1.2}" cy="{y-0.8}" r="0.6" fill="white" opacity="0.7"/>'
|
|
406
|
+
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}"/>'
|
|
407
|
+
if full: s += f'<circle cx="{rx+1.2}" cy="{y-0.8}" r="0.6" fill="white" opacity="0.7"/>'
|
|
408
|
+
elif ei == 3:
|
|
409
|
+
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}"/>'
|
|
410
|
+
if full: s += f'<circle cx="{lx+1.5}" cy="{y-1}" r="0.8" fill="white" opacity="0.8"/>'
|
|
411
|
+
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}"/>'
|
|
412
|
+
if full: s += f'<circle cx="{rx+1.5}" cy="{y-1}" r="0.8" fill="white" opacity="0.8"/>'
|
|
413
|
+
elif ei == 4:
|
|
414
|
+
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}"/>'
|
|
415
|
+
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"/>'
|
|
416
|
+
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}"/>'
|
|
417
|
+
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"/>'
|
|
418
|
+
elif ei == 5:
|
|
419
|
+
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"/>'
|
|
420
|
+
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"/>'
|
|
421
|
+
elif ei == 6:
|
|
422
|
+
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}"/>'
|
|
423
|
+
s += f'<circle cx="{lx+1.5}" cy="{y-1}" r="1" fill="white" opacity="0.9"/>'
|
|
424
|
+
if full:
|
|
425
|
+
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"/>'
|
|
426
|
+
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"/>'
|
|
427
|
+
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}"/>'
|
|
428
|
+
s += f'<circle cx="{rx+1.5}" cy="{y-1}" r="1" fill="white" opacity="0.9"/>'
|
|
429
|
+
if full:
|
|
430
|
+
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"/>'
|
|
431
|
+
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"/>'
|
|
432
|
+
elif ei == 7:
|
|
433
|
+
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}"/>'
|
|
434
|
+
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}"/>'
|
|
435
|
+
else:
|
|
436
|
+
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}"/>'
|
|
437
|
+
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}"/>'
|
|
438
|
+
|
|
439
|
+
if full and ei not in (1, 5):
|
|
440
|
+
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"/>'
|
|
441
|
+
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"/>'
|
|
442
|
+
return s
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _render_eyebrows(bi: int, brow_color: str) -> str:
|
|
446
|
+
lx, rx, y = 25, 39, 27
|
|
447
|
+
if bi == 0:
|
|
448
|
+
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"/>'
|
|
449
|
+
f'<line x1="{rx-3}" y1="{y-0.5}" x2="{rx+3}" y2="{y}" stroke="{brow_color}" stroke-width="0.7" stroke-linecap="round"/>')
|
|
450
|
+
if bi == 1:
|
|
451
|
+
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"/>'
|
|
452
|
+
f'<line x1="{rx-3.5}" y1="{y}" x2="{rx+3.5}" y2="{y}" stroke="{brow_color}" stroke-width="1.2" stroke-linecap="round"/>')
|
|
453
|
+
if bi == 2:
|
|
454
|
+
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"/>'
|
|
455
|
+
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"/>')
|
|
456
|
+
if bi == 3:
|
|
457
|
+
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"/>'
|
|
458
|
+
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"/>')
|
|
459
|
+
if bi == 4:
|
|
460
|
+
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"/>'
|
|
461
|
+
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
462
|
return ""
|
|
243
463
|
|
|
244
464
|
|
|
245
|
-
def
|
|
246
|
-
|
|
247
|
-
if
|
|
248
|
-
if
|
|
249
|
-
if
|
|
250
|
-
if
|
|
465
|
+
def _render_nose(ni: int, nose_fill: str) -> str:
|
|
466
|
+
cx, y = 32, 39
|
|
467
|
+
if ni == 0: return f'<ellipse cx="{cx}" cy="{y}" rx="2" ry="1.2" fill="{nose_fill}" opacity="0.35"/>'
|
|
468
|
+
if ni == 1: return f'<circle cx="{cx}" cy="{y}" r="1.8" fill="{nose_fill}" opacity="0.5"/>'
|
|
469
|
+
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"/>'
|
|
470
|
+
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"/>'
|
|
471
|
+
return f'<ellipse cx="{cx}" cy="{y}" rx="2" ry="1.2" fill="{nose_fill}" opacity="0.35"/>'
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _render_mouth(mi: int, lip_color: str, is_dark: bool) -> str:
|
|
475
|
+
cx, y = 32, 45
|
|
476
|
+
tc = "#e8e0d8" if is_dark else "#ffffff"
|
|
477
|
+
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"/>'
|
|
478
|
+
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"/>'
|
|
479
|
+
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"/>'
|
|
480
|
+
if mi == 3: return f'<ellipse cx="{cx}" cy="{y+1}" rx="2.5" ry="3" fill="{lip_color}" opacity="0.7"/>'
|
|
481
|
+
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"/>'
|
|
482
|
+
if mi == 5:
|
|
483
|
+
return (f'<path d="M{cx-5} {y} Q{cx} {y+6} {cx+5} {y}" fill="{tc}" stroke="{lip_color}" stroke-width="1"/>'
|
|
484
|
+
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"/>')
|
|
485
|
+
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"/>'
|
|
486
|
+
if mi == 7:
|
|
487
|
+
return (f'<ellipse cx="{cx}" cy="{y+1}" rx="3.5" ry="2" fill="{lip_color}" opacity="0.25"/>'
|
|
488
|
+
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"/>')
|
|
489
|
+
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"/>'
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _render_accessory(ai: int, acc_color: str, glasses_color: str, earring_color: str, headband_color: str) -> str:
|
|
493
|
+
if ai == 0: return ""
|
|
494
|
+
if ai == 1: return '<circle cx="40" cy="44" r="0.8" fill="#3a2a2a"/>'
|
|
495
|
+
if ai == 2:
|
|
496
|
+
return (f'<g fill="none" stroke="{glasses_color}" stroke-width="1">'
|
|
497
|
+
'<circle cx="25" cy="33" r="5.5"/><circle cx="39" cy="33" r="5.5"/>'
|
|
498
|
+
'<line x1="30.5" y1="33" x2="33.5" y2="33"/>'
|
|
499
|
+
'<line x1="19.5" y1="33" x2="14" y2="31"/>'
|
|
500
|
+
'<line x1="44.5" y1="33" x2="50" y2="31"/></g>')
|
|
501
|
+
if ai == 3:
|
|
502
|
+
return (f'<g fill="none" stroke="{glasses_color}" stroke-width="1">'
|
|
503
|
+
'<rect x="19" y="29" width="12" height="8" rx="1.5"/>'
|
|
504
|
+
'<rect x="33" y="29" width="12" height="8" rx="1.5"/>'
|
|
505
|
+
'<line x1="31" y1="33" x2="33" y2="33"/>'
|
|
506
|
+
'<line x1="19" y1="33" x2="14" y2="31"/>'
|
|
507
|
+
'<line x1="45" y1="33" x2="50" y2="31"/></g>')
|
|
508
|
+
if ai == 4:
|
|
509
|
+
return (f'<circle cx="10" cy="38" r="1.5" fill="{earring_color}"/>'
|
|
510
|
+
f'<circle cx="10" cy="41" r="2" fill="{earring_color}" opacity="0.8"/>')
|
|
511
|
+
if ai == 5:
|
|
512
|
+
return f'<rect x="13" y="20" width="38" height="3.5" rx="1.5" fill="{headband_color}" opacity="0.85"/>'
|
|
513
|
+
if ai == 6:
|
|
514
|
+
return ('<g fill="#a0785a" opacity="0.35">'
|
|
515
|
+
'<circle cx="21" cy="40" r="0.6"/><circle cx="23" cy="42" r="0.5"/>'
|
|
516
|
+
'<circle cx="19" cy="41.5" r="0.5"/><circle cx="43" cy="40" r="0.6"/>'
|
|
517
|
+
'<circle cx="41" cy="42" r="0.5"/><circle cx="45" cy="41.5" r="0.5"/></g>')
|
|
518
|
+
if ai == 7:
|
|
519
|
+
return (f'<circle cx="10" cy="37" r="1.2" fill="{earring_color}"/>'
|
|
520
|
+
f'<circle cx="54" cy="37" r="1.2" fill="{earring_color}"/>')
|
|
521
|
+
if ai == 8:
|
|
522
|
+
return (f'<g fill="none" stroke="{glasses_color}" stroke-width="1.2">'
|
|
523
|
+
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"/>'
|
|
524
|
+
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"/>'
|
|
525
|
+
'<line x1="31" y1="32" x2="33" y2="32"/>'
|
|
526
|
+
'<line x1="19" y1="31" x2="14" y2="29"/>'
|
|
527
|
+
'<line x1="45" y1="31" x2="50" y2="29"/></g>')
|
|
528
|
+
if ai == 9:
|
|
529
|
+
return ('<g>'
|
|
530
|
+
'<rect x="38" y="38" width="8" height="4" rx="1" fill="#f0d0a0" transform="rotate(-15 42 40)"/>'
|
|
531
|
+
'<line x1="40" y1="39" x2="40" y2="41" stroke="#c0a080" stroke-width="0.4" transform="rotate(-15 42 40)"/>'
|
|
532
|
+
'<line x1="42" y1="39" x2="42" y2="41" stroke="#c0a080" stroke-width="0.4" transform="rotate(-15 42 40)"/>'
|
|
533
|
+
'<line x1="44" y1="39" x2="44" y2="41" stroke="#c0a080" stroke-width="0.4" transform="rotate(-15 42 40)"/></g>')
|
|
251
534
|
return ""
|
|
252
535
|
|
|
253
536
|
|
|
254
537
|
def render_svg(
|
|
255
538
|
wallet_address: str,
|
|
256
539
|
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,
|
|
540
|
+
flat: bool = False,
|
|
541
|
+
detail: str = "auto",
|
|
265
542
|
) -> 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
543
|
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
|
-
|
|
544
|
+
|
|
545
|
+
full = detail == "full" or (detail == "auto" and size >= 48)
|
|
546
|
+
skin = SKIN_COLORS[t.skin_color % len(SKIN_COLORS)]
|
|
547
|
+
eye_col = EYE_COLORS[t.eye_color % len(EYE_COLORS)]
|
|
548
|
+
hair_col = HAIR_COLORS[t.hair_color % len(HAIR_COLORS)]
|
|
549
|
+
bg_col = BG_COLORS[t.bg_color % len(BG_COLORS)]
|
|
550
|
+
|
|
551
|
+
derived = derive_skin_colors(skin)
|
|
552
|
+
gid = "sf" + _to_base36(_djb2(wallet_address))
|
|
553
|
+
|
|
554
|
+
hi = t.hair_style % 10
|
|
555
|
+
ai = effective_accessory(t)
|
|
556
|
+
|
|
557
|
+
glasses_color = "#4a4a5a"
|
|
558
|
+
earring_color = blend(skin, "#d4a840", 0.4)
|
|
559
|
+
headband_color = blend(hair_col, "#c04040", 0.5)
|
|
560
|
+
|
|
561
|
+
bg_fill = bg_col if flat else f"url(#{gid}bg)"
|
|
562
|
+
|
|
563
|
+
parts = []
|
|
564
|
+
parts.append(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="{size}" height="{size}">')
|
|
565
|
+
parts.append(_build_defs(gid, skin, derived["skin_hi"], derived["skin_lo"],
|
|
566
|
+
hair_col, bg_col, derived["cheek_color"],
|
|
567
|
+
derived["cheek_opacity"], flat, full))
|
|
568
|
+
parts.append(f'<rect x="0" y="0" width="64" height="64" fill="{bg_fill}" opacity="1" rx="4"/>')
|
|
569
|
+
parts.append(_render_hair_back(hi, gid, flat))
|
|
570
|
+
parts.append(_render_ears(derived["ear_fill"], derived["ear_shadow"]))
|
|
571
|
+
parts.append(_render_face(gid, skin, flat))
|
|
572
|
+
if full:
|
|
573
|
+
parts.append(_render_face_overlays(gid))
|
|
574
|
+
if ai == 5:
|
|
575
|
+
parts.append(_render_accessory(5, derived["acc_color"], glasses_color, earring_color, headband_color))
|
|
576
|
+
parts.append(_render_hair_front(hi, gid, hair_col, skin, flat))
|
|
577
|
+
parts.append(_render_eyes(t.eye_style % 8, eye_col, derived["eye_white"], derived["lid_color"], full))
|
|
578
|
+
parts.append(_render_eyebrows(t.eyebrows % 5, derived["brow_color"]))
|
|
579
|
+
parts.append(_render_nose(t.nose % 4, derived["nose_fill"]))
|
|
580
|
+
parts.append(_render_mouth(t.mouth % 8, derived["lip_color"], derived["is_dark"]))
|
|
581
|
+
if ai != 0 and ai != 5:
|
|
582
|
+
parts.append(_render_accessory(ai, derived["acc_color"], glasses_color, earring_color, headband_color))
|
|
583
|
+
parts.append("</svg>")
|
|
299
584
|
return "".join(p for p in parts if p)
|
|
300
585
|
|
|
301
586
|
|
|
302
587
|
def render_data_uri(wallet_address: str, **kwargs) -> str:
|
|
303
|
-
"""Render as a data URI for use in <img> tags or HTML emails."""
|
|
304
588
|
from urllib.parse import quote
|
|
305
589
|
svg = render_svg(wallet_address, **kwargs)
|
|
306
590
|
return f"data:image/svg+xml;charset=utf-8,{quote(svg)}"
|
|
@@ -308,40 +592,38 @@ def render_data_uri(wallet_address: str, **kwargs) -> str:
|
|
|
308
592
|
|
|
309
593
|
# ─── Description ──────────────────────────────────────────────
|
|
310
594
|
|
|
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
|
-
|
|
595
|
+
_SKIN_DESC = {0: "porcelain", 1: "ivory", 2: "fair", 3: "light", 4: "sand",
|
|
596
|
+
5: "golden", 6: "warm", 7: "caramel", 8: "brown", 9: "deep"}
|
|
597
|
+
_EYE_STYLE_DESC = {0: "round, wide-open", 1: "small and minimal", 2: "almond-shaped",
|
|
598
|
+
3: "wide and expressive", 4: "relaxed, half-lidded", 5: "joyful, crescent-shaped",
|
|
599
|
+
6: "bright and sparkling", 7: "gentle and narrow"}
|
|
600
|
+
_EYE_COLOR_DESC = {0: "dark brown", 1: "blue", 2: "green", 3: "hazel", 4: "gray"}
|
|
601
|
+
_BROW_DESC = {0: "wispy", 1: "straight", 2: "natural", 3: "elegantly arched", 4: "sharply angled"}
|
|
602
|
+
_NOSE_DESC = {0: "a subtle shadow nose", 1: "a small button nose", 2: "a soft curved nose",
|
|
603
|
+
3: "a button nose with visible nostrils"}
|
|
604
|
+
_MOUTH_DESC = {0: "a gentle smile", 1: "a calm, neutral expression", 2: "a happy grin",
|
|
605
|
+
3: "a surprised O-shaped mouth", 4: "a confident smirk", 5: "a wide, toothy grin",
|
|
606
|
+
6: "a flat, straight expression", 7: "a soft pout"}
|
|
607
|
+
_HAIR_STYLE_DESC = {0: "bald, with no hair", 1: "short, neatly cropped hair", 2: "bouncy, curly hair",
|
|
608
|
+
3: "side-swept hair", 4: "a voluminous puff",
|
|
609
|
+
5: "long hair that falls past the shoulders", 6: "a clean bob cut",
|
|
610
|
+
7: "a close buzz cut", 8: "flowing, wavy hair", 9: "a neat topknot"}
|
|
611
|
+
_HAIR_COLOR_DESC = {0: "jet black", 1: "espresso brown", 2: "walnut", 3: "honey blonde",
|
|
612
|
+
4: "copper red", 5: "silver", 6: "charcoal", 7: "burgundy",
|
|
613
|
+
8: "strawberry", 9: "ginger"}
|
|
614
|
+
_ACC_DESC = {0: "", 1: "a beauty mark", 2: "round glasses", 3: "rectangular glasses",
|
|
615
|
+
4: "a dangling earring", 5: "a headband", 6: "freckles", 7: "stud earrings",
|
|
616
|
+
8: "aviator sunglasses", 9: "a band-aid"}
|
|
617
|
+
_BG_DESC = {0: "rose", 1: "olive", 2: "sage", 3: "fern", 4: "mint",
|
|
618
|
+
5: "ocean", 6: "sky", 7: "lavender", 8: "orchid", 9: "blush"}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _build_paragraph(
|
|
622
|
+
t, ai: int,
|
|
326
623
|
perspective: str = "third",
|
|
327
624
|
name: Optional[str] = None,
|
|
328
625
|
include_background: bool = True,
|
|
329
626
|
) -> 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
627
|
if perspective == "first":
|
|
346
628
|
subj = f"I'm {name}. I have" if name else "I have"
|
|
347
629
|
im = "I'm"
|
|
@@ -349,27 +631,19 @@ def describe_appearance(
|
|
|
349
631
|
subj = f"{name} has" if name else "This SolFace has"
|
|
350
632
|
im = "They're"
|
|
351
633
|
|
|
352
|
-
# Build parts list
|
|
353
634
|
parts = []
|
|
635
|
+
parts.append(f"{subj} a squircle face with {_SKIN_DESC.get(t.skin_color, 'warm')} skin")
|
|
354
636
|
|
|
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
637
|
eye_s = _EYE_STYLE_DESC.get(t.eye_style, "round")
|
|
362
638
|
eye_c = _EYE_COLOR_DESC.get(t.eye_color, "dark")
|
|
363
639
|
parts.append(f"{eye_s} {eye_c} eyes")
|
|
364
640
|
|
|
365
|
-
# Eyebrows
|
|
366
641
|
brows = _BROW_DESC.get(t.eyebrows, "")
|
|
367
642
|
if brows:
|
|
368
643
|
parts.append(f"{brows} eyebrows")
|
|
369
644
|
|
|
370
|
-
# Hair
|
|
371
645
|
if t.hair_style == 0:
|
|
372
|
-
parts.append("and is bald")
|
|
646
|
+
parts.append("and am bald" if perspective == "first" else "and is bald")
|
|
373
647
|
else:
|
|
374
648
|
hc = _HAIR_COLOR_DESC.get(t.hair_color, "")
|
|
375
649
|
hs = _HAIR_STYLE_DESC.get(t.hair_style, "")
|
|
@@ -378,7 +652,6 @@ def describe_appearance(
|
|
|
378
652
|
else:
|
|
379
653
|
parts.append(f"and {hc} {hs}")
|
|
380
654
|
|
|
381
|
-
# Assemble main sentence
|
|
382
655
|
desc = parts[0]
|
|
383
656
|
if len(parts) > 2:
|
|
384
657
|
desc += ", " + ", ".join(parts[1:-1]) + ", " + parts[-1]
|
|
@@ -386,29 +659,19 @@ def describe_appearance(
|
|
|
386
659
|
desc += " and " + parts[1]
|
|
387
660
|
desc += "."
|
|
388
661
|
|
|
389
|
-
# Nose
|
|
390
662
|
nose = _NOSE_DESC.get(t.nose, "")
|
|
391
663
|
if nose:
|
|
392
|
-
if perspective == "first"
|
|
393
|
-
nose_subj = "I have"
|
|
394
|
-
else:
|
|
395
|
-
nose_subj = f"{name} has" if name else "They have"
|
|
664
|
+
nose_subj = "I have" if perspective == "first" else (f"{name} has" if name else "They have")
|
|
396
665
|
desc += f" {nose_subj} {nose}."
|
|
397
666
|
|
|
398
|
-
|
|
399
|
-
acc = _ACC_DESC.get(t.accessory, "")
|
|
667
|
+
acc = _ACC_DESC.get(ai, "")
|
|
400
668
|
if acc:
|
|
401
669
|
desc += f" {im} wearing {acc}."
|
|
402
670
|
|
|
403
|
-
# Mouth
|
|
404
671
|
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"
|
|
672
|
+
mouth_subj = "I have" if perspective == "first" else (f"{name} has" if name else "They have")
|
|
409
673
|
desc += f" {mouth_subj} {mouth}."
|
|
410
674
|
|
|
411
|
-
# Background
|
|
412
675
|
if include_background:
|
|
413
676
|
bg = _BG_DESC.get(t.bg_color, "colorful")
|
|
414
677
|
desc += f" The background is {bg}."
|
|
@@ -416,26 +679,354 @@ def describe_appearance(
|
|
|
416
679
|
return desc
|
|
417
680
|
|
|
418
681
|
|
|
419
|
-
def
|
|
420
|
-
|
|
421
|
-
|
|
682
|
+
def _build_structured(t, ai: int, include_background: bool = True) -> str:
|
|
683
|
+
lines = [
|
|
684
|
+
"Face: squircle",
|
|
685
|
+
f"Skin: {_SKIN_DESC.get(t.skin_color, 'warm')}",
|
|
686
|
+
f"Eyes: {_EYE_STYLE_DESC.get(t.eye_style, 'round')}, {_EYE_COLOR_DESC.get(t.eye_color, 'dark')}",
|
|
687
|
+
f"Eyebrows: {_BROW_DESC.get(t.eyebrows, 'wispy')}",
|
|
688
|
+
]
|
|
689
|
+
nose = _NOSE_DESC.get(t.nose, "")
|
|
690
|
+
if nose:
|
|
691
|
+
lines.append(f"Nose: {nose.lstrip('a ')}")
|
|
692
|
+
lines.append(f"Mouth: {_MOUTH_DESC.get(t.mouth, 'smile')}")
|
|
693
|
+
if t.hair_style == 0:
|
|
694
|
+
lines.append("Hair: bald")
|
|
695
|
+
else:
|
|
696
|
+
hs = _HAIR_STYLE_DESC.get(t.hair_style, "")
|
|
697
|
+
hc = _HAIR_COLOR_DESC.get(t.hair_color, "")
|
|
698
|
+
lines.append(f"Hair: {hc} {hs[2:] if hs.startswith('a ') else hs}")
|
|
699
|
+
acc = _ACC_DESC.get(ai, "")
|
|
700
|
+
if acc:
|
|
701
|
+
lines.append(f"Accessory: {acc}")
|
|
702
|
+
if include_background:
|
|
703
|
+
lines.append(f"Background: {_BG_DESC.get(t.bg_color, 'colorful')}")
|
|
704
|
+
return "\n".join(lines)
|
|
422
705
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
""
|
|
706
|
+
|
|
707
|
+
def _build_compact(t, ai: int) -> str:
|
|
708
|
+
parts = []
|
|
709
|
+
parts.append("squircle face")
|
|
710
|
+
parts.append(f"{_SKIN_DESC.get(t.skin_color, 'warm')} skin")
|
|
711
|
+
parts.append(f"{_EYE_COLOR_DESC.get(t.eye_color, 'dark')} {_EYE_STYLE_DESC.get(t.eye_style, 'round')} eyes")
|
|
712
|
+
if t.hair_style == 0:
|
|
713
|
+
parts.append("bald")
|
|
714
|
+
else:
|
|
715
|
+
raw = _HAIR_STYLE_DESC.get(t.hair_style, "hair")
|
|
716
|
+
hs = raw.split(",")[-1].strip() if "," in raw else raw
|
|
717
|
+
hc = _HAIR_COLOR_DESC.get(t.hair_color, "")
|
|
718
|
+
parts.append(f"{hc} {hs[2:] if hs.startswith('a ') else hs}")
|
|
719
|
+
acc = _ACC_DESC.get(ai, "")
|
|
720
|
+
if acc:
|
|
721
|
+
parts.append(acc)
|
|
722
|
+
mouth = _MOUTH_DESC.get(t.mouth, "smiling")
|
|
723
|
+
if mouth.startswith("a "):
|
|
724
|
+
mouth = mouth[2:]
|
|
725
|
+
parts.append(mouth)
|
|
726
|
+
return ", ".join(parts)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def describe_appearance(
|
|
730
|
+
wallet_address: str,
|
|
731
|
+
format: str = "paragraph",
|
|
732
|
+
perspective: str = "third",
|
|
733
|
+
name: Optional[str] = None,
|
|
734
|
+
include_background: bool = True,
|
|
735
|
+
) -> str:
|
|
736
|
+
t = generate_traits(wallet_address)
|
|
737
|
+
ai = effective_accessory(t)
|
|
738
|
+
if format == "structured":
|
|
739
|
+
return _build_structured(t, ai, include_background)
|
|
740
|
+
if format == "compact":
|
|
741
|
+
return _build_compact(t, ai)
|
|
742
|
+
return _build_paragraph(t, ai, perspective, name, include_background)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def sol_face_alt_text(wallet_address: str) -> str:
|
|
746
|
+
return f"SolFace avatar: {describe_appearance(wallet_address, format='compact', include_background=False)}"
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def agent_appearance_prompt(wallet_address: str, agent_name: Optional[str] = None) -> str:
|
|
427
750
|
desc = describe_appearance(wallet_address, perspective="first", name=agent_name, include_background=False)
|
|
428
751
|
h = trait_hash(wallet_address)
|
|
429
752
|
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."
|
|
430
753
|
|
|
431
754
|
|
|
755
|
+
|
|
756
|
+
# ─── SolNames v1 — Deterministic Name Derivation ────────────────
|
|
757
|
+
|
|
758
|
+
SOLNAMES_VERSION = "v1"
|
|
759
|
+
|
|
760
|
+
ADJECTIVES = [
|
|
761
|
+
"Frost","Solar","Coral","Amber","Mossy","Lunar","Misty","Alpine","Autumn","Breeze",
|
|
762
|
+
"Cerulean","Cloudy","Cosmic","Crystal","Dewy","Dusty","Earthy","Floral","Forest","Frozen",
|
|
763
|
+
"Glacial","Golden","Grassy","Harbor","Ivory","Jasmine","Lagoon","Leafy","Lush","Marine",
|
|
764
|
+
"Meadow","Mineral","Mosaic","Nectar","Nordic","Oceanic","Orchid","Pearly","Petal","Polar",
|
|
765
|
+
"Prairie","Rain","River","Rocky","Sandy","Savanna","Shore","Sierra","Snowy","Spring",
|
|
766
|
+
"Starry","Stone","Summer","Sunset","Tidal","Timber","Tropic","Tundra","Valley","Verdant",
|
|
767
|
+
"Winding","Winter","Woody","Bloom","Blossom","Brook","Canyon","Cedar","Cloud","Comet",
|
|
768
|
+
"Copper","Dawn","Delta","Dune","Eclipse","Fern","Flame","Flora","Glade","Azure",
|
|
769
|
+
"Cobalt","Crimson","Indigo","Scarlet","Sapphire","Emerald","Ruby","Garnet","Onyx","Jade",
|
|
770
|
+
"Turquoise","Magenta","Plum","Russet","Tawny","Burnt","Gilded","Platinum","Bronze","Chrome",
|
|
771
|
+
"Pearl","Opal","Blush","Rosy","Dusky","Inky","Ashen","Cream","Slate","Charcoal",
|
|
772
|
+
"Steel","Pewter","Honey","Saffron","Citrus","Lemon","Tangerine","Peach","Apricot","Vermilion",
|
|
773
|
+
"Mauve","Lilac","Periwinkle","Cerise","Maroon","Burgundy","Wine","Berry","Cherry","Mint",
|
|
774
|
+
"Olive","Teal","Navy","Cyan","Neon","Pastel","Muted","Lustrous","Gleaming","Glossy",
|
|
775
|
+
"Shining","Glowing","Luminous","Brilliant","Sparkling","Shimmering","Iridescent","Prismatic","Spectral","Twilight",
|
|
776
|
+
"Sunrise","Sunlit","Moonlit","Starlit","Candlelit","Firelit","Sunbeam","Afterglow","Halo","Flicker",
|
|
777
|
+
"Glow","Ray","Beam","Blaze","Glint","Spark","Arc","Prism","Spectrum","Rainbow",
|
|
778
|
+
"Aurora","Nebula","Stellar","Astral","Lucent","Frosted","Swift","Bold","Keen","Brave",
|
|
779
|
+
"Noble","Serene","Fierce","Gentle","Agile","Alert","Astute","Candid","Clever","Daring",
|
|
780
|
+
"Eager","Earnest","Fair","Gallant","Graceful","Hardy","Honest","Humble","Jovial","Joyful",
|
|
781
|
+
"Kind","Loyal","Merry","Mindful","Nimble","Patient","Plucky","Proud","Quick","Ready",
|
|
782
|
+
"Resilient","Savvy","Sincere","Skilled","Steady","Stout","Strong","Sure","Tender","True",
|
|
783
|
+
"Valiant","Willing","Wise","Witty","Worthy","Zealous","Active","Adept","Ardent","Avid",
|
|
784
|
+
"Benign","Bright","Civil","Clean","Clear","Composed","Content","Courtly","Crafty","Curious",
|
|
785
|
+
"Decent","Devout","Diligent","Direct","Driven","Dynamic","Elegant","Elite","Even","Exact",
|
|
786
|
+
"Famed","Fervent","Fine","Firm","Fit","Fluid","Focal","Fond","Frank","Free",
|
|
787
|
+
"Fresh","Friendly","Frugal","Gifted","Glad","Grand","Great","Green","Grounded","Hale",
|
|
788
|
+
"Happy","Hearty","Heroic","Ideal","Infinite","Inner","Intact","Intent","Just","Lively",
|
|
789
|
+
"Lucky","Major","Mellow","Mighty","Modest","Natural","Neat","Optimal","Original","Pacific",
|
|
790
|
+
"Peaceful","Pious","Pleased","Poised","Polite","Popular","Potent","Precious","Premier","Prime",
|
|
791
|
+
"Proper","Proven","Pure","Quiet","Rapid","Rare","Real","Regal","Rich","Rising",
|
|
792
|
+
"Robust","Royal","Sacred","Safe","Scenic","Secure","Senior","Sharp","Simple","Smart",
|
|
793
|
+
"Smooth","Snug","Social","Solid","Sound","Special","Stable","Stately","Still","Super",
|
|
794
|
+
"Supreme","Sweet","Thorough","Tidy","Top","Total","Tough","Trim","Trusted","Ultimate",
|
|
795
|
+
"Unique","United","Upper","Useful","Valid","Valued","Varied","Vital","Vivid","Vocal",
|
|
796
|
+
"Whole","Wide","Young","Zesty","Iron","Gold","Silver","Marble","Granite","Velvet",
|
|
797
|
+
"Satin","Silk","Linen","Cotton","Suede","Denim","Canvas","Leather","Wooden","Bamboo",
|
|
798
|
+
"Wicker","Woven","Braided","Knit","Lace","Mesh","Fiber","Glass","Mirror","Diamond",
|
|
799
|
+
"Flint","Basalt","Obsidian","Chalk","Clay","Gravel","Pebble","Cobble","Brick","Tile",
|
|
800
|
+
"Resin","Fossil","Shell","Bone","Feather","Plush","Brushed","Polished","Hammered","Forged",
|
|
801
|
+
"Cast","Molten","Tempered","Etched","Carved","Sculpted","Spun","Pressed","Rolled","Folded",
|
|
802
|
+
"Layered","Stacked","Ribbed","Matte","Lacquered","Enameled","Glazed","Painted","Dyed","Stained",
|
|
803
|
+
"Refined","Distilled","Purified","Filtered","Alloyed","Plated","Coated","Sealed","Bonded","Fused",
|
|
804
|
+
"Welded","Riveted","Textured","Grained","Veined","Flecked","Speckled","Dappled","Streaked","Banded",
|
|
805
|
+
"Striped","Checkered","Waxed","Oiled","Cured","Tanned","Smoked","Burnished","Antiqued","Patina",
|
|
806
|
+
"Weathered","Aged","Rustic","Hewn","Vast","Broad","Deep","High","Tall","Giant",
|
|
807
|
+
"Colossal","Massive","Immense","Boundless","Endless","Sweeping","Spanning","Extended","Towering","Soaring",
|
|
808
|
+
"Lofty","Elevated","Raised","Summit","Apex","Crest","Crown","Pinnacle","Zenith","Ridge",
|
|
809
|
+
"Ledge","Level","Hollow","Compact","Dense","Thick","Narrow","Slender","Micro","Mini",
|
|
810
|
+
"Small","Ample","Hefty","Jumbo","Mega","Ultra","Macro","Central","Core","Outer",
|
|
811
|
+
"Lateral","Radial","Spiral","Orbital","Linear","Planar","Spherical","Rounded","Angular","Pointed",
|
|
812
|
+
"Tapered","Curved","Arced","Twisted","Coiled","Fractal","Cellular","Quantum","Photon","Plasma",
|
|
813
|
+
"Kinetic","Static","Charged","Maximal","Atomic","Nano","Tiered","Strata","Nested","Stepped",
|
|
814
|
+
"Graded","Scaled","Proportioned","Modular","Symmetric","Parallel","Flowing","Drifting","Gliding","Sailing",
|
|
815
|
+
"Floating","Climbing","Leaping","Bounding","Sprinting","Dashing","Rushing","Surging","Cascading","Rolling",
|
|
816
|
+
"Tumbling","Spinning","Whirling","Twisting","Swirling","Pulsing","Beating","Humming","Buzzing","Ringing",
|
|
817
|
+
"Chiming","Singing","Dancing","Swaying","Waving","Rippling","Bubbling","Fizzing","Crackling","Snapping",
|
|
818
|
+
"Clicking","Tapping","Drumming","Thrumming","Vibrant","Magnetic","Electric","Blazing","Burning","Fiery",
|
|
819
|
+
"Volcanic","Thermal","Warming","Cooling","Chilling","Brisk","Crisp","Breezy","Gusty","Windy",
|
|
820
|
+
"Airy","Light","Buoyant","Weightless","Fleet","Peppy","Zippy","Snappy","Speedy","Sonic",
|
|
821
|
+
"Turbo","Warp","Express","Instant","Darting","Flying","Jetting","Cruising","Coasting","Rhythmic",
|
|
822
|
+
"Cycling","Pacing","Striding","Marching","Trotting","Galloping","Bouncing","Vaulting","Arching","Launched",
|
|
823
|
+
"Propelled","Hovering","Orbiting","Revolving","Rotating","Pivoting","Swinging","Rocking","Tilting","Shifting",
|
|
824
|
+
"Sliding","Skating","Surfing","Diving","Plunging","Dipping","Wading","Celestial","Ethereal","Mystic",
|
|
825
|
+
"Arcane","Ancient","Eternal","Timeless","Ageless","Enduring","Lasting","Perpetual","Legendary","Mythic",
|
|
826
|
+
"Epic","Fabled","Storied","Historic","Classic","Vintage","Retro","Modern","Future","Digital",
|
|
827
|
+
"Cyber","Virtual","Pixel","Binary","Omega","Alpha","Beta","Gamma","Sigma","Theta",
|
|
828
|
+
"Lambda","Kappa","Zeta","Epsilon","Omni","Dual","Triple","Nexus","Vertex","Vector",
|
|
829
|
+
"Matrix","Cipher","Enigma","Phantom","Mirage","Dream","Vision","Oracle","Prophet","Sage",
|
|
830
|
+
"Shaman","Druid","Artisan","Maestro","Virtuoso","Savant","Prodigy","Maven","Guru","Mentor",
|
|
831
|
+
"Guide","Scout","Pioneer","Voyager","Wanderer","Nomad","Rover","Ranger","Sentinel","Guardian",
|
|
832
|
+
"Keeper","Warden","Champion","Vanguard","Herald","Emissary","Envoy","Ambassador","Steward","Curator",
|
|
833
|
+
"Patron","Benefactor","Founder","Architect","Builder","Maker","Crafter","Weaver","Forger","Smith",
|
|
834
|
+
"Wright","Mason","Brewer","Baker","Tanner","Dyer","Scribe","Bard","Minstrel","Troubadour",
|
|
835
|
+
"Storyteller","Chronicler","Lorekeeper","Archivist","Tranquil","Placid","Hushed","Muffled","Soft","Subtle",
|
|
836
|
+
"Faint","Dim","Pale","Warm","Cool","Taut","Tense","Cozy","Homey","Pastoral",
|
|
837
|
+
"Sylvan","Bucolic","Idyllic","Quaint","Charming","Lovely","Pretty","Dapper","Natty","Chic",
|
|
838
|
+
"Posh","Classy","Fancy","Ornate","Lavish","Opulent","Majestic","Dignified","August","Solemn",
|
|
839
|
+
"Sober","Heartfelt","Genuine","Authentic","Novel","Singular","Distinct","Chosen","Select","Premium",
|
|
840
|
+
"Deluxe","Superior","Exquisite","Superb","Splendid","Glorious","Sublime","Resonant","Harmonic","Melodic",
|
|
841
|
+
"Lyrical","Poetic","Seamless","Effortless","Organic","Primal","Devoted","Dedicated","Committed","Focused",
|
|
842
|
+
"Determined","Resolute","Steadfast","Unwavering","Constant","Faithful","Reliable","Dependable","Trusty","Anchored",
|
|
843
|
+
"Moored","Sheltered","Protected","Guarded","Shielded","Fortified","Reinforced","Braced","Bolstered","Supported",
|
|
844
|
+
"Upheld","Sustained","Preserved","Conserved","Stored","Vaulted","Locked","Secured","Fastened","Linked",
|
|
845
|
+
"Joined","Connected","Paired","Matched","Balanced","Aligned","Centered","Aimed","Directed","Guided",
|
|
846
|
+
"Steered","Piloted","Navigated","Charted","Mapped","Tracked","Pursued","Discovered","Unveiled","Displayed",
|
|
847
|
+
"Presented","Granted","Bestowed","Conferred","Sapient","Sentient","Radiant","Fertile","Abundant","Bountiful",
|
|
848
|
+
"Plentiful","Prolific","Thriving","Flourishing","Blooming","Budding","Growing","Expanding","Advancing","Progressing",
|
|
849
|
+
"Evolving","Maturing","Ripening","Developing","Emerging","Nascent","Incipient","Dawning","Unfolding","Awakening",
|
|
850
|
+
"Stirring","Kindling","Igniting","Sparking","Triggering","Launching","Initiating","Pioneering","Trailblazing","Groundbreaking",
|
|
851
|
+
"Innovative","Inventive","Creative","Imaginative","Inspired","Visionary","Prophetic","Prescient","Insightful","Perceptive",
|
|
852
|
+
"Observant","Watchful","Vigilant","Attentive","Thoughtful","Considerate","Caring","Nurturing","Fostering","Cultivating",
|
|
853
|
+
"Tending","Pruning","Trimming","Shaping","Forming","Molding","Fashioning","Designing","Drafting","Sketching",
|
|
854
|
+
"Drawing","Painting","Coloring","Tinting","Shading","Blending","Mixing","Merging","Combining","Uniting",
|
|
855
|
+
"Fusing","Melding","Weaving","Knitting","Stitching","Sewing","Quilting","Patching","Mending","Healing",
|
|
856
|
+
"Restoring","Renewing","Reviving","Refreshing","Rejuvenating","Invigorating","Energizing","Empowering","Strengthening","Fortifying",
|
|
857
|
+
"Hardening","Tempering","Seasoning","Curing","Aging","Mellowing","Softening","Smoothing","Leveling","Planing",
|
|
858
|
+
"Sanding","Buffing","Burnishing","Radiating","Beaming","Lighting","Illuminating","Brightening","Clarifying","Purifying",
|
|
859
|
+
"Filtering","Distilling","Condensing","Concentrating","Focusing","Directing","Channeling","Terraced","Ascending","Streaming",
|
|
860
|
+
"Umber","Careful","Perfecting","Glittering","Twinkling","Verdure","Auroral","Boreal","Austral","Temperate",
|
|
861
|
+
]
|
|
862
|
+
|
|
863
|
+
NOUNS = [
|
|
864
|
+
"Falcon","Hawk","Eagle","Owl","Heron","Crane","Swan","Dove","Raven","Finch",
|
|
865
|
+
"Robin","Wren","Lark","Jay","Ibis","Kite","Osprey","Condor","Pelican","Stork",
|
|
866
|
+
"Sparrow","Tern","Puffin","Parrot","Toucan","Kingfisher","Flamingo","Quail","Pheasant","Grouse",
|
|
867
|
+
"Oriole","Warbler","Thrush","Starling","Magpie","Swallow","Martin","Plover","Curlew","Sandpiper",
|
|
868
|
+
"Wolf","Fox","Bear","Stag","Elk","Moose","Bison","Lynx","Cougar","Panther",
|
|
869
|
+
"Jaguar","Leopard","Tiger","Lion","Cheetah","Gazelle","Antelope","Impala","Zebra","Giraffe",
|
|
870
|
+
"Rhino","Hippo","Otter","Beaver","Badger","Marten","Ferret","Mink","Hare","Rabbit",
|
|
871
|
+
"Squirrel","Chipmunk","Porcupine","Hedgehog","Armadillo","Pangolin","Lemur","Gibbon","Tamarin","Capybara",
|
|
872
|
+
"Chinchilla","Ocelot","Margay","Coati","Kinkajou","Tapir","Okapi","Kudu","Oryx","Chamois",
|
|
873
|
+
"Orca","Dolphin","Whale","Narwhal","Walrus","Seal","Manatee","Turtle","Iguana","Gecko",
|
|
874
|
+
"Chameleon","Newt","Salamander","Crab","Lobster","Seahorse","Starfish","Octopus","Squid","Jellyfish",
|
|
875
|
+
"Stingray","Barracuda","Marlin","Sailfish","Trout","Salmon","Mantis","Cricket","Firefly","Dragonfly",
|
|
876
|
+
"Cedar","Oak","Pine","Birch","Maple","Elm","Ash","Willow","Cypress","Redwood",
|
|
877
|
+
"Sequoia","Spruce","Fir","Larch","Yew","Beech","Alder","Poplar","Aspen","Walnut",
|
|
878
|
+
"Hickory","Teak","Mahogany","Ebony","Bamboo","Palm","Acacia","Baobab","Banyan","Olive",
|
|
879
|
+
"Laurel","Magnolia","Lotus","Orchid","Iris","Lily","Rose","Tulip","Daisy","Aster",
|
|
880
|
+
"Dahlia","Peony","Jasmine","Violet","Clover","Fern","Moss","Lichen","Ivy","Vine",
|
|
881
|
+
"Reed","Sage","Basil","Thyme","Mint","Dill","Fennel","Sorrel","Yarrow","Thistle",
|
|
882
|
+
"Heather","Bluebell","Primrose","Marigold","Sunflower","Zinnia","Pansy","Poppy","Crocus","Snowdrop",
|
|
883
|
+
"Foxglove","Honeysuckle","Wisteria","Hibiscus","Plumeria","Gardenia","Camellia","Begonia","Azalea","Oleander",
|
|
884
|
+
"Canyon","Ridge","Peak","Summit","Cliff","Bluff","Mesa","Butte","Plateau","Terrace",
|
|
885
|
+
"Valley","Basin","Gorge","Ravine","Gulch","Fjord","Inlet","Bay","Cove","Harbor",
|
|
886
|
+
"Lagoon","Reef","Atoll","Isle","Peninsula","Cape","Shoal","Bank","Ledge","Shelf",
|
|
887
|
+
"Dune","Desert","Steppe","Tundra","Glacier","Moraine","Geyser","Oasis","Delta","Estuary",
|
|
888
|
+
"Marsh","Bog","Swamp","Fen","Moor","Heath","Meadow","Prairie","Savanna","Glen",
|
|
889
|
+
"Dale","Dell","Vale","Hollow","Grove","Copse","Thicket","Taiga","Mangrove","Wetland",
|
|
890
|
+
"Cascade","Brook","Creek","Stream","River","Lake","Pond","Pool","Tarn","Loch",
|
|
891
|
+
"Falls","Fountain","Grotto","Cavern","Cave","Tunnel","Arch","Bridge","Ford","Pass",
|
|
892
|
+
"Trail","Path","Route","Caldera","Crater","Vent","Spire","Needle","Dome","Gateway",
|
|
893
|
+
"Portal","Threshold","Verge","Brink","Edge","Rim","Brow","Crest","Promontory","Headland",
|
|
894
|
+
"Spit","Tombolo","Isthmus","Strait","Narrows","Sound","Bight","Reach","Stretch","Expanse",
|
|
895
|
+
"Clearing","Glade","Knoll","Hillock","Mound","Tor","Crag","Scarp","Escarpment","Comet",
|
|
896
|
+
"Meteor","Asteroid","Nebula","Galaxy","Quasar","Pulsar","Star","Sun","Moon","Planet",
|
|
897
|
+
"Orbit","Eclipse","Corona","Flare","Nova","Cosmos","Void","Abyss","Ether","Zenith",
|
|
898
|
+
"Nadir","Horizon","Aurora","Meridian","Equinox","Solstice","Phase","Cycle","Epoch","Eon",
|
|
899
|
+
"Era","Dawn","Dusk","Twilight","Night","Noon","Morning","Evening","Sunset","Sunrise",
|
|
900
|
+
"Daybreak","Nightfall","Starlight","Moonbeam","Sunray","Skyline","Firmament","Canopy","Infinity","Vega",
|
|
901
|
+
"Rigel","Altair","Deneb","Sirius","Polaris","Arcturus","Antares","Lyra","Orion","Draco",
|
|
902
|
+
"Phoenix","Hydra","Corvus","Cygnus","Aquila","Andromeda","Pegasus","Perseus","Gemini","Centauri",
|
|
903
|
+
"Cassiopeia","Scorpius","Sagittarius","Capella","Procyon","Aldebaran","Betelgeuse","Spica","Prism","Quill",
|
|
904
|
+
"Forge","Anvil","Helm","Rune","Atlas","Compass","Anchor","Beacon","Bell","Bugle",
|
|
905
|
+
"Drum","Flute","Harp","Horn","Lyre","Pipe","Gong","Chime","Cymbal","Fiddle",
|
|
906
|
+
"Trumpet","Viola","Cello","Oboe","Lute","Mandolin","Zither","Sitar","Banjo","Lantern",
|
|
907
|
+
"Torch","Candle","Lamp","Globe","Lens","Scope","Mirror","Frame","Easel","Palette",
|
|
908
|
+
"Brush","Chisel","Mallet","Hammer","Tongs","Ladle","Kettle","Crucible","Mortar","Pestle",
|
|
909
|
+
"Flask","Vial","Beaker","Alembic","Furnace","Kiln","Loom","Shuttle","Bobbin","Spool",
|
|
910
|
+
"Thimble","Pin","Clasp","Buckle","Brooch","Pendant","Amulet","Talisman","Charm","Token",
|
|
911
|
+
"Coin","Medal","Badge","Shield","Banner","Flag","Pennant","Sigil","Stamp","Emblem",
|
|
912
|
+
"Totem","Icon","Statue","Obelisk","Monolith","Cairn","Tablet","Scroll","Tome","Codex",
|
|
913
|
+
"Ledger","Journal","Chronicle","Almanac","Manual","Gazette","Folio","Pamphlet","Broadsheet","Parchment",
|
|
914
|
+
"Vellum","Papyrus","Inkwell","Quiver","Satchel","Pouch","Casket","Coffer","Chest","Crate",
|
|
915
|
+
"Barrel","Quartz","Opal","Onyx","Agate","Garnet","Topaz","Beryl","Zircon","Spinel",
|
|
916
|
+
"Peridot","Jasper","Amethyst","Citrine","Tourmaline","Malachite","Azurite","Lapis","Pyrite","Galena",
|
|
917
|
+
"Mica","Talc","Gypsum","Calcite","Dolomite","Basalt","Granite","Marble","Slate","Shale",
|
|
918
|
+
"Sandstone","Limestone","Obsidian","Pumice","Tuff","Chert","Flint","Chalcedony","Carnelian","Sardonyx",
|
|
919
|
+
"Moonstone","Sunstone","Labradorite","Tanzanite","Kunzite","Rhodonite","Sodalite","Chrysocolla","Aventurine","Fluorite",
|
|
920
|
+
"Seraphinite","Charoite","Sugilite","Larimar","Prehnite","Danburite","Scolecite","Celestite","Amazonite","Howlite",
|
|
921
|
+
"Lepidolite","Storm","Thunder","Lightning","Breeze","Gale","Squall","Typhoon","Cyclone","Monsoon",
|
|
922
|
+
"Tempest","Zephyr","Chinook","Mistral","Sirocco","Foehn","Bora","Rain","Drizzle","Shower",
|
|
923
|
+
"Downpour","Deluge","Flood","Torrent","Current","Tide","Wave","Swell","Surf","Spray",
|
|
924
|
+
"Foam","Mist","Fog","Haze","Dew","Frost","Ice","Snow","Sleet","Hail",
|
|
925
|
+
"Rime","Thaw","Flow","Drift","Eddy","Vortex","Whirl","Spiral","Funnel","Column",
|
|
926
|
+
"Plume","Wisp","Streak","Band","Front","Trough","Rainbow","Halo","Mirage","Shimmer",
|
|
927
|
+
"Glimmer","Tower","Turret","Bastion","Citadel","Fortress","Castle","Palace","Manor","Lodge",
|
|
928
|
+
"Cabin","Cottage","Villa","Ranch","Barn","Silo","Mill","Foundry","Workshop","Studio",
|
|
929
|
+
"Gallery","Museum","Library","Archive","Treasury","Vault","Chamber","Hall","Court","Plaza",
|
|
930
|
+
"Garden","Balcony","Porch","Arcade","Colonnade","Pergola","Pavilion","Gazebo","Kiosk","Chapel",
|
|
931
|
+
"Temple","Shrine","Monastery","Abbey","Cathedral","Basilica","Minaret","Pagoda","Stupa","Pyramid",
|
|
932
|
+
"Arena","Stadium","Forum","Market","Bazaar","Emporium","Depot","Station","Terminal","Dock",
|
|
933
|
+
"Wharf","Pier","Jetty","Quay","Marina","Lighthouse","Watchtower","Outpost","Camp","Shelter",
|
|
934
|
+
"Refuge","Sanctuary","Haven","Retreat","Nest","Aerie","Burrow","Den","Lair","Roost",
|
|
935
|
+
"Perch","Bower","Arbor","Alcove","Sextant","Astrolabe","Sundial","Hourglass","Pendulum","Gyroscope",
|
|
936
|
+
"Turbine","Dynamo","Generator","Piston","Valve","Lever","Pulley","Winch","Derrick","Gantry",
|
|
937
|
+
"Scaffold","Trellis","Lattice","Grid","Net","Web","Axle","Gear","Cog","Sprocket",
|
|
938
|
+
"Chain","Link","Cable","Wire","Thread","Cord","Rope","Rigging","Mast","Boom",
|
|
939
|
+
"Spar","Keel","Hull","Rudder","Tiller","Wheel","Chart","Map","Gauge","Meter",
|
|
940
|
+
"Scale","Caliper","Ruler","Level","Plumb","Protractor","Template","Stencil","Mold","Die",
|
|
941
|
+
"Lathe","Bellows","Retort","Condenser","Centrifuge","Spectrometer","Oscilloscope","Theodolite","Transit","Sounding",
|
|
942
|
+
"Probe","Sensor","Relay","Switch","Circuit","Diode","Quest","Saga","Legend","Myth",
|
|
943
|
+
"Fable","Ballad","Anthem","Hymn","Ode","Sonnet","Verse","Stanza","Canto","Chorus",
|
|
944
|
+
"Refrain","Motif","Theme","Arc","Prologue","Epilogue","Prelude","Overture","Finale","Crescendo",
|
|
945
|
+
"Cadence","Tempo","Rhythm","Pulse","Beat","Tone","Note","Chord","Harmony","Melody",
|
|
946
|
+
"Lyric","Opus","Suite","Etude","Fugue","Canon","March","Waltz","Rondo","Aria",
|
|
947
|
+
"Duet","Trio","Quartet","Ensemble","Guild","League","Order","Council","Assembly","Conclave",
|
|
948
|
+
"Synod","Quorum","Cohort","Legion","Brigade","Battalion","Regiment","Division","Corps","Fleet",
|
|
949
|
+
"Squadron","Patrol","Convoy","Caravan","Expedition","Voyage","Journey","Trek","Odyssey","Passage",
|
|
950
|
+
"Crossing","Venture","Enterprise","Mission","Campaign","Crusade","Pilgrimage","Safari","Sortie","Foray",
|
|
951
|
+
"Rally","Charge","Advance","Concord","Ember","Blaze","Inferno","Pyre","Spark","Cinder",
|
|
952
|
+
"Smoke","Vapor","Steam","Nimbus","Cumulus","Cirrus","Stratus","Billow","Puff","Wake",
|
|
953
|
+
"Ripple","Surge","Ebb","Flux","Gush","Rush","Spring","Well","Font","Source",
|
|
954
|
+
"Origin","Root","Seed","Sprout","Bud","Bloom","Petal","Leaf","Branch","Limb",
|
|
955
|
+
"Trunk","Bark","Grain","Fiber","Pulp","Core","Sap","Resin","Nectar","Pollen",
|
|
956
|
+
"Spore","Frond","Tendril","Runner","Shoot","Stalk","Stem","Thorn","Burr","Cone",
|
|
957
|
+
"Acorn","Pinecone","Nutshell","Keystone","Capstone","Milestone","Cornerstone","Foundation","Bedrock","Pillar",
|
|
958
|
+
"Buttress","Parapet","Battlement","Barbican","Gatehouse","Drawbridge","Portcullis","Moat","Stockade","Palisade",
|
|
959
|
+
"Bulwark","Levee","Embankment","Causeway","Aqueduct","Viaduct","Trestle","Span","Lintel","Plinth",
|
|
960
|
+
"Pedestal","Dais","Rostrum","Podium","Stage","Platform","Deck","Landing","Berth","Channel",
|
|
961
|
+
"Lock","Weir","Dam","Spillway","Flume","Chute","Sluice","Nozzle","Spout","Faucet",
|
|
962
|
+
"Cistern","Wellspring","Headwater","Watershed","Alluvium","Loam","Humus","Peat","Marl","Silt",
|
|
963
|
+
"Writ","Clamp","Eyepiece","Traverse","Stride","Parley","Char","Trace","Confluence","Rampart",
|
|
964
|
+
]
|
|
965
|
+
|
|
966
|
+
BLOCKED_COMBOS = set() # Add any offensive adj+noun combinations here
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def derive_name(wallet: str, fmt: str = "display") -> str:
|
|
970
|
+
"""Derive a deterministic name from a Solana wallet address.
|
|
971
|
+
|
|
972
|
+
Args:
|
|
973
|
+
wallet: Base58 wallet address
|
|
974
|
+
fmt: Name format - "short", "display" (default), "tag", or "full"
|
|
975
|
+
|
|
976
|
+
Returns:
|
|
977
|
+
Formatted name string
|
|
978
|
+
"""
|
|
979
|
+
identity = derive_identity(wallet)
|
|
980
|
+
return identity[fmt] if fmt != "display" else identity["name"]
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def derive_identity(wallet: str) -> dict:
|
|
984
|
+
"""Derive the full identity bundle for a wallet address."""
|
|
985
|
+
domain = f"solnames-{SOLNAMES_VERSION}:"
|
|
986
|
+
hash_bytes = hashlib.sha256((domain + wallet).encode("utf-8")).digest()
|
|
987
|
+
hex_str = hash_bytes.hex()
|
|
988
|
+
|
|
989
|
+
# Seed PRNG from first 4 bytes (big-endian unsigned)
|
|
990
|
+
seed = struct.unpack(">I", hash_bytes[0:4])[0]
|
|
991
|
+
rng = _mulberry32(seed)
|
|
992
|
+
|
|
993
|
+
# Pick first adj+noun pair, retrying if blocked
|
|
994
|
+
adj1 = ADJECTIVES[math.floor(rng() * len(ADJECTIVES))]
|
|
995
|
+
noun1 = NOUNS[math.floor(rng() * len(NOUNS))]
|
|
996
|
+
while (adj1 + noun1) in BLOCKED_COMBOS:
|
|
997
|
+
adj1 = ADJECTIVES[math.floor(rng() * len(ADJECTIVES))]
|
|
998
|
+
noun1 = NOUNS[math.floor(rng() * len(NOUNS))]
|
|
999
|
+
|
|
1000
|
+
# Pick second adj+noun pair for full format
|
|
1001
|
+
adj2 = ADJECTIVES[math.floor(rng() * len(ADJECTIVES))]
|
|
1002
|
+
noun2 = NOUNS[math.floor(rng() * len(NOUNS))]
|
|
1003
|
+
|
|
1004
|
+
# Discriminator from bytes 8-9
|
|
1005
|
+
discriminator = hex_str[16:20]
|
|
1006
|
+
|
|
1007
|
+
return {
|
|
1008
|
+
"short": adj1,
|
|
1009
|
+
"name": adj1 + noun1,
|
|
1010
|
+
"tag": adj1 + noun1 + "#" + discriminator,
|
|
1011
|
+
"full": adj1 + noun1 + "-" + adj2 + noun2,
|
|
1012
|
+
"adjective": adj1,
|
|
1013
|
+
"noun": noun1,
|
|
1014
|
+
"hash": hex_str,
|
|
1015
|
+
"discriminator": discriminator,
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def generate_name(wallet: str, **kwargs) -> str:
|
|
1020
|
+
"""Deprecated: use derive_name() instead. Returns display format for backward compat."""
|
|
1021
|
+
return derive_name(wallet, "display")
|
|
1022
|
+
|
|
432
1023
|
# ─── CLI ──────────────────────────────────────────────────────
|
|
433
1024
|
|
|
434
1025
|
if __name__ == "__main__":
|
|
435
1026
|
import sys
|
|
436
1027
|
|
|
437
1028
|
if len(sys.argv) < 2:
|
|
438
|
-
print("Usage: python solfaces.py <wallet_address> [--svg] [--json] [--describe] [--size N]")
|
|
1029
|
+
print("Usage: python solfaces.py <wallet_address> [--svg] [--json] [--describe] [--size N] [--flat]")
|
|
439
1030
|
sys.exit(1)
|
|
440
1031
|
|
|
441
1032
|
wallet = sys.argv[1]
|
|
@@ -449,8 +1040,10 @@ if __name__ == "__main__":
|
|
|
449
1040
|
sys.exit(1)
|
|
450
1041
|
size = int(sys.argv[idx + 1])
|
|
451
1042
|
|
|
1043
|
+
use_flat = "--flat" in args
|
|
1044
|
+
|
|
452
1045
|
if "--svg" in args:
|
|
453
|
-
print(render_svg(wallet, size=size))
|
|
1046
|
+
print(render_svg(wallet, size=size, flat=use_flat))
|
|
454
1047
|
elif "--json" in args:
|
|
455
1048
|
import json
|
|
456
1049
|
t = generate_traits(wallet)
|
|
@@ -468,8 +1061,8 @@ if __name__ == "__main__":
|
|
|
468
1061
|
labels = get_trait_labels(t)
|
|
469
1062
|
print(f"SolFace for {wallet[:8]}...{wallet[-4:]}")
|
|
470
1063
|
print(f"Hash: {trait_hash(wallet)}")
|
|
471
|
-
print(
|
|
1064
|
+
print("---")
|
|
472
1065
|
for k, v in labels.items():
|
|
473
1066
|
print(f" {k}: {v}")
|
|
474
|
-
print(
|
|
1067
|
+
print("---")
|
|
475
1068
|
print(describe_appearance(wallet))
|