Artaxias commited on
Commit
be8235a
Β·
verified Β·
1 Parent(s): ec2eb04

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +298 -0
  2. requirements.txt +9 -0
app.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import json
4
+ import shutil
5
+ from pathlib import Path
6
+ from PIL import Image
7
+ import torch
8
+ import clip
9
+ import numpy as np
10
+ import requests
11
+ from io import BytesIO
12
+ import tempfile
13
+
14
+ class SmartCLIPClassifierNextCloudShare:
15
+ def __init__(self, share_url, share_password, progress_callback=None):
16
+
17
+ self.share_url = share_url.rstrip('/')
18
+ self.share_password = share_password
19
+ self.progress_callback = progress_callback
20
+
21
+ self.session = requests.Session()
22
+ self.session.auth = (self.get_share_token(), share_password)
23
+
24
+ self.temp_dir = tempfile.mkdtemp()
25
+
26
+ self.categories = [
27
+ "1_Booth",
28
+ "2_Business_Interaction",
29
+ "3_Buyer_Delegation",
30
+ "4_Aisle",
31
+ "5_Conference",
32
+ "6_Fairground",
33
+ "7_Products",
34
+ "8_Registration",
35
+ "9_Miscellaneous"
36
+ ]
37
+
38
+ self.log("Loading CLIP model...")
39
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
40
+ self.model, self.preprocess = clip.load("ViT-B/32", device=self.device)
41
+ self.log(f"βœ… CLIP loaded on {self.device}")
42
+
43
+ self.log("πŸ” Scanning NextCloud share...")
44
+ self.all_files = self.list_files("")
45
+ self.log(f"Found {len(self.all_files)} total files")
46
+
47
+ self.load_deep_analysis()
48
+ self.get_image_list()
49
+
50
+ def log(self, message):
51
+ if self.progress_callback:
52
+ self.progress_callback(message)
53
+ print(message)
54
+
55
+ def get_share_token(self):
56
+ return self.share_url.split('/s/')[-1]
57
+
58
+ def get_webdav_url(self, path=""):
59
+ token = self.get_share_token()
60
+ base = self.share_url.rsplit('/s/', 1)[0]
61
+ if path:
62
+ return f"{base}/public.php/webdav/{path}"
63
+ return f"{base}/public.php/webdav/"
64
+
65
+ def download_file(self, filename):
66
+ url = self.get_webdav_url(filename)
67
+ response = self.session.get(url)
68
+ response.raise_for_status()
69
+ return response.content
70
+
71
+ def upload_file(self, local_path, remote_filename):
72
+ url = self.get_webdav_url(remote_filename)
73
+ with open(local_path, 'rb') as f:
74
+ response = self.session.put(url, data=f)
75
+ response.raise_for_status()
76
+
77
+ def list_files(self, remote_path=""):
78
+ url = self.get_webdav_url(remote_path)
79
+ response = self.session.request('PROPFIND', url, headers={'Depth': '1'})
80
+ response.raise_for_status()
81
+
82
+ files = []
83
+ lines = response.text.split('<d:href>')
84
+ for line in lines:
85
+ if '</d:href>' in line:
86
+ href = line.split('</d:href>')[0]
87
+ if '/webdav/' in href:
88
+ filename = href.split('/webdav/')[-1]
89
+ if filename and not filename.endswith('/'):
90
+ files.append(filename)
91
+
92
+ return files
93
+
94
+ def create_folder(self, folder_name):
95
+ url = self.get_webdav_url(folder_name)
96
+ try:
97
+ self.session.request('MKCOL', url)
98
+ except:
99
+ pass
100
+
101
+ def get_image_list(self):
102
+ self.log("Filtering images...")
103
+ valid_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
104
+
105
+ self.images = [f for f in self.all_files if Path(f).suffix.lower() in valid_extensions]
106
+ self.images.sort()
107
+ self.log(f"βœ… Found {len(self.images)} images to classify")
108
+
109
+ def load_deep_analysis(self):
110
+ self.log("Looking for deep_training_analysis.json...")
111
+
112
+ json_file = None
113
+ for f in self.all_files:
114
+ if f.endswith('.json') and 'deep_training_analysis' in f.lower():
115
+ json_file = f
116
+ self.log(f"Found: {json_file}")
117
+ break
118
+
119
+ if not json_file:
120
+ self.log("⚠️ deep_training_analysis.json not found - using fallback embeddings")
121
+ self.category_embeddings = {cat: self.create_text_embedding(cat) for cat in self.categories}
122
+ return
123
+
124
+ try:
125
+ content = self.download_file(json_file)
126
+ self.deep_analysis = json.loads(content.decode('utf-8'))
127
+
128
+ self.log("πŸ“š Loaded deep training analysis")
129
+
130
+ self.category_embeddings = {}
131
+
132
+ for category in self.categories:
133
+ if category in self.deep_analysis:
134
+ data = self.deep_analysis[category]
135
+ avg_embedding = torch.tensor(data['avg_embedding'], dtype=torch.float32).to(self.device)
136
+ avg_embedding = avg_embedding / avg_embedding.norm()
137
+ self.category_embeddings[category] = avg_embedding
138
+
139
+ self.log(f" {category}: {data['num_training_images']} training images")
140
+ else:
141
+ self.category_embeddings[category] = self.create_text_embedding(category)
142
+
143
+ except Exception as e:
144
+ self.log(f"❌ Error loading deep analysis: {e}")
145
+ self.category_embeddings = {cat: self.create_text_embedding(cat) for cat in self.categories}
146
+
147
+ def create_text_embedding(self, category):
148
+ descriptions = {
149
+ "1_Booth": "a photo of an exhibition booth at a trade show",
150
+ "2_Business_Interaction": "a photo of business people talking at a trade show",
151
+ "3_Buyer_Delegation": "a photo of a large group visiting a trade show",
152
+ "4_Aisle": "a photo of a trade show aisle between booths",
153
+ "5_Conference": "a photo of a conference presentation or seminar",
154
+ "6_Fairground": "a photo of an exhibition hall or fairground",
155
+ "7_Products": "a photo of products on display",
156
+ "8_Registration": "a photo of a registration desk or entry gate",
157
+ "9_Miscellaneous": "a miscellaneous trade show photo"
158
+ }
159
+
160
+ text = descriptions.get(category, "a photo")
161
+ text_input = clip.tokenize([text]).to(self.device)
162
+
163
+ with torch.no_grad():
164
+ text_features = self.model.encode_text(text_input)
165
+ text_features = text_features / text_features.norm(dim=-1, keepdim=True)
166
+
167
+ return text_features[0]
168
+
169
+ def classify_image(self, filename):
170
+ try:
171
+ img_data = self.download_file(filename)
172
+
173
+ img = Image.open(BytesIO(img_data)).convert('RGB')
174
+ img_input = self.preprocess(img).unsqueeze(0).to(self.device)
175
+
176
+ with torch.no_grad():
177
+ img_features = self.model.encode_image(img_input)
178
+ img_features = img_features / img_features.norm(dim=-1, keepdim=True)
179
+ img_features = img_features[0]
180
+
181
+ similarities = {}
182
+ for category, cat_embedding in self.category_embeddings.items():
183
+ similarity = (img_features @ cat_embedding).item()
184
+ similarities[category] = similarity
185
+
186
+ best_category = max(similarities, key=similarities.get)
187
+ confidence = similarities[best_category]
188
+
189
+ local_path = os.path.join(self.temp_dir, Path(filename).name)
190
+ with open(local_path, 'wb') as f:
191
+ f.write(img_data)
192
+
193
+ category_folder = f"Classified/{best_category}"
194
+ self.create_folder("Classified")
195
+ self.create_folder(category_folder)
196
+
197
+ remote_dest = f"{category_folder}/{Path(filename).name}"
198
+ self.upload_file(local_path, remote_dest)
199
+
200
+ os.remove(local_path)
201
+
202
+ return best_category, confidence
203
+
204
+ except Exception as e:
205
+ self.log(f"❌ Error: {str(e)}")
206
+ return "9_Miscellaneous", 0.0
207
+
208
+ def run(self):
209
+ self.log("πŸš€ Starting classification...")
210
+
211
+ self.create_folder("Classified")
212
+ for cat in self.categories:
213
+ self.create_folder(f"Classified/{cat}")
214
+
215
+ stats = {cat: 0 for cat in self.categories}
216
+ confidences = {cat: [] for cat in self.categories}
217
+
218
+ for i, filename in enumerate(self.images, 1):
219
+ self.log(f"[{i}/{len(self.images)}] {Path(filename).name}...")
220
+
221
+ category, confidence = self.classify_image(filename)
222
+
223
+ stats[category] += 1
224
+ confidences[category].append(confidence)
225
+
226
+ self.log(f" β†’ {category} (confidence: {confidence:.3f})")
227
+
228
+ self.log("βœ… CLASSIFICATION COMPLETE!")
229
+ self.log("πŸ“ Results uploaded to: Classified/")
230
+
231
+ result_text = "**Results Summary:**\n\n"
232
+ for cat in self.categories:
233
+ count = stats[cat]
234
+ if count > 0:
235
+ avg_conf = sum(confidences[cat]) / len(confidences[cat])
236
+ result_text += f"- **{cat}**: {count} images (avg confidence: {avg_conf:.3f})\n"
237
+
238
+ shutil.rmtree(self.temp_dir)
239
+
240
+ return result_text
241
+
242
+ def classify_photos(share_url, share_password, progress=gr.Progress()):
243
+ if not share_url or not share_password:
244
+ return "❌ Please enter both the share URL and password"
245
+
246
+ try:
247
+ logs = []
248
+
249
+ def log_callback(message):
250
+ logs.append(message)
251
+ progress(0.5, desc=message)
252
+
253
+ classifier = SmartCLIPClassifierNextCloudShare(
254
+ share_url,
255
+ share_password,
256
+ progress_callback=log_callback
257
+ )
258
+
259
+ result = classifier.run()
260
+
261
+ return result
262
+
263
+ except Exception as e:
264
+ return f"❌ Error: {str(e)}\n\nPlease check your share URL and password and try again."
265
+
266
+ # Gradio Interface
267
+ with gr.Blocks(title="Trade Show Photo Classifier") as demo:
268
+ gr.Markdown("# πŸ€– Trade Show Photo Classifier")
269
+ gr.Markdown("Automatically classify your trade show photos using AI-powered image recognition")
270
+
271
+ with gr.Row():
272
+ with gr.Column():
273
+ share_url = gr.Textbox(
274
+ label="NextCloud Share URL",
275
+ placeholder="https://cloud2.messefrankfurtexchange.com/s/...",
276
+ info="Enter the public share link to your NextCloud folder containing the photos"
277
+ )
278
+ share_password = gr.Textbox(
279
+ label="Share Password",
280
+ type="password",
281
+ info="Enter the password for the NextCloud share"
282
+ )
283
+ classify_btn = gr.Button("πŸš€ Start Classification", variant="primary")
284
+
285
+ with gr.Row():
286
+ output = gr.Markdown(label="Results")
287
+
288
+ classify_btn.click(
289
+ fn=classify_photos,
290
+ inputs=[share_url, share_password],
291
+ outputs=output
292
+ )
293
+
294
+ gr.Markdown("---")
295
+ gr.Markdown("*Powered by OpenAI CLIP | Deployed on Hugging Face Spaces*")
296
+
297
+ if __name__ == "__main__":
298
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ torch
3
+ torchvision
4
+ Pillow
5
+ requests
6
+ ftfy
7
+ regex
8
+ tqdm
9
+ git+https://github.com/openai/CLIP.git