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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/dist/agent/index.cjs +51 -0
  4. package/dist/agent/index.cjs.map +1 -0
  5. package/dist/agent/index.d.cts +65 -0
  6. package/dist/agent/index.d.ts +65 -0
  7. package/dist/agent/index.js +6 -0
  8. package/dist/agent/index.js.map +1 -0
  9. package/dist/agent/mcp-server.cjs +836 -0
  10. package/dist/chunk-2DIKGLXZ.cjs +126 -0
  11. package/dist/chunk-2DIKGLXZ.cjs.map +1 -0
  12. package/dist/chunk-A6N3RPEA.cjs +111 -0
  13. package/dist/chunk-A6N3RPEA.cjs.map +1 -0
  14. package/dist/chunk-CVFO7YHY.cjs +97 -0
  15. package/dist/chunk-CVFO7YHY.cjs.map +1 -0
  16. package/dist/chunk-H3SK3MNX.cjs +409 -0
  17. package/dist/chunk-H3SK3MNX.cjs.map +1 -0
  18. package/dist/chunk-KSGFMW33.js +401 -0
  19. package/dist/chunk-KSGFMW33.js.map +1 -0
  20. package/dist/chunk-LQWJRHGC.js +86 -0
  21. package/dist/chunk-LQWJRHGC.js.map +1 -0
  22. package/dist/chunk-RX6D5FGH.js +211 -0
  23. package/dist/chunk-RX6D5FGH.js.map +1 -0
  24. package/dist/chunk-SNJABBAT.js +107 -0
  25. package/dist/chunk-SNJABBAT.js.map +1 -0
  26. package/dist/chunk-VMNATBH3.cjs +222 -0
  27. package/dist/chunk-VMNATBH3.cjs.map +1 -0
  28. package/dist/chunk-WURY4QGH.js +117 -0
  29. package/dist/chunk-WURY4QGH.js.map +1 -0
  30. package/dist/core/index.cjs +82 -0
  31. package/dist/core/index.cjs.map +1 -0
  32. package/dist/core/index.d.cts +104 -0
  33. package/dist/core/index.d.ts +104 -0
  34. package/dist/core/index.js +5 -0
  35. package/dist/core/index.js.map +1 -0
  36. package/dist/index.cjs +100 -0
  37. package/dist/index.cjs.map +1 -0
  38. package/dist/index.d.cts +4 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +7 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/react/index.cjs +543 -0
  43. package/dist/react/index.cjs.map +1 -0
  44. package/dist/react/index.d.cts +28 -0
  45. package/dist/react/index.d.ts +28 -0
  46. package/dist/react/index.js +541 -0
  47. package/dist/react/index.js.map +1 -0
  48. package/dist/solfaces.cdn.global.js +3 -0
  49. package/dist/solfaces.cdn.global.js.map +1 -0
  50. package/dist/themes/index.cjs +48 -0
  51. package/dist/themes/index.cjs.map +1 -0
  52. package/dist/themes/index.d.cts +14 -0
  53. package/dist/themes/index.d.ts +14 -0
  54. package/dist/themes/index.js +3 -0
  55. package/dist/themes/index.js.map +1 -0
  56. package/dist/traits-DAFZnXeS.d.cts +61 -0
  57. package/dist/traits-DAFZnXeS.d.ts +61 -0
  58. package/dist/vanilla/index.cjs +43 -0
  59. package/dist/vanilla/index.cjs.map +1 -0
  60. package/dist/vanilla/index.d.cts +7 -0
  61. package/dist/vanilla/index.d.ts +7 -0
  62. package/dist/vanilla/index.js +39 -0
  63. package/dist/vanilla/index.js.map +1 -0
  64. package/package.json +100 -0
  65. package/python/solfaces.py +475 -0
  66. 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))