Fraser commited on
Commit
cc4ae68
·
1 Parent(s): 542f3b7
Files changed (5) hide show
  1. API_DOCUMENTATION.md +287 -236
  2. CLAUDE.md +196 -0
  3. README.md +125 -6
  4. app.py +545 -867
  5. requirements.txt +3 -1
API_DOCUMENTATION.md CHANGED
@@ -1,8 +1,8 @@
1
- # Piclets Server API Documentation
2
 
3
  ## Overview
4
 
5
- The Piclets Server provides a Gradio-based API for saving and sharing AI-generated creatures (Piclets) from the Piclets game. This backend supports the main game client with persistent storage and sharing capabilities.
6
 
7
  ## Quick Start
8
 
@@ -16,325 +16,376 @@ python app.py
16
  - **Web Interface**: http://localhost:7860
17
  - **Programmatic Access**: Use Gradio Client to connect to the space
18
 
 
 
 
 
 
 
 
 
 
 
 
19
  ## API Endpoints
20
 
21
- ### 1. Save Piclet
22
- **Endpoint**: `/save_piclet_api`
 
23
  **Method**: Gradio function call
24
- **Purpose**: Save a new Piclet with optional image
25
 
26
  **Input Parameters**:
27
- - `piclet_json` (string): JSON-formatted Piclet data
28
- - `image_file` (file, optional): Image file for the Piclet
 
 
 
 
 
 
29
 
30
- **Response Format**:
31
  ```json
32
  {
33
- "success": true,
34
- "piclet_id": "uuid-string",
35
- "message": "Piclet saved successfully"
36
  }
37
  ```
38
 
39
- **Error Response**:
40
  ```json
41
  {
42
- "success": false,
43
- "error": "Error description"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
  ```
46
 
47
- ### 2. Get Piclet
48
- **Endpoint**: `/get_piclet_api`
 
 
 
 
 
 
 
 
 
 
 
49
  **Method**: Gradio function call
50
- **Purpose**: Retrieve a Piclet by ID
51
 
52
  **Input Parameters**:
53
- - `piclet_id` (string): Unique Piclet identifier
 
 
 
 
 
 
54
 
55
- **Response Format**:
56
  ```json
57
  {
58
  "success": true,
 
59
  "piclet": {
60
- "id": "uuid-string",
61
- "name": "Piclet Name",
62
- "description": "Description",
63
- "tier": "medium",
64
- "primaryType": "space",
65
- "baseStats": { ... },
66
- "movepool": [ ... ],
67
- "created_at": "2024-07-26T10:30:00"
68
  }
69
  }
70
  ```
71
 
72
- ### 3. List Piclets
73
- **Endpoint**: `/list_piclets_api`
 
 
 
 
 
 
 
 
 
74
  **Method**: Gradio function call
75
- **Purpose**: Get a list of all saved Piclets
76
 
77
  **Input Parameters**:
78
- - `limit` (integer): Maximum number of results (1-100, default: 50)
 
 
 
 
 
 
 
 
79
 
80
- **Response Format**:
81
  ```json
82
  {
83
  "success": true,
84
- "piclets": [
85
- {
86
- "id": "uuid-string",
87
- "name": "Piclet Name",
88
- "primaryType": "space",
89
- "tier": "medium",
90
- "created_at": "2024-07-26T10:30:00",
91
- "has_image": true
92
- }
93
- ],
94
- "count": 1
95
  }
96
  ```
97
 
98
- ### 4. Get Piclet Image
99
- **Endpoint**: `/get_piclet_image_api`
 
100
  **Method**: Gradio function call
101
- **Purpose**: Retrieve the image associated with a Piclet
102
 
103
  **Input Parameters**:
104
- - `piclet_id` (string): Unique Piclet identifier
 
 
 
 
 
105
 
106
- **Response**: Image file or None if not found
 
 
 
 
 
 
107
 
108
- ## Required JSON Format for Piclets
 
 
 
109
 
110
- When saving a Piclet, the JSON must include these required fields:
 
 
 
 
 
111
 
 
112
  ```json
113
  {
114
- "name": "Stellar Wolf",
115
- "description": "A majestic wolf creature infused with cosmic energy",
116
- "tier": "medium",
117
- "primaryType": "space",
118
- "secondaryType": "beast",
119
- "baseStats": {
120
- "hp": 85,
121
- "attack": 90,
122
- "defense": 70,
123
- "speed": 80
124
- },
125
- "nature": "Bold",
126
- "specialAbility": {
127
- "name": "Cosmic Howl",
128
- "description": "Increases attack when health is low"
129
- },
130
- "movepool": [
131
  {
132
- "name": "Star Strike",
133
- "type": "space",
134
- "power": 75,
135
- "accuracy": 90,
136
- "pp": 15,
137
- "priority": 0,
138
- "flags": [],
139
- "effects": [
140
- {
141
- "type": "damage",
142
- "target": "opponent",
143
- "amount": "normal"
144
- }
145
- ]
146
  }
147
  ]
148
  }
149
  ```
150
 
151
- ### Field Specifications
152
-
153
- #### Required Fields
154
- - **name**: String - Piclet's name
155
- - **primaryType**: String - One of: beast, bug, aquatic, flora, mineral, space, machina, structure, culture, cuisine
156
- - **baseStats**: Object with hp, attack, defense, speed (integers)
157
- - **movepool**: Array of move objects (minimum 1 move)
158
 
159
- #### Optional Fields
160
- - **description**: String - Flavor text
161
- - **tier**: String - low, medium, high, legendary
162
- - **secondaryType**: String - Same options as primaryType or null
163
- - **nature**: String - Personality trait
164
- - **specialAbility**: Object with name and description
165
 
166
- #### Move Object Format
167
  ```json
168
  {
169
- "name": "Move Name",
170
- "type": "attack_type",
171
- "power": 75,
172
- "accuracy": 90,
173
- "pp": 15,
174
- "priority": 0,
175
- "flags": ["contact", "bite"],
176
- "effects": [
 
177
  {
178
- "type": "damage",
179
- "target": "opponent",
180
- "amount": "normal"
 
 
181
  }
182
  ]
183
  }
184
  ```
185
 
186
- ## Frontend Integration
187
-
188
- ### Using Gradio Client (JavaScript)
 
189
 
190
- ```javascript
191
- // Connect to the Piclets server
192
- const client = await window.gradioClient.Client.connect("your-hf-space-name");
193
-
194
- // Save a Piclet
195
- async function savePiclet(picletData, imageFile = null) {
196
- const result = await client.predict("/save_piclet_api", [
197
- JSON.stringify(picletData),
198
- imageFile
199
- ]);
200
- return JSON.parse(result.data[0]);
201
  }
 
202
 
203
- // Get a Piclet
204
- async function getPiclet(picletId) {
205
- const result = await client.predict("/get_piclet_api", [picletId]);
206
- return JSON.parse(result.data[0]);
 
 
 
 
 
 
 
 
207
  }
 
208
 
209
- // List Piclets
210
- async function listPiclets(limit = 20) {
211
- const result = await client.predict("/list_piclets_api", [limit]);
212
- return JSON.parse(result.data[0]);
213
- }
214
 
215
- // Get Piclet Image
216
- async function getPicletImage(picletId) {
217
- const result = await client.predict("/get_piclet_image_api", [picletId]);
218
- return result.data[0]; // Returns image path or null
219
- }
220
- ```
221
 
222
- ### Example Usage
 
 
 
 
 
 
 
 
223
 
224
- ```javascript
225
- // Example Piclet data
226
- const picletData = {
227
- name: "Thunder Drake",
228
- primaryType: "space",
229
- baseStats: { hp: 90, attack: 85, defense: 75, speed: 95 },
230
- movepool: [
231
- {
232
- name: "Lightning Bolt",
233
- type: "space",
234
- power: 80,
235
- accuracy: 95,
236
- pp: 15,
237
- priority: 0,
238
- flags: [],
239
- effects: [{ type: "damage", target: "opponent", amount: "normal" }]
240
- }
241
- ]
242
- };
243
-
244
- // Save the Piclet
245
- const saveResult = await savePiclet(picletData);
246
- if (saveResult.success) {
247
- console.log(`Piclet saved with ID: ${saveResult.piclet_id}`);
248
-
249
- // Retrieve it back
250
- const retrievedPiclet = await getPiclet(saveResult.piclet_id);
251
- console.log(retrievedPiclet);
252
- }
253
- ```
254
 
255
- ## Storage Structure
256
 
257
- The server uses a file-based storage system:
 
 
 
 
 
258
 
259
- ```
260
- data/
261
- ├── piclets/ # Individual Piclet JSON files
262
- │ ├── uuid1.json
263
- │ ├── uuid2.json
264
- │ └── ...
265
- ├── images/ # Piclet images
266
- │ ├── uuid1.png
267
- │ ├── uuid2.jpg
268
- │ └── ...
269
- └── collections/ # Future: Shared collections
270
- └── ...
271
- ```
272
 
273
  ## Error Handling
274
 
275
- All API endpoints return consistent error formats:
276
 
277
  ```json
278
  {
279
  "success": false,
280
- "error": "Descriptive error message"
281
  }
282
  ```
283
 
284
- ### Common Errors
285
- - **Missing required field**: When JSON is missing name, primaryType, baseStats, or movepool
286
- - **Invalid JSON format**: When the input string is not valid JSON
287
- - **Piclet not found**: When requesting a non-existent Piclet ID
288
- - **File system errors**: Issues with saving/reading files
289
-
290
- ## Deployment to Hugging Face Spaces
291
-
292
- 1. **Push to Repository**: The space automatically deploys when you push to the connected git repository
293
- 2. **Environment**: Python 3.9+ with Gradio 5.38.2
294
- 3. **Persistent Storage**: Data persists between space restarts
295
- 4. **Public Access**: The API is accessible to anyone with the space URL
296
-
297
- ### Space Configuration (in README.md)
298
- ```yaml
299
- ---
300
- title: Piclets Server
301
- emoji: 🦀
302
- colorFrom: yellow
303
- colorTo: pink
304
- sdk: gradio
305
- sdk_version: 5.38.2
306
- app_file: app.py
307
- pinned: false
308
- ---
309
- ```
310
-
311
- ## Future Enhancements
312
-
313
- ### Planned Features
314
- - **Collections**: Save and share groups of Piclets
315
- - **User Authentication**: Personal Piclet collections
316
- - **Battle Statistics**: Track battle performance
317
- - **Online Multiplayer**: Real-time battle coordination
318
- - **Search and Filtering**: Find Piclets by type, tier, name
319
- - **Import/Export**: Bulk operations
320
-
321
- ### API Extensions
322
- - `create_collection_api(name, piclet_ids)`
323
- - `get_collection_api(collection_id)`
324
- - `search_piclets_api(query, filters)`
325
- - `get_battle_stats_api(piclet_id)`
326
-
327
- ## Security Considerations
328
-
329
- - **Input Validation**: All JSON inputs are validated
330
- - **File Size Limits**: Images are limited by Gradio defaults
331
- - **Rate Limiting**: Handled by Hugging Face Spaces infrastructure
332
- - **Content Filtering**: Future enhancement for inappropriate content
 
 
 
 
 
 
 
 
333
 
334
  ## Support
335
 
336
  For issues or questions:
337
- 1. Check the error messages in API responses
338
- 2. Verify JSON format matches the specification
339
- 3. Ensure all required fields are present
340
- 4. Test with the example data provided
 
1
+ # Piclets Discovery Server API Documentation
2
 
3
  ## Overview
4
 
5
+ The Piclets Discovery Server provides a Gradio-based API for the Piclets discovery game. Each real-world object has ONE canonical Piclet, with variations tracked based on attributes. All data is stored in a public HuggingFace Dataset.
6
 
7
  ## Quick Start
8
 
 
16
  - **Web Interface**: http://localhost:7860
17
  - **Programmatic Access**: Use Gradio Client to connect to the space
18
 
19
+ ### Frontend Integration
20
+ ```javascript
21
+ import { Client } from "@gradio/client";
22
+
23
+ const client = await Client.connect("Fraser/piclets-server");
24
+ const result = await client.predict("/search_piclet", {
25
+ object_name: "pillow",
26
+ attributes: ["velvet"]
27
+ });
28
+ ```
29
+
30
  ## API Endpoints
31
 
32
+ ### 1. Search Piclet
33
+ **Endpoint**: `/search_piclet`
34
+ **Purpose**: Search for canonical Piclet or variations
35
  **Method**: Gradio function call
 
36
 
37
  **Input Parameters**:
38
+ ```json
39
+ {
40
+ "object_name": "pillow",
41
+ "attributes": ["velvet", "blue"]
42
+ }
43
+ ```
44
+
45
+ **Response Types**:
46
 
47
+ **New Object** (no Piclet exists):
48
  ```json
49
  {
50
+ "status": "new",
51
+ "message": "No Piclet found for 'pillow'",
52
+ "piclet": null
53
  }
54
  ```
55
 
56
+ **Existing Canonical** (exact match):
57
  ```json
58
  {
59
+ "status": "existing",
60
+ "message": "Found canonical Piclet for 'pillow'",
61
+ "piclet": {
62
+ "objectName": "pillow",
63
+ "typeId": "pillow_canonical",
64
+ "discoveredBy": "user123",
65
+ "discoveredAt": "2024-07-26T10:30:00",
66
+ "scanCount": 42,
67
+ "picletData": { /* full Piclet data */ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ **Variation Found**:
73
+ ```json
74
+ {
75
+ "status": "variation",
76
+ "message": "Found variation of 'pillow'",
77
+ "piclet": { /* variation data */ },
78
+ "canonicalId": "pillow_canonical"
79
  }
80
  ```
81
 
82
+ **New Variation Suggested**:
83
+ ```json
84
+ {
85
+ "status": "new_variation",
86
+ "message": "No variation found for 'pillow' with attributes ['velvet', 'blue']",
87
+ "canonicalId": "pillow_canonical",
88
+ "piclet": null
89
+ }
90
+ ```
91
+
92
+ ### 2. Create Canonical
93
+ **Endpoint**: `/create_canonical`
94
+ **Purpose**: Register the first discovery of an object
95
  **Method**: Gradio function call
 
96
 
97
  **Input Parameters**:
98
+ ```json
99
+ {
100
+ "object_name": "pillow",
101
+ "piclet_data": "{ /* JSON string of Piclet instance */ }",
102
+ "username": "discoverer123"
103
+ }
104
+ ```
105
 
106
+ **Success Response**:
107
  ```json
108
  {
109
  "success": true,
110
+ "message": "Created canonical Piclet for 'pillow'",
111
  "piclet": {
112
+ "objectName": "pillow",
113
+ "typeId": "pillow_canonical",
114
+ "discoveredBy": "discoverer123",
115
+ "discoveredAt": "2024-07-26T10:30:00",
116
+ "scanCount": 1,
117
+ "picletData": { /* full Piclet data */ }
 
 
118
  }
119
  }
120
  ```
121
 
122
+ **Error Response**:
123
+ ```json
124
+ {
125
+ "success": false,
126
+ "error": "Failed to save canonical Piclet"
127
+ }
128
+ ```
129
+
130
+ ### 3. Create Variation
131
+ **Endpoint**: `/create_variation`
132
+ **Purpose**: Add a variation to an existing canonical Piclet
133
  **Method**: Gradio function call
 
134
 
135
  **Input Parameters**:
136
+ ```json
137
+ {
138
+ "canonical_id": "pillow_canonical",
139
+ "attributes": ["velvet", "blue"],
140
+ "piclet_data": "{ /* JSON string of variation data */ }",
141
+ "username": "player456",
142
+ "object_name": "pillow"
143
+ }
144
+ ```
145
 
146
+ **Success Response**:
147
  ```json
148
  {
149
  "success": true,
150
+ "message": "Created variation of 'pillow'",
151
+ "piclet": {
152
+ "typeId": "pillow_001",
153
+ "attributes": ["velvet", "blue"],
154
+ "discoveredBy": "player456",
155
+ "discoveredAt": "2024-07-26T11:00:00",
156
+ "scanCount": 1,
157
+ "picletData": { /* variation data */ }
158
+ }
 
 
159
  }
160
  ```
161
 
162
+ ### 4. Increment Scan Count
163
+ **Endpoint**: `/increment_scan_count`
164
+ **Purpose**: Track how many times a Piclet has been discovered
165
  **Method**: Gradio function call
 
166
 
167
  **Input Parameters**:
168
+ ```json
169
+ {
170
+ "piclet_id": "pillow_canonical",
171
+ "object_name": "pillow"
172
+ }
173
+ ```
174
 
175
+ **Success Response**:
176
+ ```json
177
+ {
178
+ "success": true,
179
+ "scanCount": 43
180
+ }
181
+ ```
182
 
183
+ ### 5. Get Recent Activity
184
+ **Endpoint**: `/get_recent_activity`
185
+ **Purpose**: Get global discovery feed
186
+ **Method**: Gradio function call
187
 
188
+ **Input Parameters**:
189
+ ```json
190
+ {
191
+ "limit": 20
192
+ }
193
+ ```
194
 
195
+ **Response**:
196
  ```json
197
  {
198
+ "success": true,
199
+ "activities": [
200
+ {
201
+ "type": "discovery",
202
+ "objectName": "pillow",
203
+ "typeId": "pillow_canonical",
204
+ "discoveredBy": "user123",
205
+ "discoveredAt": "2024-07-26T10:30:00",
206
+ "scanCount": 42
207
+ },
 
 
 
 
 
 
 
208
  {
209
+ "type": "variation",
210
+ "objectName": "pillow",
211
+ "typeId": "pillow_001",
212
+ "attributes": ["velvet", "blue"],
213
+ "discoveredBy": "user456",
214
+ "discoveredAt": "2024-07-26T11:00:00",
215
+ "scanCount": 5
 
 
 
 
 
 
 
216
  }
217
  ]
218
  }
219
  ```
220
 
221
+ ### 6. Get Leaderboard
222
+ **Endpoint**: `/get_leaderboard`
223
+ **Purpose**: Get top discoverers by rarity score
224
+ **Method**: Gradio function call
 
 
 
225
 
226
+ **Input Parameters**:
227
+ ```json
228
+ {
229
+ "limit": 10
230
+ }
231
+ ```
232
 
233
+ **Response**:
234
  ```json
235
  {
236
+ "success": true,
237
+ "leaderboard": [
238
+ {
239
+ "rank": 1,
240
+ "username": "explorer123",
241
+ "totalFinds": 156,
242
+ "uniqueFinds": 45,
243
+ "rarityScore": 2340
244
+ },
245
  {
246
+ "rank": 2,
247
+ "username": "hunter456",
248
+ "totalFinds": 134,
249
+ "uniqueFinds": 38,
250
+ "rarityScore": 1890
251
  }
252
  ]
253
  }
254
  ```
255
 
256
+ ### 7. Get User Profile
257
+ **Endpoint**: `/get_user_profile`
258
+ **Purpose**: Get individual user's discovery statistics
259
+ **Method**: Gradio function call
260
 
261
+ **Input Parameters**:
262
+ ```json
263
+ {
264
+ "username": "player123"
 
 
 
 
 
 
 
265
  }
266
+ ```
267
 
268
+ **Response**:
269
+ ```json
270
+ {
271
+ "success": true,
272
+ "profile": {
273
+ "username": "player123",
274
+ "joinedAt": "2024-07-01T10:00:00",
275
+ "discoveries": ["pillow_canonical", "chair_002", "lamp_canonical"],
276
+ "uniqueFinds": 2,
277
+ "totalFinds": 3,
278
+ "rarityScore": 250
279
+ }
280
  }
281
+ ```
282
 
283
+ ## Object Normalization Rules
 
 
 
 
284
 
285
+ The server normalizes object names for consistent storage:
 
 
 
 
 
286
 
287
+ 1. Convert to lowercase
288
+ 2. Remove articles (the, a, an)
289
+ 3. Handle pluralization:
290
+ - `pillows` → `pillow`
291
+ - `berries` → `berry`
292
+ - `leaves` → `leaf`
293
+ - `boxes` → `box`
294
+ 4. Replace spaces with underscores
295
+ 5. Remove special characters
296
 
297
+ Examples:
298
+ - `"The Blue Pillow"` → `pillow`
299
+ - `"wooden chairs"` → `wooden_chair`
300
+ - `"A pair of glasses"` → `pair_of_glass`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
+ ## Rarity Tiers
303
 
304
+ Based on scan count:
305
+ - **Legendary**: ≤ 5 scans
306
+ - **Epic**: 6-20 scans
307
+ - **Rare**: 21-50 scans
308
+ - **Uncommon**: 51-100 scans
309
+ - **Common**: > 100 scans
310
 
311
+ ## Scoring System
312
+
313
+ - **Canonical Discovery**: +100 rarity points
314
+ - **Variation Discovery**: +50 rarity points
315
+ - **Scan Bonus**: Additional points based on rarity tier
 
 
 
 
 
 
 
 
316
 
317
  ## Error Handling
318
 
319
+ All endpoints return consistent error structures:
320
 
321
  ```json
322
  {
323
  "success": false,
324
+ "error": "Description of what went wrong"
325
  }
326
  ```
327
 
328
+ Common error scenarios:
329
+ - Piclet not found
330
+ - Invalid JSON data
331
+ - Failed to save to dataset
332
+ - Network/connection errors
333
+
334
+ ## Rate Limiting
335
+
336
+ Currently no rate limiting implemented. For production:
337
+ - Consider adding per-user rate limits
338
+ - Implement cooldowns for discoveries
339
+ - Cache frequent requests
340
+
341
+ ## Authentication
342
+
343
+ **Current**: Username-based (no passwords)
344
+ - Users provide username in requests
345
+ - All data is publicly visible
346
+ - No sensitive information stored
347
+
348
+ **Future Options**:
349
+ - HuggingFace OAuth integration
350
+ - API keys for verified users
351
+ - Session-based authentication
352
+
353
+ ## Data Storage
354
+
355
+ All data stored in HuggingFace Dataset:
356
+ - Repository: `Fraser/piclets`
357
+ - Type: Public dataset
358
+ - Structure:
359
+ - `piclets/` - Canonical and variation data
360
+ - `users/` - User profiles
361
+ - `metadata/` - Global statistics
362
+
363
+ ## Best Practices
364
+
365
+ 1. **Always normalize object names** before searching
366
+ 2. **Check for existing Piclets** before creating new ones
367
+ 3. **Increment scan counts** when rediscovering
368
+ 4. **Cache responses** on the client side
369
+ 5. **Handle network errors** gracefully
370
+ 6. **Validate JSON data** before sending
371
+
372
+ ## Example Workflow
373
+
374
+ 1. User scans an object (e.g., pillow)
375
+ 2. Extract object name and attributes from caption
376
+ 3. Search for existing Piclet
377
+ 4. If new:
378
+ - Create canonical Piclet
379
+ - Award discovery bonus
380
+ 5. If variation:
381
+ - Create or retrieve variation
382
+ - Update scan count
383
+ 6. Update user profile
384
+ 7. Refresh activity feed
385
 
386
  ## Support
387
 
388
  For issues or questions:
389
+ - Check CLAUDE.md for implementation details
390
+ - Review example code in app.py
391
+ - Open an issue in the repository
 
CLAUDE.md ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This is a Hugging Face Space that serves as a backend server for the Piclets Discovery game. It uses Gradio to provide API endpoints and HuggingFace Datasets for persistent storage.
8
+
9
+ **Core Concept**: Each real-world object has ONE canonical Piclet! Players discover these by scanning objects, with variations tracked based on attributes (e.g., "velvet pillow" is a variation of the canonical "pillow").
10
+
11
+ ## Architecture
12
+
13
+ ### Storage System
14
+ - **HuggingFace Dataset**: `Fraser/piclets` (public dataset repository)
15
+ - **Structure**:
16
+ ```
17
+ piclets/
18
+ {normalized_object_name}.json # e.g., pillow.json
19
+ users/
20
+ {username}.json # User profiles
21
+ metadata/
22
+ stats.json # Global statistics
23
+ leaderboard.json # Top discoverers
24
+ ```
25
+
26
+ ### Object Normalization
27
+ Objects are normalized for consistent storage:
28
+ - Convert to lowercase
29
+ - Remove articles (the, a, an)
30
+ - Handle pluralization (pillows → pillow)
31
+ - Replace spaces with underscores
32
+ - Remove special characters
33
+
34
+ Examples:
35
+ - "The Blue Pillow" → `pillow`
36
+ - "wooden chairs" → `wooden_chair`
37
+ - "glasses" → `glass` (special case handling)
38
+
39
+ ### Piclet Data Structure
40
+ ```json
41
+ {
42
+ "canonical": {
43
+ "objectName": "pillow",
44
+ "typeId": "pillow_canonical",
45
+ "discoveredBy": "username",
46
+ "discoveredAt": "2024-07-26T10:30:00",
47
+ "scanCount": 42,
48
+ "picletData": {
49
+ // Full Piclet instance data
50
+ }
51
+ },
52
+ "variations": [
53
+ {
54
+ "typeId": "pillow_001",
55
+ "attributes": ["velvet", "blue"],
56
+ "discoveredBy": "username2",
57
+ "discoveredAt": "2024-07-26T11:00:00",
58
+ "scanCount": 5,
59
+ "picletData": {
60
+ // Full variation data
61
+ }
62
+ }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ ## API Endpoints
68
+
69
+ ### Discovery Endpoints
70
+
71
+ 1. **search_piclet**: Find canonical or variation Piclets
72
+ - Input: `object_name`, `attributes[]`
73
+ - Returns: `status` (new/existing/variation), `piclet` data
74
+
75
+ 2. **create_canonical**: Register first discovery of an object
76
+ - Input: `object_name`, `piclet_data`, `username`
77
+ - Returns: Created canonical Piclet
78
+
79
+ 3. **create_variation**: Add variation to existing canonical
80
+ - Input: `canonical_id`, `attributes[]`, `piclet_data`, `username`, `object_name`
81
+ - Returns: Created variation
82
+
83
+ 4. **increment_scan_count**: Track popularity
84
+ - Input: `piclet_id`, `object_name`
85
+ - Returns: Updated scan count
86
+
87
+ ### Social Endpoints
88
+
89
+ 5. **get_recent_activity**: Global discovery feed
90
+ - Input: `limit` (default 20)
91
+ - Returns: Recent discoveries with metadata
92
+
93
+ 6. **get_leaderboard**: Top discoverers
94
+ - Input: `limit` (default 10)
95
+ - Returns: Ranked users by rarity score
96
+
97
+ 7. **get_user_profile**: Individual stats
98
+ - Input: `username`
99
+ - Returns: User's discoveries and statistics
100
+
101
+ ## Rarity System
102
+
103
+ Scan count determines rarity:
104
+ - **Legendary**: ≤ 5 scans
105
+ - **Epic**: 6-20 scans
106
+ - **Rare**: 21-50 scans
107
+ - **Uncommon**: 51-100 scans
108
+ - **Common**: > 100 scans
109
+
110
+ Rarity scoring for leaderboard:
111
+ - Canonical discovery: +100 points
112
+ - Variation discovery: +50 points
113
+ - Additional bonuses based on rarity tier
114
+
115
+ ## Authentication Strategy
116
+
117
+ **Username-based system** (no passwords):
118
+ - Users provide username in requests
119
+ - All data is public (embracing open discovery)
120
+ - User profiles track discoveries and stats
121
+ - Future: Could integrate HuggingFace OAuth
122
+
123
+ ## Integration with Frontend
124
+
125
+ The frontend (`../piclets/`) connects using Gradio Client:
126
+
127
+ ```javascript
128
+ // Frontend connection
129
+ const client = await window.gradioClient.Client.connect("Fraser/piclets-server");
130
+
131
+ // Search for Piclet
132
+ const result = await client.predict("/search_piclet", {
133
+ object_name: "pillow",
134
+ attributes: ["velvet", "blue"]
135
+ });
136
+
137
+ // Create canonical
138
+ const created = await client.predict("/create_canonical", {
139
+ object_name: "pillow",
140
+ piclet_data: JSON.stringify(picletData),
141
+ username: "player123"
142
+ });
143
+ ```
144
+
145
+ ## Development
146
+
147
+ ### Local Testing
148
+ ```bash
149
+ pip install -r requirements.txt
150
+ python app.py
151
+ # Access at http://localhost:7860
152
+ ```
153
+
154
+ ### Deployment
155
+ Push to HuggingFace Space repository:
156
+ ```bash
157
+ git add -A && git commit -m "Update" && git push
158
+ ```
159
+
160
+ ### Environment Variables
161
+ - `HF_TOKEN`: **Required** - HuggingFace write token (set in Space Secrets)
162
+ - `DATASET_REPO`: Target dataset (default: "Fraser/piclets")
163
+
164
+ ## Key Implementation Details
165
+
166
+ ### Variation Matching
167
+ - Uses set intersection to find attribute overlap
168
+ - 50% match threshold for variations
169
+ - Attributes are normalized and trimmed
170
+
171
+ ### Caching Strategy
172
+ - Local cache in `cache/` directory
173
+ - HuggingFace hub caching for downloads
174
+ - Temporary files for uploads
175
+
176
+ ### Error Handling
177
+ - Graceful fallbacks for missing data
178
+ - Default user profiles for new users
179
+ - Try-catch blocks around all dataset operations
180
+
181
+ ## Future Enhancements
182
+
183
+ 1. **Activity Log**: Separate timeline file for better performance
184
+ 2. **Image Storage**: Store Piclet images in dataset
185
+ 3. **Badges/Achievements**: Track discovery milestones
186
+ 4. **Trading System**: Allow users to trade variations
187
+ 5. **Seasonal Events**: Time-limited discoveries
188
+ 6. **OAuth Integration**: Verified HuggingFace accounts
189
+
190
+ ## Security Considerations
191
+
192
+ - All data is public by design
193
+ - No sensitive information stored
194
+ - Username-only system (no passwords)
195
+ - Input validation on all endpoints
196
+ - Rate limiting should be added for production
README.md CHANGED
@@ -1,13 +1,132 @@
1
  ---
2
- title: Piclets Server
3
- emoji: 🦀
4
- colorFrom: yellow
5
- colorTo: pink
6
  sdk: gradio
7
  sdk_version: 5.38.2
8
  app_file: app.py
9
  pinned: false
10
- short_description: Backend for the Piclets game.
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Piclets Discovery Server
3
+ emoji: 🔍
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 5.38.2
8
  app_file: app.py
9
  pinned: false
10
+ short_description: Discover unique Piclets for every real-world object!
11
  ---
12
 
13
+ # 🔍 Piclets Discovery Server
14
+
15
+ A Hugging Face Space that serves as the backend for Piclets - a discovery game where each real-world object has ONE unique canonical creature!
16
+
17
+ ## Key Features
18
+
19
+ - **Canonical System**: Each real-world object has exactly one official Piclet
20
+ - **Variation Tracking**: Discover unique variations based on object attributes
21
+ - **Discovery Database**: Public HuggingFace dataset stores all discoveries
22
+ - **Leaderboard System**: Track top discoverers by rarity score
23
+ - **Rarity Tiers**: From Common to Legendary based on scan counts
24
+
25
+ ## Game Flow
26
+
27
+ 1. **Scan**: Players photograph real-world objects
28
+ 2. **Identify**: AI captions extract the object name and attributes
29
+ 3. **Discover**: First scanner creates the canonical Piclet
30
+ 4. **Variations**: Find unique versions based on visual attributes
31
+ 5. **Track**: All discoveries stored in public HuggingFace dataset
32
+
33
+ ## Documentation
34
+
35
+ - [API_DOCUMENTATION.md](API_DOCUMENTATION.md) - Complete API reference with examples
36
+ - [CLAUDE.md](CLAUDE.md) - Technical implementation details
37
+
38
+ ## Quick Start
39
+
40
+ ### Local Development
41
+ ```bash
42
+ pip install -r requirements.txt
43
+ python app.py
44
+ # Server runs at http://localhost:7860
45
+ ```
46
+
47
+ ### Deploy to Hugging Face Spaces
48
+
49
+ 1. **Create the Space**:
50
+ - Go to https://huggingface.co/new-space
51
+ - Choose Gradio SDK
52
+ - Set to public
53
+
54
+ 2. **Set up HF_TOKEN**:
55
+ - Go to Space Settings → Repository secrets
56
+ - Add `HF_TOKEN` with write permissions to `Fraser/piclets` dataset
57
+
58
+ 3. **Push the code**:
59
+ ```bash
60
+ git add -A && git commit -m "Initial deployment" && git push
61
+ ```
62
+
63
+ ## Data Storage
64
+
65
+ All discoveries are stored in the public dataset: `Fraser/piclets`
66
+
67
+ ```
68
+ piclets/
69
+ pillow.json # Canonical Piclet + all variations
70
+ chair.json # Each file contains one object type
71
+ lamp.json
72
+ ...
73
+
74
+ users/
75
+ player123.json # User profile with discoveries
76
+ explorer456.json # Tracks unique finds and scores
77
+ ...
78
+
79
+ metadata/
80
+ stats.json # Global game statistics
81
+ leaderboard.json # Top discoverers by rarity score
82
+ ```
83
+
84
+ ## Frontend Integration
85
+
86
+ ### JavaScript/TypeScript
87
+ ```javascript
88
+ import { Client } from "@gradio/client";
89
+
90
+ const client = await Client.connect("Fraser/piclets-server");
91
+
92
+ // Search for existing Piclet
93
+ const result = await client.predict("/search_piclet", {
94
+ object_name: "pillow",
95
+ attributes: ["velvet", "blue"]
96
+ });
97
+
98
+ // Create new canonical Piclet
99
+ if (result.status === "new") {
100
+ const created = await client.predict("/create_canonical", {
101
+ object_name: "pillow",
102
+ piclet_data: JSON.stringify(picletData),
103
+ username: "discoverer123"
104
+ });
105
+ }
106
+ ```
107
+
108
+ ### Python
109
+ ```python
110
+ from gradio_client import Client
111
+
112
+ client = Client("Fraser/piclets-server")
113
+
114
+ # Search for a Piclet
115
+ result = client.predict(
116
+ "pillow", # object_name
117
+ ["velvet", "blue"], # attributes
118
+ api_name="/search_piclet"
119
+ )
120
+ ```
121
+
122
+ ## Tech Stack
123
+
124
+ - **Gradio**: API framework and web interface
125
+ - **HuggingFace Datasets**: Persistent storage backend
126
+ - **Python**: Core server logic
127
+
128
+ ## License
129
+
130
+ Public domain - all discoveries are shared openly!
131
+
132
+ For more details, check out the [Hugging Face Spaces documentation](https://huggingface.co/docs/hub/spaces-config-reference).
app.py CHANGED
@@ -1,947 +1,625 @@
1
  import gradio as gr
2
  import json
3
  import os
4
- import uuid
5
  from datetime import datetime
6
  from typing import Dict, List, Optional, Tuple
7
- import hashlib
8
- import hmac
9
- import time
10
  from pathlib import Path
 
11
 
12
- # Create data directories
13
- DATA_DIR = Path("data")
14
- PICLETS_DIR = DATA_DIR / "piclets"
15
- COLLECTIONS_DIR = DATA_DIR / "collections"
16
- IMAGES_DIR = DATA_DIR / "images"
17
 
18
- for dir_path in [DATA_DIR, PICLETS_DIR, COLLECTIONS_DIR, IMAGES_DIR]:
19
- dir_path.mkdir(exist_ok=True)
20
 
21
- # Secret key for verification (in production, use environment variable)
22
- SECRET_KEY = os.getenv("PICLET_SECRET_KEY", "piclets-dev-key-change-in-production")
 
 
 
 
23
 
24
- class PicletVerification:
25
- """Handles Piclet authenticity verification"""
26
-
27
- @staticmethod
28
- def create_signature(piclet_data: dict, timestamp: int, generation_data: dict = None) -> str:
29
- """
30
- Create a cryptographic signature for a Piclet
31
- Uses HMAC-SHA256 with the secret key
32
- """
33
- # Create deterministic string from core Piclet data
34
- core_data = {
35
- "name": piclet_data.get("name", ""),
36
- "primaryType": piclet_data.get("primaryType", ""),
37
- "baseStats": piclet_data.get("baseStats", {}),
38
- "movepool": piclet_data.get("movepool", []),
39
- "timestamp": timestamp
40
- }
41
-
42
- # Add generation metadata if provided
43
- if generation_data:
44
- core_data["generation_data"] = generation_data
45
-
46
- # Create deterministic JSON string (sorted keys)
47
- data_string = json.dumps(core_data, sort_keys=True, separators=(',', ':'))
48
-
49
- # Create HMAC signature
50
- signature = hmac.new(
51
- SECRET_KEY.encode('utf-8'),
52
- data_string.encode('utf-8'),
53
- hashlib.sha256
54
- ).hexdigest()
55
-
56
- return signature
57
-
58
- @staticmethod
59
- def verify_signature(piclet_data: dict, provided_signature: str, timestamp: int, generation_data: dict = None) -> bool:
60
- """
61
- Verify a Piclet's signature
62
- Returns True if signature is valid
63
- """
64
- try:
65
- expected_signature = PicletVerification.create_signature(piclet_data, timestamp, generation_data)
66
- return hmac.compare_digest(expected_signature, provided_signature)
67
- except Exception as e:
68
- print(f"Signature verification error: {e}")
69
- return False
70
-
71
  @staticmethod
72
- def create_verification_data(piclet_data: dict, image_caption: str = None, concept_string: str = None) -> dict:
73
  """
74
- Create complete verification data for a Piclet
75
- Includes signature, timestamp, and generation metadata
76
  """
77
- timestamp = int(time.time())
78
-
79
- # Generation metadata
80
- generation_data = {
81
- "image_caption": image_caption or "",
82
- "concept_string": concept_string or "",
83
- "generation_method": "official_app"
84
- }
85
-
86
- # Create signature
87
- signature = PicletVerification.create_signature(piclet_data, timestamp, generation_data)
88
-
89
- return {
90
- "signature": signature,
91
- "timestamp": timestamp,
92
- "generation_data": generation_data,
93
- "verification_version": "1.0"
94
- }
95
-
 
 
 
 
 
 
 
 
 
 
96
  @staticmethod
97
- def is_verified_piclet(piclet_data: dict) -> Tuple[bool, str]:
98
- """
99
- Check if a Piclet has valid verification
100
- Returns: (is_verified, status_message)
101
- """
102
  try:
103
- # Check for verification data
104
- if "verification" not in piclet_data:
105
- return False, "No verification data found"
106
-
107
- verification = piclet_data["verification"]
108
- required_fields = ["signature", "timestamp", "generation_data"]
109
-
110
- for field in required_fields:
111
- if field not in verification:
112
- return False, f"Missing verification field: {field}"
113
-
114
- # Verify signature
115
- is_valid = PicletVerification.verify_signature(
116
- piclet_data,
117
- verification["signature"],
118
- verification["timestamp"],
119
- verification["generation_data"]
120
  )
121
-
122
- if not is_valid:
123
- return False, "Invalid signature - Piclet may be modified or fake"
124
-
125
- # Check timestamp (reject if too old - 24 hours)
126
- current_time = int(time.time())
127
- piclet_time = verification["timestamp"]
128
-
129
- # Allow some flexibility for testing (24 hours)
130
- if current_time - piclet_time > 86400: # 24 hours
131
- return False, "Piclet signature is too old (>24 hours)"
132
-
133
- return True, "Verified authentic Piclet"
134
-
135
  except Exception as e:
136
- return False, f"Verification error: {str(e)}"
 
137
 
138
- class PicletStorage:
139
- """Handles saving and retrieving Piclet data using file-based storage"""
140
-
141
  @staticmethod
142
- def save_piclet(piclet_data: dict, image_file=None) -> Tuple[bool, str]:
143
- """
144
- Save a Piclet with optional image file
145
- Returns: (success, piclet_id_or_error_message)
146
- """
147
  try:
148
- # Generate unique ID
149
- piclet_id = str(uuid.uuid4())
150
-
151
- # Add metadata
152
- piclet_data["id"] = piclet_id
153
- piclet_data["created_at"] = datetime.now().isoformat()
154
-
155
- # Handle image file if provided
156
- if image_file is not None:
157
- # Generate image filename
158
- image_ext = Path(image_file.name).suffix if hasattr(image_file, 'name') else '.png'
159
- image_filename = f"{piclet_id}{image_ext}"
160
- image_path = IMAGES_DIR / image_filename
161
-
162
- # Save image file
163
- if hasattr(image_file, 'save'):
164
- image_file.save(str(image_path))
165
- else:
166
- # Handle different image input types
167
- with open(image_path, 'wb') as f:
168
- if hasattr(image_file, 'read'):
169
- f.write(image_file.read())
170
- else:
171
- f.write(image_file)
172
-
173
- piclet_data["image_filename"] = image_filename
174
-
175
- # Save Piclet data
176
- piclet_file = PICLETS_DIR / f"{piclet_id}.json"
177
- with open(piclet_file, 'w') as f:
178
- json.dump(piclet_data, f, indent=2)
179
-
180
- return True, piclet_id
181
-
182
  except Exception as e:
183
- return False, f"Error saving Piclet: {str(e)}"
184
-
 
185
  @staticmethod
186
- def get_piclet(piclet_id: str) -> Tuple[bool, dict]:
187
- """
188
- Retrieve a Piclet by ID
189
- Returns: (success, piclet_data_or_error_message)
190
- """
191
  try:
192
- piclet_file = PICLETS_DIR / f"{piclet_id}.json"
193
- if not piclet_file.exists():
194
- return False, {"error": "Piclet not found"}
195
-
196
- with open(piclet_file, 'r') as f:
197
- piclet_data = json.load(f)
198
-
199
- return True, piclet_data
200
-
201
- except Exception as e:
202
- return False, {"error": f"Error retrieving Piclet: {str(e)}"}
203
-
 
 
 
 
 
 
 
 
 
 
204
  @staticmethod
205
- def list_piclets(limit: int = 50) -> Tuple[bool, List[dict]]:
206
- """
207
- List all saved Piclets with basic info
208
- Returns: (success, list_of_piclet_summaries)
209
- """
210
  try:
211
- piclets = []
212
- piclet_files = list(PICLETS_DIR.glob("*.json"))
213
-
214
- # Sort by creation time (newest first)
215
- piclet_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
216
-
217
- for piclet_file in piclet_files[:limit]:
218
- try:
219
- with open(piclet_file, 'r') as f:
220
- data = json.load(f)
221
-
222
- # Create summary
223
- summary = {
224
- "id": data.get("id"),
225
- "name": data.get("name", "Unnamed Piclet"),
226
- "primaryType": data.get("primaryType"),
227
- "tier": data.get("tier"),
228
- "created_at": data.get("created_at"),
229
- "has_image": "image_filename" in data,
230
- "verified": data.get("verified", False)
231
- }
232
- piclets.append(summary)
233
-
234
- except Exception as e:
235
- print(f"Error reading {piclet_file}: {e}")
236
- continue
237
-
238
- return True, piclets
239
-
240
  except Exception as e:
241
- return False, [{"error": f"Error listing Piclets: {str(e)}"}]
242
-
 
243
  @staticmethod
244
- def get_piclet_image(piclet_id: str):
245
- """Get the image file for a Piclet"""
246
  try:
247
- # First get the Piclet data to find image filename
248
- success, piclet_data = PicletStorage.get_piclet(piclet_id)
249
- if not success:
250
- return None
251
-
252
- if "image_filename" not in piclet_data:
253
- return None
254
-
255
- image_path = IMAGES_DIR / piclet_data["image_filename"]
256
- if image_path.exists():
257
- return str(image_path)
258
-
259
- return None
260
-
 
 
 
 
 
 
261
  except Exception as e:
262
- print(f"Error getting image for {piclet_id}: {e}")
263
- return None
 
 
264
 
265
- def save_piclet_api(piclet_json: str, signature: str = "", image_file=None) -> str:
266
  """
267
- API endpoint to save a Piclet
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  """
269
  try:
270
- # Parse the JSON data
271
- piclet_data = json.loads(piclet_json)
272
-
273
- # Validate required fields
274
- required_fields = ["name", "primaryType", "baseStats", "movepool"]
275
- for field in required_fields:
276
- if field not in piclet_data:
277
- return json.dumps({
278
- "success": False,
279
- "error": f"Missing required field: {field}"
280
- })
281
-
282
- # Verify Piclet authenticity
283
- is_verified, verification_message = PicletVerification.is_verified_piclet(piclet_data)
284
-
285
- if not is_verified:
286
- return json.dumps({
287
- "success": False,
288
- "error": f"Verification failed: {verification_message}",
289
- "verification_required": True
290
- })
291
-
292
- # Add verification status to stored data
293
- piclet_data["verified"] = True
294
- piclet_data["verification_message"] = verification_message
295
-
296
- # Save the Piclet
297
- success, result = PicletStorage.save_piclet(piclet_data, image_file)
298
-
299
- if success:
300
- return json.dumps({
301
  "success": True,
302
- "piclet_id": result,
303
- "message": "Verified Piclet saved successfully",
304
- "verified": True
305
- })
306
  else:
307
- return json.dumps({
308
  "success": False,
309
- "error": result
310
- })
311
-
312
- except json.JSONDecodeError:
313
- return json.dumps({
314
- "success": False,
315
- "error": "Invalid JSON format"
316
- })
317
  except Exception as e:
318
- return json.dumps({
319
  "success": False,
320
- "error": f"Unexpected error: {str(e)}"
321
- })
322
 
323
- def get_piclet_api(piclet_id: str) -> str:
324
  """
325
- API endpoint to retrieve a Piclet by ID
326
  """
327
  try:
328
- if not piclet_id.strip():
329
- return json.dumps({
 
 
 
 
330
  "success": False,
331
- "error": "Piclet ID is required"
332
- })
333
-
334
- success, result = PicletStorage.get_piclet(piclet_id.strip())
335
-
336
- if success:
337
- return json.dumps({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  "success": True,
339
- "piclet": result
340
- })
 
341
  else:
342
- return json.dumps({
343
  "success": False,
344
- "error": result.get("error", "Unknown error")
345
- })
346
-
347
  except Exception as e:
348
- return json.dumps({
349
  "success": False,
350
- "error": f"Unexpected error: {str(e)}"
351
- })
352
 
353
- def list_piclets_api(limit: int = 50) -> str:
354
  """
355
- API endpoint to list all saved Piclets
356
  """
357
  try:
358
- limit = max(1, min(limit, 100)) # Limit between 1 and 100
359
-
360
- success, result = PicletStorage.list_piclets(limit)
361
-
362
- if success:
363
- return json.dumps({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  "success": True,
365
- "piclets": result,
366
- "count": len(result)
367
- })
368
  else:
369
- return json.dumps({
370
  "success": False,
371
- "error": "Failed to list Piclets"
372
- })
373
-
374
  except Exception as e:
375
- return json.dumps({
376
  "success": False,
377
- "error": f"Unexpected error: {str(e)}"
378
- })
379
 
380
- def get_piclet_image_api(piclet_id: str):
381
  """
382
- API endpoint to get a Piclet's image
383
  """
384
  try:
385
- if not piclet_id.strip():
386
- return None
387
-
388
- image_path = PicletStorage.get_piclet_image(piclet_id.strip())
389
- return image_path
390
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  except Exception as e:
392
- print(f"Error in get_piclet_image_api: {e}")
393
- return None
 
 
 
394
 
395
- def sign_piclet_api(piclet_json: str, image_caption: str = "", concept_string: str = "") -> str:
396
  """
397
- API endpoint to sign a Piclet (for static frontend integration)
398
- This allows static sites to generate verified Piclets without exposing the secret key
399
  """
400
  try:
401
- # Parse the JSON data
402
- piclet_data = json.loads(piclet_json)
403
-
404
- # Validate required fields
405
- required_fields = ["name", "primaryType", "baseStats", "movepool"]
406
- for field in required_fields:
407
- if field not in piclet_data:
408
- return json.dumps({
409
- "success": False,
410
- "error": f"Missing required field: {field}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  })
412
-
413
- # Create verification data using server's secret key
414
- verification_data = PicletVerification.create_verification_data(
415
- piclet_data,
416
- image_caption=image_caption,
417
- concept_string=concept_string
418
- )
419
-
420
- # Add verification to Piclet data
421
- verified_piclet = {**piclet_data, "verification": verification_data}
422
-
423
- return json.dumps({
424
  "success": True,
425
- "verified_piclet": verified_piclet,
426
- "signature": verification_data["signature"],
427
- "message": "Piclet signed successfully - ready for storage"
428
- })
429
-
430
- except json.JSONDecodeError:
431
- return json.dumps({
432
- "success": False,
433
- "error": "Invalid JSON format"
434
- })
435
  except Exception as e:
436
- return json.dumps({
437
  "success": False,
438
- "error": f"Signing error: {str(e)}"
439
- })
 
440
 
441
- def sign_and_save_piclet_api(piclet_json: str, image_caption: str = "", concept_string: str = "", image_file=None) -> str:
442
  """
443
- API endpoint to sign AND save a Piclet in one step (convenience method)
444
- Perfect for static frontends - no secret key needed on client side
445
  """
446
  try:
447
- # First, sign the Piclet
448
- sign_result_json = sign_piclet_api(piclet_json, image_caption, concept_string)
449
- sign_result = json.loads(sign_result_json)
450
-
451
- if not sign_result["success"]:
452
- return sign_result_json
453
-
454
- # Then save the verified Piclet
455
- verified_piclet_json = json.dumps(sign_result["verified_piclet"])
456
- save_result_json = save_piclet_api(verified_piclet_json, sign_result["signature"], image_file)
457
- save_result = json.loads(save_result_json)
458
-
459
- if save_result["success"]:
460
- return json.dumps({
461
- "success": True,
462
- "piclet_id": save_result["piclet_id"],
463
- "message": "Piclet signed and saved successfully",
464
- "verified": True,
465
- "signature": sign_result["signature"]
466
- })
467
- else:
468
- return save_result_json
469
-
470
  except Exception as e:
471
- return json.dumps({
472
  "success": False,
473
- "error": f"Sign and save error: {str(e)}"
474
- })
475
-
476
- # Create example Piclet data for testing
477
- def create_example_piclet() -> str:
478
- """Create an example Piclet for testing"""
479
- example_piclet = {
480
- "name": "Stellar Wolf",
481
- "description": "A majestic wolf creature infused with cosmic energy",
482
- "tier": "medium",
483
- "primaryType": "space",
484
- "secondaryType": "beast",
485
- "baseStats": {
486
- "hp": 85,
487
- "attack": 90,
488
- "defense": 70,
489
- "speed": 80
490
- },
491
- "nature": "Bold",
492
- "specialAbility": {
493
- "name": "Cosmic Howl",
494
- "description": "Increases attack when health is low"
495
- },
496
- "movepool": [
497
- {
498
- "name": "Star Strike",
499
- "type": "space",
500
- "power": 75,
501
- "accuracy": 90,
502
- "pp": 15,
503
- "priority": 0,
504
- "flags": [],
505
- "effects": [
506
- {
507
- "type": "damage",
508
- "target": "opponent",
509
- "amount": "normal"
510
- }
511
- ]
512
- },
513
- {
514
- "name": "Lunar Bite",
515
- "type": "beast",
516
- "power": 80,
517
- "accuracy": 85,
518
- "pp": 12,
519
- "priority": 0,
520
- "flags": ["contact", "bite"],
521
- "effects": [
522
- {
523
- "type": "damage",
524
- "target": "opponent",
525
- "amount": "normal"
526
- }
527
- ]
528
- }
529
- ]
530
- }
531
-
532
- return json.dumps(example_piclet, indent=2)
533
 
534
- # Create the Gradio interface
535
- with gr.Blocks(title="Piclets Server API", theme=gr.themes.Soft()) as app:
536
  gr.Markdown("""
537
- # 🦀 Piclets Server API
538
-
539
- Backend server for saving and sharing Piclets (AI-generated creatures) from the Piclets game.
540
-
541
- ## Quick Start
542
- 1. **Save a Piclet**: Use the JSON format below with optional image upload
543
- 2. **Retrieve a Piclet**: Enter a Piclet ID to get the full data
544
- 3. **List Piclets**: See all saved Piclets with summaries
545
- 4. **Get Images**: Retrieve the original image for any Piclet
546
  """)
547
-
548
- with gr.Tabs():
549
- # Sign and Save Tab (For Static Frontends)
550
- with gr.Tab("✍️ Sign & Save Piclet"):
551
- gr.Markdown("### Sign and Save a Piclet (One Step - Perfect for Static Sites)")
552
- gr.Markdown("🌟 **For Static Frontends**: No secret key needed! Server signs your Piclet automatically.")
553
-
554
- with gr.Row():
555
- with gr.Column(scale=2):
556
- unsigned_json_input = gr.Textbox(
557
- label="Unsigned Piclet JSON Data",
558
- placeholder="Paste your unsigned Piclet JSON here...",
559
- lines=12,
560
- value=create_example_piclet()
561
- )
562
-
563
- with gr.Row():
564
- caption_input = gr.Textbox(
565
- label="Image Caption",
566
- placeholder="AI-generated image description...",
567
- lines=2
568
- )
569
- concept_input = gr.Textbox(
570
- label="Concept String",
571
- placeholder="Creature concept from AI...",
572
- lines=2
573
- )
574
-
575
- with gr.Column(scale=1):
576
- sign_save_image_input = gr.File(
577
- label="Piclet Image (Optional)",
578
- file_types=["image"]
579
- )
580
-
581
- sign_save_btn = gr.Button("✍️ Sign & Save Piclet", variant="primary")
582
-
583
- sign_save_output = gr.JSON(label="Sign & Save Result")
584
-
585
- sign_save_btn.click(
586
- fn=sign_and_save_piclet_api,
587
- inputs=[unsigned_json_input, caption_input, concept_input, sign_save_image_input],
588
- outputs=sign_save_output
589
- )
590
-
591
- # Sign Only Tab
592
- with gr.Tab("🔏 Sign Only"):
593
- gr.Markdown("### Sign a Piclet (Two-Step Process)")
594
- gr.Markdown("📝 **Advanced**: Sign first, then save separately. Useful for batch operations.")
595
-
596
- with gr.Row():
597
- with gr.Column(scale=2):
598
- sign_json_input = gr.Textbox(
599
- label="Unsigned Piclet JSON Data",
600
- placeholder="Paste your unsigned Piclet JSON here...",
601
- lines=10,
602
- value=create_example_piclet()
603
- )
604
-
605
- with gr.Row():
606
- sign_caption_input = gr.Textbox(
607
- label="Image Caption",
608
- placeholder="AI-generated image description..."
609
- )
610
- sign_concept_input = gr.Textbox(
611
- label="Concept String",
612
- placeholder="Creature concept from AI..."
613
- )
614
-
615
- with gr.Column(scale=2):
616
- sign_btn = gr.Button("🔏 Sign Piclet", variant="secondary")
617
- sign_output = gr.JSON(label="Signing Result")
618
-
619
- sign_btn.click(
620
- fn=sign_piclet_api,
621
- inputs=[sign_json_input, sign_caption_input, sign_concept_input],
622
- outputs=sign_output
623
- )
624
-
625
- # Save Piclet Tab (For Pre-Signed)
626
- with gr.Tab("💾 Save Pre-Signed"):
627
- gr.Markdown("### Save a Pre-Signed Piclet")
628
- gr.Markdown("⚠️ **For Advanced Users**: Save a Piclet that was already signed elsewhere.")
629
-
630
- with gr.Row():
631
- with gr.Column(scale=2):
632
- piclet_json_input = gr.Textbox(
633
- label="Piclet JSON Data",
634
- placeholder="Paste your Piclet JSON here...",
635
- lines=15,
636
- value=create_example_piclet()
637
- )
638
-
639
- with gr.Column(scale=1):
640
- image_input = gr.File(
641
- label="Piclet Image (Optional)",
642
- file_types=["image"]
643
- )
644
-
645
- save_btn = gr.Button("💾 Save Piclet", variant="primary")
646
-
647
- save_output = gr.JSON(label="Save Result")
648
-
649
- save_btn.click(
650
- fn=save_piclet_api,
651
- inputs=[piclet_json_input, image_input],
652
- outputs=save_output
653
- )
654
-
655
- # Get Piclet Tab
656
- with gr.Tab("🔍 Get Piclet"):
657
- gr.Markdown("### Retrieve a Piclet by ID")
658
-
659
- with gr.Row():
660
- with gr.Column(scale=2):
661
- piclet_id_input = gr.Textbox(
662
- label="Piclet ID",
663
- placeholder="Enter Piclet ID here..."
664
- )
665
-
666
- get_btn = gr.Button("🔍 Get Piclet", variant="primary")
667
-
668
- with gr.Column(scale=2):
669
- get_output = gr.JSON(label="Piclet Data")
670
-
671
- get_btn.click(
672
- fn=get_piclet_api,
673
- inputs=piclet_id_input,
674
- outputs=get_output
675
- )
676
-
677
- # List Piclets Tab
678
- with gr.Tab("📋 List Piclets"):
679
- gr.Markdown("### Browse all saved Piclets")
680
-
681
- with gr.Row():
682
- with gr.Column(scale=1):
683
- limit_input = gr.Slider(
684
- label="Max Results",
685
- minimum=1,
686
- maximum=100,
687
- value=20,
688
- step=1
689
- )
690
-
691
- list_btn = gr.Button("📋 List Piclets", variant="primary")
692
-
693
- with gr.Column(scale=3):
694
- list_output = gr.JSON(label="Piclets List")
695
-
696
- list_btn.click(
697
- fn=list_piclets_api,
698
- inputs=limit_input,
699
- outputs=list_output
700
- )
701
-
702
- # Get Image Tab
703
- with gr.Tab("🖼️ Get Image"):
704
- gr.Markdown("### Retrieve a Piclet's image")
705
-
706
- with gr.Row():
707
- with gr.Column(scale=1):
708
- image_id_input = gr.Textbox(
709
- label="Piclet ID",
710
- placeholder="Enter Piclet ID here..."
711
- )
712
-
713
- get_image_btn = gr.Button("🖼️ Get Image", variant="primary")
714
-
715
- with gr.Column(scale=2):
716
- image_output = gr.Image(label="Piclet Image")
717
-
718
- get_image_btn.click(
719
- fn=get_piclet_image_api,
720
- inputs=image_id_input,
721
- outputs=image_output
722
- )
723
-
724
- # Generate Verified Example Tab
725
- with gr.Tab("✅ Generate Verified Example"):
726
- gr.Markdown("""
727
- ### Generate a Verified Example Piclet
728
-
729
- This creates a properly signed example Piclet that will pass verification.
730
- Use this to test the verification system or as a template.
731
- """)
732
-
733
- generate_btn = gr.Button("✅ Generate Verified Example", variant="primary")
734
- verified_output = gr.Textbox(
735
- label="Verified Piclet JSON",
736
- lines=20,
737
- show_copy_button=True
738
- )
739
-
740
- generate_btn.click(
741
- fn=create_verified_example_api,
742
- outputs=verified_output
743
- )
744
-
745
- # API Documentation Tab
746
- with gr.Tab("📖 API Documentation & Verification"):
747
- gr.Markdown("""
748
- ## 🔐 Verification System
749
-
750
- **All Piclets must be verified to prevent fake or modified creatures.**
751
-
752
- ### How Verification Works:
753
- 1. **HMAC-SHA256 Signature**: Each Piclet is signed with a secret key
754
- 2. **Timestamp Validation**: Signatures expire after 24 hours
755
- 3. **Generation Metadata**: Includes image caption and concept string
756
- 4. **Tamper Detection**: Any modification invalidates the signature
757
-
758
- ### Required Verification Format:
759
- ```json
760
- {
761
- "verification": {
762
- "signature": "abc123...",
763
- "timestamp": 1690123456,
764
- "generation_data": {
765
- "image_caption": "AI-generated description",
766
- "concept_string": "Creature concept",
767
- "generation_method": "official_app"
768
- },
769
- "verification_version": "1.0"
770
- }
771
- }
772
- ```
773
-
774
- ## API Endpoints
775
-
776
- ### 1. Sign & Save Piclet (Recommended for Static Sites)
777
- - **Function**: `sign_and_save_piclet_api(piclet_json, image_caption, concept_string, image_file=None)`
778
- - **Input**: Unsigned Piclet JSON, image caption, concept string, optional image file
779
- - **Output**: JSON response with success status and Piclet ID
780
- - **✅ Perfect for**: Static sites (no secret key needed)
781
-
782
- ### 2. Sign Only
783
- - **Function**: `sign_piclet_api(piclet_json, image_caption, concept_string)`
784
- - **Input**: Unsigned Piclet JSON, image caption, concept string
785
- - **Output**: JSON response with verified Piclet and signature
786
- - **Use case**: Two-step process, batch operations
787
-
788
- ### 3. Save Pre-Signed Piclet
789
- - **Function**: `save_piclet_api(piclet_json, signature, image_file=None)`
790
- - **Input**: Signed Piclet JSON, signature string, optional image file
791
- - **Output**: JSON response with success status and Piclet ID
792
- - **⚠️ Requires**: Valid verification signature
793
-
794
- ### 2. Get Piclet
795
- - **Function**: `get_piclet_api(piclet_id)`
796
- - **Input**: Piclet ID string
797
- - **Output**: JSON response with full Piclet data
798
-
799
- ### 3. List Piclets
800
- - **Function**: `list_piclets_api(limit=50)`
801
- - **Input**: Maximum number of results (1-100)
802
- - **Output**: JSON response with Piclet summaries
803
-
804
- ### 4. Get Image
805
- - **Function**: `get_piclet_image_api(piclet_id)`
806
- - **Input**: Piclet ID string
807
- - **Output**: Image file path or None
808
-
809
- ## Required JSON Format
810
-
811
- ```json
812
- {
813
- "name": "Piclet Name",
814
- "description": "Description of the creature",
815
- "tier": "low|medium|high|legendary",
816
- "primaryType": "beast|bug|aquatic|flora|mineral|space|machina|structure|culture|cuisine",
817
- "secondaryType": "same options as primaryType or null",
818
- "baseStats": {
819
- "hp": 85,
820
- "attack": 90,
821
- "defense": 70,
822
- "speed": 80
823
- },
824
- "nature": "Personality trait",
825
- "specialAbility": {
826
- "name": "Ability Name",
827
- "description": "What the ability does"
828
- },
829
- "movepool": [
830
- {
831
- "name": "Move Name",
832
- "type": "attack type",
833
- "power": 75,
834
- "accuracy": 90,
835
- "pp": 15,
836
- "priority": 0,
837
- "flags": ["contact", "bite", etc.],
838
- "effects": [
839
- {
840
- "type": "damage|heal|modifyStats|etc.",
841
- "target": "opponent|self|all",
842
- "amount": "weak|normal|strong|extreme"
843
- }
844
- ]
845
- }
846
- ]
847
- }
848
- ```
849
-
850
- ## Frontend Integration (JavaScript)
851
-
852
- ### Include Verification Helper
853
- ```html
854
- <!-- Include crypto-js for HMAC generation -->
855
- <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
856
- <script src="verification_helper.js"></script>
857
- ```
858
-
859
- ### Static Site Usage (Recommended)
860
- ```javascript
861
- // Connect to this Gradio space
862
- const client = await window.gradioClient.Client.connect("your-space-name");
863
-
864
- // Sign and save in one step (NO SECRET KEY NEEDED!)
865
- const result = await client.predict("/sign_and_save_piclet_api", [
866
- JSON.stringify(picletData), // Unsigned Piclet data
867
- imageCaption, // AI-generated caption
868
- conceptString, // Creature concept
869
- imageFile // Optional image
870
- ]);
871
-
872
- const response = JSON.parse(result.data[0]);
873
- if (response.success) {
874
- console.log(`Piclet saved with ID: ${response.piclet_id}`);
875
- } else {
876
- console.error(`Save failed: ${response.error}`);
877
- }
878
-
879
- // Two-step process (sign first, then save)
880
- const signResult = await client.predict("/sign_piclet_api", [
881
- JSON.stringify(picletData),
882
- imageCaption,
883
- conceptString
884
- ]);
885
-
886
- const signResponse = JSON.parse(signResult.data[0]);
887
- if (signResponse.success) {
888
- const saveResult = await client.predict("/save_piclet_api", [
889
- JSON.stringify(signResponse.verified_piclet),
890
- signResponse.signature,
891
- imageFile
892
- ]);
893
- }
894
- ```
895
-
896
- ### Secret Key Management (Server Only)
897
- ```bash
898
- # Set environment variable on server (HuggingFace Spaces)
899
- PICLET_SECRET_KEY=your-super-secret-64-char-hex-key
900
-
901
- # Generate secure key:
902
- openssl rand -hex 32
903
-
904
- # ✅ Frontend doesn't need the secret key anymore!
905
- # Server handles all signing securely
906
- ```
907
-
908
- ## Storage Structure
909
-
910
- - **Data Directory**: `./data/`
911
- - **Piclets**: `./data/piclets/*.json`
912
- - **Images**: `./data/images/*`
913
- - **Collections**: `./data/collections/*.json` (future feature)
914
-
915
- All data is stored locally in the Hugging Face Space persistent storage.
916
-
917
- ## Security Features
918
-
919
- ### ✅ Verified Piclets
920
- - Generated through official app only
921
- - Cryptographically signed with HMAC-SHA256
922
- - Include generation metadata (image caption, concept)
923
- - Tamper-proof (any modification breaks signature)
924
-
925
- ### ❌ Rejected Piclets
926
- - Missing verification data
927
- - Invalid or expired signatures
928
- - Modified after generation
929
- - Created outside official app
930
-
931
- ### Environment Variables
932
- - `PICLET_SECRET_KEY`: Secret key for verification (change in production)
933
-
934
- ### Files Included
935
- - `app.py`: Main server application
936
- - `verification_helper.js`: Frontend helper functions
937
- - `requirements.txt`: Python dependencies
938
- - `API_DOCUMENTATION.md`: Complete documentation
939
- """)
940
-
941
- # Launch the app
942
  if __name__ == "__main__":
943
- app.launch(
944
- server_name="0.0.0.0",
945
- server_port=7860,
946
- share=False
947
- )
 
1
  import gradio as gr
2
  import json
3
  import os
4
+ import re
5
  from datetime import datetime
6
  from typing import Dict, List, Optional, Tuple
7
+ from huggingface_hub import HfApi, hf_hub_download, list_repo_files
 
 
8
  from pathlib import Path
9
+ import tempfile
10
 
11
+ # HuggingFace configuration
12
+ HF_TOKEN = os.getenv("HF_TOKEN") # Required for writing to dataset
13
+ DATASET_REPO = "Fraser/piclets" # Public dataset repository
14
+ DATASET_TYPE = "dataset"
 
15
 
16
+ # Initialize HuggingFace API with token if available
17
+ api = HfApi(token=HF_TOKEN) if HF_TOKEN else HfApi()
18
 
19
+ # Cache directory for local operations
20
+ CACHE_DIR = Path("cache")
21
+ CACHE_DIR.mkdir(exist_ok=True)
22
+
23
+ class PicletDiscoveryService:
24
+ """Manages Piclet discovery using HuggingFace datasets"""
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  @staticmethod
27
+ def normalize_object_name(name: str) -> str:
28
  """
29
+ Normalize object names for consistent storage and lookup
30
+ Examples: "The Blue Pillow" -> "pillow", "wooden chairs" -> "wooden_chair"
31
  """
32
+ if not name:
33
+ return "unknown"
34
+
35
+ # Convert to lowercase and strip
36
+ name = name.lower().strip()
37
+
38
+ # Remove articles (the, a, an)
39
+ name = re.sub(r'^(the|a|an)\s+', '', name)
40
+
41
+ # Remove special characters except spaces
42
+ name = re.sub(r'[^a-z0-9\s]', '', name)
43
+
44
+ # Handle common plurals (basic pluralization rules)
45
+ if name.endswith('ies') and len(name) > 4:
46
+ name = name[:-3] + 'y' # berries -> berry
47
+ elif name.endswith('ves') and len(name) > 4:
48
+ name = name[:-3] + 'f' # leaves -> leaf
49
+ elif name.endswith('es') and len(name) > 3:
50
+ # Check if it's a special case like "glasses"
51
+ if not name.endswith(('ses', 'xes', 'zes', 'ches', 'shes')):
52
+ name = name[:-2] # boxes -> box (but keep glasses)
53
+ elif name.endswith('s') and len(name) > 2 and not name.endswith('ss'):
54
+ name = name[:-1] # chairs -> chair (but keep glass)
55
+
56
+ # Replace spaces with underscores
57
+ name = re.sub(r'\s+', '_', name.strip())
58
+
59
+ return name
60
+
61
  @staticmethod
62
+ def load_piclet_data(object_name: str) -> Optional[dict]:
63
+ """Load Piclet data from HuggingFace dataset"""
 
 
 
64
  try:
65
+ normalized_name = PicletDiscoveryService.normalize_object_name(object_name)
66
+ file_path = f"piclets/{normalized_name}.json"
67
+
68
+ # Download the file from HuggingFace
69
+ local_path = hf_hub_download(
70
+ repo_id=DATASET_REPO,
71
+ filename=file_path,
72
+ repo_type=DATASET_TYPE,
73
+ token=HF_TOKEN,
74
+ cache_dir=str(CACHE_DIR)
 
 
 
 
 
 
 
75
  )
76
+
77
+ with open(local_path, 'r') as f:
78
+ return json.load(f)
 
 
 
 
 
 
 
 
 
 
 
79
  except Exception as e:
80
+ print(f"Could not load piclet data for {object_name}: {e}")
81
+ return None
82
 
 
 
 
83
  @staticmethod
84
+ def save_piclet_data(object_name: str, data: dict) -> bool:
85
+ """Save Piclet data to HuggingFace dataset"""
 
 
 
86
  try:
87
+ normalized_name = PicletDiscoveryService.normalize_object_name(object_name)
88
+ file_path = f"piclets/{normalized_name}.json"
89
+
90
+ # Create a temporary file
91
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
92
+ json.dump(data, f, indent=2)
93
+ temp_path = f.name
94
+
95
+ # Upload to HuggingFace
96
+ api.upload_file(
97
+ path_or_fileobj=temp_path,
98
+ path_in_repo=file_path,
99
+ repo_id=DATASET_REPO,
100
+ repo_type=DATASET_TYPE,
101
+ commit_message=f"Update piclet: {normalized_name}"
102
+ )
103
+
104
+ # Clean up
105
+ os.unlink(temp_path)
106
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  except Exception as e:
108
+ print(f"Failed to save piclet data: {e}")
109
+ return False
110
+
111
  @staticmethod
112
+ def load_user_data(username: str) -> dict:
113
+ """Load user profile from dataset"""
 
 
 
114
  try:
115
+ file_path = f"users/{username.lower()}.json"
116
+ local_path = hf_hub_download(
117
+ repo_id=DATASET_REPO,
118
+ filename=file_path,
119
+ repo_type=DATASET_TYPE,
120
+ token=HF_TOKEN,
121
+ cache_dir=str(CACHE_DIR)
122
+ )
123
+
124
+ with open(local_path, 'r') as f:
125
+ return json.load(f)
126
+ except:
127
+ # Return default user profile if not found
128
+ return {
129
+ "username": username,
130
+ "joinedAt": datetime.now().isoformat(),
131
+ "discoveries": [],
132
+ "uniqueFinds": 0,
133
+ "totalFinds": 0,
134
+ "rarityScore": 0
135
+ }
136
+
137
  @staticmethod
138
+ def save_user_data(username: str, data: dict) -> bool:
139
+ """Save user profile to dataset"""
 
 
 
140
  try:
141
+ file_path = f"users/{username.lower()}.json"
142
+
143
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
144
+ json.dump(data, f, indent=2)
145
+ temp_path = f.name
146
+
147
+ api.upload_file(
148
+ path_or_fileobj=temp_path,
149
+ path_in_repo=file_path,
150
+ repo_id=DATASET_REPO,
151
+ repo_type=DATASET_TYPE,
152
+ commit_message=f"Update user profile: {username}"
153
+ )
154
+
155
+ os.unlink(temp_path)
156
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  except Exception as e:
158
+ print(f"Failed to save user data: {e}")
159
+ return False
160
+
161
  @staticmethod
162
+ def update_global_stats() -> dict:
163
+ """Update and return global statistics"""
164
  try:
165
+ # Try to load existing stats
166
+ try:
167
+ local_path = hf_hub_download(
168
+ repo_id=DATASET_REPO,
169
+ filename="metadata/stats.json",
170
+ repo_type=DATASET_TYPE,
171
+ token=HF_TOKEN,
172
+ cache_dir=str(CACHE_DIR)
173
+ )
174
+ with open(local_path, 'r') as f:
175
+ stats = json.load(f)
176
+ except:
177
+ stats = {
178
+ "totalDiscoveries": 0,
179
+ "uniqueObjects": 0,
180
+ "totalVariations": 0,
181
+ "lastUpdated": datetime.now().isoformat()
182
+ }
183
+
184
+ return stats
185
  except Exception as e:
186
+ print(f"Failed to update global stats: {e}")
187
+ return {}
188
+
189
+ # API Endpoints
190
 
191
+ def search_piclet(object_name: str, attributes: List[str]) -> dict:
192
  """
193
+ Search for canonical Piclet or variations
194
+ Returns matching piclet or None
195
+ """
196
+ piclet_data = PicletDiscoveryService.load_piclet_data(object_name)
197
+
198
+ if not piclet_data:
199
+ return {
200
+ "status": "new",
201
+ "message": f"No Piclet found for '{object_name}'",
202
+ "piclet": None
203
+ }
204
+
205
+ # Check if searching for canonical (no attributes)
206
+ if not attributes or len(attributes) == 0:
207
+ return {
208
+ "status": "existing",
209
+ "message": f"Found canonical Piclet for '{object_name}'",
210
+ "piclet": piclet_data.get("canonical")
211
+ }
212
+
213
+ # Search for matching variation
214
+ variations = piclet_data.get("variations", [])
215
+ for variation in variations:
216
+ var_attrs = set(variation.get("attributes", []))
217
+ search_attrs = set(attributes)
218
+
219
+ # Check for close match (at least 50% overlap)
220
+ overlap = len(var_attrs.intersection(search_attrs))
221
+ if overlap >= len(search_attrs) * 0.5:
222
+ return {
223
+ "status": "variation",
224
+ "message": f"Found variation of '{object_name}'",
225
+ "piclet": variation,
226
+ "canonicalId": piclet_data["canonical"]["typeId"]
227
+ }
228
+
229
+ # No variation found, suggest creating one
230
+ return {
231
+ "status": "new_variation",
232
+ "message": f"No variation found for '{object_name}' with attributes {attributes}",
233
+ "canonicalId": piclet_data["canonical"]["typeId"],
234
+ "piclet": None
235
+ }
236
+
237
+ def create_canonical(object_name: str, piclet_data: str, username: str) -> dict:
238
+ """
239
+ Create a new canonical Piclet
240
  """
241
  try:
242
+ piclet_json = json.loads(piclet_data) if isinstance(piclet_data, str) else piclet_data
243
+
244
+ # Create canonical entry
245
+ canonical_data = {
246
+ "canonical": {
247
+ "objectName": object_name,
248
+ "typeId": f"{PicletDiscoveryService.normalize_object_name(object_name)}_canonical",
249
+ "discoveredBy": username,
250
+ "discoveredAt": datetime.now().isoformat(),
251
+ "scanCount": 1,
252
+ "picletData": piclet_json
253
+ },
254
+ "variations": []
255
+ }
256
+
257
+ # Save to dataset
258
+ if PicletDiscoveryService.save_piclet_data(object_name, canonical_data):
259
+ # Update user profile
260
+ user_data = PicletDiscoveryService.load_user_data(username)
261
+ user_data["discoveries"].append(canonical_data["canonical"]["typeId"])
262
+ user_data["uniqueFinds"] += 1
263
+ user_data["totalFinds"] += 1
264
+ user_data["rarityScore"] += 100 # Bonus for canonical discovery
265
+ PicletDiscoveryService.save_user_data(username, user_data)
266
+
267
+ return {
 
 
 
 
 
268
  "success": True,
269
+ "message": f"Created canonical Piclet for '{object_name}'",
270
+ "piclet": canonical_data["canonical"]
271
+ }
 
272
  else:
273
+ return {
274
  "success": False,
275
+ "error": "Failed to save canonical Piclet"
276
+ }
 
 
 
 
 
 
277
  except Exception as e:
278
+ return {
279
  "success": False,
280
+ "error": str(e)
281
+ }
282
 
283
+ def create_variation(canonical_id: str, attributes: List[str], piclet_data: str, username: str, object_name: str) -> dict:
284
  """
285
+ Create a variation of an existing canonical Piclet
286
  """
287
  try:
288
+ piclet_json = json.loads(piclet_data) if isinstance(piclet_data, str) else piclet_data
289
+
290
+ # Load existing data
291
+ existing_data = PicletDiscoveryService.load_piclet_data(object_name)
292
+ if not existing_data:
293
+ return {
294
  "success": False,
295
+ "error": f"Canonical Piclet not found for '{object_name}'"
296
+ }
297
+
298
+ # Create variation entry
299
+ variation_id = f"{PicletDiscoveryService.normalize_object_name(object_name)}_{len(existing_data['variations']) + 1:03d}"
300
+ variation = {
301
+ "typeId": variation_id,
302
+ "attributes": attributes,
303
+ "discoveredBy": username,
304
+ "discoveredAt": datetime.now().isoformat(),
305
+ "scanCount": 1,
306
+ "picletData": piclet_json
307
+ }
308
+
309
+ # Add to variations
310
+ existing_data["variations"].append(variation)
311
+
312
+ # Save updated data
313
+ if PicletDiscoveryService.save_piclet_data(object_name, existing_data):
314
+ # Update user profile
315
+ user_data = PicletDiscoveryService.load_user_data(username)
316
+ user_data["discoveries"].append(variation_id)
317
+ user_data["totalFinds"] += 1
318
+ user_data["rarityScore"] += 50 # Bonus for variation discovery
319
+ PicletDiscoveryService.save_user_data(username, user_data)
320
+
321
+ return {
322
  "success": True,
323
+ "message": f"Created variation of '{object_name}'",
324
+ "piclet": variation
325
+ }
326
  else:
327
+ return {
328
  "success": False,
329
+ "error": "Failed to save variation"
330
+ }
 
331
  except Exception as e:
332
+ return {
333
  "success": False,
334
+ "error": str(e)
335
+ }
336
 
337
+ def increment_scan_count(piclet_id: str, object_name: str) -> dict:
338
  """
339
+ Increment the scan count for a Piclet
340
  """
341
  try:
342
+ data = PicletDiscoveryService.load_piclet_data(object_name)
343
+ if not data:
344
+ return {
345
+ "success": False,
346
+ "error": "Piclet not found"
347
+ }
348
+
349
+ # Check canonical
350
+ if data["canonical"]["typeId"] == piclet_id:
351
+ data["canonical"]["scanCount"] = data["canonical"].get("scanCount", 0) + 1
352
+ scan_count = data["canonical"]["scanCount"]
353
+ else:
354
+ # Check variations
355
+ for variation in data["variations"]:
356
+ if variation["typeId"] == piclet_id:
357
+ variation["scanCount"] = variation.get("scanCount", 0) + 1
358
+ scan_count = variation["scanCount"]
359
+ break
360
+ else:
361
+ return {
362
+ "success": False,
363
+ "error": "Piclet ID not found"
364
+ }
365
+
366
+ # Save updated data
367
+ if PicletDiscoveryService.save_piclet_data(object_name, data):
368
+ return {
369
  "success": True,
370
+ "scanCount": scan_count
371
+ }
 
372
  else:
373
+ return {
374
  "success": False,
375
+ "error": "Failed to update scan count"
376
+ }
 
377
  except Exception as e:
378
+ return {
379
  "success": False,
380
+ "error": str(e)
381
+ }
382
 
383
+ def get_recent_activity(limit: int = 20) -> dict:
384
  """
385
+ Get recent discoveries across all users
386
  """
387
  try:
388
+ activities = []
389
+
390
+ # List all piclet files
391
+ try:
392
+ files = list_repo_files(
393
+ repo_id=DATASET_REPO,
394
+ repo_type=DATASET_TYPE,
395
+ token=HF_TOKEN
396
+ )
397
+ piclet_files = [f for f in files if f.startswith("piclets/") and f.endswith(".json")]
398
+ except:
399
+ piclet_files = []
400
+
401
+ # Load recent piclets (simplified - in production, maintain a separate activity log)
402
+ for file_path in piclet_files[-limit:]:
403
+ try:
404
+ object_name = file_path.replace("piclets/", "").replace(".json", "")
405
+ data = PicletDiscoveryService.load_piclet_data(object_name)
406
+
407
+ if data:
408
+ # Add canonical discovery
409
+ canonical = data["canonical"]
410
+ activities.append({
411
+ "type": "discovery",
412
+ "objectName": object_name,
413
+ "typeId": canonical["typeId"],
414
+ "discoveredBy": canonical["discoveredBy"],
415
+ "discoveredAt": canonical["discoveredAt"],
416
+ "scanCount": canonical.get("scanCount", 1)
417
+ })
418
+
419
+ # Add recent variations
420
+ for variation in data.get("variations", [])[-5:]:
421
+ activities.append({
422
+ "type": "variation",
423
+ "objectName": object_name,
424
+ "typeId": variation["typeId"],
425
+ "attributes": variation["attributes"],
426
+ "discoveredBy": variation["discoveredBy"],
427
+ "discoveredAt": variation["discoveredAt"],
428
+ "scanCount": variation.get("scanCount", 1)
429
+ })
430
+ except:
431
+ continue
432
+
433
+ # Sort by discovery date
434
+ activities.sort(key=lambda x: x.get("discoveredAt", ""), reverse=True)
435
+
436
+ return {
437
+ "success": True,
438
+ "activities": activities[:limit]
439
+ }
440
  except Exception as e:
441
+ return {
442
+ "success": False,
443
+ "error": str(e),
444
+ "activities": []
445
+ }
446
 
447
+ def get_leaderboard(limit: int = 10) -> dict:
448
  """
449
+ Get top discoverers
 
450
  """
451
  try:
452
+ leaderboard = []
453
+
454
+ # List all user files
455
+ try:
456
+ files = list_repo_files(
457
+ repo_id=DATASET_REPO,
458
+ repo_type=DATASET_TYPE,
459
+ token=HF_TOKEN
460
+ )
461
+ user_files = [f for f in files if f.startswith("users/") and f.endswith(".json")]
462
+ except:
463
+ user_files = []
464
+
465
+ # Load user data
466
+ for file_path in user_files:
467
+ try:
468
+ username = file_path.replace("users/", "").replace(".json", "")
469
+ user_data = PicletDiscoveryService.load_user_data(username)
470
+
471
+ leaderboard.append({
472
+ "username": username,
473
+ "totalFinds": user_data.get("totalFinds", 0),
474
+ "uniqueFinds": user_data.get("uniqueFinds", 0),
475
+ "rarityScore": user_data.get("rarityScore", 0)
476
  })
477
+ except:
478
+ continue
479
+
480
+ # Sort by rarity score
481
+ leaderboard.sort(key=lambda x: x["rarityScore"], reverse=True)
482
+
483
+ # Add ranks
484
+ for i, entry in enumerate(leaderboard[:limit]):
485
+ entry["rank"] = i + 1
486
+
487
+ return {
 
488
  "success": True,
489
+ "leaderboard": leaderboard[:limit]
490
+ }
 
 
 
 
 
 
 
 
491
  except Exception as e:
492
+ return {
493
  "success": False,
494
+ "error": str(e),
495
+ "leaderboard": []
496
+ }
497
 
498
+ def get_user_profile(username: str) -> dict:
499
  """
500
+ Get user's discovery profile
 
501
  """
502
  try:
503
+ user_data = PicletDiscoveryService.load_user_data(username)
504
+ return {
505
+ "success": True,
506
+ "profile": user_data
507
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  except Exception as e:
509
+ return {
510
  "success": False,
511
+ "error": str(e),
512
+ "profile": None
513
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
 
515
+ # Create Gradio interface
516
+ with gr.Blocks(title="Piclets Discovery Server") as app:
517
  gr.Markdown("""
518
+ # 🔍 Piclets Discovery Server
519
+
520
+ Backend service for the Piclets discovery game. Each real-world object has ONE canonical Piclet!
 
 
 
 
 
 
521
  """)
522
+
523
+ with gr.Tab("Search Piclet"):
524
+ with gr.Row():
525
+ with gr.Column():
526
+ search_object = gr.Textbox(label="Object Name", placeholder="e.g., pillow")
527
+ search_attrs = gr.Textbox(label="Attributes (comma-separated)", placeholder="e.g., velvet, blue")
528
+ search_btn = gr.Button("Search", variant="primary")
529
+ with gr.Column():
530
+ search_result = gr.JSON(label="Search Result")
531
+
532
+ search_btn.click(
533
+ fn=lambda obj, attrs: search_piclet(obj, [a.strip() for a in attrs.split(",")] if attrs else []),
534
+ inputs=[search_object, search_attrs],
535
+ outputs=search_result
536
+ )
537
+
538
+ with gr.Tab("Create Canonical"):
539
+ with gr.Row():
540
+ with gr.Column():
541
+ canonical_object = gr.Textbox(label="Object Name")
542
+ canonical_data = gr.Textbox(label="Piclet Data (JSON)", lines=10)
543
+ canonical_user = gr.Textbox(label="Username")
544
+ canonical_btn = gr.Button("Create Canonical", variant="primary")
545
+ with gr.Column():
546
+ canonical_result = gr.JSON(label="Creation Result")
547
+
548
+ canonical_btn.click(
549
+ fn=create_canonical,
550
+ inputs=[canonical_object, canonical_data, canonical_user],
551
+ outputs=canonical_result
552
+ )
553
+
554
+ with gr.Tab("Create Variation"):
555
+ with gr.Row():
556
+ with gr.Column():
557
+ var_object = gr.Textbox(label="Object Name")
558
+ var_canonical = gr.Textbox(label="Canonical ID")
559
+ var_attrs = gr.Textbox(label="Variation Attributes (comma-separated)")
560
+ var_data = gr.Textbox(label="Piclet Data (JSON)", lines=10)
561
+ var_user = gr.Textbox(label="Username")
562
+ var_btn = gr.Button("Create Variation", variant="primary")
563
+ with gr.Column():
564
+ var_result = gr.JSON(label="Creation Result")
565
+
566
+ var_btn.click(
567
+ fn=lambda obj, cid, attrs, data, user: create_variation(
568
+ cid, [a.strip() for a in attrs.split(",")] if attrs else [], data, user, obj
569
+ ),
570
+ inputs=[var_object, var_canonical, var_attrs, var_data, var_user],
571
+ outputs=var_result
572
+ )
573
+
574
+ with gr.Tab("Activity Feed"):
575
+ activity_limit = gr.Slider(5, 50, value=20, label="Number of Activities")
576
+ activity_btn = gr.Button("Get Recent Activity")
577
+ activity_result = gr.JSON(label="Recent Discoveries")
578
+
579
+ activity_btn.click(
580
+ fn=get_recent_activity,
581
+ inputs=activity_limit,
582
+ outputs=activity_result
583
+ )
584
+
585
+ with gr.Tab("Leaderboard"):
586
+ leader_limit = gr.Slider(5, 20, value=10, label="Top N Discoverers")
587
+ leader_btn = gr.Button("Get Leaderboard")
588
+ leader_result = gr.JSON(label="Top Discoverers")
589
+
590
+ leader_btn.click(
591
+ fn=get_leaderboard,
592
+ inputs=leader_limit,
593
+ outputs=leader_result
594
+ )
595
+
596
+ with gr.Tab("User Profile"):
597
+ profile_user = gr.Textbox(label="Username")
598
+ profile_btn = gr.Button("Get Profile")
599
+ profile_result = gr.JSON(label="User Profile")
600
+
601
+ profile_btn.click(
602
+ fn=get_user_profile,
603
+ inputs=profile_user,
604
+ outputs=profile_result
605
+ )
606
+
607
+ # API Documentation
608
+ gr.Markdown("""
609
+ ## API Endpoints
610
+
611
+ All endpoints accept JSON and return JSON responses.
612
+
613
+ - **search_piclet**: Search for canonical or variation Piclets
614
+ - **create_canonical**: Register a new canonical Piclet
615
+ - **create_variation**: Add a variation to existing canonical
616
+ - **increment_scan_count**: Track discovery popularity
617
+ - **get_recent_activity**: Global discovery feed
618
+ - **get_leaderboard**: Top discoverers
619
+ - **get_user_profile**: Individual discovery stats
620
+
621
+ See API_DOCUMENTATION.md for detailed usage.
622
+ """)
623
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  if __name__ == "__main__":
625
+ app.launch()
 
 
 
 
requirements.txt CHANGED
@@ -1,2 +1,4 @@
1
  gradio==5.38.2
2
- Pillow>=9.0.0
 
 
 
1
  gradio==5.38.2
2
+ Pillow>=9.0.0
3
+ huggingface_hub>=0.20.0
4
+ datasets>=2.15.0