Clément PEPONNET commited on
Commit
0eadc0f
·
1 Parent(s): 4f7ad0f

Initialize project with main application and configuration files

Browse files
Files changed (4) hide show
  1. .gitignore +51 -0
  2. README.md +84 -7
  3. app.py +793 -0
  4. requirements.txt +3 -0
.gitignore ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data directory
2
+ data/
3
+ *.json
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ # Virtual environments
28
+ venv/
29
+ ENV/
30
+ env/
31
+ .env
32
+ .venv
33
+
34
+ # IDE
35
+ .vscode/
36
+ .idea/
37
+ *.swp
38
+ *.swo
39
+
40
+ # OS
41
+ .DS_Store
42
+ Thumbs.db
43
+
44
+ # Logs
45
+ *.log
46
+
47
+ # Images (except reference)
48
+ *.png
49
+ *.jpg
50
+ *.jpeg
51
+ !reference_sticker.png
README.md CHANGED
@@ -1,14 +1,91 @@
1
  ---
2
- title: Tchoupinoux Inviders
3
- emoji: 🔥
4
- colorFrom: yellow
5
- colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 5.47.2
8
  app_file: app.py
9
  pinned: false
10
- license: mit
11
- short_description: 🎯 Gradio app déployée sur Hugging Face pour chasser des sti
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Tchoupinoux Inviders - Sticker Hunt
3
+ emoji: 🎯
4
+ colorFrom: black
5
+ colorTo: green
6
  sdk: gradio
7
  sdk_version: 5.47.2
8
  app_file: app.py
9
  pinned: false
 
 
10
  ---
11
 
12
+ # Tchoupinoux Inviders - Sticker Hunt
13
+
14
+ A gamified sticker collection application where users hunt for B15 Tchoupinoux stickers around office locations.
15
+
16
+ ## Features
17
+
18
+ - **User Authentication**: Register and login to track your personal collection
19
+ - **Camera Integration**: Capture stickers directly from your device
20
+ - **Location-Based Tracking**: Select from predefined locations
21
+ - **Points System**:
22
+ - 50 points for stickers in Tour Franklin
23
+ - 100 points for stickers elsewhere
24
+ - **Collection Gallery**: View all your found stickers
25
+ - **Leaderboard**: Compete with other collectors
26
+
27
+ ## How to Use
28
+
29
+ 1. **Register/Login**: Create an account or login with existing credentials
30
+ 2. **Hunt**: Use your camera to capture stickers you find
31
+ 3. **Select Location**: Choose where you found the sticker
32
+ 4. **Submit**: The app will verify the sticker and add it to your collection
33
+ 5. **Collect**: Track your progress in the Collection tab
34
+
35
+ ## Tech Stack
36
+
37
+ - **Framework**: Gradio
38
+ - **Storage**: JSON-based file system
39
+ - **Authentication**: SHA-256 password hashing
40
+ - **Image Processing**: PIL + NumPy
41
+
42
+ ## Deployment
43
+
44
+ This app is designed to run on Hugging Face Spaces.
45
+
46
+ ### Setup Instructions
47
+
48
+ 1. Create a new Space on Hugging Face
49
+ 2. Choose "Gradio" as the SDK
50
+ 3. Upload `app.py` and `requirements.txt`
51
+ 4. The app will automatically start
52
+
53
+ ### File Structure
54
+
55
+ ```
56
+ .
57
+ ├── app.py # Main application
58
+ ├── requirements.txt # Python dependencies
59
+ └── data/ # Auto-created data directory
60
+ ├── users.json # User accounts
61
+ ├── collections.json # User collections
62
+ ├── stickers.json # Sticker configurations
63
+ └── images/ # Captured sticker images
64
+ ```
65
+
66
+ ## Configuration
67
+
68
+ Edit the stickers configuration in the code to add new locations:
69
+
70
+ ```python
71
+ stickers = {
72
+ "Location Name": {
73
+ "count": 2, # Number of stickers at this location
74
+ "points": 50, # Points per sticker
75
+ "found_indices": []
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## Note on Sticker Verification
81
+
82
+ The current implementation uses a basic image verification system. For production use, consider implementing:
83
+
84
+ - CLIP-based similarity matching
85
+ - Custom trained computer vision model
86
+ - Template matching algorithms
87
+ - OCR for text detection
88
+
89
+ ## License
90
+
91
+ MIT License
app.py ADDED
@@ -0,0 +1,793 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ B15 Tchoupinoux Sticker Hunt Application
3
+ A Gradio-based app for collecting stickers around office locations
4
+ """
5
+
6
+ import gradio as gr
7
+ import json
8
+ import os
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ import hashlib
12
+ from PIL import Image
13
+ import numpy as np
14
+
15
+ # ============================================================================
16
+ # CONFIGURATION
17
+ # ============================================================================
18
+
19
+ DATA_DIR = Path("data")
20
+ DATA_DIR.mkdir(exist_ok=True)
21
+
22
+ USERS_FILE = DATA_DIR / "users.json"
23
+ COLLECTIONS_FILE = DATA_DIR / "collections.json"
24
+ STICKERS_FILE = DATA_DIR / "stickers.json"
25
+
26
+ # Color scheme
27
+ PRIMARY_COLOR = "#000000" # Black
28
+ SECONDARY_COLOR = "#FFFFFF" # White
29
+ ACCENT_COLOR = "#7CFC00" # Light green
30
+
31
+ # Points system
32
+ POINTS_FRANKLIN = 50
33
+ POINTS_ELSEWHERE = 100
34
+
35
+
36
+ # ============================================================================
37
+ # DATA STRUCTURES
38
+ # ============================================================================
39
+
40
+
41
+ def init_data_files():
42
+ """Initialize data files if they don't exist"""
43
+
44
+ # Initialize users file
45
+ if not USERS_FILE.exists():
46
+ with open(USERS_FILE, "w") as f:
47
+ json.dump({}, f)
48
+
49
+ # Initialize stickers configuration
50
+ if not STICKERS_FILE.exists():
51
+ stickers = {
52
+ "Tour Franklin - 22e étage - B15": {
53
+ "count": 1,
54
+ "points": POINTS_FRANKLIN,
55
+ "found_indices": [],
56
+ },
57
+ "Tour Franklin - 18e étage - B15": {
58
+ "count": 1,
59
+ "points": POINTS_FRANKLIN,
60
+ "found_indices": [],
61
+ },
62
+ "Tour Franklin - 17e étage - B15": {
63
+ "count": 1,
64
+ "points": POINTS_FRANKLIN,
65
+ "found_indices": [],
66
+ },
67
+ "Tour Franklin - 16e étage - B15": {
68
+ "count": 2,
69
+ "points": POINTS_FRANKLIN,
70
+ "found_indices": [],
71
+ },
72
+ "Tour Franklin - 15e étage - B15": {
73
+ "count": 1,
74
+ "points": POINTS_FRANKLIN,
75
+ "found_indices": [],
76
+ },
77
+ "Tour Franklin - RDC - Entrée": {
78
+ "count": 1,
79
+ "points": POINTS_FRANKLIN,
80
+ "found_indices": [],
81
+ },
82
+ "Le Forum": {
83
+ "count": 1,
84
+ "points": POINTS_ELSEWHERE,
85
+ "found_indices": [],
86
+ },
87
+ "Le Philiosophe ": {
88
+ "count": 1,
89
+ "points": POINTS_ELSEWHERE,
90
+ "found_indices": [],
91
+ },
92
+ "Autre location": {
93
+ "count": 1,
94
+ "points": POINTS_ELSEWHERE,
95
+ "found_indices": [],
96
+ },
97
+ }
98
+ with open(STICKERS_FILE, "w") as f:
99
+ json.dump(stickers, f, indent=2)
100
+
101
+ # Initialize collections file
102
+ if not COLLECTIONS_FILE.exists():
103
+ with open(COLLECTIONS_FILE, "w") as f:
104
+ json.dump({}, f)
105
+
106
+
107
+ def load_json(filepath):
108
+ """Load JSON data from file"""
109
+ with open(filepath, "r") as f:
110
+ return json.load(f)
111
+
112
+
113
+ def save_json(filepath, data):
114
+ """Save JSON data to file"""
115
+ with open(filepath, "w") as f:
116
+ json.dump(data, f, indent=2)
117
+
118
+
119
+ # ============================================================================
120
+ # USER MANAGEMENT
121
+ # ============================================================================
122
+
123
+
124
+ def hash_password(password):
125
+ """Hash password using SHA-256"""
126
+ return hashlib.sha256(password.encode()).hexdigest()
127
+
128
+
129
+ def register_user(username, password):
130
+ """Register a new user"""
131
+ if not username or not password:
132
+ return False, "Username and password are required"
133
+
134
+ users = load_json(USERS_FILE)
135
+
136
+ if username in users:
137
+ return False, "Username already exists"
138
+
139
+ users[username] = {
140
+ "password": hash_password(password),
141
+ "points": 0,
142
+ "created_at": datetime.now().isoformat(),
143
+ }
144
+
145
+ save_json(USERS_FILE, users)
146
+
147
+ # Initialize user collection
148
+ collections = load_json(COLLECTIONS_FILE)
149
+ collections[username] = []
150
+ save_json(COLLECTIONS_FILE, collections)
151
+
152
+ return True, "Registration successful"
153
+
154
+
155
+ def authenticate_user(username, password):
156
+ """Authenticate user credentials"""
157
+ if not username or not password:
158
+ return False, "Username and password are required"
159
+
160
+ users = load_json(USERS_FILE)
161
+
162
+ if username not in users:
163
+ return False, "Invalid username or password"
164
+
165
+ if users[username]["password"] != hash_password(password):
166
+ return False, "Invalid username or password"
167
+
168
+ return True, f"Welcome back, {username}"
169
+
170
+
171
+ def get_user_points(username):
172
+ """Get user's current points"""
173
+ users = load_json(USERS_FILE)
174
+ return users.get(username, {}).get("points", 0)
175
+
176
+
177
+ def update_user_points(username, points_to_add):
178
+ """Update user's points"""
179
+ users = load_json(USERS_FILE)
180
+ if username in users:
181
+ users[username]["points"] += points_to_add
182
+ save_json(USERS_FILE, users)
183
+
184
+
185
+ # ============================================================================
186
+ # STICKER VERIFICATION
187
+ # ============================================================================
188
+
189
+
190
+ def verify_sticker_in_image(image):
191
+ """
192
+ Verify if the image contains a B15 Tchoupinoux sticker
193
+
194
+ This is a simplified version. In production, you would use:
195
+ - CLIP model for image similarity
196
+ - Custom trained model
197
+ - Template matching
198
+
199
+ For now, we'll use basic image properties as a placeholder
200
+ """
201
+ if image is None:
202
+ return False, 0.0
203
+
204
+ # Convert to numpy array if needed
205
+ if isinstance(image, Image.Image):
206
+ img_array = np.array(image)
207
+ else:
208
+ img_array = image
209
+
210
+ # Basic checks - in production, replace with actual ML model
211
+ # Check if image has sufficient contrast (black and white)
212
+ gray = np.mean(img_array, axis=2) if len(img_array.shape) == 3 else img_array
213
+ contrast = np.std(gray)
214
+
215
+ # Check if image has some dark areas (black from sticker)
216
+ dark_pixels = np.sum(gray < 100) / gray.size
217
+
218
+ # Simple heuristic - replace with actual model
219
+ confidence = min(1.0, (contrast / 100) * (dark_pixels * 5))
220
+
221
+ # Threshold for acceptance
222
+ is_valid = confidence > 0.3
223
+
224
+ return is_valid, confidence
225
+
226
+
227
+ # ============================================================================
228
+ # COLLECTION MANAGEMENT
229
+ # ============================================================================
230
+
231
+
232
+ def add_to_collection(username, location, image_path):
233
+ """Add a found sticker to user's collection"""
234
+ collections = load_json(COLLECTIONS_FILE)
235
+ stickers = load_json(STICKERS_FILE)
236
+
237
+ if username not in collections:
238
+ collections[username] = []
239
+
240
+ # Check if location is valid
241
+ if location not in stickers:
242
+ return False, "Invalid location", 0
243
+
244
+ # Check if user already found all stickers at this location
245
+ user_found_at_location = sum(
246
+ 1 for item in collections[username] if item["location"] == location
247
+ )
248
+ if user_found_at_location >= stickers[location]["count"]:
249
+ return False, f"You already found all stickers at {location}", 0
250
+
251
+ # Add to collection
252
+ collection_item = {
253
+ "location": location,
254
+ "timestamp": datetime.now().isoformat(),
255
+ "image": image_path,
256
+ }
257
+
258
+ collections[username].append(collection_item)
259
+ save_json(COLLECTIONS_FILE, collections)
260
+
261
+ # Update points
262
+ points = stickers[location]["points"]
263
+ update_user_points(username, points)
264
+
265
+ return True, f"Sticker found! +{points} points", points
266
+
267
+
268
+ def get_user_collection(username):
269
+ """Get user's collection"""
270
+ collections = load_json(COLLECTIONS_FILE)
271
+ return collections.get(username, [])
272
+
273
+
274
+ def get_leaderboard():
275
+ """Get top users by points"""
276
+ users = load_json(USERS_FILE)
277
+ leaderboard = [(username, data["points"]) for username, data in users.items()]
278
+ leaderboard.sort(key=lambda x: x[1], reverse=True)
279
+ return leaderboard[:10]
280
+
281
+
282
+ # ============================================================================
283
+ # GRADIO INTERFACE
284
+ # ============================================================================
285
+
286
+ # Global state for current user
287
+ current_user = {"username": None}
288
+
289
+
290
+ def login_interface(username, password):
291
+ """Handle login"""
292
+ success, message = authenticate_user(username, password)
293
+ if success:
294
+ current_user["username"] = username
295
+ points = get_user_points(username)
296
+ user_html = f"""
297
+ <div style="background: {SECONDARY_COLOR};
298
+ border: 3px solid {PRIMARY_COLOR};
299
+ box-shadow: inset 0 0 0 1px {ACCENT_COLOR}, 0 0 20px rgba(124, 252, 0, 0.2);
300
+ border-radius: 8px;
301
+ padding: 15px; text-align: center; margin-bottom: 20px;">
302
+ <h2 style="color: {PRIMARY_COLOR}; margin: 0; font-size: 1.5em;">
303
+ 👤 {username} • <span style="color: {ACCENT_COLOR}; font-weight: bold;">⚡ {points} pts</span>
304
+ </h2>
305
+ </div>
306
+ """
307
+ return (
308
+ gr.update(visible=False), # Hide login
309
+ gr.update(visible=True), # Show main app
310
+ user_html,
311
+ message,
312
+ )
313
+ return (gr.update(visible=True), gr.update(visible=False), "", message)
314
+
315
+
316
+ def register_interface(username, password):
317
+ """Handle registration"""
318
+ success, message = register_user(username, password)
319
+ return message
320
+
321
+
322
+ def capture_sticker(image, location):
323
+ """Handle sticker capture and verification"""
324
+ if current_user["username"] is None:
325
+ return "Please login first", None, ""
326
+
327
+ if image is None:
328
+ return "Please take a photo first", None, ""
329
+
330
+ if not location:
331
+ return "Please select a location", None, ""
332
+
333
+ # Verify sticker in image
334
+ is_valid, confidence = verify_sticker_in_image(image)
335
+
336
+ if not is_valid:
337
+ return (
338
+ f"No valid sticker detected (confidence: {confidence:.2%}). Please try again.",
339
+ None,
340
+ "",
341
+ )
342
+
343
+ # Save image
344
+ img_dir = DATA_DIR / "images" / current_user["username"]
345
+ img_dir.mkdir(parents=True, exist_ok=True)
346
+ img_path = img_dir / f"{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
347
+ Image.fromarray(image).save(img_path)
348
+
349
+ # Add to collection
350
+ success, message, points = add_to_collection(
351
+ current_user["username"], location, str(img_path)
352
+ )
353
+
354
+ if success:
355
+ total_points = get_user_points(current_user["username"])
356
+ user_html = f"""
357
+ <div style="background: {SECONDARY_COLOR};
358
+ border: 3px solid {PRIMARY_COLOR};
359
+ box-shadow: inset 0 0 0 1px {ACCENT_COLOR}, 0 0 20px rgba(124, 252, 0, 0.2);
360
+ border-radius: 8px;
361
+ padding: 15px; text-align: center; margin-bottom: 20px;">
362
+ <h2 style="color: {PRIMARY_COLOR}; margin: 0; font-size: 1.5em;">
363
+ 👤 {current_user['username']} • <span style="color: {ACCENT_COLOR}; font-weight: bold;">⚡ {total_points} pts</span>
364
+ </h2>
365
+ </div>
366
+ """
367
+ return (message, image, user_html)
368
+ else:
369
+ user_html = f"""
370
+ <div style="background: {SECONDARY_COLOR};
371
+ border: 3px solid {PRIMARY_COLOR};
372
+ box-shadow: inset 0 0 0 1px {ACCENT_COLOR}, 0 0 20px rgba(124, 252, 0, 0.2);
373
+ border-radius: 8px;
374
+ padding: 15px; text-align: center; margin-bottom: 20px;">
375
+ <h2 style="color: {PRIMARY_COLOR}; margin: 0; font-size: 1.5em;">
376
+ 👤 {current_user['username']} • <span style="color: {ACCENT_COLOR}; font-weight: bold;">⚡ {get_user_points(current_user['username'])} pts</span>
377
+ </h2>
378
+ </div>
379
+ """
380
+ return message, None, user_html
381
+
382
+
383
+ def load_collection():
384
+ """Load user's collection for display"""
385
+ if current_user["username"] is None:
386
+ return []
387
+
388
+ collection = get_user_collection(current_user["username"])
389
+
390
+ # Create gallery items
391
+ gallery_items = []
392
+ for item in collection:
393
+ if os.path.exists(item["image"]):
394
+ gallery_items.append(
395
+ (item["image"], f"{item['location']}\n{item['timestamp'][:10]}")
396
+ )
397
+
398
+ return gallery_items
399
+
400
+
401
+ def load_leaderboard():
402
+ """Load leaderboard"""
403
+ leaderboard = get_leaderboard()
404
+
405
+ if not leaderboard:
406
+ return "<p style='text-align: center; color: #7CFC00; font-weight: bold;'>No users yet</p>"
407
+
408
+ output = ""
409
+ for idx, (username, points) in enumerate(leaderboard, 1):
410
+ medal = (
411
+ "🥇" if idx == 1 else "🥈" if idx == 2 else "🥉" if idx == 3 else f"#{idx}"
412
+ )
413
+
414
+ # Different styling for top 3
415
+ if idx <= 3:
416
+ bg_color = (
417
+ "rgba(124, 252, 0, 0.2)"
418
+ if idx == 1
419
+ else "rgba(124, 252, 0, 0.15)" if idx == 2 else "rgba(124, 252, 0, 0.1)"
420
+ )
421
+ border_color = (
422
+ "#7CFC00" if idx == 1 else "#6FE000" if idx == 2 else "#5DD000"
423
+ )
424
+ border_width = "4px" if idx == 1 else "3px"
425
+ output += f"""
426
+ <div style="background: {bg_color}; border: {border_width} solid {border_color};
427
+ padding: 15px; margin: 10px 0; border-radius: 8px;
428
+ box-shadow: 0 0 15px rgba(124, 252, 0, 0.3);">
429
+ <span style="font-size: 1.5em;">{medal}</span>
430
+ <strong style="color: #000; font-size: 1.2em; margin-left: 10px;">{username}</strong>
431
+ <span style="float: right; color: {border_color}; font-weight: bold; font-size: 1.2em;">⚡ {points} pts</span>
432
+ </div>
433
+ """
434
+ else:
435
+ output += f"""
436
+ <div style="background: #f5f5f5; border: 2px solid #333;
437
+ padding: 12px; margin: 8px 0; border-radius: 5px;">
438
+ <span style="color: #666;">{medal}</span>
439
+ <strong style="color: #000; margin-left: 10px;">{username}</strong>
440
+ <span style="float: right; color: #7CFC00; font-weight: bold;">{points} pts</span>
441
+ </div>
442
+ """
443
+
444
+ return output
445
+
446
+
447
+ def logout():
448
+ """Handle logout"""
449
+ current_user["username"] = None
450
+ return (
451
+ gr.update(visible=True), # Show login
452
+ gr.update(visible=False), # Hide main app
453
+ "",
454
+ "Logged out successfully",
455
+ )
456
+
457
+
458
+ # ============================================================================
459
+ # BUILD INTERFACE
460
+ # ============================================================================
461
+
462
+
463
+ def build_app():
464
+ """Build the Gradio application"""
465
+ init_data_files()
466
+
467
+ # Get available locations
468
+ stickers = load_json(STICKERS_FILE)
469
+ locations = list(stickers.keys())
470
+
471
+ # Custom CSS
472
+ css = f"""
473
+ .gradio-container {{
474
+ font-family: 'Arial', sans-serif;
475
+ background-color: {SECONDARY_COLOR} !important;
476
+ }}
477
+ .primary-btn {{
478
+ background-color: {PRIMARY_COLOR} !important;
479
+ color: {SECONDARY_COLOR} !important;
480
+ border: 3px solid {ACCENT_COLOR} !important;
481
+ font-weight: bold !important;
482
+ text-transform: uppercase !important;
483
+ letter-spacing: 1px !important;
484
+ transition: all 0.3s ease !important;
485
+ }}
486
+ .primary-btn:hover {{
487
+ background-color: {ACCENT_COLOR} !important;
488
+ color: {PRIMARY_COLOR} !important;
489
+ transform: scale(1.05) !important;
490
+ box-shadow: 0 0 20px {ACCENT_COLOR} !important;
491
+ }}
492
+ .header-box {{
493
+ background-color: {PRIMARY_COLOR} !important;
494
+ padding: 20px !important;
495
+ border-radius: 8px !important;
496
+ border: 4px solid {ACCENT_COLOR} !important;
497
+ margin-bottom: 20px !important;
498
+ box-shadow: 0 0 30px rgba(124, 252, 0, 0.3) !important;
499
+ }}
500
+ .header-box h1, .header-box h3 {{
501
+ color: {SECONDARY_COLOR} !important;
502
+ margin: 0 !important;
503
+ }}
504
+ .tabs {{
505
+ border-bottom: 3px solid {ACCENT_COLOR} !important;
506
+ }}
507
+ .tab-nav button.selected {{
508
+ border-bottom: 4px solid {ACCENT_COLOR} !important;
509
+ color: {PRIMARY_COLOR} !important;
510
+ font-weight: bold !important;
511
+ background-color: rgba(124, 252, 0, 0.1) !important;
512
+ }}
513
+ .tab-nav button:hover {{
514
+ color: {ACCENT_COLOR} !important;
515
+ background-color: rgba(124, 252, 0, 0.05) !important;
516
+ }}
517
+ .gr-button-secondary {{
518
+ background-color: {SECONDARY_COLOR} !important;
519
+ border: 3px solid {PRIMARY_COLOR} !important;
520
+ color: {PRIMARY_COLOR} !important;
521
+ font-weight: bold !important;
522
+ }}
523
+ .gr-button-secondary:hover {{
524
+ background-color: {PRIMARY_COLOR} !important;
525
+ color: {SECONDARY_COLOR} !important;
526
+ border-color: {ACCENT_COLOR} !important;
527
+ box-shadow: 0 0 15px rgba(124, 252, 0, 0.3) !important;
528
+ }}
529
+ .gr-form {{
530
+ border: 2px solid {PRIMARY_COLOR} !important;
531
+ background-color: {SECONDARY_COLOR} !important;
532
+ }}
533
+ .gr-input, .gr-dropdown {{
534
+ border: 2px solid {PRIMARY_COLOR} !important;
535
+ background-color: {SECONDARY_COLOR} !important;
536
+ color: {PRIMARY_COLOR} !important;
537
+ }}
538
+ .gr-input:focus, .gr-dropdown:focus {{
539
+ border-color: {ACCENT_COLOR} !important;
540
+ box-shadow: 0 0 10px rgba(124, 252, 0, 0.3) !important;
541
+ }}
542
+ .user-info-box {{
543
+ background: {SECONDARY_COLOR} !important;
544
+ border: 3px solid {PRIMARY_COLOR} !important;
545
+ border-radius: 8px !important;
546
+ padding: 15px !important;
547
+ text-align: center !important;
548
+ box-shadow: 0 0 20px rgba(124, 252, 0, 0.2), inset 0 0 0 1px {ACCENT_COLOR} !important;
549
+ }}
550
+ .user-info-box h2 {{
551
+ color: {PRIMARY_COLOR} !important;
552
+ margin: 0 !important;
553
+ font-size: 1.5em !important;
554
+ }}
555
+ .status-success {{
556
+ color: {ACCENT_COLOR} !important;
557
+ font-weight: bold !important;
558
+ }}
559
+ .gallery-item {{
560
+ border: 2px solid {PRIMARY_COLOR} !important;
561
+ transition: all 0.3s ease !important;
562
+ }}
563
+ .gallery-item:hover {{
564
+ border-color: {ACCENT_COLOR} !important;
565
+ box-shadow: 0 0 15px rgba(124, 252, 0, 0.4) !important;
566
+ transform: scale(1.05) !important;
567
+ }}
568
+ .section-header {{
569
+ background: linear-gradient(90deg, transparent, rgba(124, 252, 0, 0.15), transparent) !important;
570
+ border-left: 4px solid {ACCENT_COLOR} !important;
571
+ border-right: 4px solid {ACCENT_COLOR} !important;
572
+ padding: 15px !important;
573
+ margin-bottom: 20px !important;
574
+ }}
575
+ """
576
+
577
+ with gr.Blocks(css=css, theme=gr.themes.Monochrome()) as app:
578
+
579
+ # Header
580
+ with gr.Group():
581
+ gr.HTML(
582
+ f"""
583
+ <div style="background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%);
584
+ padding: 40px; border-radius: 12px;
585
+ border: 4px solid {ACCENT_COLOR}; text-align: center; margin-bottom: 20px;
586
+ box-shadow: 0 0 40px rgba(124, 252, 0, 0.4), inset 0 0 60px rgba(124, 252, 0, 0.1);">
587
+ <h1 style="color: {SECONDARY_COLOR}; margin: 0; font-size: 3em;
588
+ text-shadow: 0 0 20px rgba(124, 252, 0, 0.6);">
589
+ 🦝 TCHOUPINOUX INVIDERS
590
+ </h1>
591
+ <h3 style="color: {ACCENT_COLOR}; margin: 15px 0 0 0; font-weight: bold;
592
+ letter-spacing: 3px; text-transform: uppercase;
593
+ text-shadow: 0 0 10px rgba(124, 252, 0, 0.8);">
594
+ ⚡ STICKER HUNT CHALLENGE ⚡
595
+ </h3>
596
+ </div>
597
+ """
598
+ )
599
+
600
+ # ====================================================================
601
+ # LOGIN SCREEN
602
+ # ====================================================================
603
+ with gr.Column(visible=True) as login_screen:
604
+ gr.HTML(
605
+ f"""
606
+ <div style="text-align: center; padding: 20px; margin-bottom: 20px;
607
+ background: linear-gradient(90deg, transparent, rgba(124, 252, 0, 0.15), transparent);
608
+ border-top: 3px solid {ACCENT_COLOR}; border-bottom: 3px solid {ACCENT_COLOR};">
609
+ <h2 style="color: {PRIMARY_COLOR}; margin: 0; text-transform: uppercase;
610
+ letter-spacing: 2px;">
611
+ 🔐 Login or Register
612
+ </h2>
613
+ </div>
614
+ """
615
+ )
616
+
617
+ with gr.Tab("🔑 Login"):
618
+ login_username = gr.Textbox(
619
+ label="Username", placeholder="Enter your username"
620
+ )
621
+ login_password = gr.Textbox(
622
+ label="Password", type="password", placeholder="Enter your password"
623
+ )
624
+ login_btn = gr.Button("🚀 LOGIN", variant="primary")
625
+ login_message = gr.Textbox(
626
+ label="", interactive=False, show_label=False
627
+ )
628
+
629
+ with gr.Tab("✨ Register"):
630
+ reg_username = gr.Textbox(
631
+ label="Username", placeholder="Choose a username"
632
+ )
633
+ reg_password = gr.Textbox(
634
+ label="Password", type="password", placeholder="Choose a password"
635
+ )
636
+ reg_btn = gr.Button("⚡ REGISTER", variant="primary")
637
+ reg_message = gr.Textbox(label="", interactive=False, show_label=False)
638
+
639
+ # ====================================================================
640
+ # MAIN APPLICATION
641
+ # ====================================================================
642
+ with gr.Column(visible=False) as main_app:
643
+
644
+ # User info header
645
+ user_info = gr.HTML(
646
+ f"""
647
+ <div style="background: {SECONDARY_COLOR};
648
+ border: 3px solid {PRIMARY_COLOR};
649
+ box-shadow: inset 0 0 0 1px {ACCENT_COLOR}, 0 0 20px rgba(124, 252, 0, 0.2);
650
+ border-radius: 8px;
651
+ padding: 15px; text-align: center; margin-bottom: 20px;">
652
+ <h2 style="color: {PRIMARY_COLOR}; margin: 0; font-size: 1.5em;">
653
+ 👤 User
654
+ </h2>
655
+ </div>
656
+ """
657
+ )
658
+
659
+ with gr.Tabs():
660
+
661
+ # HUNT TAB
662
+ with gr.Tab("🎯 Hunt"):
663
+ gr.HTML(
664
+ f"""
665
+ <div style="text-align: center; padding: 20px;
666
+ background: linear-gradient(90deg, transparent, rgba(124, 252, 0, 0.15), transparent);
667
+ border-left: 4px solid {ACCENT_COLOR}; border-right: 4px solid {ACCENT_COLOR};
668
+ margin-bottom: 20px;">
669
+ <h3 style="color: {PRIMARY_COLOR}; margin: 0; text-transform: uppercase;
670
+ letter-spacing: 2px;">
671
+ 📸 Capture a Sticker
672
+ </h3>
673
+ </div>
674
+ """
675
+ )
676
+
677
+ camera = gr.Image(
678
+ type="numpy",
679
+ label="Take a photo of the sticker",
680
+ sources=["webcam"],
681
+ webcam_options=gr.WebcamOptions(mirror=True),
682
+ )
683
+
684
+ location_dropdown = gr.Dropdown(
685
+ choices=locations,
686
+ label="Select location",
687
+ info="Where did you find this sticker?",
688
+ )
689
+
690
+ capture_btn = gr.Button("🚀 SUBMIT", variant="primary", size="lg")
691
+
692
+ capture_message = gr.Textbox(label="Status", interactive=False)
693
+ capture_preview = gr.Image(label="Captured sticker", visible=True)
694
+
695
+ # COLLECTION TAB
696
+ with gr.Tab("🏆 Collection"):
697
+ gr.HTML(
698
+ f"""
699
+ <div style="text-align: center; padding: 20px;
700
+ background: linear-gradient(90deg, transparent, rgba(124, 252, 0, 0.15), transparent);
701
+ border-left: 4px solid {ACCENT_COLOR}; border-right: 4px solid {ACCENT_COLOR};
702
+ margin-bottom: 20px;">
703
+ <h3 style="color: {PRIMARY_COLOR}; margin: 0; text-transform: uppercase;
704
+ letter-spacing: 2px;">
705
+ 🎨 Your Collected Stickers
706
+ </h3>
707
+ </div>
708
+ """
709
+ )
710
+
711
+ refresh_btn = gr.Button(
712
+ "🔄 Refresh Collection", variant="secondary"
713
+ )
714
+
715
+ collection_gallery = gr.Gallery(
716
+ label="Your stickers",
717
+ show_label=False,
718
+ columns=3,
719
+ height="auto",
720
+ )
721
+
722
+ # LEADERBOARD TAB
723
+ with gr.Tab("⚡ Leaderboard"):
724
+ gr.HTML(
725
+ f"""
726
+ <div style="text-align: center; padding: 20px;
727
+ background: linear-gradient(90deg, transparent, rgba(124, 252, 0, 0.15), transparent);
728
+ border-left: 4px solid {ACCENT_COLOR}; border-right: 4px solid {ACCENT_COLOR};
729
+ margin-bottom: 20px;">
730
+ <h3 style="color: {PRIMARY_COLOR}; margin: 0; text-transform: uppercase;
731
+ letter-spacing: 2px;">
732
+ 👑 Top Collectors
733
+ </h3>
734
+ </div>
735
+ """
736
+ )
737
+
738
+ refresh_leaderboard_btn = gr.Button(
739
+ "🔄 Refresh", variant="secondary"
740
+ )
741
+
742
+ leaderboard_display = gr.HTML()
743
+
744
+ logout_btn = gr.Button("🚪 Logout", variant="stop")
745
+
746
+ # ====================================================================
747
+ # EVENT HANDLERS
748
+ # ====================================================================
749
+
750
+ # Login
751
+ login_btn.click(
752
+ fn=login_interface,
753
+ inputs=[login_username, login_password],
754
+ outputs=[login_screen, main_app, user_info, login_message],
755
+ )
756
+
757
+ # Register
758
+ reg_btn.click(
759
+ fn=register_interface,
760
+ inputs=[reg_username, reg_password],
761
+ outputs=[reg_message],
762
+ )
763
+
764
+ # Capture sticker
765
+ capture_btn.click(
766
+ fn=capture_sticker,
767
+ inputs=[camera, location_dropdown],
768
+ outputs=[capture_message, capture_preview, user_info],
769
+ )
770
+
771
+ # Refresh collection
772
+ refresh_btn.click(fn=load_collection, outputs=[collection_gallery])
773
+
774
+ # Refresh leaderboard
775
+ refresh_leaderboard_btn.click(
776
+ fn=load_leaderboard, outputs=[leaderboard_display]
777
+ )
778
+
779
+ # Logout
780
+ logout_btn.click(
781
+ fn=logout, outputs=[login_screen, main_app, user_info, login_message]
782
+ )
783
+
784
+ return app
785
+
786
+
787
+ # ============================================================================
788
+ # MAIN
789
+ # ============================================================================
790
+
791
+ if __name__ == "__main__":
792
+ app = build_app()
793
+ app.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio
2
+ Pillow
3
+ numpy