solfaces 1.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/LICENSE +21 -0
- package/README.md +518 -0
- package/dist/agent/index.cjs +51 -0
- package/dist/agent/index.cjs.map +1 -0
- package/dist/agent/index.d.cts +65 -0
- package/dist/agent/index.d.ts +65 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/mcp-server.cjs +836 -0
- package/dist/chunk-2DIKGLXZ.cjs +126 -0
- package/dist/chunk-2DIKGLXZ.cjs.map +1 -0
- package/dist/chunk-A6N3RPEA.cjs +111 -0
- package/dist/chunk-A6N3RPEA.cjs.map +1 -0
- package/dist/chunk-CVFO7YHY.cjs +97 -0
- package/dist/chunk-CVFO7YHY.cjs.map +1 -0
- package/dist/chunk-H3SK3MNX.cjs +409 -0
- package/dist/chunk-H3SK3MNX.cjs.map +1 -0
- package/dist/chunk-KSGFMW33.js +401 -0
- package/dist/chunk-KSGFMW33.js.map +1 -0
- package/dist/chunk-LQWJRHGC.js +86 -0
- package/dist/chunk-LQWJRHGC.js.map +1 -0
- package/dist/chunk-RX6D5FGH.js +211 -0
- package/dist/chunk-RX6D5FGH.js.map +1 -0
- package/dist/chunk-SNJABBAT.js +107 -0
- package/dist/chunk-SNJABBAT.js.map +1 -0
- package/dist/chunk-VMNATBH3.cjs +222 -0
- package/dist/chunk-VMNATBH3.cjs.map +1 -0
- package/dist/chunk-WURY4QGH.js +117 -0
- package/dist/chunk-WURY4QGH.js.map +1 -0
- package/dist/core/index.cjs +82 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +104 -0
- package/dist/core/index.d.ts +104 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.cjs +100 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +543 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +28 -0
- package/dist/react/index.d.ts +28 -0
- package/dist/react/index.js +541 -0
- package/dist/react/index.js.map +1 -0
- package/dist/solfaces.cdn.global.js +3 -0
- package/dist/solfaces.cdn.global.js.map +1 -0
- package/dist/themes/index.cjs +48 -0
- package/dist/themes/index.cjs.map +1 -0
- package/dist/themes/index.d.cts +14 -0
- package/dist/themes/index.d.ts +14 -0
- package/dist/themes/index.js +3 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/traits-DAFZnXeS.d.cts +61 -0
- package/dist/traits-DAFZnXeS.d.ts +61 -0
- package/dist/vanilla/index.cjs +43 -0
- package/dist/vanilla/index.cjs.map +1 -0
- package/dist/vanilla/index.d.cts +7 -0
- package/dist/vanilla/index.d.ts +7 -0
- package/dist/vanilla/index.js +39 -0
- package/dist/vanilla/index.js.map +1 -0
- package/package.json +100 -0
- package/python/solfaces.py +475 -0
- package/skill.md +463 -0
package/package.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "solfaces",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Deterministic wallet avatars for the Solana ecosystem. Zero-dependency trait engine with React, vanilla JS, and server-side rendering support.",
|
|
5
|
+
"author": "https://github.com/jorger3301",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/jorger3301/solfaces"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://solfaces.dev",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"solana",
|
|
14
|
+
"avatar",
|
|
15
|
+
"wallet",
|
|
16
|
+
"identity",
|
|
17
|
+
"profile",
|
|
18
|
+
"pfp",
|
|
19
|
+
"generative",
|
|
20
|
+
"deterministic",
|
|
21
|
+
"svg",
|
|
22
|
+
"react",
|
|
23
|
+
"web3",
|
|
24
|
+
"defi",
|
|
25
|
+
"ai-agent",
|
|
26
|
+
"mcp",
|
|
27
|
+
"tool-use",
|
|
28
|
+
"function-calling"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"main": "./dist/index.cjs",
|
|
32
|
+
"module": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js",
|
|
38
|
+
"require": "./dist/index.cjs"
|
|
39
|
+
},
|
|
40
|
+
"./react": {
|
|
41
|
+
"types": "./dist/react/index.d.ts",
|
|
42
|
+
"import": "./dist/react/index.js",
|
|
43
|
+
"require": "./dist/react/index.cjs"
|
|
44
|
+
},
|
|
45
|
+
"./vanilla": {
|
|
46
|
+
"types": "./dist/vanilla/index.d.ts",
|
|
47
|
+
"import": "./dist/vanilla/index.js",
|
|
48
|
+
"require": "./dist/vanilla/index.cjs"
|
|
49
|
+
},
|
|
50
|
+
"./themes": {
|
|
51
|
+
"types": "./dist/themes/index.d.ts",
|
|
52
|
+
"import": "./dist/themes/index.js",
|
|
53
|
+
"require": "./dist/themes/index.cjs"
|
|
54
|
+
},
|
|
55
|
+
"./core": {
|
|
56
|
+
"types": "./dist/core/index.d.ts",
|
|
57
|
+
"import": "./dist/core/index.js",
|
|
58
|
+
"require": "./dist/core/index.cjs"
|
|
59
|
+
},
|
|
60
|
+
"./agent": {
|
|
61
|
+
"types": "./dist/agent/index.d.ts",
|
|
62
|
+
"import": "./dist/agent/index.js",
|
|
63
|
+
"require": "./dist/agent/index.cjs"
|
|
64
|
+
},
|
|
65
|
+
"./cdn": "./dist/solfaces.cdn.global.js"
|
|
66
|
+
},
|
|
67
|
+
"bin": {
|
|
68
|
+
"solfaces-mcp": "./dist/agent/mcp-server.cjs"
|
|
69
|
+
},
|
|
70
|
+
"files": [
|
|
71
|
+
"dist",
|
|
72
|
+
"python",
|
|
73
|
+
"skill.md",
|
|
74
|
+
"README.md",
|
|
75
|
+
"LICENSE"
|
|
76
|
+
],
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "tsup",
|
|
79
|
+
"dev": "tsup --watch",
|
|
80
|
+
"typecheck": "tsc --noEmit",
|
|
81
|
+
"lint": "eslint src/",
|
|
82
|
+
"prepublishOnly": "npm run build"
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"@types/node": "^25.3.2",
|
|
86
|
+
"@types/react": "^19.0.0",
|
|
87
|
+
"react": "^19.0.0",
|
|
88
|
+
"tsup": "^8.0.0",
|
|
89
|
+
"typescript": "^5.5.0"
|
|
90
|
+
},
|
|
91
|
+
"peerDependencies": {
|
|
92
|
+
"react": ">=18.0.0"
|
|
93
|
+
},
|
|
94
|
+
"peerDependenciesMeta": {
|
|
95
|
+
"react": {
|
|
96
|
+
"optional": true
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"sideEffects": false
|
|
100
|
+
}
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SOLFACES — Python Port
|
|
3
|
+
Deterministic wallet avatar generation for Python backends, bots, and scripts.
|
|
4
|
+
Zero dependencies. Generates identical traits to the JavaScript version.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from solfaces import generate_traits, render_svg, describe_appearance
|
|
8
|
+
|
|
9
|
+
traits = generate_traits("7xKXq...")
|
|
10
|
+
svg = render_svg("7xKXq...", size=256)
|
|
11
|
+
desc = describe_appearance("7xKXq...")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Optional, Dict, Any
|
|
17
|
+
import ctypes
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ─── Types ────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SolFaceTraits:
|
|
24
|
+
face_shape: int # 0-3
|
|
25
|
+
skin_color: int # 0-5
|
|
26
|
+
eye_style: int # 0-7
|
|
27
|
+
eye_color: int # 0-4
|
|
28
|
+
eyebrows: int # 0-4
|
|
29
|
+
nose: int # 0-3
|
|
30
|
+
mouth: int # 0-5
|
|
31
|
+
hair_style: int # 0-7
|
|
32
|
+
hair_color: int # 0-7
|
|
33
|
+
accessory: int # 0-5
|
|
34
|
+
bg_color: int # 0-4
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict[str, int]:
|
|
37
|
+
return {
|
|
38
|
+
"faceShape": self.face_shape,
|
|
39
|
+
"skinColor": self.skin_color,
|
|
40
|
+
"eyeStyle": self.eye_style,
|
|
41
|
+
"eyeColor": self.eye_color,
|
|
42
|
+
"eyebrows": self.eyebrows,
|
|
43
|
+
"nose": self.nose,
|
|
44
|
+
"mouth": self.mouth,
|
|
45
|
+
"hairStyle": self.hair_style,
|
|
46
|
+
"hairColor": self.hair_color,
|
|
47
|
+
"accessory": self.accessory,
|
|
48
|
+
"bgColor": self.bg_color,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ─── Color Palettes ──────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
SKIN_COLORS = ["#ffd5b0", "#f4c794", "#e0a370", "#c68642", "#8d5524", "#4a2c17"]
|
|
55
|
+
EYE_COLORS = ["#3d2b1f", "#4a80c4", "#5a9a5a", "#c89430", "#8a8a8a"]
|
|
56
|
+
HAIR_COLORS = ["#1a1a1a", "#6b3a2a", "#d4a844", "#c44a20", "#c8e64a", "#6090e0", "#14F195", "#e040c0"]
|
|
57
|
+
BG_COLORS = ["#c8e64a", "#6090e0", "#14F195", "#e8dcc8", "#f85149"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ─── Hashing (djb2) — exact JS parity ────────────────────────
|
|
61
|
+
|
|
62
|
+
def _djb2(s: str) -> int:
|
|
63
|
+
"""DJB2 hash — matches JavaScript implementation exactly."""
|
|
64
|
+
hash_val = 5381
|
|
65
|
+
for ch in s:
|
|
66
|
+
# Replicate JS: ((hash << 5) + hash + charCode) | 0
|
|
67
|
+
hash_val = ctypes.c_int32((hash_val << 5) + hash_val + ord(ch)).value
|
|
68
|
+
# Return unsigned 32-bit (>>> 0 in JS)
|
|
69
|
+
return hash_val & 0xFFFFFFFF
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ─── PRNG (mulberry32) — exact JS parity ─────────────────────
|
|
73
|
+
|
|
74
|
+
def _mulberry32(seed: int):
|
|
75
|
+
"""Mulberry32 PRNG — matches JavaScript implementation exactly."""
|
|
76
|
+
s = ctypes.c_int32(seed).value
|
|
77
|
+
|
|
78
|
+
def next_val() -> float:
|
|
79
|
+
nonlocal s
|
|
80
|
+
s = ctypes.c_int32(s + 0x6D2B79F5).value
|
|
81
|
+
|
|
82
|
+
# Math.imul(s ^ (s >>> 15), 1 | s)
|
|
83
|
+
a = (s ^ ((s & 0xFFFFFFFF) >> 15)) & 0xFFFFFFFF
|
|
84
|
+
b = (1 | s) & 0xFFFFFFFF
|
|
85
|
+
t = ctypes.c_int32(_imul(a, b)).value
|
|
86
|
+
|
|
87
|
+
# (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
88
|
+
c = (t ^ ((t & 0xFFFFFFFF) >> 7)) & 0xFFFFFFFF
|
|
89
|
+
d = (61 | t) & 0xFFFFFFFF
|
|
90
|
+
old_t = t
|
|
91
|
+
t = (ctypes.c_int32(old_t + _imul(c, d)).value) ^ old_t
|
|
92
|
+
|
|
93
|
+
# ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
94
|
+
result = ((t ^ ((t & 0xFFFFFFFF) >> 14)) & 0xFFFFFFFF)
|
|
95
|
+
return result / 4294967296
|
|
96
|
+
|
|
97
|
+
return next_val
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _imul(a: int, b: int) -> int:
|
|
101
|
+
"""Emulate Math.imul — 32-bit integer multiply."""
|
|
102
|
+
a = a & 0xFFFFFFFF
|
|
103
|
+
b = b & 0xFFFFFFFF
|
|
104
|
+
result = (a * b) & 0xFFFFFFFF
|
|
105
|
+
if result >= 0x80000000:
|
|
106
|
+
result -= 0x100000000
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ─── Trait Generation ─────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
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
|
+
seed = _djb2(wallet_address)
|
|
118
|
+
rand = _mulberry32(seed)
|
|
119
|
+
|
|
120
|
+
return SolFaceTraits(
|
|
121
|
+
face_shape=int(rand() * 4),
|
|
122
|
+
skin_color=int(rand() * 6),
|
|
123
|
+
eye_style=int(rand() * 8),
|
|
124
|
+
eye_color=int(rand() * 5),
|
|
125
|
+
eyebrows=int(rand() * 5),
|
|
126
|
+
nose=int(rand() * 4),
|
|
127
|
+
mouth=int(rand() * 6),
|
|
128
|
+
hair_style=int(rand() * 8),
|
|
129
|
+
hair_color=int(rand() * 8),
|
|
130
|
+
accessory=int(rand() * 6),
|
|
131
|
+
bg_color=int(rand() * 5),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def trait_hash(wallet_address: str) -> str:
|
|
136
|
+
"""Return 8-char hex hash for cache keys."""
|
|
137
|
+
return f"{_djb2(wallet_address):08x}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ─── Trait Labels ─────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
FACE_LABELS = ["Round", "Square", "Oval", "Hexagon"]
|
|
143
|
+
EYE_LABELS = ["Round", "Dots", "Almond", "Wide", "Sleepy", "Winking", "Lashes", "Narrow"]
|
|
144
|
+
BROW_LABELS = ["None", "Thin", "Thick", "Arched", "Angled"]
|
|
145
|
+
NOSE_LABELS = ["None", "Dot", "Triangle", "Button"]
|
|
146
|
+
MOUTH_LABELS = ["Smile", "Neutral", "Grin", "Open", "Smirk", "Wide Smile"]
|
|
147
|
+
HAIR_LABELS = ["Bald", "Short", "Spiky", "Swept", "Mohawk", "Long", "Bob", "Buzz"]
|
|
148
|
+
ACC_LABELS = ["None", "None", "Round Glasses", "Square Glasses", "Earring", "Bandana"]
|
|
149
|
+
HAIR_COLOR_LABELS = ["Black", "Brown", "Blonde", "Ginger", "Neon Lime", "Neon Blue", "Solana Mint", "Neon Magenta"]
|
|
150
|
+
SKIN_LABELS = ["Light Peach", "Warm Tan", "Golden Brown", "Medium Brown", "Deep Brown", "Rich Dark Brown"]
|
|
151
|
+
EYE_COLOR_LABELS = ["Dark Brown", "Blue", "Green", "Amber", "Gray"]
|
|
152
|
+
BG_COLOR_LABELS = ["Lime", "Blue", "Mint", "Sand", "Red"]
|
|
153
|
+
|
|
154
|
+
def get_trait_labels(traits: SolFaceTraits) -> Dict[str, str]:
|
|
155
|
+
"""Human-readable labels for all traits."""
|
|
156
|
+
return {
|
|
157
|
+
"faceShape": FACE_LABELS[traits.face_shape],
|
|
158
|
+
"skinColor": SKIN_LABELS[traits.skin_color] if traits.skin_color < len(SKIN_LABELS) else "Warm Tan",
|
|
159
|
+
"eyeStyle": EYE_LABELS[traits.eye_style],
|
|
160
|
+
"eyeColor": EYE_COLOR_LABELS[traits.eye_color] if traits.eye_color < len(EYE_COLOR_LABELS) else "Dark Brown",
|
|
161
|
+
"eyebrows": BROW_LABELS[traits.eyebrows],
|
|
162
|
+
"nose": NOSE_LABELS[traits.nose],
|
|
163
|
+
"mouth": MOUTH_LABELS[traits.mouth],
|
|
164
|
+
"hairStyle": HAIR_LABELS[traits.hair_style],
|
|
165
|
+
"hairColor": HAIR_COLOR_LABELS[traits.hair_color],
|
|
166
|
+
"accessory": ACC_LABELS[traits.accessory],
|
|
167
|
+
"bgColor": BG_COLOR_LABELS[traits.bg_color] if traits.bg_color < len(BG_COLOR_LABELS) else "Lime",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ─── SVG Rendering ────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
def _render_face(t: SolFaceTraits, skin: str) -> str:
|
|
174
|
+
if t.face_shape == 0: return f'<circle cx="32" cy="34" r="20" fill="{skin}"/>'
|
|
175
|
+
if t.face_shape == 1: return f'<rect x="12" y="14" width="40" height="40" rx="8" ry="8" fill="{skin}"/>'
|
|
176
|
+
if t.face_shape == 2: return f'<ellipse cx="32" cy="34" rx="18" ry="22" fill="{skin}"/>'
|
|
177
|
+
if t.face_shape == 3: return f'<path d="M32 12 L50 24 L50 44 L32 56 L14 44 L14 24 Z" fill="{skin}" stroke-linejoin="round"/>'
|
|
178
|
+
return f'<circle cx="32" cy="34" r="20" fill="{skin}"/>'
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _render_eyes(t: SolFaceTraits, c: str, w: str = "white") -> str:
|
|
182
|
+
l, r, y = 24, 40, 30
|
|
183
|
+
if t.eye_style == 0:
|
|
184
|
+
return f'<circle cx="{l}" cy="{y}" r="3.5" fill="{w}"/><circle cx="{l+1}" cy="{y}" r="2" fill="{c}"/><circle cx="{r}" cy="{y}" r="3.5" fill="{w}"/><circle cx="{r+1}" cy="{y}" r="2" fill="{c}"/>'
|
|
185
|
+
if t.eye_style == 1:
|
|
186
|
+
return f'<circle cx="{l}" cy="{y}" r="2" fill="{c}"/><circle cx="{r}" cy="{y}" r="2" fill="{c}"/>'
|
|
187
|
+
if t.eye_style == 2:
|
|
188
|
+
return f'<ellipse cx="{l}" cy="{y}" rx="4" ry="2.5" fill="{w}"/><circle cx="{l+0.5}" cy="{y}" r="1.5" fill="{c}"/><ellipse cx="{r}" cy="{y}" rx="4" ry="2.5" fill="{w}"/><circle cx="{r+0.5}" cy="{y}" r="1.5" fill="{c}"/>'
|
|
189
|
+
if t.eye_style == 3:
|
|
190
|
+
return f'<circle cx="{l}" cy="{y}" r="4.5" fill="{w}"/><circle cx="{l}" cy="{y+0.5}" r="2.5" fill="{c}"/><circle cx="{r}" cy="{y}" r="4.5" fill="{w}"/><circle cx="{r}" cy="{y+0.5}" r="2.5" fill="{c}"/>'
|
|
191
|
+
if t.eye_style == 4:
|
|
192
|
+
return f'<ellipse cx="{l}" cy="{y+1}" rx="3.5" ry="2" fill="{w}"/><circle cx="{l}" cy="{y+1}" r="1.5" fill="{c}"/><line x1="{l-4}" y1="{y-0.5}" x2="{l+4}" y2="{y-0.5}" stroke="{c}" stroke-width="1" stroke-linecap="round"/><ellipse cx="{r}" cy="{y+1}" rx="3.5" ry="2" fill="{w}"/><circle cx="{r}" cy="{y+1}" r="1.5" fill="{c}"/><line x1="{r-4}" y1="{y-0.5}" x2="{r+4}" y2="{y-0.5}" stroke="{c}" stroke-width="1" stroke-linecap="round"/>'
|
|
193
|
+
if t.eye_style == 5:
|
|
194
|
+
return f'<path d="M{l-3} {y} Q{l} {y+3} {l+3} {y}" fill="none" stroke="{c}" stroke-width="1.5" stroke-linecap="round"/><circle cx="{r}" cy="{y}" r="3.5" fill="{w}"/><circle cx="{r+1}" cy="{y}" r="2" fill="{c}"/>'
|
|
195
|
+
if t.eye_style == 6:
|
|
196
|
+
return f'<circle cx="{l}" cy="{y}" r="3" fill="{w}"/><circle cx="{l+0.5}" cy="{y}" r="1.5" fill="{c}"/><line x1="{l+2}" y1="{y-3}" x2="{l+3.5}" y2="{y-4.5}" stroke="{c}" stroke-width="0.8" stroke-linecap="round"/><line x1="{l+3}" y1="{y-2}" x2="{l+4.5}" y2="{y-3}" stroke="{c}" stroke-width="0.8" stroke-linecap="round"/><circle cx="{r}" cy="{y}" r="3" fill="{w}"/><circle cx="{r+0.5}" cy="{y}" r="1.5" fill="{c}"/><line x1="{r+2}" y1="{y-3}" x2="{r+3.5}" y2="{y-4.5}" stroke="{c}" stroke-width="0.8" stroke-linecap="round"/><line x1="{r+3}" y1="{y-2}" x2="{r+4.5}" y2="{y-3}" stroke="{c}" stroke-width="0.8" stroke-linecap="round"/>'
|
|
197
|
+
if t.eye_style == 7:
|
|
198
|
+
return f'<ellipse cx="{l}" cy="{y}" rx="4" ry="1.2" fill="{w}"/><ellipse cx="{l+0.5}" cy="{y}" rx="2" ry="1" fill="{c}"/><ellipse cx="{r}" cy="{y}" rx="4" ry="1.2" fill="{w}"/><ellipse cx="{r+0.5}" cy="{y}" rx="2" ry="1" fill="{c}"/>'
|
|
199
|
+
return f'<circle cx="{l}" cy="{y}" r="3" fill="{w}"/><circle cx="{l+1}" cy="{y}" r="2" fill="{c}"/><circle cx="{r}" cy="{y}" r="3" fill="{w}"/><circle cx="{r+1}" cy="{y}" r="2" fill="{c}"/>'
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _render_eyebrows(t: SolFaceTraits, col: str = "#2a2020") -> str:
|
|
203
|
+
l, r, y = 24, 40, 25
|
|
204
|
+
if t.eyebrows == 0: return ""
|
|
205
|
+
if t.eyebrows == 1: return f'<line x1="{l-3}" y1="{y}" x2="{l+3}" y2="{y}" stroke="{col}" stroke-width="0.8" stroke-linecap="round"/><line x1="{r-3}" y1="{y}" x2="{r+3}" y2="{y}" stroke="{col}" stroke-width="0.8" stroke-linecap="round"/>'
|
|
206
|
+
if t.eyebrows == 2: return f'<line x1="{l-3.5}" y1="{y}" x2="{l+3.5}" y2="{y}" stroke="{col}" stroke-width="2" stroke-linecap="round"/><line x1="{r-3.5}" y1="{y}" x2="{r+3.5}" y2="{y}" stroke="{col}" stroke-width="2" stroke-linecap="round"/>'
|
|
207
|
+
if t.eyebrows == 3: return f'<path d="M{l-3.5} {y+1} Q{l} {y-2} {l+3.5} {y+1}" fill="none" stroke="{col}" stroke-width="1" stroke-linecap="round"/><path d="M{r-3.5} {y+1} Q{r} {y-2} {r+3.5} {y+1}" fill="none" stroke="{col}" stroke-width="1" stroke-linecap="round"/>'
|
|
208
|
+
if t.eyebrows == 4: return f'<line x1="{l-3}" y1="{y-1}" x2="{l+3}" y2="{y+1}" stroke="{col}" stroke-width="1.2" stroke-linecap="round"/><line x1="{r-3}" y1="{y+1}" x2="{r+3}" y2="{y-1}" stroke="{col}" stroke-width="1.2" stroke-linecap="round"/>'
|
|
209
|
+
return ""
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _render_nose(t: SolFaceTraits, skin: str, nose_col: Optional[str] = None) -> str:
|
|
213
|
+
cx, y = 32, 36
|
|
214
|
+
sh = nose_col if nose_col else (skin + "aa")
|
|
215
|
+
if t.nose == 0: return ""
|
|
216
|
+
if t.nose == 1: return f'<circle cx="{cx}" cy="{y}" r="1.5" fill="{sh}"/>'
|
|
217
|
+
if t.nose == 2: return f'<path d="M{cx} {y-1.5} L{cx+2.5} {y+2} L{cx-2.5} {y+2} Z" fill="{sh}"/>'
|
|
218
|
+
if t.nose == 3: return f'<circle cx="{cx-1.5}" cy="{y}" r="1" fill="{sh}"/><circle cx="{cx+1.5}" cy="{y}" r="1" fill="{sh}"/>'
|
|
219
|
+
return ""
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _render_mouth(t: SolFaceTraits, col: str = "#c05050", teeth_col: str = "white") -> str:
|
|
223
|
+
cx, y = 32, 42
|
|
224
|
+
if t.mouth == 0: return f'<path d="M{cx-4} {y} Q{cx} {y+4} {cx+4} {y}" fill="none" stroke="{col}" stroke-width="1.2" stroke-linecap="round"/>'
|
|
225
|
+
if t.mouth == 1: return f'<line x1="{cx-3}" y1="{y+1}" x2="{cx+3}" y2="{y+1}" stroke="{col}" stroke-width="1.2" stroke-linecap="round"/>'
|
|
226
|
+
if t.mouth == 2: return f'<path d="M{cx-6} {y} Q{cx} {y+5} {cx+6} {y}" fill="none" stroke="{col}" stroke-width="1.5" stroke-linecap="round"/>'
|
|
227
|
+
if t.mouth == 3: return f'<ellipse cx="{cx}" cy="{y+1}" rx="3" ry="2.5" fill="{col}" opacity="0.8"/>'
|
|
228
|
+
if t.mouth == 4: return f'<path d="M{cx-4} {y+1} Q{cx-1} {y+1} {cx+4} {y-1}" fill="none" stroke="{col}" stroke-width="1.2" stroke-linecap="round"/>'
|
|
229
|
+
if t.mouth == 5: return f'<path d="M{cx-6} {y} Q{cx} {y+6} {cx+6} {y}" fill="{teeth_col}" stroke="{col}" stroke-width="1"/>'
|
|
230
|
+
return f'<path d="M{cx-4} {y} Q{cx} {y+4} {cx+4} {y}" fill="none" stroke="{col}" stroke-width="1.2" stroke-linecap="round"/>'
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _render_hair(t: SolFaceTraits, col: str) -> str:
|
|
234
|
+
if t.hair_style == 0: return ""
|
|
235
|
+
if t.hair_style == 1: return f'<rect x="14" y="12" width="36" height="12" rx="6" ry="6" fill="{col}"/>'
|
|
236
|
+
if t.hair_style == 2: return f'<g fill="{col}"><rect x="14" y="16" width="36" height="8" rx="2"/><polygon points="18,16 22,6 26,16"/><polygon points="26,16 30,4 34,16"/><polygon points="34,16 38,6 42,16"/><polygon points="42,16 46,10 48,16"/></g>'
|
|
237
|
+
if t.hair_style == 3: return f'<g fill="{col}"><rect x="14" y="14" width="36" height="10" rx="4"/><path d="M14 18 Q8 14 10 8 Q14 10 20 14 Z"/></g>'
|
|
238
|
+
if t.hair_style == 4: return f'<rect x="26" y="4" width="12" height="20" rx="4" ry="2" fill="{col}"/>'
|
|
239
|
+
if t.hair_style == 5: return f'<g fill="{col}"><rect x="14" y="12" width="36" height="10" rx="4"/><rect x="10" y="18" width="8" height="24" rx="3"/><rect x="46" y="18" width="8" height="24" rx="3"/></g>'
|
|
240
|
+
if t.hair_style == 6: return f'<path d="M12 22 Q12 10 32 10 Q52 10 52 22 L52 38 Q52 42 48 42 L48 26 Q48 16 32 16 Q16 16 16 26 L16 42 Q12 42 12 38 Z" fill="{col}"/>'
|
|
241
|
+
if t.hair_style == 7: return f'<rect x="15" y="13" width="34" height="9" rx="8" ry="4" fill="{col}" opacity="0.7"/>'
|
|
242
|
+
return ""
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _render_accessory(t: SolFaceTraits, col: str = "#444") -> str:
|
|
246
|
+
if t.accessory <= 1: return ""
|
|
247
|
+
if t.accessory == 2: return f'<g fill="none" stroke="{col}" stroke-width="1"><circle cx="24" cy="30" r="5"/><circle cx="40" cy="30" r="5"/><line x1="29" y1="30" x2="35" y2="30"/><line x1="19" y1="30" x2="14" y2="28"/><line x1="45" y1="30" x2="50" y2="28"/></g>'
|
|
248
|
+
if t.accessory == 3: return f'<g fill="none" stroke="{col}" stroke-width="1"><rect x="19" y="26" width="10" height="8" rx="1"/><rect x="35" y="26" width="10" height="8" rx="1"/><line x1="29" y1="30" x2="35" y2="30"/><line x1="19" y1="30" x2="14" y2="28"/><line x1="45" y1="30" x2="50" y2="28"/></g>'
|
|
249
|
+
if t.accessory == 4: return f'<circle cx="11" cy="36" r="2" fill="{col}" stroke="{col}" stroke-width="0.5"/>'
|
|
250
|
+
if t.accessory == 5: return f'<g><rect x="12" y="20" width="40" height="4" rx="1" fill="{col}"/><path d="M12 22 L8 26 L12 24 Z" fill="{col}"/></g>'
|
|
251
|
+
return ""
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def render_svg(
|
|
255
|
+
wallet_address: str,
|
|
256
|
+
size: int = 64,
|
|
257
|
+
bg_opacity: float = 0.15,
|
|
258
|
+
bg_radius: int = 4,
|
|
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,
|
|
265
|
+
) -> 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
|
+
t = generate_traits(wallet_address)
|
|
275
|
+
co = color_overrides or {}
|
|
276
|
+
|
|
277
|
+
skin = co.get("skin") or SKIN_COLORS[t.skin_color % len(SKIN_COLORS)]
|
|
278
|
+
eye_col = co.get("eyes") or EYE_COLORS[t.eye_color % len(EYE_COLORS)]
|
|
279
|
+
hair_col = co.get("hair") or HAIR_COLORS[t.hair_color % len(HAIR_COLORS)]
|
|
280
|
+
bg_col = co.get("bg") or BG_COLORS[t.bg_color % len(BG_COLORS)]
|
|
281
|
+
mouth_col = co.get("mouth") or mouth_color
|
|
282
|
+
brow_col = co.get("eyebrow") or eyebrow_color
|
|
283
|
+
acc_col = co.get("accessory") or accessory_color
|
|
284
|
+
eye_white = co.get("eye_white") or eye_white_color
|
|
285
|
+
nose_col = co.get("nose") or nose_color
|
|
286
|
+
|
|
287
|
+
parts = [
|
|
288
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="{size}" height="{size}">',
|
|
289
|
+
f'<rect x="0" y="0" width="64" height="64" fill="{bg_col}" opacity="{bg_opacity}" rx="{bg_radius}"/>',
|
|
290
|
+
_render_hair(t, hair_col),
|
|
291
|
+
_render_face(t, skin),
|
|
292
|
+
_render_eyes(t, eye_col, eye_white),
|
|
293
|
+
_render_eyebrows(t, brow_col),
|
|
294
|
+
_render_nose(t, skin, nose_col),
|
|
295
|
+
_render_mouth(t, mouth_col, eye_white),
|
|
296
|
+
_render_accessory(t, acc_col),
|
|
297
|
+
"</svg>",
|
|
298
|
+
]
|
|
299
|
+
return "".join(p for p in parts if p)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def render_data_uri(wallet_address: str, **kwargs) -> str:
|
|
303
|
+
"""Render as a data URI for use in <img> tags or HTML emails."""
|
|
304
|
+
from urllib.parse import quote
|
|
305
|
+
svg = render_svg(wallet_address, **kwargs)
|
|
306
|
+
return f"data:image/svg+xml;charset=utf-8,{quote(svg)}"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ─── Description ──────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
_FACE_DESC = {0: "round", 1: "square with softly rounded corners", 2: "oval", 3: "angular, hexagonal"}
|
|
312
|
+
_SKIN_DESC = {0: "light peach", 1: "warm tan", 2: "golden brown", 3: "medium brown", 4: "deep brown", 5: "rich dark brown"}
|
|
313
|
+
_EYE_STYLE_DESC = {0: "round, wide-open", 1: "small and dot-like", 2: "almond-shaped", 3: "wide and expressive", 4: "sleepy, half-lidded", 5: "playfully winking", 6: "adorned with lashes", 7: "narrow and observant"}
|
|
314
|
+
_EYE_COLOR_DESC = {0: "dark brown", 1: "blue", 2: "green", 3: "amber", 4: "gray"}
|
|
315
|
+
_BROW_DESC = {0: "", 1: "thin", 2: "thick, prominent", 3: "elegantly arched", 4: "sharply angled"}
|
|
316
|
+
_NOSE_DESC = {0: "", 1: "a small dot nose", 2: "a triangular nose", 3: "a button nose with visible nostrils"}
|
|
317
|
+
_HAIR_STYLE_DESC = {0: "bald, with no hair", 1: "short, neatly cropped hair", 2: "tall, spiky hair", 3: "side-swept hair", 4: "a bold mohawk", 5: "long hair that falls past the shoulders", 6: "a clean bob cut", 7: "a close buzz cut"}
|
|
318
|
+
_HAIR_COLOR_DESC = {0: "jet black", 1: "brown", 2: "blonde", 3: "ginger red", 4: "neon lime green", 5: "neon blue", 6: "Solana mint green", 7: "neon magenta"}
|
|
319
|
+
_MOUTH_DESC = {0: "a gentle smile", 1: "a neutral, straight expression", 2: "a wide grin", 3: "a small, open mouth", 4: "a confident smirk", 5: "a broad, toothy smile"}
|
|
320
|
+
_ACC_DESC = {0: "", 1: "", 2: "round glasses", 3: "square-framed glasses", 4: "a gold earring", 5: "a red bandana"}
|
|
321
|
+
_BG_DESC = {0: "lime green", 1: "blue", 2: "Solana mint green", 3: "warm sand", 4: "red"}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def describe_appearance(
|
|
325
|
+
wallet_address: str,
|
|
326
|
+
perspective: str = "third",
|
|
327
|
+
name: Optional[str] = None,
|
|
328
|
+
include_background: bool = True,
|
|
329
|
+
) -> 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
|
+
if perspective == "first":
|
|
346
|
+
subj = f"I'm {name}. I have" if name else "I have"
|
|
347
|
+
im = "I'm"
|
|
348
|
+
else:
|
|
349
|
+
subj = f"{name} has" if name else "This SolFace has"
|
|
350
|
+
im = "They're"
|
|
351
|
+
|
|
352
|
+
# Build parts list
|
|
353
|
+
parts = []
|
|
354
|
+
|
|
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
|
+
eye_s = _EYE_STYLE_DESC.get(t.eye_style, "round")
|
|
362
|
+
eye_c = _EYE_COLOR_DESC.get(t.eye_color, "dark")
|
|
363
|
+
parts.append(f"{eye_s} {eye_c} eyes")
|
|
364
|
+
|
|
365
|
+
# Eyebrows
|
|
366
|
+
brows = _BROW_DESC.get(t.eyebrows, "")
|
|
367
|
+
if brows:
|
|
368
|
+
parts.append(f"{brows} eyebrows")
|
|
369
|
+
|
|
370
|
+
# Hair
|
|
371
|
+
if t.hair_style == 0:
|
|
372
|
+
parts.append("and is bald")
|
|
373
|
+
else:
|
|
374
|
+
hc = _HAIR_COLOR_DESC.get(t.hair_color, "")
|
|
375
|
+
hs = _HAIR_STYLE_DESC.get(t.hair_style, "")
|
|
376
|
+
if hs.startswith("a "):
|
|
377
|
+
parts.append(f"and a {hc} {hs[2:]}")
|
|
378
|
+
else:
|
|
379
|
+
parts.append(f"and {hc} {hs}")
|
|
380
|
+
|
|
381
|
+
# Assemble main sentence
|
|
382
|
+
desc = parts[0]
|
|
383
|
+
if len(parts) > 2:
|
|
384
|
+
desc += ", " + ", ".join(parts[1:-1]) + ", " + parts[-1]
|
|
385
|
+
elif len(parts) == 2:
|
|
386
|
+
desc += " and " + parts[1]
|
|
387
|
+
desc += "."
|
|
388
|
+
|
|
389
|
+
# Nose
|
|
390
|
+
nose = _NOSE_DESC.get(t.nose, "")
|
|
391
|
+
if nose:
|
|
392
|
+
if perspective == "first":
|
|
393
|
+
nose_subj = "I have"
|
|
394
|
+
else:
|
|
395
|
+
nose_subj = f"{name} has" if name else "They have"
|
|
396
|
+
desc += f" {nose_subj} {nose}."
|
|
397
|
+
|
|
398
|
+
# Accessory
|
|
399
|
+
acc = _ACC_DESC.get(t.accessory, "")
|
|
400
|
+
if acc:
|
|
401
|
+
desc += f" {im} wearing {acc}."
|
|
402
|
+
|
|
403
|
+
# Mouth
|
|
404
|
+
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"
|
|
409
|
+
desc += f" {mouth_subj} {mouth}."
|
|
410
|
+
|
|
411
|
+
# Background
|
|
412
|
+
if include_background:
|
|
413
|
+
bg = _BG_DESC.get(t.bg_color, "colorful")
|
|
414
|
+
desc += f" The background is {bg}."
|
|
415
|
+
|
|
416
|
+
return desc
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def agent_appearance_prompt(wallet_address: str, agent_name: Optional[str] = None) -> str:
|
|
420
|
+
"""
|
|
421
|
+
Generate a system prompt snippet describing an AI agent's SolFace.
|
|
422
|
+
|
|
423
|
+
Usage:
|
|
424
|
+
prompt = agent_appearance_prompt("7xKXq...", "Atlas")
|
|
425
|
+
system_prompt = f"You are Atlas, an AI agent. {prompt}"
|
|
426
|
+
"""
|
|
427
|
+
desc = describe_appearance(wallet_address, perspective="first", name=agent_name, include_background=False)
|
|
428
|
+
h = trait_hash(wallet_address)
|
|
429
|
+
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
|
+
|
|
431
|
+
|
|
432
|
+
# ─── CLI ──────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
if __name__ == "__main__":
|
|
435
|
+
import sys
|
|
436
|
+
|
|
437
|
+
if len(sys.argv) < 2:
|
|
438
|
+
print("Usage: python solfaces.py <wallet_address> [--svg] [--json] [--describe] [--size N]")
|
|
439
|
+
sys.exit(1)
|
|
440
|
+
|
|
441
|
+
wallet = sys.argv[1]
|
|
442
|
+
args = set(sys.argv[2:])
|
|
443
|
+
|
|
444
|
+
size = 64
|
|
445
|
+
if "--size" in args:
|
|
446
|
+
idx = sys.argv.index("--size")
|
|
447
|
+
if idx + 1 >= len(sys.argv):
|
|
448
|
+
print("Error: --size requires a value", file=sys.stderr)
|
|
449
|
+
sys.exit(1)
|
|
450
|
+
size = int(sys.argv[idx + 1])
|
|
451
|
+
|
|
452
|
+
if "--svg" in args:
|
|
453
|
+
print(render_svg(wallet, size=size))
|
|
454
|
+
elif "--json" in args:
|
|
455
|
+
import json
|
|
456
|
+
t = generate_traits(wallet)
|
|
457
|
+
print(json.dumps({
|
|
458
|
+
"wallet": wallet,
|
|
459
|
+
"hash": trait_hash(wallet),
|
|
460
|
+
"traits": t.to_dict(),
|
|
461
|
+
"labels": get_trait_labels(t),
|
|
462
|
+
"description": describe_appearance(wallet),
|
|
463
|
+
}, indent=2))
|
|
464
|
+
elif "--describe" in args:
|
|
465
|
+
print(describe_appearance(wallet))
|
|
466
|
+
else:
|
|
467
|
+
t = generate_traits(wallet)
|
|
468
|
+
labels = get_trait_labels(t)
|
|
469
|
+
print(f"SolFace for {wallet[:8]}...{wallet[-4:]}")
|
|
470
|
+
print(f"Hash: {trait_hash(wallet)}")
|
|
471
|
+
print(f"---")
|
|
472
|
+
for k, v in labels.items():
|
|
473
|
+
print(f" {k}: {v}")
|
|
474
|
+
print(f"---")
|
|
475
|
+
print(describe_appearance(wallet))
|