Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- app.py +582 -0
- condition_grader.py +99 -0
- config.toml +10 -0
- defect_matcher.py +105 -0
- description_extractor.py +84 -0
- domain_validator.py +96 -0
- price_predictor.py +392 -0
- requirements.txt +13 -3
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 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|