dgwyer commited on
Commit
bdfa063
·
verified ·
1 Parent(s): 148bd2d

deploy at 2025-05-13 17:11:52.021690

Browse files
Files changed (15) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +10 -0
  3. README.md +8 -9
  4. bl-pred.png +0 -0
  5. black.jpg +0 -0
  6. chocolate.jpg +0 -0
  7. cl-pred.png +0 -0
  8. drop.jpg +0 -0
  9. export.pkl +3 -0
  10. lab-logo.png +0 -0
  11. logo-white.png +3 -0
  12. main.py +280 -0
  13. requirements.txt +8 -0
  14. yellow.jpg +0 -0
  15. yl-pred.png +0 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ logo-white.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+ WORKDIR /code
3
+ COPY --link --chown=1000 . .
4
+ RUN mkdir -p /tmp/cache/
5
+ RUN chmod a+rwx -R /tmp/cache/
6
+ ENV HF_HUB_CACHE=HF_HOME
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ ENV PYTHONUNBUFFERED=1 PORT=7860
10
+ CMD ["python", "main.py"]
README.md CHANGED
@@ -1,12 +1,11 @@
 
1
  ---
2
- title: Labrador Classifier
3
- emoji: 😻
4
- colorFrom: yellow
5
- colorTo: yellow
6
- sdk: static
 
7
  pinned: false
8
- license: apache-2.0
9
- short_description: A FastHTML AI web app to classify Labrador dogs
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+
2
  ---
3
+ title: dgwyer/labrador-classifier
4
+ emoji: 🚀
5
+ colorFrom: purple
6
+ colorTo: red
7
+ sdk: docker
8
+ app_file: app.py
9
  pinned: false
10
+ termination_grace_period: 2m
 
11
  ---
 
 
bl-pred.png ADDED
black.jpg ADDED
chocolate.jpg ADDED
cl-pred.png ADDED
drop.jpg ADDED
export.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:24d4f52d4bff038df5654354e0343afb3cc55a05f80adb85a3442c2d3bdcbbaa
3
+ size 46979070
lab-logo.png ADDED
logo-white.png ADDED

Git LFS Details

  • SHA256: 432bdd47255b48366843ec21d209e774f93b807e1fd0894fc983aad0dc7bd03b
  • Pointer size: 131 Bytes
  • Size of remote file: 103 kB
main.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastai.vision.all import *
2
+ from fasthtml.common import *
3
+ from PIL import Image
4
+ from io import BytesIO
5
+ from starlette.responses import JSONResponse
6
+ from fasthtml_hf import setup_hf_backup
7
+
8
+ app, rt = fast_app(live=True, pico=False, hdrs=(
9
+ Script(src="https://cdn.tailwindcss.com?plugins=forms,typography"),
10
+ # Add custom styles
11
+ Style("""
12
+ body { background-color: #f9fafb; }
13
+ .card-shadow { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
14
+ .gradient-bg { background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%); }
15
+ """)
16
+ ))
17
+
18
+ # JavaScript remains unchanged
19
+ drag_drop_js = """
20
+ function setupDragAndDrop() {
21
+ const dropZone = document.getElementById('drop-zone');
22
+ const previewImg = document.getElementById('preview-img');
23
+ const filenameInput = document.getElementById('selected-filename');
24
+
25
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e =>
26
+ dropZone.addEventListener(e, e => { e.preventDefault(); e.stopPropagation(); }, false));
27
+
28
+ ['dragenter', 'dragover'].forEach(e =>
29
+ dropZone.addEventListener(e, () => dropZone.classList.add('border-blue-500', 'bg-blue-50'), false));
30
+
31
+ ['dragleave', 'drop'].forEach(e =>
32
+ dropZone.addEventListener(e, () => dropZone.classList.remove('border-blue-500', 'bg-blue-50'), false));
33
+
34
+ dropZone.addEventListener('drop', e => {
35
+ if (e.dataTransfer.files.length) {
36
+ const file = e.dataTransfer.files[0];
37
+ const formData = new FormData();
38
+ formData.append('file', file);
39
+
40
+ fetch('/upload', { method: 'POST', body: formData })
41
+ .then(response => response.json())
42
+ .then(data => {
43
+ previewImg.src = '/drop.jpg?' + new Date().getTime();
44
+ filenameInput.value = 'drop.jpg';
45
+ })
46
+ .catch(error => console.error('Error:', error));
47
+ }
48
+ }, false);
49
+ }
50
+ document.addEventListener('DOMContentLoaded', setupDragAndDrop);
51
+ """
52
+
53
+ # Enhanced image item component
54
+ def image_item(filename, color, label):
55
+ return Div(
56
+ Div(
57
+ Img(src=f"/{filename}",
58
+ cls=f"w-full h-auto cursor-pointer rounded-lg border-[5px] border-{color}",
59
+ onclick=f"document.getElementById('preview-img').src='/{filename}'; document.getElementById('selected-filename').value='{filename}';"),
60
+ cls="overflow-hidden"
61
+ ),
62
+ P(f"{label} Labrador", cls="text-sm font-medium text-gray-700 mt-2 text-center"),
63
+ cls="w-[180px]" # Increased width from 130px to 180px
64
+ )
65
+
66
+ @rt('/')
67
+ def get():
68
+ return Div(
69
+ # Header with gradient background, logo, and Twitter link
70
+ Div(
71
+ Div(
72
+ # Flex container for all header elements with space-between
73
+ Div(
74
+ # Left side with logo and text
75
+ Div(
76
+ # Logo
77
+ Img(src="/lab-logo.png", alt="Labrador Classifier Logo",
78
+ cls="h-20 w-auto mr-4"),
79
+
80
+ # Text content
81
+ Div(
82
+ H1("Labrador Classifier", cls="text-2xl font-bold text-white m-0"),
83
+ P("Identify the type of Labrador using AI", cls="text-blue-100 m-0"),
84
+ ),
85
+
86
+ # Make this a flex container to align logo and text
87
+ cls="flex items-center"
88
+ ),
89
+
90
+ # Right side with Twitter link
91
+ A(
92
+ Img(src="/logo-white.png", alt="Twitter",
93
+ cls="h-6 w-auto transition-transform hover:scale-110"),
94
+ href="https://x.com/dgwyer",
95
+ title="Follow me for more AI content!",
96
+ target="_blank",
97
+ rel="noopener noreferrer",
98
+ cls="flex items-center"
99
+ ),
100
+
101
+ # Flex container properties to push items to opposite ends
102
+ cls="flex justify-between items-center"
103
+ ),
104
+ cls="max-w-6xl mx-auto px-4 py-6"
105
+ ),
106
+ cls="gradient-bg w-full mb-8"
107
+ ),
108
+
109
+ # Main content container
110
+ Div(
111
+ # Left column
112
+ Div(
113
+ # Selected image section
114
+ Div(
115
+ H2("Image Analysis", cls="text-xl font-semibold text-gray-800 mb-4 pb-2 border-b"),
116
+
117
+ # Drop zone and preview
118
+ Div(
119
+ Div(
120
+ Img(id="preview-img", src="/black.jpg",
121
+ cls="w-full h-auto object-contain rounded-lg mb-4 mx-auto block min-h-[200px] max-h-[200px]"),
122
+ P("Drag & Drop Image Here",
123
+ cls="text-gray-500 text-sm absolute bottom-4 left-0 right-0 text-center bg-white bg-opacity-75 py-2"),
124
+ id="drop-zone",
125
+ cls="relative w-full border-2 border-dashed border-blue-300 p-8 rounded-xl text-center cursor-pointer transition-colors bg-blue-50 bg-opacity-50 mb-4 hover:bg-blue-100 hover:border-blue-400" # Increased padding to p-8
126
+ ),
127
+ # Prediction form
128
+ Form(
129
+ Input(type="hidden", id="selected-filename", name="filename", value="black.jpg"),
130
+ Button('Analyze Image', type="submit",
131
+ cls="w-full py-3 px-4 rounded-lg bg-blue-600 text-white font-medium shadow-md hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"),
132
+ hx_post="/loading", hx_target="#predictions", cls="mt-3"
133
+ ),
134
+ cls="w-full max-w-lg mx-auto" # Changed to max-width instead of percentage
135
+ ),
136
+ cls="bg-white rounded-xl p-6 mb-6 card-shadow"
137
+ ),
138
+
139
+ # Sample images section
140
+ Div(
141
+ H2("Sample Images", cls="text-xl font-semibold text-gray-800 mb-4 pb-2 border-b"),
142
+ P("Click an image to analyze", cls="text-gray-600 mb-4"),
143
+ Div(
144
+ image_item("black.jpg", "gray-800", "Black"),
145
+ image_item("yellow.jpg", "yellow-500", "Yellow"),
146
+ image_item("chocolate.jpg", "amber-700", "Chocolate"),
147
+ cls="flex flex-row justify-center gap-8 mx-auto" # Increased gap from 6 to 8
148
+ ),
149
+ cls="bg-white rounded-xl p-6 card-shadow"
150
+ ),
151
+ cls="w-full lg:w-2/3 pr-0 lg:pr-8" # Increased right padding
152
+ ),
153
+
154
+ # Right column
155
+ Div(
156
+ Div(
157
+ H2('Results', cls="text-xl font-semibold text-gray-800 mb-4 pb-2 border-b"),
158
+ Div(
159
+ Div(
160
+ NotStr('<svg class="w-12 h-12 text-blue-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"></path></svg>'),
161
+ H3("Ready for Analysis", cls="text-lg font-medium text-gray-700 text-center"),
162
+ P('Click the "Analyze Image" button to identify the type of Labrador.',
163
+ cls="text-gray-600 text-center"),
164
+ cls="py-8"
165
+ ),
166
+ id="predictions",
167
+ cls="bg-gray-50 rounded-lg p-4 min-h-[250px] flex items-center"
168
+ ),
169
+ cls="bg-white rounded-xl p-6 card-shadow sticky top-6"
170
+ ),
171
+ cls="w-full lg:w-1/3 mt-6 lg:mt-0"
172
+ ),
173
+ cls="flex flex-col lg:flex-row gap-6 max-w-6xl mx-auto px-4" # Added max-width and padding
174
+ ),
175
+
176
+ # Footer
177
+ Div(
178
+ P("© 2025 Labrador Classifier • By David Gwyer • Powered by FastAI and FastHTML",
179
+ cls="text-center text-gray-500 text-sm"),
180
+ cls="mt-12 py-6 border-t max-w-6xl mx-auto px-4" # Added max-width and padding
181
+ ),
182
+
183
+ Script(drag_drop_js),
184
+ cls="min-h-screen"
185
+ )
186
+
187
+ @rt('/upload')
188
+ async def post(file: UploadFile):
189
+ img = Image.open(BytesIO(await file.read())).resize((128, 128), Image.LANCZOS)
190
+ img.save("drop.jpg")
191
+ return JSONResponse({"success": True, "filename": "drop.jpg"})
192
+
193
+ @rt('/loading')
194
+ def post(filename: str = "black.jpg"):
195
+ return Div(
196
+ Div(
197
+ Div(
198
+ NotStr('<svg class="w-12 h-12 animate-spin text-blue-600 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>'),
199
+ H3("Analyzing Image", cls="text-lg font-medium text-gray-700 text-center"),
200
+ P("Please wait while we process your image...", cls="text-gray-600 text-center"),
201
+ cls="py-8"
202
+ ),
203
+ cls="flex items-center justify-center"
204
+ ),
205
+ cls="bg-gray-50 rounded-lg p-4 min-h-[250px]",
206
+ hx_get=f"/process?filename={filename}",
207
+ hx_trigger="load",
208
+ hx_swap="outerHTML"
209
+ )
210
+
211
+ @rt('/process')
212
+ def get(filename: str = "black.jpg"):
213
+ # Model inference
214
+ labrador_learner = load_learner('export.pkl')
215
+ prediction = labrador_learner.predict(filename)
216
+
217
+ # Extract prediction data
218
+ label, class_idx, probabilities = prediction
219
+ confidence = probabilities[class_idx.item()].item() * 100
220
+
221
+ # Determine which prediction image to show based on the label
222
+ pred_image = ""
223
+ if label == "black":
224
+ pred_image = "bl-pred.png"
225
+ elif label == "yellow":
226
+ pred_image = "yl-pred.png"
227
+ elif label == "chocolate":
228
+ pred_image = "cl-pred.png"
229
+
230
+ return Div(
231
+ Div(
232
+ # Success icon
233
+ NotStr('<svg class="w-12 h-12 text-green-500 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>'),
234
+
235
+ # Results
236
+ H3('Analysis Complete', cls="text-lg font-medium text-gray-700 text-center mb-3"),
237
+
238
+ # Prediction image
239
+ Div(
240
+ Img(src=f"/{pred_image}", alt=f"{label.capitalize()} Labrador",
241
+ cls="w-32 h-32 mx-auto mb-4 object-contain"),
242
+ cls="text-center"
243
+ ),
244
+
245
+ # Prediction card
246
+ Div(
247
+ Div(
248
+ Div(
249
+ P("Prediction", cls="text-xs font-medium text-gray-500 uppercase tracking-wide"),
250
+ P(f'{label.capitalize()} Labrador',
251
+ cls="text-lg font-bold text-blue-600"),
252
+ cls="flex-grow"
253
+ ),
254
+ Div(
255
+ P("Confidence", cls="text-xs font-medium text-gray-500 uppercase tracking-wide"),
256
+ P(f'{confidence:.1f}%',
257
+ cls="text-lg font-bold text-gray-800"),
258
+ cls="text-right"
259
+ ),
260
+ cls="flex justify-between items-center"
261
+ ),
262
+
263
+ # Progress bar for confidence
264
+ Div(
265
+ Div(
266
+ cls=f"h-2 bg-blue-600 rounded-full",
267
+ style=f"width: {confidence}%"
268
+ ),
269
+ cls="w-full bg-gray-200 rounded-full h-2 mt-2"
270
+ ),
271
+
272
+ cls="bg-white rounded-lg p-4 shadow-sm border border-gray-200"
273
+ )
274
+ ),
275
+ cls="bg-gray-50 rounded-lg p-6 min-h-[250px]"
276
+ )
277
+
278
+ setup_hf_backup(app)
279
+
280
+ serve()
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fasthtml-hf
2
+ fastai
3
+ python-fasthtml
4
+ Pillow
5
+ starlette
6
+ uvicorn>=0.29
7
+ python-multipart
8
+ huggingface-hub>=0.20.0
yellow.jpg ADDED
yl-pred.png ADDED