palakmathur commited on
Commit
31fce09
·
verified ·
1 Parent(s): e1038b8

Upload 8 files

Browse files
app.py ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from PIL import Image, ImageFile
3
+ import sys
4
+ import os
5
+ import json
6
+ from huggingface_hub import hf_hub_download
7
+ from io import BytesIO
8
+ from datetime import datetime
9
+ HF_USERNAME = "palakmathur"
10
+ # sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src'))
11
+
12
+ from domain_validator import DomainValidator
13
+ from description_extractor import DescriptionExtractor
14
+ from defect_matcher import DefectMatcher
15
+ from condition_grader import ConditionGrader
16
+ from predictor.price_predictor import PricePredictor
17
+
18
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
19
+
20
+ st.set_page_config(
21
+ page_title="Device Defect Diagnosis System",
22
+ layout="wide",
23
+ initial_sidebar_state="expanded"
24
+ )
25
+
26
+ st.markdown("""
27
+ <style>
28
+
29
+ .main-header {
30
+ font-size: 2.5rem;
31
+ font-weight: bold;
32
+ color: #1f77b4;
33
+ text-align: center;
34
+ padding: 1rem;
35
+ }
36
+ .sub-header {
37
+ font-size: 1.2rem;
38
+ color: #666;
39
+ text-align: center;
40
+ margin-bottom: 2rem;
41
+ }
42
+ .metric-card {
43
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
44
+ padding: 1.5rem;
45
+ border-radius: 10px;
46
+ color: white;
47
+ margin: 0.5rem 0;
48
+ }
49
+ .success-box {
50
+ background: linear-gradient(135deg, #1e7e34 0%, #28a745 100%);
51
+ border-left: 5px solid #4cff4c;
52
+ padding: 1rem;
53
+ margin: 1rem 0;
54
+ border-radius: 5px;
55
+ color: white;
56
+ font-weight: 500;
57
+ box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
58
+ }
59
+ .warning-box {
60
+ background: linear-gradient(135deg, #c43a00 0%, #dc3545 100%);
61
+ border-left: 5px solid #ffcc00;
62
+ padding: 1rem;
63
+ margin: 1rem 0;
64
+ border-radius: 5px;
65
+ color: white;
66
+ font-weight: 500;
67
+ box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
68
+ }
69
+ .error-box {
70
+ background-color: #f8d7da;
71
+ border-left: 5px solid #dc3545;
72
+ padding: 1rem;
73
+ margin: 1rem 0;
74
+ border-radius: 5px;
75
+ }
76
+ .defect-name {
77
+ font-size: 1.5rem;
78
+ font-weight: bold;
79
+ color: white;
80
+ padding: 0.5rem 0;
81
+ margin: 0.5rem 0;
82
+ }
83
+ .defect-info {
84
+ background: linear-gradient(135deg, #1a5490 0%, #1f77b4 100%);
85
+ padding: 1.2rem;
86
+ border-radius: 8px;
87
+ border-left: 4px solid #5dade2;
88
+ margin: 1rem 0;
89
+ box-shadow: 0 2px 8px rgba(31, 119, 180, 0.3);
90
+ }
91
+ </style>
92
+ """, unsafe_allow_html=True)
93
+
94
+
95
+ @st.cache_resource
96
+ def load_system():
97
+ """Load the diagnosis system (cached)"""
98
+ with st.spinner(" Loading models... This may take a minute..."):
99
+ validator = DomainValidator()
100
+ extractor = DescriptionExtractor()
101
+ matcher = DefectMatcher(use_finetuned=True)
102
+ grader = ConditionGrader()
103
+ try:
104
+ predictor = PricePredictor()
105
+ predictor.load_model()
106
+ except:
107
+ predictor = None
108
+
109
+ return validator, extractor, matcher, grader, predictor
110
+
111
+
112
+ def main():
113
+ st.markdown('<div class="main-header"> Device Defect Diagnosis System</div>', unsafe_allow_html=True)
114
+ st.markdown('<div class="sub-header">Upload a device image to detect defects and estimate resale value</div>', unsafe_allow_html=True)
115
+
116
+ validator, extractor, matcher, grader, predictor = load_system()
117
+
118
+ with st.sidebar:
119
+ st.header("Instructions:-")
120
+ st.markdown("""
121
+ 1. **Upload** a device image (phone/laptop)
122
+ 2. **Describe** the issue (optional)
123
+ 3. **Enter** device details for pricing
124
+ 4. **Click** Diagnose to get results
125
+
126
+ ---
127
+
128
+ ### Supported Devices
129
+ - Smartphones
130
+ - Laptops
131
+ - Desktop computers
132
+
133
+ ---
134
+
135
+ ### Understanding Results
136
+
137
+ **Confidence Score**
138
+ How certain the model is about its detection (0-100%)
139
+ - 90%+ : High confidence
140
+ - 70-90% : Good confidence
141
+ - <70% : Review needed
142
+
143
+ **Severity Score** (0-10)
144
+ Impact of the defect on device functionality
145
+ - 8-10 : Critical (needs repair)
146
+ - 5-7 : Moderate (affects usage)
147
+ - 0-4 : Minor (cosmetic)
148
+
149
+ **Condition Score** (0-10)
150
+ Overall device condition after inspection
151
+ - 9-10 : Excellent (Grade A)
152
+ - 7-9 : Good (Grade B)
153
+ - 5-7 : Fair (Grade C)
154
+ - 3-5 : Poor (Grade D)
155
+ - 0-3 : Bad (Grade F)
156
+
157
+ ---
158
+
159
+ ### Detected Issues:-
160
+ - Screen damage
161
+ - Physical defects
162
+ - Component failures
163
+ - And more...
164
+ """)
165
+
166
+ st.markdown("---")
167
+ st.markdown("### System Stats :-")
168
+ st.metric("Models Loaded", "5/5")
169
+ st.metric("Defect Types", "15+")
170
+ st.metric("Avg Response Time", "8.2s")
171
+
172
+ col1, col2 = st.columns([1, 1])
173
+
174
+ with col1:
175
+ st.header(" Upload & Configure")
176
+
177
+ uploaded_file = st.file_uploader(
178
+ "Choose a device image",
179
+ type=['jpg', 'jpeg', 'png'],
180
+ help="Upload a clear image of your device"
181
+ )
182
+
183
+ if uploaded_file:
184
+ image = Image.open(uploaded_file)
185
+ st.image(image, caption="Uploaded Image", use_container_width=False)
186
+
187
+ description = st.text_area(
188
+ "Describe the issue (optional)",
189
+ placeholder="e.g., My phone screen cracked after I dropped it. Touch is not working properly.",
190
+ height=100,
191
+ help="Providing a description improves accuracy"
192
+ )
193
+
194
+ st.subheader(" Device Information (for price prediction)")
195
+
196
+ col_a, col_b = st.columns(2)
197
+
198
+ with col_a:
199
+ brand = st.selectbox(
200
+ "Brand",
201
+ ["Apple", "Samsung", "OnePlus", "Dell", "HP", "Lenovo", "Other"]
202
+ )
203
+
204
+ device_type = st.selectbox(
205
+ "Device Type",
206
+ ["Phone", "Laptop"]
207
+ )
208
+
209
+ model_options = {
210
+ "Apple": {
211
+ "Phone": ["iPhone 15 Pro Max", "iPhone 15 Pro", "iPhone 15", "iPhone 14 Pro Max", "iPhone 14 Pro", "iPhone 14", "iPhone 13 Pro Max", "iPhone 13 Pro", "iPhone 13", "iPhone 12 Pro Max", "iPhone 12 Pro", "iPhone 12", "iPhone 11 Pro Max", "iPhone 11 Pro", "iPhone 11", "iPhone XS Max", "iPhone XS", "iPhone XR", "iPhone X", "iPhone 8 Plus", "iPhone 8"],
212
+ "Laptop": ["MacBook Pro 16\" M3 Max", "MacBook Pro 14\" M3 Pro", "MacBook Pro 16\" M2 Max", "MacBook Pro 14\" M2 Pro", "MacBook Air M3", "MacBook Air M2", "MacBook Air M1", "MacBook Pro 13\" M1"]
213
+ },
214
+ "Samsung": {
215
+ "Phone": ["Galaxy S24 Ultra", "Galaxy S24+", "Galaxy S24", "Galaxy S23 Ultra", "Galaxy S23+", "Galaxy S23", "Galaxy S22 Ultra", "Galaxy S22+", "Galaxy S22", "Galaxy Z Fold 5", "Galaxy Z Flip 5", "Galaxy Z Fold 4", "Galaxy Z Flip 4", "Galaxy A54", "Galaxy A34"],
216
+ "Laptop": ["Galaxy Book4 Pro", "Galaxy Book3 Ultra", "Galaxy Book3 Pro 360", "Galaxy Book2 Pro", "Galaxy Book2"]
217
+ },
218
+ "OnePlus": {
219
+ "Phone": ["OnePlus 12", "OnePlus 11", "OnePlus 10 Pro", "OnePlus 9 Pro", "OnePlus 9", "OnePlus 8T", "OnePlus Nord 3", "OnePlus Nord 2"],
220
+ "Laptop": []
221
+ },
222
+ "Dell": {
223
+ "Phone": [],
224
+ "Laptop": ["XPS 15", "XPS 13", "XPS 17", "Inspiron 15", "Inspiron 14", "Latitude 5430", "Latitude 7430", "Alienware m15", "Alienware x15", "Vostro 15"]
225
+ },
226
+ "HP": {
227
+ "Phone": [],
228
+ "Laptop": ["Pavilion 15", "Pavilion 14", "Envy 15", "Envy 13", "Spectre x360 14", "Spectre x360 16", "EliteBook 840", "EliteBook 850", "ProBook 450", "Omen 15"]
229
+ },
230
+ "Lenovo": {
231
+ "Phone": [],
232
+ "Laptop": ["ThinkPad X1 Carbon", "ThinkPad X1 Yoga", "ThinkPad T14", "ThinkPad T16", "IdeaPad Slim 5", "IdeaPad Gaming 3", "Yoga 9i", "Yoga 7i", "Legion 5 Pro"]
233
+ },
234
+ "Other": {
235
+ "Phone": ["Other Model"],
236
+ "Laptop": ["Other Model"]
237
+ }
238
+ }
239
+
240
+ available_models = model_options.get(brand, {}).get(device_type, ["Other Model"])
241
+
242
+ with col_b:
243
+ if available_models:
244
+ model = st.selectbox("Model", available_models)
245
+ else:
246
+ st.warning(f"No {device_type} models available for {brand}")
247
+ model = st.text_input("Model (enter manually)", "")
248
+
249
+ original_price = st.number_input("Original Price (₹)", min_value=0, value=79900, step=1000)
250
+
251
+ age_months = st.slider("Age (months)", 0, 60, 18, help="How old is the device?")
252
+
253
+ with col2:
254
+ st.header("Diagnosis Results :- ")
255
+
256
+ if uploaded_file:
257
+ if st.button(" Diagnose Device", type="primary", use_container_width=False):
258
+
259
+ temp_path = "temp_upload.jpg"
260
+ if image.mode == 'RGBA':
261
+ image = image.convert('RGB')
262
+ image.save(temp_path)
263
+
264
+ progress_bar = st.progress(0)
265
+ status_text = st.empty()
266
+
267
+ try:
268
+ status_text.text("Stage 1/5: Validating device...")
269
+ progress_bar.progress(20)
270
+
271
+ validation = validator.validate(temp_path)
272
+
273
+ if not validation['is_valid']:
274
+ st.markdown(f'<div class="error-box"> <b>Invalid Image</b><br>{validation["reason"]}</div>', unsafe_allow_html=True)
275
+ os.remove(temp_path)
276
+ return
277
+
278
+ status_text.text("Stage 2/5: Processing description...")
279
+ progress_bar.progress(40)
280
+
281
+ if description:
282
+ desc_info = extractor.extract(description)
283
+ search_text = extractor.create_search_text(desc_info)
284
+ else:
285
+ search_text = None
286
+
287
+ status_text.text(" Stage 3/5: Detecting defects...")
288
+ progress_bar.progress(60)
289
+
290
+ match_result = matcher.match(temp_path, search_text, top_k=3)
291
+ defect = match_result['top_match']
292
+
293
+ status_text.text("Stage 4/5: Grading condition...")
294
+ progress_bar.progress(80)
295
+
296
+ grading = grader.assign_grade([defect])
297
+
298
+ status_text.text("Stage 5/5: Predicting price...")
299
+ progress_bar.progress(90)
300
+
301
+ price_result = None
302
+ if predictor:
303
+ try:
304
+ device_info = {
305
+ 'brand': brand,
306
+ 'model': model,
307
+ 'original_price': original_price,
308
+ 'age_months': age_months
309
+ }
310
+ price_result = predictor.predict(device_info, [defect], grading['condition_score'])
311
+ except:
312
+ pass
313
+
314
+ progress_bar.progress(100)
315
+ status_text.text(" Diagnosis complete!!!")
316
+
317
+ st.markdown("---")
318
+
319
+ st.markdown(f'<div class="success-box"> <b>Valid {validation["device_type"].title()} Detected</b> (Confidence: {validation["confidence"]:.1%})</div>', unsafe_allow_html=True)
320
+
321
+ st.subheader(" Detected Issue")
322
+
323
+ severity_color = "#dc3545" if defect.get('critical', False) else "#ffc107" if defect['severity_score'] > 6 else "#28a745"
324
+
325
+ st.markdown(f'<div class="defect-info"><div class="defect-name"> {defect["name"]}</div></div>', unsafe_allow_html=True)
326
+
327
+ col_r1, col_r2 = st.columns(2)
328
+ col_r1.metric(
329
+ "Confidence",
330
+ f"{match_result['confidence']:.1%}",
331
+ help="How certain the AI is about this detection. Higher is better."
332
+ )
333
+ col_r2.metric(
334
+ "Severity",
335
+ f"{defect['severity_score']}/10",
336
+ help="Impact of this defect: 0-4 (Minor), 5-7 (Moderate), 8-10 (Critical)"
337
+ )
338
+
339
+ if defect['severity_score'] >= 8:
340
+ severity_msg = " **High Severity:** This defect significantly impacts device functionality and requires immediate attention."
341
+ elif defect['severity_score'] >= 5:
342
+ severity_msg = " **Moderate Severity:** This defect affects device usage. Consider repair before resale."
343
+ else:
344
+ severity_msg = " **Low Severity:** This is mostly a cosmetic issue with minimal impact on functionality."
345
+
346
+ st.info(severity_msg)
347
+
348
+ if defect.get('critical', False):
349
+ st.markdown(f'<div class="warning-box"> <b>Critical Issue Detected</b><br>Immediate attention required</div>', unsafe_allow_html=True)
350
+
351
+ st.subheader(" Condition Assessment")
352
+
353
+ col_c1, col_c2 = st.columns(2)
354
+ col_c1.metric(
355
+ "Grade",
356
+ grading['grade'],
357
+ help="Letter grade from A (Excellent) to F (Bad) based on overall condition"
358
+ )
359
+ col_c2.metric(
360
+ "Condition Score",
361
+ f"{grading['condition_score']}/10",
362
+ help="Numerical score: 10 is perfect, 0 is non-functional. Affects resale value directly."
363
+ )
364
+
365
+ st.caption(f"**{grading['description']}**")
366
+
367
+ with st.expander("What does this grade mean???", expanded=False):
368
+ grade_info = {
369
+ 'A': "**Excellent Condition** - Device is like new with minimal to no defects. Commands premium resale price.",
370
+ 'B': "**Good Condition** - Device shows minor wear but fully functional. Good resale value.",
371
+ 'C': "**Fair Condition** - Device has noticeable defects but still usable. Moderate resale value.",
372
+ 'D': "**Poor Condition** - Device has significant issues affecting usability. Low resale value.",
373
+ 'F': "**Bad Condition** - Device has critical defects or is barely functional. Very low resale value."
374
+ }
375
+ st.write(grade_info.get(grading['grade'], "Assessment completed"))
376
+
377
+ st.write("""\n**How it's calculated:**
378
+ - Based on severity and number of defects
379
+ - Physical damage has higher impact
380
+ - Critical defects significantly lower the grade
381
+ - Overall device age and wear considered""")
382
+
383
+ if price_result:
384
+ st.subheader("Resale Valuation")
385
+
386
+ col_p1, col_p2, col_p3 = st.columns(3)
387
+ col_p1.metric(
388
+ "Estimated Price",
389
+ f"₹{price_result['predicted_price']:,}",
390
+ help="AI-predicted resale price based on condition, defects, and market data"
391
+ )
392
+ col_p2.metric(
393
+ "Min Price",
394
+ f"₹{price_result['price_range']['min']:,}",
395
+ help="Minimum expected price in current market conditions"
396
+ )
397
+ col_p3.metric(
398
+ "Max Price",
399
+ f"₹{price_result['price_range']['max']:,}",
400
+ help="Maximum expected price for this condition"
401
+ )
402
+
403
+ st.caption(f"**Prediction Confidence:** {price_result['confidence']:.0%} - How reliable this price estimate is based on available data")
404
+
405
+ with st.expander(" Price Impact Factors"):
406
+ for factor in price_result['depreciation_factors']:
407
+ st.markdown(f"• **{factor['factor']}**: {factor['impact']} - {factor['description']}")
408
+
409
+ if len(match_result['all_matches']) > 1:
410
+ with st.expander(" Alternative Diagnoses"):
411
+ for i, alt in enumerate(match_result['all_matches'][1:3], 2):
412
+ st.markdown(f"{i}. **{alt['defect']['name']}** - Confidence: {alt['confidence']:.1%}")
413
+
414
+ st.markdown("---")
415
+
416
+ try:
417
+ from fpdf import FPDF
418
+ from fpdf.enums import XPos, YPos
419
+
420
+ class DiagnosisReport(FPDF):
421
+ def header(self):
422
+ self.set_font('Helvetica', 'B', 20)
423
+ self.set_text_color(31, 119, 180)
424
+ self.cell(0, 10, 'Device Diagnosis Report', new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
425
+ self.set_font('Helvetica', '', 10)
426
+ self.set_text_color(100, 100, 100)
427
+ self.cell(0, 6, f"Generated: {datetime.now().strftime('%B %d, %Y at %I:%M %p')}", new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
428
+ self.ln(10)
429
+
430
+ def footer(self):
431
+ self.set_y(-15)
432
+ self.set_font('Helvetica', 'I', 8)
433
+ self.set_text_color(128, 128, 128)
434
+
435
+
436
+ pdf = DiagnosisReport()
437
+ pdf.add_page()
438
+ pdf.set_auto_page_break(auto=True, margin=15)
439
+
440
+ pdf.set_font('Helvetica', 'B', 14)
441
+ pdf.set_text_color(44, 62, 80)
442
+ pdf.cell(0, 10, 'Device Information', new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
443
+ pdf.ln(2)
444
+
445
+ pdf.set_font('Helvetica', '', 11)
446
+ pdf.set_text_color(0, 0, 0)
447
+ device_info = [
448
+ ('Device Type:', validation['device_type'].title()),
449
+ ('Brand:', brand),
450
+ ('Model:', model),
451
+ ('Age:', f"{age_months} months"),
452
+ ('Detection Confidence:', f"{validation['confidence']:.1%}")
453
+ ]
454
+ for label, value in device_info:
455
+ pdf.set_font('Helvetica', 'B', 11)
456
+ pdf.cell(60, 8, label, border=1, new_x=XPos.RIGHT, new_y=YPos.TOP, align='L')
457
+ pdf.set_font('Helvetica', '', 11)
458
+ pdf.cell(0, 8, str(value), border=1, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
459
+
460
+ pdf.ln(8)
461
+
462
+ pdf.set_font('Helvetica', 'B', 14)
463
+ pdf.set_text_color(44, 62, 80)
464
+ pdf.cell(0, 10, 'Detected Issue', new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
465
+ pdf.ln(2)
466
+
467
+ pdf.set_font('Helvetica', '', 11)
468
+ pdf.set_text_color(0, 0, 0)
469
+ defect_info = [
470
+ ('Defect Name:', defect['name']),
471
+ ('Confidence:', f"{match_result['confidence']:.1%}"),
472
+ ('Severity Score:', f"{defect['severity_score']}/10"),
473
+ ('Critical:', 'Yes' if defect.get('critical', False) else 'No')
474
+ ]
475
+ for label, value in defect_info:
476
+ pdf.set_font('Helvetica', 'B', 11)
477
+ pdf.cell(60, 8, label, border=1, new_x=XPos.RIGHT, new_y=YPos.TOP, align='L')
478
+ pdf.set_font('Helvetica', '', 11)
479
+ pdf.cell(0, 8, str(value), border=1, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
480
+
481
+ pdf.ln(8)
482
+
483
+ pdf.set_font('Helvetica', 'B', 14)
484
+ pdf.set_text_color(44, 62, 80)
485
+ pdf.cell(0, 10, 'Condition Assessment', new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
486
+ pdf.ln(2)
487
+
488
+ pdf.set_font('Helvetica', '', 11)
489
+ pdf.set_text_color(0, 0, 0)
490
+ condition_info = [
491
+ ('Grade:', grading['grade']),
492
+ ('Condition Score:', f"{grading['condition_score']}/10"),
493
+ ('Description:', grading['description'])
494
+ ]
495
+ for label, value in condition_info:
496
+ pdf.set_font('Helvetica', 'B', 11)
497
+ pdf.cell(60, 8, label, border=1, new_x=XPos.RIGHT, new_y=YPos.TOP, align='L')
498
+ pdf.set_font('Helvetica', '', 11)
499
+ pdf.cell(0, 8, str(value), border=1, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
500
+
501
+ pdf.ln(8)
502
+
503
+ if price_result:
504
+ pdf.set_font('Helvetica', 'B', 14)
505
+ pdf.set_text_color(44, 62, 80)
506
+ pdf.cell(0, 10, 'Resale Valuation', new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
507
+ pdf.ln(2)
508
+
509
+ pdf.set_font('Helvetica', '', 11)
510
+ pdf.set_text_color(0, 0, 0)
511
+ pricing_info = [
512
+ ('Estimated Price:', f"Rs. {price_result['predicted_price']:,}"),
513
+ ('Price Range:', f"Rs. {price_result['price_range']['min']:,} - Rs. {price_result['price_range']['max']:,}"),
514
+ ('Prediction Confidence:', f"{price_result['confidence']:.0%}")
515
+ ]
516
+ for label, value in pricing_info:
517
+ pdf.set_font('Helvetica', 'B', 11)
518
+ pdf.cell(60, 8, label, border=1, new_x=XPos.RIGHT, new_y=YPos.TOP, align='L')
519
+ pdf.set_font('Helvetica', '', 11)
520
+ pdf.cell(0, 8, str(value), border=1, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
521
+
522
+ pdf_data = bytes(pdf.output())
523
+
524
+ st.download_button(
525
+ " Download Full Report (PDF)",
526
+ data=pdf_data,
527
+ file_name=f"diagnosis_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
528
+ mime="application/pdf"
529
+ )
530
+ except (ImportError, Exception) as e:
531
+ st.warning(f" PDF generation failed: {str(e)}\n")
532
+ report = {
533
+ 'device': {
534
+ 'type': validation['device_type'],
535
+ 'brand': brand,
536
+ 'model': model,
537
+ 'age_months': age_months
538
+ },
539
+ 'defect': {
540
+ 'name': defect['name'],
541
+ 'confidence': match_result['confidence'],
542
+ 'severity': defect['severity_score']
543
+ },
544
+ 'condition': {
545
+ 'grade': grading['grade'],
546
+ 'score': grading['condition_score']
547
+ }
548
+ }
549
+
550
+ if price_result:
551
+ report['pricing'] = {
552
+ 'estimated_price': price_result['predicted_price'],
553
+ 'price_range': price_result['price_range']
554
+ }
555
+
556
+ st.download_button(
557
+ "📥 Download Full Report (JSON)",
558
+ data=json.dumps(report, indent=2),
559
+ file_name="diagnosis_report.json",
560
+ mime="application/json"
561
+ )
562
+
563
+ os.remove(temp_path)
564
+
565
+ except Exception as e:
566
+ st.error(f" Error during diagnosis: {str(e)}")
567
+ if os.path.exists(temp_path):
568
+ os.remove(temp_path)
569
+ else:
570
+ st.info(" Upload an image to begin diagnosis")
571
+
572
+ st.markdown("---")
573
+ st.markdown("""
574
+ <div style='text-align: center; color: #666; padding: 2rem;'>
575
+ <p><b>Device Defect Diagnosis System</b></p>
576
+ <p>Built with CLIP, BERT, and XGBoost</p>
577
+ </div>
578
+ """, unsafe_allow_html=True)
579
+
580
+
581
+ if __name__ == "__main__":
582
+ main()
condition_grader.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class ConditionGrader:
2
+ def __init__(self):
3
+
4
+ self.grade_criteria = {
5
+ 'A': {
6
+ 'description': 'Excellent - Like New',
7
+ 'max_defects': 0,
8
+ 'min_condition_score': 9.0,
9
+ 'max_severity': 0
10
+ },
11
+ 'B': {
12
+ 'description': 'Good - Minor Cosmetic Issues',
13
+ 'max_defects': 2,
14
+ 'min_condition_score': 7.0,
15
+ 'max_severity': 5
16
+ },
17
+ 'C': {
18
+ 'description': 'Fair - Moderate Damage',
19
+ 'max_defects': 3,
20
+ 'min_condition_score': 5.0,
21
+ 'max_severity': 7
22
+ },
23
+ 'D': {
24
+ 'description': 'Poor - Significant Damage',
25
+ 'max_defects': 5,
26
+ 'min_condition_score': 3.0,
27
+ 'max_severity': 9
28
+ },
29
+ 'F': {
30
+ 'description': 'Unacceptable - Major Damage/Non-functional',
31
+ 'max_defects': 999,
32
+ 'min_condition_score': 0,
33
+ 'max_severity': 10
34
+ }
35
+ }
36
+
37
+ def calculate_condition_score(self, defects):
38
+
39
+ if not defects:
40
+ return 10.0
41
+ total_severity = sum(d['severity_score'] for d in defects)
42
+ num_defects = len(defects)
43
+ avg_severity = total_severity / num_defects
44
+ has_critical = any(d['critical'] for d in defects)
45
+ condition_score = 10 - avg_severity
46
+ if num_defects > 1:
47
+ condition_score -= (num_defects - 1) * 0.5
48
+ if has_critical:
49
+ condition_score -= 2.0
50
+
51
+ return max(0.0, min(10.0, condition_score))
52
+
53
+ def assign_grade(self, defects, condition_score=None):
54
+
55
+ if condition_score is None:
56
+ condition_score = self.calculate_condition_score(defects)
57
+
58
+ num_defects = len(defects)
59
+ max_severity = max([d['severity_score'] for d in defects], default=0)
60
+ has_critical = any(d.get('critical', False) for d in defects)
61
+
62
+ if num_defects == 0:
63
+ grade = 'A'
64
+ elif has_critical or max_severity >= 9:
65
+ if num_defects >= 2:
66
+ grade = 'F'
67
+ else:
68
+ grade = 'D'
69
+ elif condition_score >= 9:
70
+ grade = 'A'
71
+ elif condition_score >= 7:
72
+ grade = 'B'
73
+ elif condition_score >= 5:
74
+ grade = 'C'
75
+ elif condition_score >= 3:
76
+ grade = 'D'
77
+ else:
78
+ grade = 'F'
79
+
80
+ return {
81
+ 'grade': grade,
82
+ 'description': self.grade_criteria[grade]['description'],
83
+ 'condition_score': round(condition_score, 2),
84
+ 'breakdown': {
85
+ 'num_defects': num_defects,
86
+ 'max_severity': max_severity,
87
+ 'has_critical': has_critical,
88
+ 'defect_categories': list(set(d['category'] for d in defects))
89
+ }
90
+ }
91
+
92
+ def get_grade_info(self, grade):
93
+ return self.grade_criteria.get(grade, {})
94
+
95
+
96
+ if __name__ == "__main__":
97
+ grader = ConditionGrader()
98
+
99
+
config.toml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor = "#1f77b4"
3
+ backgroundColor = "#ffffff"
4
+ secondaryBackgroundColor = "#f0f2f6"
5
+ textColor = "#262730"
6
+
7
+ [server]
8
+ headless = true
9
+ port = 7860
10
+ enableCORS = false
defect_matcher.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import CLIPProcessor, CLIPModel
2
+ from PIL import Image
3
+ import torch
4
+ import json
5
+ import numpy as np
6
+ from sklearn.metrics.pairwise import cosine_similarity
7
+ import os
8
+
9
+ class DefectMatcher:
10
+
11
+ def __init__(self, defect_db_path="data/defect_database.json",
12
+ use_finetuned=True,
13
+ finetuned_model_path="models/finetuned_clip/best_model"):
14
+
15
+ if use_finetuned and os.path.exists(finetuned_model_path):
16
+
17
+ self.model = CLIPModel.from_pretrained(finetuned_model_path)
18
+ self.processor = CLIPProcessor.from_pretrained(finetuned_model_path)
19
+ self.model_type = "fine-tuned"
20
+ else:
21
+ self.model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
22
+ self.processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
23
+ self.model_type = "pre-trained"
24
+
25
+ self.model.eval()
26
+
27
+ with open(defect_db_path, 'r') as f:
28
+ self.defect_db = json.load(f)['defects']
29
+
30
+ self._precompute_defect_embeddings()
31
+
32
+ def _precompute_defect_embeddings(self):
33
+ defect_descriptions = [d['description'] for d in self.defect_db]
34
+
35
+ inputs = self.processor(
36
+ text=defect_descriptions,
37
+ return_tensors="pt",
38
+ padding=True,
39
+ truncation=True
40
+ )
41
+
42
+ with torch.no_grad():
43
+ text_features = self.model.get_text_features(**inputs)
44
+ text_features = text_features / text_features.norm(dim=-1, keepdim=True)
45
+
46
+ self.defect_embeddings = text_features.cpu().numpy()
47
+
48
+ def match(self, image_path, description_text=None, top_k=3):
49
+ image = Image.open(image_path).convert('RGB')
50
+
51
+ if description_text and len(description_text.strip()) > 0:
52
+ inputs = self.processor(
53
+ text=[description_text],
54
+ images=image,
55
+ return_tensors="pt",
56
+ padding=True
57
+ )
58
+
59
+ with torch.no_grad():
60
+ image_features = self.model.get_image_features(pixel_values=inputs['pixel_values'])
61
+ text_features = self.model.get_text_features(input_ids=inputs['input_ids'])
62
+
63
+ image_features = image_features / image_features.norm(dim=-1, keepdim=True)
64
+ text_features = text_features / text_features.norm(dim=-1, keepdim=True)
65
+
66
+ fused_features = 0.6 * image_features + 0.4 * text_features
67
+ fused_features = fused_features / fused_features.norm(dim=-1, keepdim=True)
68
+
69
+ query_embedding = fused_features.cpu().numpy()
70
+ method = "multimodal"
71
+
72
+ else:
73
+ inputs = self.processor(
74
+ images=image,
75
+ return_tensors="pt"
76
+ )
77
+
78
+ with torch.no_grad():
79
+ image_features = self.model.get_image_features(**inputs)
80
+ image_features = image_features / image_features.norm(dim=-1, keepdim=True)
81
+
82
+ query_embedding = image_features.cpu().numpy()
83
+ method = "image_only"
84
+ similarities = cosine_similarity(query_embedding, self.defect_embeddings)[0]
85
+
86
+ top_indices = np.argsort(similarities)[::-1][:top_k]
87
+
88
+ matches = []
89
+ for idx in top_indices:
90
+ defect = self.defect_db[idx]
91
+ score = float(similarities[idx])
92
+ matches.append({
93
+ 'defect': defect,
94
+ 'similarity_score': score,
95
+ 'confidence': score
96
+ })
97
+
98
+ return {
99
+ 'top_match': matches[0]['defect'],
100
+ 'confidence': matches[0]['confidence'],
101
+ 'all_matches': matches,
102
+ 'method': method,
103
+ 'match_count': len(matches)
104
+ }
105
+
description_extractor.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import torch
3
+ from transformers import pipeline
4
+ #2
5
+ class DescriptionExtractor:
6
+
7
+ def __init__(self):
8
+ self.summarizer = pipeline(
9
+ "summarization",
10
+ model="facebook/bart-large-cnn"
11
+ )
12
+ self.part_keywords = [
13
+ "screen", "display", "glass", "battery", "power",
14
+ "charging port", "port", "hinge", "keyboard", "keys",
15
+ "speaker", "audio", "microphone", "body", "frame",
16
+ "casing", "lid", "touchpad", "camera"
17
+ ]
18
+
19
+ self.symptom_keywords = [
20
+ "crack", "broken", "damage", "not working", "loose",
21
+ "drain", "hot", "overheat", "scratch", "dent",
22
+ "bent", "water", "liquid", "sound", "audio"
23
+ ]
24
+
25
+ def extract(self, description):
26
+
27
+ if not description or len(description.strip()) < 5:
28
+ return {
29
+ 'original': description,
30
+ 'summary': description,
31
+ 'affected_parts': [],
32
+ 'symptoms': [],
33
+ 'keywords': [],
34
+ 'length_category': 'none'
35
+ }
36
+
37
+ desc_lower = description.lower()
38
+
39
+ word_count = len(description.split())
40
+ if word_count < 10:
41
+ length_category = 'short'
42
+ summary = description
43
+ elif word_count < 50:
44
+ length_category = 'medium'
45
+ summary = description
46
+ else:
47
+ length_category = 'long'
48
+ try:
49
+ summary_result = self.summarizer(
50
+ description,
51
+ max_length=50,
52
+ min_length=10,
53
+ do_sample=False
54
+ )
55
+ summary = summary_result[0]['summary_text']
56
+ except:
57
+ summary = ' '.join(description.split()[:40]) + "..."
58
+ affected_parts = [
59
+ part for part in self.part_keywords
60
+ if part in desc_lower
61
+ ]
62
+
63
+ symptoms = [
64
+ symptom for symptom in self.symptom_keywords
65
+ if symptom in desc_lower
66
+ ]
67
+ keywords = list(set(affected_parts + symptoms))
68
+
69
+ return {
70
+ 'original': description,
71
+ 'summary': summary,
72
+ 'affected_parts': affected_parts,
73
+ 'symptoms': symptoms,
74
+ 'keywords': keywords,
75
+ 'length_category': length_category,
76
+ 'word_count': word_count
77
+ }
78
+
79
+ def create_search_text(self, description_info):
80
+ if not description_info['keywords']:
81
+ return description_info['summary']
82
+ search_text = f"{description_info['summary']} {' '.join(description_info['keywords'])}"
83
+ return search_text
84
+
domain_validator.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import CLIPProcessor, CLIPModel
2
+ from PIL import Image
3
+ import torch
4
+ #1
5
+ class DomainValidator:
6
+
7
+ def __init__(self):
8
+ self.model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
9
+ self.processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
10
+ self.model.eval()
11
+
12
+ self.valid_categories = [
13
+ "a smartphone or mobile phone",
14
+ "a laptop computer",
15
+ "a desktop computer or monitor",
16
+ "electronic device with screen"
17
+ ]
18
+
19
+ self.invalid_categories = [
20
+ "a person or human face",
21
+ "an animal or pet",
22
+ "nature or landscape",
23
+ "food or drink",
24
+ "random object or item"
25
+ ]
26
+ def validate(self, image_path, threshold=0.3):
27
+
28
+ try:
29
+ image = Image.open(image_path).convert('RGB')
30
+
31
+ all_categories = self.valid_categories + self.invalid_categories
32
+
33
+ inputs = self.processor(
34
+ text=all_categories,
35
+ images=image,
36
+ return_tensors="pt",
37
+ padding=True
38
+ )
39
+
40
+ with torch.no_grad():
41
+ outputs = self.model(**inputs)
42
+ logits = outputs.logits_per_image
43
+ probs = logits.softmax(dim=1)[0]
44
+
45
+ max_idx = probs.argmax().item()
46
+ max_prob = probs[max_idx].item()
47
+ detected_category = all_categories[max_idx]
48
+
49
+ is_valid = detected_category in self.valid_categories
50
+
51
+ if is_valid and max_prob > threshold:
52
+ return {
53
+ 'is_valid': True,
54
+ 'confidence': max_prob,
55
+ 'detected_category': detected_category,
56
+ 'reason': f"Valid device detected: {detected_category}",
57
+ 'device_type': self._extract_device_type(detected_category)
58
+ }
59
+ elif is_valid:
60
+ return {
61
+ 'is_valid': False,
62
+ 'confidence': max_prob,
63
+ 'detected_category': detected_category,
64
+ 'reason': f"Device detected but confidence too low ({max_prob:.2f})",
65
+ 'device_type': None
66
+ }
67
+ else:
68
+ return {
69
+ 'is_valid': False,
70
+ 'confidence': max_prob,
71
+ 'detected_category': detected_category,
72
+ 'reason': f"Not a device - detected: {detected_category}",
73
+ 'device_type': None
74
+ }
75
+
76
+ except Exception as e:
77
+ return {
78
+ 'is_valid': False,
79
+ 'confidence': 0.0,
80
+ 'detected_category': 'error',
81
+ 'reason': f"Error processing image: {str(e)}",
82
+ 'device_type': None
83
+ }
84
+
85
+ def _extract_device_type(self, category):
86
+ """Extract simple device type from category"""
87
+ if "phone" in category.lower():
88
+ return "phone"
89
+ elif "laptop" in category.lower():
90
+ return "laptop"
91
+ elif "computer" in category.lower() or "monitor" in category.lower():
92
+ return "computer"
93
+ else:
94
+ return "device"
95
+
96
+
price_predictor.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import xgboost as xgb
4
+ from sklearn.model_selection import train_test_split
5
+ from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
6
+ from sklearn.preprocessing import LabelEncoder
7
+ import joblib
8
+ import json
9
+ import os
10
+
11
+ class PricePredictor:
12
+
13
+ def __init__(self):
14
+ self.model = None
15
+ self.label_encoders = {}
16
+ self.feature_columns = []
17
+ self.is_trained = False
18
+
19
+ def prepare_features(self, df):
20
+ df = df.copy()
21
+ categorical_cols = ['brand', 'model', 'condition_grade']
22
+
23
+ for col in categorical_cols:
24
+ if col not in self.label_encoders:
25
+ self.label_encoders[col] = LabelEncoder()
26
+ df[f'{col}_encoded'] = self.label_encoders[col].fit_transform(df[col])
27
+ else:
28
+ df[f'{col}_encoded'] = df[col].map(
29
+ lambda x: self.label_encoders[col].transform([x])[0]
30
+ if x in self.label_encoders[col].classes_
31
+ else -1
32
+ )
33
+
34
+ feature_cols = [
35
+ 'original_price',
36
+ 'age_months',
37
+ 'brand_encoded',
38
+ 'model_encoded',
39
+ 'num_defects',
40
+ 'has_screen_damage',
41
+ 'has_water_damage',
42
+ 'has_battery_issue',
43
+ 'has_physical_damage',
44
+ 'has_critical_defect',
45
+ 'total_severity_score',
46
+ 'avg_severity_score',
47
+ 'total_repair_cost',
48
+ 'condition_score',
49
+ 'condition_grade_encoded'
50
+ ]
51
+
52
+ self.feature_columns = feature_cols
53
+
54
+ return df[feature_cols]
55
+
56
+ def train(self, csv_path, test_size=0.2, random_state=42):
57
+
58
+ df = pd.read_csv(csv_path)
59
+ X = self.prepare_features(df)
60
+ y = df['resale_price']
61
+
62
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=random_state)
63
+
64
+ self.model = xgb.XGBRegressor(
65
+ n_estimators=200,
66
+ max_depth=6,
67
+ learning_rate=0.1,
68
+ subsample=0.8,
69
+ colsample_bytree=0.8,
70
+ random_state=random_state,
71
+ objective='reg:squarederror',
72
+ n_jobs=-1
73
+ )
74
+ self.model.fit(
75
+ X_train, y_train,
76
+ eval_set=[(X_test, y_test)],
77
+ verbose=False)
78
+ y_pred_train = self.model.predict(X_train)
79
+ y_pred_test = self.model.predict(X_test)
80
+ train_mae = mean_absolute_error(y_train, y_pred_train)
81
+ test_mae = mean_absolute_error(y_test, y_pred_test)
82
+ train_rmse = np.sqrt(mean_squared_error(y_train, y_pred_train))
83
+ test_rmse = np.sqrt(mean_squared_error(y_test, y_pred_test))
84
+ train_r2 = r2_score(y_train, y_pred_train)
85
+ test_r2 = r2_score(y_test, y_pred_test)
86
+
87
+ errors = np.abs(y_test - y_pred_test)
88
+ within_500 = np.sum(errors <= 500) / len(y_test) * 100
89
+ within_1000 = np.sum(errors <= 1000) / len(y_test) * 100
90
+ within_2000 = np.sum(errors <= 2000) / len(y_test) * 100
91
+
92
+ mape = np.mean(np.abs((y_test - y_pred_test) / y_test)) * 100
93
+
94
+ feature_importance = pd.DataFrame({
95
+ 'feature': self.feature_columns,
96
+ 'importance': self.model.feature_importances_
97
+ }).sort_values('importance', ascending=False)
98
+
99
+ for idx, row in feature_importance.head(10).iterrows():
100
+ print(f" {row['feature']:.<30} {row['importance']:.3f}")
101
+ sample_indices = np.random.choice(len(y_test), min(5, len(y_test)), replace=False)
102
+ for idx in sample_indices:
103
+ actual = y_test.iloc[idx]
104
+ predicted = y_pred_test[idx]
105
+ error = abs(actual - predicted)
106
+ print(f" Actual: ₹{actual:>7,} | Predicted: ₹{predicted:>7,.0f} | Error: ₹{error:>6,.0f}")
107
+
108
+ self.is_trained = True
109
+
110
+ return {
111
+ 'train_mae': train_mae,
112
+ 'test_mae': test_mae,
113
+ 'train_rmse': train_rmse,
114
+ 'test_rmse': test_rmse,
115
+ 'train_r2': train_r2,
116
+ 'test_r2': test_r2,
117
+ 'mape': mape,
118
+ 'within_500': within_500,
119
+ 'within_1000': within_1000,
120
+ 'within_2000': within_2000 }
121
+
122
+
123
+
124
+ def predict(self, device_info, defects, condition_score):
125
+
126
+ if not self.is_trained:
127
+ raise ValueError("Model not trained. Call train() first or load_model().")
128
+
129
+ num_defects = len(defects)
130
+ has_screen_damage = int(any(d.get('category') == 'screen' for d in defects))
131
+ has_water_damage = int(any(d.get('category') == 'water' for d in defects))
132
+ has_battery_issue = int(any(d.get('category') == 'battery' for d in defects))
133
+ has_physical_damage = int(any(d.get('category') == 'physical' for d in defects))
134
+ has_critical_defect = int(any(d.get('critical', False) for d in defects))
135
+
136
+ total_severity = sum(d.get('severity_score', 0) for d in defects)
137
+ avg_severity = total_severity / num_defects if num_defects > 0 else 0
138
+ total_repair_cost = sum(d.get('repair_cost', 0) for d in defects)
139
+
140
+ features = {
141
+ 'original_price': device_info['original_price'],
142
+ 'age_months': device_info['age_months'],
143
+ 'brand': device_info['brand'],
144
+ 'model': device_info['model'],
145
+ 'num_defects': num_defects,
146
+ 'has_screen_damage': has_screen_damage,
147
+ 'has_water_damage': has_water_damage,
148
+ 'has_battery_issue': has_battery_issue,
149
+ 'has_physical_damage': has_physical_damage,
150
+ 'has_critical_defect': has_critical_defect,
151
+ 'total_severity_score': total_severity,
152
+ 'avg_severity_score': avg_severity,
153
+ 'total_repair_cost': total_repair_cost,
154
+ 'condition_score': condition_score,
155
+ 'condition_grade': self._score_to_grade(condition_score)
156
+ }
157
+ df = pd.DataFrame([features])
158
+ X = self.prepare_features(df)
159
+ predicted_price = self.model.predict(X)[0]
160
+ confidence_margin = max(500, 0.10 * predicted_price)
161
+ predicted_price = round(predicted_price / 100) * 100
162
+ price_min = max(500, round((predicted_price - confidence_margin) / 100) * 100)
163
+ price_max = round((predicted_price + confidence_margin) / 100) * 100
164
+
165
+ return {
166
+ 'predicted_price': int(predicted_price),
167
+ 'price_range': {
168
+ 'min': int(price_min),
169
+ 'max': int(price_max)
170
+ },
171
+ 'confidence': 0.85, # Based on model R² score
172
+ 'depreciation_factors': self._analyze_depreciation(device_info, defects),
173
+ 'feature_contributions': self._get_feature_contributions(X)
174
+ }
175
+
176
+ def _score_to_grade(self, score):
177
+ if score >= 9:
178
+ return 'A'
179
+ elif score >= 7:
180
+ return 'B'
181
+ elif score >= 5:
182
+ return 'C'
183
+ elif score >= 3:
184
+ return 'D'
185
+ else:
186
+ return 'F'
187
+
188
+ def _analyze_depreciation(self, device_info, defects):
189
+ factors = []
190
+ age_years = device_info['age_months'] / 12
191
+ if age_years <= 1:
192
+ age_factor = -15 * age_years
193
+ else:
194
+ age_factor = -15 - (10 * (age_years - 1))
195
+
196
+ factors.append({
197
+ 'factor': 'Age Depreciation',
198
+ 'impact': f"{age_factor:.1f}%",
199
+ 'description': f"{device_info['age_months']} months old"
200
+ })
201
+ for defect in defects:
202
+ impact_pct = defect.get('price_impact', 0) * 100
203
+ factors.append({
204
+ 'factor': defect.get('name', 'Unknown defect'),
205
+ 'impact': f"{impact_pct:.1f}%",
206
+ 'description': f"Repair cost: ₹{defect.get('repair_cost', 0):,}"
207
+ })
208
+
209
+ return factors
210
+
211
+ def _get_feature_contributions(self, X):
212
+ feature_importance = dict(zip(self.feature_columns, self.model.feature_importances_))
213
+
214
+ top_features = sorted(feature_importance.items(), key=lambda x: x[1], reverse=True)[:5]
215
+
216
+ contributions = []
217
+ for feature, importance in top_features:
218
+ contributions.append({
219
+ 'feature': feature.replace('_', ' ').title(),
220
+ 'importance': f"{importance:.3f}"
221
+ })
222
+
223
+ return contributions
224
+
225
+ def save_model(self, path='models/price_model.pkl'):
226
+ if not self.is_trained:
227
+ raise ValueError("No trained model to save")
228
+ os.makedirs(os.path.dirname(path), exist_ok=True)
229
+
230
+ joblib.dump({
231
+ 'model': self.model,
232
+ 'label_encoders': self.label_encoders,
233
+ 'feature_columns': self.feature_columns
234
+ }, path)
235
+
236
+ def load_model(self, path='models/price_model.pkl'):
237
+
238
+ if not os.path.exists(path):
239
+ raise FileNotFoundError(f"Model file not found: {path}")
240
+
241
+ data = joblib.load(path)
242
+ self.model = data['model']
243
+ self.label_encoders = data['label_encoders']
244
+ self.feature_columns = data['feature_columns']
245
+ self.is_trained = True
246
+
247
+
248
+ def train_model():
249
+
250
+ predictor = PricePredictor()
251
+ dataset_path = 'data/pricing_dataset.csv'
252
+ if not os.path.exists(dataset_path):
253
+ print(f"\ Dataset not found: {dataset_path}")
254
+ return None
255
+ metrics = predictor.train(dataset_path)
256
+ predictor.save_model()
257
+ return predictor, metrics
258
+
259
+ def test_predictions():
260
+ predictor = PricePredictor()
261
+ predictor.load_model()
262
+ test_cases = [
263
+ {
264
+ 'name': 'iPhone 13 - Cracked Screen',
265
+ 'device': {
266
+ 'brand': 'Apple',
267
+ 'model': 'iPhone 13',
268
+ 'original_price': 79900,
269
+ 'age_months': 24
270
+ },
271
+ 'defects': [
272
+ {
273
+ 'id': 'SCR001',
274
+ 'name': 'Cracked Screen',
275
+ 'severity_score': 9,
276
+ 'price_impact': -0.35,
277
+ 'repair_cost': 4000,
278
+ 'category': 'screen',
279
+ 'critical': True
280
+ }
281
+ ],
282
+ 'condition_score': 6.0
283
+ },
284
+ {
285
+ 'name': 'Samsung S22 - Perfect Condition',
286
+ 'device': {
287
+ 'brand': 'Samsung',
288
+ 'model': 'Galaxy S22',
289
+ 'original_price': 72999,
290
+ 'age_months': 18
291
+ },
292
+ 'defects': [],
293
+ 'condition_score': 9.5
294
+ },
295
+ {
296
+ 'name': 'MacBook Air - Multiple Issues',
297
+ 'device': {
298
+ 'brand': 'Apple',
299
+ 'model': 'MacBook Air M2',
300
+ 'original_price': 119900,
301
+ 'age_months': 12
302
+ },
303
+ 'defects': [
304
+ {
305
+ 'id': 'KEY001',
306
+ 'name': 'Keyboard Malfunction',
307
+ 'severity_score': 6,
308
+ 'price_impact': -0.18,
309
+ 'repair_cost': 2500,
310
+ 'category': 'keyboard',
311
+ 'critical': False
312
+ },
313
+ {
314
+ 'id': 'BAT001',
315
+ 'name': 'Battery Drain',
316
+ 'severity_score': 6,
317
+ 'price_impact': -0.18,
318
+ 'repair_cost': 2500,
319
+ 'category': 'battery',
320
+ 'critical': False
321
+ }
322
+ ],
323
+ 'condition_score': 5.5
324
+ },
325
+ {
326
+ 'name': 'OnePlus 11 - Water Damage',
327
+ 'device': {
328
+ 'brand': 'OnePlus',
329
+ 'model': 'OnePlus 11',
330
+ 'original_price': 56999,
331
+ 'age_months': 6
332
+ },
333
+ 'defects': [
334
+ {
335
+ 'id': 'WTR001',
336
+ 'name': 'Water Damage',
337
+ 'severity_score': 10,
338
+ 'price_impact': -0.60,
339
+ 'repair_cost': 8000,
340
+ 'category': 'water',
341
+ 'critical': True
342
+ }
343
+ ],
344
+ 'condition_score': 2.0
345
+ }
346
+ ]
347
+
348
+ for i, test in enumerate(test_cases, 1):
349
+
350
+ device = test['device']
351
+ print(f"\nDevice: {device['brand']} {device['model']}")
352
+ print(f" Original Price: ₹{device['original_price']:,}")
353
+ print(f" Age: {device['age_months']} months ({device['age_months']/12:.1f} years)")
354
+ print(f" Defects: {len(test['defects'])}")
355
+
356
+ if test['defects']:
357
+ print(f"\n Detected Defects:")
358
+ for defect in test['defects']:
359
+ print(f" • {defect['name']} (Severity: {defect['severity_score']}/10)")
360
+
361
+ print(f"\n Condition Score: {test['condition_score']}/10")
362
+
363
+ result = predictor.predict(
364
+ test['device'],
365
+ test['defects'],
366
+ test['condition_score']
367
+ )
368
+
369
+ print(f"\n PREDICTED RESALE PRICE: ₹{result['predicted_price']:,}")
370
+ print(f" Range: ₹{result['price_range']['min']:,} - ₹{result['price_range']['max']:,}")
371
+ print(f" Confidence: {result['confidence']:.0%}")
372
+
373
+ print(f"\n Price Impact Factors:")
374
+ for factor in result['depreciation_factors']:
375
+ print(f" • {factor['factor']}: {factor['impact']}")
376
+ print(f" {factor['description']}")
377
+
378
+ print(f"\n Top Feature Contributions:")
379
+ for contrib in result['feature_contributions']:
380
+ print(f" • {contrib['feature']}: {contrib['importance']}")
381
+
382
+
383
+ if __name__ == "__main__":
384
+ import sys
385
+
386
+ if len(sys.argv) > 1 and sys.argv[1] == 'test':
387
+ test_predictions()
388
+ else:
389
+ predictor, metrics = train_model()
390
+ if predictor:
391
+ input("Press Enter to run test predictions...")
392
+ test_predictions()
requirements.txt CHANGED
@@ -1,3 +1,13 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ transformers
3
+ Pillow
4
+ xgboost
5
+ scikit-learn
6
+ pandas
7
+ numpy
8
+ streamlit
9
+ huggingface_hub
10
+ matplotlib
11
+ seaborn
12
+ joblib
13
+ fpdf2