thejagstudio commited on
Commit
411eadc
·
verified ·
1 Parent(s): 1727df4

Upload 4 files

Browse files
Files changed (4) hide show
  1. main.py +97 -0
  2. metadata.json +436 -0
  3. requirements.txt +4 -0
  4. templates/index.html +450 -0
main.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, render_template, jsonify
2
+ import cv2
3
+ import numpy as np
4
+ import tensorflow as tf
5
+ import pandas as pd
6
+ import base64
7
+ import json
8
+
9
+ # Initialize Flask app
10
+ app = Flask(__name__)
11
+
12
+ # Load model and data at startup
13
+ model = tf.keras.models.load_model("sneaker_category_predictor_v2.h5")
14
+
15
+ # Define expected columns for one-hot encoding
16
+ with open("metadata.json", "r") as f:
17
+ METADATA_COLUMNS = json.load(f)
18
+
19
+
20
+ def encode_metadata(data):
21
+ # Create DataFrame with single row
22
+ df = pd.DataFrame({k: [v.lower()] for k, v in data.items()})
23
+
24
+ # Initialize empty DataFrame with all possible columns
25
+ encoded = pd.DataFrame()
26
+
27
+ # Encode each feature maintaining consistent columns
28
+ for feature, possible_values in METADATA_COLUMNS.items():
29
+ feature_encoded = pd.get_dummies(df[feature], prefix=feature)
30
+ # Add missing columns with 0s
31
+ for value in possible_values:
32
+ col_name = f"{feature}_{value}"
33
+ if col_name not in feature_encoded.columns:
34
+ feature_encoded[col_name] = 0
35
+ encoded = pd.concat([encoded, feature_encoded], axis=1)
36
+
37
+ # Ensure consistent column order
38
+ all_columns = [
39
+ f"{feat}_{val}" for feat, vals in METADATA_COLUMNS.items() for val in vals
40
+ ]
41
+ encoded = encoded.reindex(columns=all_columns, fill_value=0)
42
+
43
+ return encoded.values.astype(np.float32)
44
+
45
+
46
+ @app.route("/")
47
+ def index():
48
+ global METADATA_COLUMNS
49
+ return render_template("index.html", metadata=METADATA_COLUMNS)
50
+
51
+
52
+ @app.route("/predict", methods=["POST"])
53
+ def predict():
54
+ try:
55
+ data = request.json
56
+
57
+ # Process image
58
+ img_data = base64.b64decode(data["image"])
59
+ img_array = np.frombuffer(img_data, np.uint8)
60
+ img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
61
+ img = cv2.resize(img, (224, 224))
62
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
63
+ img = img / 255.0
64
+ img = np.expand_dims(img, axis=0)
65
+
66
+ # Encode metadata with consistent columns
67
+ metadata = encode_metadata(
68
+ {
69
+ "brand": data["brand"],
70
+ "color": data["color"],
71
+ "gender": data["gender"],
72
+ "midsole": data["midsole"],
73
+ "upperMaterial": data["upperMaterial"],
74
+ }
75
+ )
76
+ # Make prediction
77
+ predictions = model.predict([img, metadata])
78
+ categories = [
79
+ "Lifestyle",
80
+ "Running",
81
+ "Other",
82
+ "Cleat",
83
+ "Sandal",
84
+ "Basketball",
85
+ "Boot",
86
+ "Skateboarding",
87
+ ]
88
+ confidenceList = predictions[0].tolist()
89
+
90
+ return jsonify({"categories": categories, "confidence": confidenceList})
91
+
92
+ except Exception as e:
93
+ return jsonify({"error": str(e)}), 400
94
+
95
+
96
+ if __name__ == "__main__":
97
+ app.run(host="0.0.0.0", port=7860)
metadata.json ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "color": [
3
+ "Black",
4
+ "Blue",
5
+ "Brown",
6
+ "Copper",
7
+ "Cream",
8
+ "Gold",
9
+ "Green",
10
+ "Grey",
11
+ "Multi-Color",
12
+ "Orange",
13
+ "Pink",
14
+ "Purple",
15
+ "Red",
16
+ "Silver",
17
+ "Tan",
18
+ "Teal",
19
+ "White",
20
+ "Yellow"
21
+ ],
22
+ "midsole": [
23
+ "",
24
+ "10CELL",
25
+ "A-Flashfoam",
26
+ "ABZORB",
27
+ "ACTEVA",
28
+ "Adaptive Lacing",
29
+ "Adiplus",
30
+ "Adiprene",
31
+ "Adiprene+",
32
+ "AeroCore",
33
+ "Air",
34
+ "Air Cushion",
35
+ "AmpliFoam",
36
+ "AmpliFoam+",
37
+ "Anima",
38
+ "Anima PBX",
39
+ "Astroride",
40
+ "BOOM",
41
+ "Bio IP Foam",
42
+ "BioMoGo",
43
+ "Blushield",
44
+ "Boost",
45
+ "Bounce",
46
+ "Bounce 2.0",
47
+ "C-CAP",
48
+ "CMEVA",
49
+ "CUSH+",
50
+ "CX Foam",
51
+ "Cell",
52
+ "Charged",
53
+ "Cloud",
54
+ "CloudTec",
55
+ "CloudTec Phase",
56
+ "Cloudfoam",
57
+ "Cloudfoam Comfort",
58
+ "ComfiRide",
59
+ "ComfyCush",
60
+ "Contact Cushion",
61
+ "Crater Foam",
62
+ "Crepe",
63
+ "Cushlon",
64
+ "Cushlon 3.0",
65
+ "Cushlon ST2",
66
+ "DA2 Plus",
67
+ "DD Anima",
68
+ "DMX",
69
+ "DMX Foam",
70
+ "DMX Microbubbles",
71
+ "DMX SHEAR",
72
+ "DMXRide",
73
+ "DNA FLASH",
74
+ "DNA FLASH v2",
75
+ "DNA GOLD",
76
+ "DNA LOFT",
77
+ "DNA LOFT v2",
78
+ "DNA LOFT v3",
79
+ "DNA Tuned",
80
+ "Digital Light Synthesis",
81
+ "Dreamstrike",
82
+ "Dreamstrike+",
83
+ "DynaSoft",
84
+ "ENCAP",
85
+ "ERS",
86
+ "EVA",
87
+ "Encap",
88
+ "Energized",
89
+ "Energy Foam",
90
+ "Energy Return System",
91
+ "EnergyCell",
92
+ "EnergyCell+",
93
+ "Enerzy",
94
+ "Enerzy Lite",
95
+ "Enerzy NXT",
96
+ "FF BLAST",
97
+ "Feet You Wear",
98
+ "Flightspeed",
99
+ "FloatPro",
100
+ "FloatPro Foam",
101
+ "Floatride",
102
+ "Floatride Energy",
103
+ "Floatride Foam",
104
+ "Floatride Fuel",
105
+ "Flow",
106
+ "Flyplate",
107
+ "FlyteFoam",
108
+ "FlyteFoam Blast",
109
+ "FlyteFoam Blast Max",
110
+ "FlyteFoam Blast+",
111
+ "FlyteFoam Blast+ Eco",
112
+ "FlyteFoam Propel",
113
+ "FlyteFoam Turbo",
114
+ "FlyteFoam Turbo+",
115
+ "Formula 23",
116
+ "Free",
117
+ "Fresh Foam",
118
+ "Fresh Foam X",
119
+ "FuelCell",
120
+ "FuelFoam",
121
+ "Fulcrum",
122
+ "Gel",
123
+ "Graphlite",
124
+ "GreenStride",
125
+ "Grid",
126
+ "HOVR",
127
+ "HOVR+",
128
+ "Helion",
129
+ "Helion HF",
130
+ "Hexalite",
131
+ "HoverSpring",
132
+ "Hyperlift",
133
+ "IMEVA",
134
+ "Ignite",
135
+ "J-Frame",
136
+ "LQDCell",
137
+ "Light Boost",
138
+ "Lightmotion",
139
+ "Lightstrike",
140
+ "Lightstrike 2.0",
141
+ "Lightstrike Pro",
142
+ "Lightweight Muscle",
143
+ "LiteRide",
144
+ "Lunarlon",
145
+ "Megaride",
146
+ "Micro G",
147
+ "Mizuno Wave",
148
+ "Monster CloudTec",
149
+ "NERGY",
150
+ "Nitro Elite",
151
+ "Nitro Foam",
152
+ "Nitrogen",
153
+ "PEBA",
154
+ "PROFLY-X",
155
+ "PUMALite",
156
+ "PWRRUN",
157
+ "PWRRUN HG",
158
+ "PWRRUN PB",
159
+ "Podulon",
160
+ "Popcush",
161
+ "Pro Flex",
162
+ "ProFly",
163
+ "ProFly+",
164
+ "ProFoam",
165
+ "ProFoam Lite",
166
+ "ProGrid",
167
+ "Profoam+",
168
+ "Progrid",
169
+ "PureGEL",
170
+ "QUIX",
171
+ "QUIX Rail",
172
+ "R-System",
173
+ "REPETITOR",
174
+ "REVlite",
175
+ "RMAT",
176
+ "React",
177
+ "ReactX",
178
+ "Renew",
179
+ "Repetitor",
180
+ "Repetitor+",
181
+ "Rider Foam",
182
+ "SCEVA",
183
+ "Shox",
184
+ "SoftFoam+",
185
+ "SoftierFoam",
186
+ "SoftierFoam+",
187
+ "Softride",
188
+ "Super Critical Foam",
189
+ "SuperFoam",
190
+ "Swirlfoam",
191
+ "TimberCush",
192
+ "TriZone",
193
+ "Trinomic",
194
+ "U4IC",
195
+ "UNILITE",
196
+ "UltraCush",
197
+ "VECTIV",
198
+ "VR3CUSH",
199
+ "Zoom Air",
200
+ "Zoom X",
201
+ "ZoomX",
202
+ "a3",
203
+ "fuzeGEL",
204
+ "infiniFOAM"
205
+ ],
206
+ "upperMaterial": [
207
+ "",
208
+ "A-WEB",
209
+ "AtomKnit",
210
+ "Atomknit",
211
+ "Battleknit",
212
+ "Birkibuc",
213
+ "Birko-Flor",
214
+ "Calf",
215
+ "Canvas",
216
+ "Celermesh",
217
+ "Chenille",
218
+ "Clone",
219
+ "Confetti",
220
+ "Cordura",
221
+ "Corduroy",
222
+ "Cotton",
223
+ "Croslite",
224
+ "D-Skin",
225
+ "Denim",
226
+ "DynaFit",
227
+ "Dyneema",
228
+ "EVA",
229
+ "FantomFit",
230
+ "Faux Fur",
231
+ "Faux Shearling",
232
+ "Felt",
233
+ "Fiberskin",
234
+ "Fibertouch",
235
+ "FitWeave",
236
+ "Fleece",
237
+ "Flexion Fit",
238
+ "Flexweave",
239
+ "FlyTouch Lite",
240
+ "FlyTouch Plus",
241
+ "FlyTouch Pro",
242
+ "Flyknit",
243
+ "Flymesh",
244
+ "Flyweave",
245
+ "Foamposite",
246
+ "Fur",
247
+ "Fusionfeel",
248
+ "Fusionskin",
249
+ "GORE-TEX",
250
+ "Glitter",
251
+ "GripControl",
252
+ "GripControl Pro",
253
+ "Gripknit",
254
+ "Hair",
255
+ "Helcor",
256
+ "Hemp",
257
+ "HybridTouch",
258
+ "HybridTouch 2.0",
259
+ "Hybridfeel",
260
+ "HydroGuard",
261
+ "Hypoknit",
262
+ "IMEVA",
263
+ "IntelliKnit",
264
+ "Jacquard",
265
+ "K-BETTER",
266
+ "Knit",
267
+ "LIGHTLOCK",
268
+ "Leather",
269
+ "Leno Weave",
270
+ "Leno-Weave",
271
+ "LightSpray",
272
+ "Linen",
273
+ "Matryx",
274
+ "Mesh",
275
+ "Mono-Sock",
276
+ "Motion Wrap 2.0",
277
+ "NDure",
278
+ "NanoWeave",
279
+ "Neoprene",
280
+ "NikeSkin",
281
+ "Nubuck",
282
+ "Nylon",
283
+ "PVC",
284
+ "PWRTAPE",
285
+ "Patent Leather",
286
+ "Polyamide",
287
+ "Polyester",
288
+ "Polyurethane",
289
+ "Pony Hair",
290
+ "Primaloft",
291
+ "Primeknit",
292
+ "Primeknit+",
293
+ "Radial Knit",
294
+ "Raffia",
295
+ "ReactX",
296
+ "Ripstop",
297
+ "Rubber",
298
+ "Satin",
299
+ "SensiFit",
300
+ "Sequin",
301
+ "Shearling",
302
+ "Sheepskin",
303
+ "SkinCage",
304
+ "Space Waste Yarn",
305
+ "SpeedForm",
306
+ "Sprintskin",
307
+ "Strikeprint",
308
+ "Strung",
309
+ "Suede",
310
+ "Suprell",
311
+ "SuprellTech",
312
+ "Suprellsoft",
313
+ "Synthetic",
314
+ "TPE",
315
+ "TPU",
316
+ "Textile",
317
+ "Tweed",
318
+ "Twill",
319
+ "Ultra Shell",
320
+ "Ultraknit",
321
+ "Ultraride",
322
+ "Ultraweave",
323
+ "Vaporposite+",
324
+ "Vaporweave",
325
+ "Velvet",
326
+ "Warp",
327
+ "Warp 2.0",
328
+ "Wool",
329
+ "Woven",
330
+ "eVent"
331
+ ],
332
+ "gender": [
333
+ "infant",
334
+ "men",
335
+ "women",
336
+ "youth"
337
+ ],
338
+ "brand": [
339
+ "424",
340
+ "ASICS",
341
+ "Acne Studios",
342
+ "Air Jordan",
343
+ "Alexander McQueen",
344
+ "Ambush",
345
+ "Amiri",
346
+ "Ann Demeulemeester",
347
+ "Anta",
348
+ "BAPE",
349
+ "Balenciaga",
350
+ "Balmain",
351
+ "Birkenstock",
352
+ "Bottega Veneta",
353
+ "Brooks",
354
+ "Burberry",
355
+ "CELINE",
356
+ "Casablanca",
357
+ "Champion",
358
+ "Chanel",
359
+ "Chloé",
360
+ "Christian Louboutin",
361
+ "Clarks",
362
+ "Common Projects",
363
+ "Converse",
364
+ "Crocs",
365
+ "Curry Brand",
366
+ "Dc",
367
+ "Diadora",
368
+ "Diesel",
369
+ "Dior",
370
+ "Dolce & Gabbana",
371
+ "Dr. Martens",
372
+ "Dries Van Noten",
373
+ "Ewing",
374
+ "Fear of God",
375
+ "Fendi",
376
+ "Fila",
377
+ "GOAT",
378
+ "Givenchy",
379
+ "Golden Goose",
380
+ "Gucci",
381
+ "HOKA",
382
+ "JW Anderson",
383
+ "Jacquemus",
384
+ "Jil Sander",
385
+ "Karhu",
386
+ "Kiko Kostadinov",
387
+ "Lanvin",
388
+ "Le Coq Sportif",
389
+ "Lemaire",
390
+ "Li-Ning",
391
+ "Loewe",
392
+ "Louis Vuitton",
393
+ "MM6 Maison Margiela",
394
+ "MSCHF",
395
+ "Maison Margiela",
396
+ "Maison Mihara Yasuhiro",
397
+ "Market",
398
+ "Marni",
399
+ "Merrell",
400
+ "Miu Miu",
401
+ "Mizuno",
402
+ "Moncler",
403
+ "New Balance",
404
+ "Nike",
405
+ "ON",
406
+ "Off-White",
407
+ "Ohana Hatake",
408
+ "Onitsuka Tiger",
409
+ "Palm Angels",
410
+ "Prada",
411
+ "Puma",
412
+ "Reebok",
413
+ "Rick Owens",
414
+ "Rigorer",
415
+ "SOREL",
416
+ "Saint Laurent",
417
+ "Salomon",
418
+ "Saucony",
419
+ "Simone Rocha",
420
+ "Suicoke",
421
+ "Supra",
422
+ "The Attico",
423
+ "The North Face",
424
+ "Timberland",
425
+ "UGG",
426
+ "Under Armour",
427
+ "Valentino",
428
+ "Vans",
429
+ "Veja",
430
+ "Versace",
431
+ "Visvim",
432
+ "YZY",
433
+ "Yeezy",
434
+ "adidas"
435
+ ]
436
+ }
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ tensorflow
2
+ numpy
3
+ pandas
4
+ flask
templates/index.html ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>AI Sneaker Category Predictor</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ 'neo-black': '#0F1116',
15
+ 'neo-blue': '#2E3BFF'
16
+ }
17
+ }
18
+ }
19
+ }
20
+ </script>
21
+ <style>
22
+ .glass-effect {
23
+ background: rgba(255, 255, 255, 0.05);
24
+ backdrop-filter: blur(10px);
25
+ border: 1px solid rgba(255, 255, 255, 0.1);
26
+ transition: all 0.3s ease;
27
+ }
28
+
29
+ .glass-effect:hover {
30
+ background: rgba(255, 255, 255, 0.08);
31
+ }
32
+
33
+ .gradient-border {
34
+ position: relative;
35
+ border: double 1px transparent;
36
+ border-radius: 0.5rem;
37
+ background-image: linear-gradient(#0F1116, #0F1116),
38
+ linear-gradient(to right, #2E3BFF, #7C3AED);
39
+ background-origin: border-box;
40
+ background-clip: padding-box, border-box;
41
+ transition: all 0.3s ease;
42
+ }
43
+
44
+ .gradient-border:hover {
45
+ background-image: linear-gradient(#0F1116, #0F1116),
46
+ linear-gradient(to right, #3E4BFF, #8C4AFD);
47
+ }
48
+
49
+ .custom-select {
50
+ position: relative;
51
+ display: inline-block;
52
+ width: 100%;
53
+ }
54
+
55
+ .custom-select select {
56
+ display: none;
57
+ }
58
+
59
+ .select-selected {
60
+ background-color: rgba(255, 255, 255, 0.05);
61
+ padding: 0.5rem 1rem;
62
+ border-radius: 0.5rem;
63
+ cursor: pointer;
64
+ }
65
+
66
+ .select-items {
67
+ position: absolute;
68
+ padding: 3px;
69
+ top: 100%;
70
+ left: 0;
71
+ right: 0;
72
+ z-index: 99;
73
+ background: rgb(0, 0, 0);
74
+ backdrop-filter: blur(10px);
75
+ border-radius: 0.5rem;
76
+ margin-top: 0.5rem;
77
+ max-height: 200px;
78
+ overflow-y: auto;
79
+ display: none;
80
+ }
81
+
82
+ .select-items div {
83
+ padding: 0.5rem 1rem;
84
+ cursor: pointer;
85
+ transition: all 0.2s;
86
+ }
87
+
88
+ .select-items div:hover {
89
+ background: #534dad96;
90
+ border-radius: 0.5rem;
91
+ }
92
+
93
+ .drop-zone {
94
+ border: 2px dashed rgba(46, 59, 255, 0.3);
95
+ border-radius: 1rem;
96
+ padding: 1rem;
97
+ text-align: center;
98
+ transition: all 0.3s ease;
99
+ }
100
+
101
+ .drop-zone.drag-over {
102
+ border-color: #2E3BFF;
103
+ background: rgba(46, 59, 255, 0.1);
104
+ }
105
+
106
+ .pulse {
107
+ animation: pulse 2s infinite;
108
+ }
109
+
110
+ @keyframes pulse {
111
+ 0% {
112
+ transform: scale(1);
113
+ }
114
+
115
+ 50% {
116
+ transform: scale(1.05);
117
+ }
118
+
119
+ 100% {
120
+ transform: scale(1);
121
+ }
122
+ }
123
+
124
+ /* Custom scrollbar */
125
+ .select-items::-webkit-scrollbar {
126
+ width: 6px;
127
+ }
128
+
129
+ .select-items::-webkit-scrollbar-track {
130
+ background: rgba(255, 255, 255, 0.1);
131
+ border-radius: 3px;
132
+ }
133
+
134
+ .select-items::-webkit-scrollbar-thumb {
135
+ background: rgba(46, 59, 255, 0.5);
136
+ border-radius: 3px;
137
+ }
138
+
139
+ .select-search {
140
+ padding: 0.5rem;
141
+ width: 100%;
142
+ background: rgba(255, 255, 255, 0.05);
143
+ border: 1px solid rgba(255, 255, 255, 0.1);
144
+ border-radius: 0.25rem;
145
+ color: white;
146
+ margin-bottom: 0.5rem;
147
+ }
148
+
149
+ .select-search:focus {
150
+ outline: none;
151
+ border-color: rgba(46, 59, 255, 0.5);
152
+ }
153
+
154
+ .select-option-hidden {
155
+ display: none;
156
+ }
157
+ </style>
158
+ </head>
159
+
160
+ <body class="bg-neo-black text-gray-100 min-h-screen">
161
+ <div class="fixed w-full h-full">
162
+ <div class="absolute top-0 left-0 w-96 h-96 bg-blue-500 rounded-full filter blur-[128px] opacity-20"></div>
163
+ <div class="absolute bottom-0 right-0 w-96 h-96 bg-purple-500 rounded-full filter blur-[128px] opacity-20"></div>
164
+ </div>
165
+
166
+ <div class="container mx-auto px-4 py-8 w-full relative">
167
+ <h1
168
+ class="text-5xl font-bold text-center mb-12 bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-purple-500 pulse">
169
+ AI Sneaker Predictor
170
+ </h1>
171
+
172
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
173
+ <div class="glass-effect p-8 rounded-xl space-y-6 col-span-1 md:col-span-2 w-full h-fit">
174
+ <div class="flex items-start justify-center gap-5 w-full">
175
+ <div id="dropZone" class="drop-zone h-full aspect-square w-full flex items-center justify-center">
176
+ <div id="imagePreview" class="hidden w-full h-full">
177
+ <img id="preview" class="w-full h-full rounded-lg shadow-lg border border-blue-500/20 bg-gradient-to-tr from-blue-500/15 to-purple-500/15" alt="Preview">
178
+ </div>
179
+ <div id="dropText" class="text-blue-300">
180
+ <svg class="w-12 h-12 mx-auto mb-4 text-blue-500" fill="none" stroke="currentColor"
181
+ viewBox="0 0 24 24">
182
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
183
+ d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
184
+ </svg>
185
+ <p class="text-lg">Drag and drop your sneaker image here</p>
186
+ <p class="text-sm text-blue-400 mt-2">or click to browse</p>
187
+ <input type="file" id="imageUpload" class="hidden" accept="image/*">
188
+ </div>
189
+ </div>
190
+
191
+ <div class="space-y-6 w-full">
192
+ <div class="custom-select">
193
+ <label class="block text-sm font-medium text-blue-300 mb-2">Brand</label>
194
+ <select id="brand" required>
195
+ {% for brand in metadata.brand %}
196
+ <option value="{{brand}}">{{brand}}</option>
197
+ {% endfor %}
198
+ </select>
199
+ </div>
200
+
201
+ <div class="custom-select">
202
+ <label class="block text-sm font-medium text-blue-300 mb-2">Color</label>
203
+ <select id="color" required>
204
+ {% for color in metadata.color %}
205
+ <option value="{{color}}">{{color}}</option>
206
+ {% endfor %}
207
+ </select>
208
+ </div>
209
+
210
+ <div class="custom-select">
211
+ <label class="block text-sm font-medium text-blue-300 mb-2">Gender</label>
212
+ <select id="gender" required>
213
+ {% for gender in metadata.gender %}
214
+ <option value="{{gender}}">{{gender}}</option>
215
+ {% endfor %}
216
+ </select>
217
+ </div>
218
+
219
+ <div class="custom-select">
220
+ <label class="block text-sm font-medium text-blue-300 mb-2">Midsole</label>
221
+ <select id="midsole" required>
222
+ {% for midsole in metadata.midsole %}
223
+ <option value="{{midsole}}">{% if midsole == "" %}Null{%else%}{{midsole}}{%endif%}</option>
224
+ {% endfor %}
225
+ </select>
226
+ </div>
227
+
228
+ <div class="custom-select">
229
+ <label class="block text-sm font-medium text-blue-300 mb-2">Upper Material</label>
230
+ <select id="upperMaterial" required>
231
+ {% for upperMaterial in metadata.upperMaterial %}
232
+ <option value="{{upperMaterial}}">{% if upperMaterial == "" %}Null{%else%}{{upperMaterial}}{%endif%}</option>
233
+ {% endfor %}
234
+ </select>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ <button
239
+ class="w-full py-4 px-6 rounded-lg font-medium transition-all duration-300 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 focus:ring-offset-neo-black transform hover:scale-105"
240
+ onclick="predict()">
241
+ <div class="flex items-center justify-center space-x-3">
242
+ <svg class="w-6 h-6 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
243
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
244
+ d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
245
+ </svg>
246
+ <span class="text-lg">Predict Category</span>
247
+ </div>
248
+ </button>
249
+ </div>
250
+
251
+ <div>
252
+ <div id="result" class="hidden glass-effect p-6 rounded-xl">
253
+ <h3 class="text-xl font-semibold mb-4 text-blue-300">AI Prediction</h3>
254
+ <div id="predictions" class="space-y-4">
255
+ <!-- Predictions will be inserted here -->
256
+ </div>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </div>
261
+
262
+ <script>
263
+ function initCustomSelects() {
264
+ document.querySelectorAll('.custom-select select').forEach(select => {
265
+ const div = document.createElement('div');
266
+ div.classList.add('select-selected', 'gradient-border');
267
+ div.textContent = select.options[select.selectedIndex].text;
268
+ select.parentElement.appendChild(div);
269
+
270
+ const itemsDiv = document.createElement('div');
271
+ itemsDiv.classList.add('select-items');
272
+
273
+ // Add search input
274
+ const searchInput = document.createElement('input');
275
+ searchInput.type = 'text';
276
+ searchInput.placeholder = 'Search...';
277
+ searchInput.classList.add('select-search');
278
+ itemsDiv.appendChild(searchInput);
279
+
280
+ const optionsContainer = document.createElement('div');
281
+ Array.from(select.options).forEach(option => {
282
+ const optionDiv = document.createElement('div');
283
+ optionDiv.textContent = option.text;
284
+ optionDiv.addEventListener('click', () => {
285
+ select.value = option.value;
286
+ div.textContent = option.text;
287
+ itemsDiv.style.display = 'none';
288
+ });
289
+ optionsContainer.appendChild(optionDiv);
290
+ });
291
+ itemsDiv.appendChild(optionsContainer);
292
+
293
+ // Add search functionality
294
+ searchInput.addEventListener('input', (e) => {
295
+ const searchText = e.target.value.toLowerCase();
296
+ Array.from(optionsContainer.children).forEach(optionDiv => {
297
+ const text = optionDiv.textContent.toLowerCase();
298
+ optionDiv.classList.toggle('select-option-hidden', !text.includes(searchText));
299
+ });
300
+ });
301
+
302
+ // Prevent dropdown from closing when clicking search
303
+ searchInput.addEventListener('click', (e) => {
304
+ e.stopPropagation();
305
+ });
306
+
307
+ select.parentElement.appendChild(itemsDiv);
308
+
309
+ div.addEventListener('click', (e) => {
310
+ e.stopPropagation();
311
+ closeAllSelect(itemsDiv);
312
+ itemsDiv.style.display = itemsDiv.style.display === 'block' ? 'none' : 'block';
313
+ if (itemsDiv.style.display === 'block') {
314
+ searchInput.focus();
315
+ searchInput.value = '';
316
+ // Show all options when opening dropdown
317
+ Array.from(optionsContainer.children).forEach(optionDiv => {
318
+ optionDiv.classList.remove('select-option-hidden');
319
+ });
320
+ }
321
+ });
322
+ });
323
+
324
+ document.addEventListener('click', () => closeAllSelect(null));
325
+ }
326
+
327
+ function closeAllSelect(elmnt) {
328
+ document.querySelectorAll('.select-items').forEach(item => {
329
+ if (item !== elmnt) item.style.display = 'none';
330
+ });
331
+ }
332
+
333
+ const dropZone = document.getElementById('dropZone');
334
+ const imageUpload = document.getElementById('imageUpload');
335
+
336
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
337
+ dropZone.addEventListener(eventName, preventDefaults, false);
338
+ });
339
+
340
+ function preventDefaults(e) {
341
+ e.preventDefault();
342
+ e.stopPropagation();
343
+ }
344
+
345
+ ['dragenter', 'dragover'].forEach(eventName => {
346
+ dropZone.addEventListener(eventName, () => dropZone.classList.add('drag-over'));
347
+ });
348
+
349
+ ['dragleave', 'drop'].forEach(eventName => {
350
+ dropZone.addEventListener(eventName, () => dropZone.classList.remove('drag-over'));
351
+ });
352
+
353
+ dropZone.addEventListener('drop', handleDrop);
354
+ dropZone.addEventListener('click', () => imageUpload.click());
355
+
356
+ function handleDrop(e) {
357
+ const dt = e.dataTransfer;
358
+ const file = dt.files[0];
359
+ handleFile(file);
360
+ }
361
+
362
+ document.getElementById('imageUpload').addEventListener('change', function (e) {
363
+ const file = e.target.files[0];
364
+ if (file) handleFile(file);
365
+ });
366
+
367
+ function handleFile(file) {
368
+ if (file) {
369
+ const reader = new FileReader();
370
+ reader.onload = function (e) {
371
+ document.getElementById('preview').src = e.target.result;
372
+ document.getElementById('imagePreview').classList.remove('hidden');
373
+ document.getElementById('dropText').classList.add('hidden');
374
+ }
375
+ reader.readAsDataURL(file);
376
+ }
377
+ }
378
+
379
+ document.addEventListener('DOMContentLoaded', initCustomSelects);
380
+
381
+ async function predict() {
382
+ const imageFile = document.getElementById('imageUpload').files[0];
383
+ if (!imageFile) {
384
+ alert('Please select an image');
385
+ return;
386
+ }
387
+
388
+ const reader = new FileReader();
389
+ reader.onload = async function (e) {
390
+ const base64Image = e.target.result.split(',')[1];
391
+
392
+ const data = {
393
+ image: base64Image,
394
+ brand: document.getElementById('brand').value,
395
+ color: document.getElementById('color').value,
396
+ gender: document.getElementById('gender').value,
397
+ midsole: document.getElementById('midsole').value,
398
+ upperMaterial: document.getElementById('upperMaterial').value
399
+ };
400
+
401
+ try {
402
+ const response = await fetch('/predict', {
403
+ method: 'POST',
404
+ headers: {
405
+ 'Content-Type': 'application/json'
406
+ },
407
+ body: JSON.stringify(data)
408
+ });
409
+
410
+ const result = await response.json();
411
+ if (result.error) {
412
+ alert('Error: ' + result.error);
413
+ } else {
414
+ document.getElementById('result').classList.remove('hidden');
415
+ const predictionsContainer = document.getElementById('predictions');
416
+ predictionsContainer.innerHTML = '';
417
+
418
+ // Sort categories by confidence
419
+ const predictions = result.categories.map((category, index) => ({
420
+ category,
421
+ confidence: result.confidence[index]
422
+ })).sort((a, b) => b.confidence - a.confidence);
423
+
424
+ predictions.forEach(({ category, confidence }) => {
425
+ const confidencePercent = (confidence * 100).toFixed(2);
426
+ const predictionHtml = `
427
+ <div class="gradient-border p-4">
428
+ <div class="flex items-center justify-between">
429
+ <p>Category: <span class="font-semibold text-blue-400">${category}</span></p>
430
+ <p>Confidence: <span class="font-semibold text-blue-400">${confidencePercent}</span>%</p>
431
+ </div>
432
+ <div class="w-full bg-gray-700/30 rounded-full h-4 mt-2">
433
+ <div class="bg-gradient-to-r from-blue-500 to-purple-500 h-4 rounded-full transition-all duration-500"
434
+ style="width: ${confidencePercent}%"></div>
435
+ </div>
436
+
437
+ </div>`;
438
+ predictionsContainer.innerHTML += predictionHtml;
439
+ });
440
+ }
441
+ } catch (error) {
442
+ alert('Error: ' + error.message);
443
+ }
444
+ };
445
+ reader.readAsDataURL(imageFile);
446
+ }
447
+ </script>
448
+ </body>
449
+
450
+ </html>