teja141290 commited on
Commit
98bf2c9
·
1 Parent(s): dec4c30

Update: Remove damage model and add Docker configuration

Browse files
Files changed (44) hide show
  1. .gitignore +39 -0
  2. Dockerfile +29 -11
  3. app/.gitignore +4 -0
  4. app/__pycache__/main.cpython-310.pyc +0 -0
  5. app/main.py +17 -4
  6. app/routers/__pycache__/inference.cpython-310.pyc +0 -0
  7. app/routers/inference.py +118 -95
  8. app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_damage.png +0 -3
  9. app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png +0 -3
  10. app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png +0 -3
  11. app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png +0 -3
  12. app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png +0 -3
  13. app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png +0 -3
  14. app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_result.json +0 -86
  15. app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_damage.png +0 -3
  16. app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png +0 -3
  17. app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png +0 -3
  18. app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png +0 -3
  19. app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png +0 -3
  20. app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png +0 -3
  21. app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png +0 -3
  22. app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png +0 -3
  23. app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_result.json +0 -83
  24. app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_damage.png +0 -3
  25. app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png +0 -3
  26. app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png +0 -3
  27. app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png +0 -3
  28. app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png +0 -3
  29. app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png +0 -3
  30. app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg +0 -3
  31. app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg +0 -3
  32. app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg +0 -3
  33. app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg +0 -3
  34. app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg +0 -3
  35. app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg +0 -3
  36. app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg +0 -3
  37. app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg +0 -3
  38. app/static/uploads/de9bda87-c237-4c6b-8748-1ab245d4ec57.jpg +0 -0
  39. app/static/uploads/f0a7c31c-d1cf-460a-abff-d32d76faa384.jpg +0 -0
  40. app/templates/index.html +530 -14
  41. app/utils/config.py +13 -0
  42. app/utils/llm_client.py +86 -0
  43. app/utils/model_loader.py +16 -0
  44. requirements.txt +4 -1
.gitignore ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ ENV/
26
+
27
+ # IDEs
28
+ .idea/
29
+ .vscode/
30
+ *.swp
31
+ *.swo
32
+
33
+ # Project specific
34
+ tmp/
35
+ .env
36
+ *.log
37
+
38
+ # Downloaded models (they will be downloaded automatically)
39
+ /tmp/models/
Dockerfile CHANGED
@@ -2,24 +2,42 @@ FROM python:3.10-slim
2
 
3
  WORKDIR /code
4
 
5
- # Install system dependencies
6
- RUN apt-get update && apt-get install -y \
 
 
 
 
7
  build-essential \
8
  libgl1-mesa-glx \
9
  libglib2.0-0 \
10
- && rm -rf /var/lib/apt/lists/*
 
 
 
 
 
11
 
12
- # Install Python dependencies
13
  COPY requirements.txt .
14
- RUN pip install --no-cache-dir -r requirements.txt
15
 
16
- # Copy app code
 
 
 
 
 
 
 
 
 
17
  COPY . .
18
 
19
- # Expose port
20
- EXPOSE 7860
 
21
 
22
- ENV PATH="/root/.local/bin:$PATH"
23
 
24
- # Run FastAPI app with uvicorn
25
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
2
 
3
  WORKDIR /code
4
 
5
+ # Set environment variable to disable pip cache
6
+ ENV PIP_NO_CACHE_DIR=1
7
+
8
+ # Install system dependencies with cleanup
9
+ RUN apt-get update && \
10
+ apt-get install -y --no-install-recommends \
11
  build-essential \
12
  libgl1-mesa-glx \
13
  libglib2.0-0 \
14
+ git \
15
+ && apt-get clean && \
16
+ rm -rf /var/lib/apt/lists/* && \
17
+ rm -rf /tmp/* && \
18
+ mkdir -p /tmp && \
19
+ chmod 777 /tmp
20
 
21
+ # Copy requirements first
22
  COPY requirements.txt .
 
23
 
24
+ # Install dependencies with improved error handling
25
+ RUN pip install --no-cache-dir --upgrade pip && \
26
+ pip install --no-cache-dir setuptools wheel && \
27
+ pip install --no-cache-dir -r requirements.txt || (echo "pip install failed" && exit 1) && \
28
+ rm -rf ~/.cache/pip && \
29
+ rm -rf /tmp/* && \
30
+ mkdir -p /tmp && \
31
+ chmod 777 /tmp
32
+
33
+ # Copy application code
34
  COPY . .
35
 
36
+ # Create necessary directories with proper permissions
37
+ RUN mkdir -p /tmp/models/parts/weights/weights && \
38
+ chmod -R 777 /tmp
39
 
40
+ EXPOSE 7860
41
 
42
+ # Added --reload flag for development convenience
43
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--reload"]
app/.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .env
2
+ *.env
3
+ __pycache__/
4
+ *.pyc
app/__pycache__/main.cpython-310.pyc CHANGED
Binary files a/app/__pycache__/main.cpython-310.pyc and b/app/__pycache__/main.cpython-310.pyc differ
 
app/main.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  # --- Model download logic (Hugging Face Hub) ---
3
  import os
4
  import requests
@@ -15,13 +14,13 @@ def download_if_missing(url, dest):
15
  print("Download complete.")
16
 
17
  # Hugging Face direct download links
18
- DAMAGE_MODEL_URL = "https://huggingface.co/AItoolstack/car_damage_detection/resolve/main/yolov8_models/damage/weights/weights/best.pt"
19
  PARTS_MODEL_URL = "https://huggingface.co/AItoolstack/car_damage_detection/resolve/main/yolov8_models/parts/weights/weights/best.pt"
20
 
21
- DAMAGE_MODEL_PATH = os.path.join("/tmp", "models", "damage", "weights", "weights", "best.pt")
22
  PARTS_MODEL_PATH = os.path.join("/tmp", "models", "parts", "weights", "weights", "best.pt")
23
 
24
- download_if_missing(DAMAGE_MODEL_URL, DAMAGE_MODEL_PATH)
25
  download_if_missing(PARTS_MODEL_URL, PARTS_MODEL_PATH)
26
 
27
  from fastapi import FastAPI, File, UploadFile, BackgroundTasks
@@ -35,6 +34,7 @@ import aiofiles
35
  from pathlib import Path
36
  import uuid
37
  from app.routers import inference
 
38
 
39
  app = FastAPI(title="Car Damage Detection API")
40
 
@@ -54,5 +54,18 @@ templates = Jinja2Templates(directory="app/templates")
54
  # Include routers
55
  app.include_router(inference.router)
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  if __name__ == "__main__":
58
  uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
 
 
1
  # --- Model download logic (Hugging Face Hub) ---
2
  import os
3
  import requests
 
14
  print("Download complete.")
15
 
16
  # Hugging Face direct download links
17
+ # DAMAGE_MODEL_URL = "https://huggingface.co/AItoolstack/car_damage_detection/resolve/main/yolov8_models/damage/weights/weights/best.pt"
18
  PARTS_MODEL_URL = "https://huggingface.co/AItoolstack/car_damage_detection/resolve/main/yolov8_models/parts/weights/weights/best.pt"
19
 
20
+ # DAMAGE_MODEL_PATH = os.path.join("/tmp", "models", "damage", "weights", "weights", "best.pt")
21
  PARTS_MODEL_PATH = os.path.join("/tmp", "models", "parts", "weights", "weights", "best.pt")
22
 
23
+ # download_if_missing(DAMAGE_MODEL_URL, DAMAGE_MODEL_PATH)
24
  download_if_missing(PARTS_MODEL_URL, PARTS_MODEL_PATH)
25
 
26
  from fastapi import FastAPI, File, UploadFile, BackgroundTasks
 
34
  from pathlib import Path
35
  import uuid
36
  from app.routers import inference
37
+ from .utils.model_loader import PARTS_MODEL_PATH
38
 
39
  app = FastAPI(title="Car Damage Detection API")
40
 
 
54
  # Include routers
55
  app.include_router(inference.router)
56
 
57
+ # Create required directories
58
+ UPLOAD_DIR = Path("tmp/uploads")
59
+ RESULTS_DIR = Path("tmp/results")
60
+
61
+ def create_dirs():
62
+ for dir_path in [UPLOAD_DIR, RESULTS_DIR]:
63
+ dir_path.mkdir(parents=True, exist_ok=True)
64
+
65
+ # Add to startup
66
+ @app.on_event("startup")
67
+ async def startup_event():
68
+ create_dirs()
69
+
70
  if __name__ == "__main__":
71
  uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
app/routers/__pycache__/inference.cpython-310.pyc CHANGED
Binary files a/app/routers/__pycache__/inference.cpython-310.pyc and b/app/routers/__pycache__/inference.cpython-310.pyc differ
 
app/routers/inference.py CHANGED
@@ -1,5 +1,4 @@
1
-
2
- from fastapi import APIRouter, Request, UploadFile, File, Form
3
  from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
4
  from fastapi.templating import Jinja2Templates
5
  from starlette.background import BackgroundTask
@@ -13,6 +12,7 @@ import base64
13
  from ultralytics import YOLO
14
  import cv2
15
  import numpy as np
 
16
 
17
 
18
  # Templates directory
@@ -30,12 +30,15 @@ os.makedirs(RESULTS_DIR, exist_ok=True)
30
  ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "tiff", "tif"}
31
 
32
  # Model paths
33
- DAMAGE_MODEL_PATH = os.path.join("/tmp", "models", "damage", "weights", "weights", "best.pt")
34
  PARTS_MODEL_PATH = os.path.join("/tmp", "models", "parts", "weights", "weights", "best.pt")
35
 
36
  # Class names for parts
37
  PARTS_CLASS_NAMES = ['headlamp', 'front_bumper', 'hood', 'door', 'rear_bumper']
38
 
 
 
 
39
  # Helper: Run YOLO inference and return results
40
  def run_yolo_inference(model_path, image_path, task='segment'):
41
  model = YOLO(model_path)
@@ -72,12 +75,16 @@ def draw_masks_and_conf(image_path, yolo_result, class_names=None):
72
  # Helper: Generate JSON output
73
  def generate_json_output(filename, damage_result, parts_result):
74
  # Damage severity: use max confidence
75
- severity_score = float(max([float(box.conf[0]) for box in damage_result.boxes], default=0))
76
- damage_regions = []
77
- for box in damage_result.boxes:
78
- x1, y1, x2, y2 = map(float, box.xyxy[0])
79
- conf = float(box.conf[0])
80
- damage_regions.append({"bbox": [x1, y1, x2, y2], "confidence": conf})
 
 
 
 
81
  # Parts
82
  parts = []
83
  for i, box in enumerate(parts_result.boxes):
@@ -129,96 +136,112 @@ def login_page(request: Request):
129
  return templates.TemplateResponse("login.html", {"request": request})
130
 
131
  @router.post("/upload", response_class=HTMLResponse)
132
- def upload_image(request: Request, file: UploadFile = File(...)):
133
- ext = file.filename.split(".")[-1].lower()
134
- if ext not in ALLOWED_EXTENSIONS:
135
- return templates.TemplateResponse("index.html", {"request": request, "error": "Unsupported file type."})
136
-
137
- # Save uploaded file
138
- session_id = str(uuid.uuid4())
139
- upload_path = os.path.join(UPLOAD_DIR, f"{session_id}.{ext}")
140
- with open(upload_path, "wb") as buffer:
141
- shutil.copyfileobj(file.file, buffer)
142
 
 
 
 
 
 
 
 
 
143
 
144
- # Run both inferences
145
- try:
146
- damage_result = run_yolo_inference(DAMAGE_MODEL_PATH, upload_path)
147
- parts_result = run_yolo_inference(PARTS_MODEL_PATH, upload_path)
148
-
149
- # Save annotated images
150
- damage_img_path = os.path.join(RESULTS_DIR, f"{session_id}_damage.png")
151
- parts_img_path = os.path.join(RESULTS_DIR, f"{session_id}_parts.png")
152
- json_path = os.path.join(RESULTS_DIR, f"{session_id}_result.json")
153
- # Use custom download endpoints for Hugging Face Spaces
154
- damage_img_url = f"/download/result/{session_id}_damage.png"
155
- parts_img_url = f"/download/result/{session_id}_parts.png"
156
- json_url = f"/download/result/{session_id}_result.json"
157
-
158
- # Defensive: set to None by default
159
- damage_img = None
160
- parts_img = None
161
- json_output = None
162
-
163
- # Only save and set if inference returns boxes
164
- if hasattr(damage_result, 'boxes') and len(damage_result.boxes) > 0:
165
- damage_img = draw_masks_and_conf(upload_path, damage_result)
166
- cv2.imwrite(damage_img_path, damage_img)
167
- if hasattr(parts_result, 'boxes') and len(parts_result.boxes) > 0:
168
- parts_img = draw_masks_and_conf(upload_path, parts_result, class_names=PARTS_CLASS_NAMES)
169
- cv2.imwrite(parts_img_path, parts_img)
170
- if (hasattr(damage_result, 'boxes') and len(damage_result.boxes) > 0) or (hasattr(parts_result, 'boxes') and len(parts_result.boxes) > 0):
171
- json_output = generate_json_output(file.filename, damage_result, parts_result)
172
- with open(json_path, "w") as jf:
173
- json.dump(json_output, jf, indent=2)
174
-
175
- # Prepare URLs for download (only if files exist)
176
- result = {
177
- "filename": file.filename,
178
- "damage_image": damage_img_url if damage_img is not None else None,
179
- "parts_image": parts_img_url if parts_img is not None else None,
180
- "json": json_output,
181
- "json_download": json_url if json_output is not None else None
182
- }
183
- # Debug log
184
- print("[DEBUG] Result dict:", result)
185
- except Exception as e:
186
- result = {
187
- "filename": file.filename,
188
- "error": f"Inference failed: {str(e)}",
189
- "damage_image": None,
190
- "parts_image": None,
191
- "json": None,
192
- "json_download": None
193
- }
194
- print("[ERROR] Inference failed:", e)
195
-
196
- import threading
197
- import time
198
- def delayed_cleanup():
199
- time.sleep(300) # 5 minutes
200
  try:
201
- os.remove(upload_path)
202
- except Exception:
203
- pass
204
- for suffix in ["_damage.png", "_parts.png", "_result.json"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  try:
206
- os.remove(os.path.join(RESULTS_DIR, f"{session_id}{suffix}"))
207
- except Exception:
208
- pass
209
-
210
- threading.Thread(target=delayed_cleanup, daemon=True).start()
211
-
212
- return templates.TemplateResponse(
213
- "index.html",
214
- {
215
- "request": request,
216
- "result": result,
217
- "original_image": f"/download/upload/{session_id}.{ext}"
218
- }
219
- )
220
-
221
- # --- Add routes to serve files from /tmp/uploads and /tmp/results ---
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  @router.get("/download/upload/{filename}")
223
  def download_uploaded_file(filename: str):
224
  file_path = os.path.join(UPLOAD_DIR, filename)
 
1
+ from fastapi import APIRouter, Request, UploadFile, File, Form, HTTPException
 
2
  from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
3
  from fastapi.templating import Jinja2Templates
4
  from starlette.background import BackgroundTask
 
12
  from ultralytics import YOLO
13
  import cv2
14
  import numpy as np
15
+ from ..utils.llm_client import GroqAnalyzer
16
 
17
 
18
  # Templates directory
 
30
  ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "tiff", "tif"}
31
 
32
  # Model paths
33
+ # DAMAGE_MODEL_PATH = os.path.join("/tmp", "models", "damage", "weights", "weights", "best.pt") # Commented for now
34
  PARTS_MODEL_PATH = os.path.join("/tmp", "models", "parts", "weights", "weights", "best.pt")
35
 
36
  # Class names for parts
37
  PARTS_CLASS_NAMES = ['headlamp', 'front_bumper', 'hood', 'door', 'rear_bumper']
38
 
39
+ # Initialize GroqAnalyzer
40
+ groq_analyzer = GroqAnalyzer()
41
+
42
  # Helper: Run YOLO inference and return results
43
  def run_yolo_inference(model_path, image_path, task='segment'):
44
  model = YOLO(model_path)
 
75
  # Helper: Generate JSON output
76
  def generate_json_output(filename, damage_result, parts_result):
77
  # Damage severity: use max confidence
78
+ if damage_result is not None and hasattr(damage_result, 'boxes'):
79
+ severity_score = float(max([float(box.conf[0]) for box in damage_result.boxes], default=0))
80
+ damage_regions = []
81
+ for box in damage_result.boxes:
82
+ x1, y1, x2, y2 = map(float, box.xyxy[0])
83
+ conf = float(box.conf[0])
84
+ damage_regions.append({"bbox": [x1, y1, x2, y2], "confidence": conf})
85
+ else:
86
+ severity_score = 0
87
+ damage_regions = []
88
  # Parts
89
  parts = []
90
  for i, box in enumerate(parts_result.boxes):
 
136
  return templates.TemplateResponse("login.html", {"request": request})
137
 
138
  @router.post("/upload", response_class=HTMLResponse)
139
+ async def upload_image(request: Request, file: UploadFile = File(...)):
140
+ try:
141
+ ext = file.filename.split(".")[-1].lower()
142
+ print(f"[DEBUG] Uploaded file extension: {ext}")
143
+ if ext not in ALLOWED_EXTENSIONS:
144
+ print(f"[DEBUG] Unsupported file type: {ext}")
145
+ return templates.TemplateResponse("index.html", {"request": request, "error": "Unsupported file type."})
 
 
 
146
 
147
+ # Save uploaded file
148
+ session_id = str(uuid.uuid4())
149
+ upload_filename = f"{session_id}_{file.filename}"
150
+ upload_path = os.path.join(UPLOAD_DIR, upload_filename)
151
+ print(f"[DEBUG] Saving uploaded file to: {upload_path}")
152
+ with open(upload_path, "wb") as buffer:
153
+ shutil.copyfileobj(file.file, buffer)
154
+ print(f"[DEBUG] File saved. Running inference...")
155
 
156
+ warning = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  try:
158
+ damage_result = None # Not used
159
+ parts_result = run_yolo_inference(PARTS_MODEL_PATH, upload_path)
160
+ print(f"[DEBUG] YOLO inference result: {parts_result}")
161
+
162
+ parts_img = None
163
+ json_output = None
164
+ parts_img_url = None
165
+ json_url = None
166
+
167
+ if hasattr(parts_result, 'boxes') and len(parts_result.boxes) > 0:
168
+ print(f"[DEBUG] Detected {len(parts_result.boxes)} parts.")
169
+ parts_img = draw_masks_and_conf(upload_path, parts_result, class_names=PARTS_CLASS_NAMES)
170
+ parts_img_filename = f"{session_id}_parts.png"
171
+ parts_img_path = os.path.join(RESULTS_DIR, parts_img_filename)
172
+ cv2.imwrite(parts_img_path, parts_img)
173
+ print(f"[DEBUG] Parts image saved to: {parts_img_path}")
174
+ parts_img_url = f"/download/result/{parts_img_filename}"
175
+
176
+ json_output = generate_json_output(file.filename, damage_result, parts_result)
177
+ json_filename = f"{session_id}_result.json"
178
+ json_path = os.path.join(RESULTS_DIR, json_filename)
179
+ with open(json_path, "w") as jf:
180
+ json.dump(json_output, jf, indent=2)
181
+ print(f"[DEBUG] JSON output saved to: {json_path}")
182
+ json_url = f"/download/result/{json_filename}"
183
+ else:
184
+ warning = "No parts detected in the image."
185
+ print("[DEBUG] No parts detected.")
186
+
187
+ llm_analysis = groq_analyzer.analyze_damage(upload_path)
188
+ print(f"[DEBUG] LLM analysis output: {llm_analysis}")
189
+
190
+ result = {
191
+ "filename": file.filename,
192
+ "parts_image": parts_img_url,
193
+ "json": json_output,
194
+ "json_download": json_url,
195
+ "llm_analysis": llm_analysis,
196
+ "warning": warning
197
+ }
198
+ print("[DEBUG] Result dict:", result)
199
+ except Exception as e:
200
+ result = {
201
+ "filename": file.filename,
202
+ "error": f"Inference failed: {str(e)}",
203
+ "parts_image": None,
204
+ "json": None,
205
+ "json_download": None,
206
+ "llm_analysis": None,
207
+ "warning": None
208
+ }
209
+ print("[ERROR] Inference failed:", e)
210
+
211
+ import threading
212
+ import time
213
+ def delayed_cleanup():
214
+ time.sleep(300) # 5 minutes
215
  try:
216
+ os.remove(upload_path)
217
+ print(f"[DEBUG] Cleaned up upload: {upload_path}")
218
+ except Exception as ce:
219
+ print(f"[DEBUG] Cleanup error (upload): {ce}")
220
+ for suffix in ["_parts.png", "_result.json"]:
221
+ try:
222
+ os.remove(os.path.join(RESULTS_DIR, f"{session_id}{suffix}"))
223
+ print(f"[DEBUG] Cleaned up result: {os.path.join(RESULTS_DIR, f'{session_id}{suffix}')}" )
224
+ except Exception as ce:
225
+ print(f"[DEBUG] Cleanup error (result): {ce}")
226
+
227
+ threading.Thread(target=delayed_cleanup, daemon=True).start()
228
+
229
+ return templates.TemplateResponse(
230
+ "index.html",
231
+ {
232
+ "request": request,
233
+ "result": result,
234
+ "original_image": f"/download/upload/{upload_filename}"
235
+ }
236
+ )
237
+ except Exception as e:
238
+ print(f"[ERROR] Inference failed: {str(e)}")
239
+ return templates.TemplateResponse(
240
+ "index.html",
241
+ {"request": request, "error": f"Error processing image: {str(e)}"}
242
+ )
243
+
244
+ # --- Serve files from /tmp/uploads and /tmp/results ---
245
  @router.get("/download/upload/{filename}")
246
  def download_uploaded_file(filename: str):
247
  file_path = os.path.join(UPLOAD_DIR, filename)
app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_damage.png DELETED

Git LFS Details

  • SHA256: 843234317ec8027fee1329fb191590a6523e6645fafa0717f71b267e6d97edf1
  • Pointer size: 132 Bytes
  • Size of remote file: 1.37 MB
app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png DELETED

Git LFS Details

  • SHA256: 7736748ab74a20eb2049e04494b66dc453b0b49d4cd41b3a7990447e6aee37d4
  • Pointer size: 132 Bytes
  • Size of remote file: 1.29 MB
app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png DELETED

Git LFS Details

  • SHA256: 843234317ec8027fee1329fb191590a6523e6645fafa0717f71b267e6d97edf1
  • Pointer size: 132 Bytes
  • Size of remote file: 1.37 MB
app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png DELETED

Git LFS Details

  • SHA256: 7736748ab74a20eb2049e04494b66dc453b0b49d4cd41b3a7990447e6aee37d4
  • Pointer size: 132 Bytes
  • Size of remote file: 1.29 MB
app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png DELETED

Git LFS Details

  • SHA256: 2a3fa666110fa57240a3e356a4bbe258b03cde0d2027db12158ef65775504e57
  • Pointer size: 132 Bytes
  • Size of remote file: 1.44 MB
app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png DELETED

Git LFS Details

  • SHA256: 8bdcf896c24e952df34be4247e25dd2ab2f306cfb78f2d5d3bcd1a22c52cb3b6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.41 MB
app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_result.json DELETED
@@ -1,86 +0,0 @@
1
- {
2
- "filename": "28.jpg",
3
- "damage": {
4
- "severity_score": 0.7707259058952332,
5
- "regions": [
6
- {
7
- "bbox": [
8
- 522.2720947265625,
9
- 530.5602416992188,
10
- 728.9972534179688,
11
- 892.2079467773438
12
- ],
13
- "confidence": 0.7707259058952332
14
- },
15
- {
16
- "bbox": [
17
- 660.1974487304688,
18
- 512.72119140625,
19
- 927.4169921875,
20
- 883.8523559570312
21
- ],
22
- "confidence": 0.6494596600532532
23
- },
24
- {
25
- "bbox": [
26
- 196.1370086669922,
27
- 277.27374267578125,
28
- 366.7127990722656,
29
- 712.44775390625
30
- ],
31
- "confidence": 0.32863757014274597
32
- }
33
- ]
34
- },
35
- "parts": [
36
- {
37
- "part": "hood",
38
- "damaged": true,
39
- "confidence": 0.9392628073692322,
40
- "damage_percentage": 0.20169149219339333,
41
- "bbox": [
42
- 411.4404296875,
43
- 342.9278869628906,
44
- 952.9636840820312,
45
- 578.835693359375
46
- ]
47
- },
48
- {
49
- "part": "headlamp",
50
- "damaged": true,
51
- "confidence": 0.9351847171783447,
52
- "damage_percentage": 1.7647002993974346,
53
- "bbox": [
54
- 531.9706420898438,
55
- 553.8896484375,
56
- 706.9000854492188,
57
- 656.3943481445312
58
- ]
59
- },
60
- {
61
- "part": "door",
62
- "damaged": true,
63
- "confidence": 0.9011728763580322,
64
- "damage_percentage": 0.29901854346557094,
65
- "bbox": [
66
- 143.66470336914062,
67
- 224.3675537109375,
68
- 391.3798522949219,
69
- 737.7504272460938
70
- ]
71
- },
72
- {
73
- "part": "front_bumper",
74
- "damaged": true,
75
- "confidence": 0.8590587377548218,
76
- "damage_percentage": 0.03409931158993679,
77
- "bbox": [
78
- 461.2880859375,
79
- 534.1759033203125,
80
- 978.0007934570312,
81
- 874.1959838867188
82
- ]
83
- }
84
- ],
85
- "cost_estimate": null
86
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_damage.png DELETED

Git LFS Details

  • SHA256: 864160d73f14a642d0c79c1d5583bc7bbd5783b7fd1a7651cb10b6b404bbc656
  • Pointer size: 132 Bytes
  • Size of remote file: 1.37 MB
app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png DELETED

Git LFS Details

  • SHA256: 434038d40e7c0ea3b9ad891c20f4909537d66bd728dc831dce8fe805390c4669
  • Pointer size: 132 Bytes
  • Size of remote file: 1.33 MB
app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png DELETED

Git LFS Details

  • SHA256: 843234317ec8027fee1329fb191590a6523e6645fafa0717f71b267e6d97edf1
  • Pointer size: 132 Bytes
  • Size of remote file: 1.37 MB
app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png DELETED

Git LFS Details

  • SHA256: 7736748ab74a20eb2049e04494b66dc453b0b49d4cd41b3a7990447e6aee37d4
  • Pointer size: 132 Bytes
  • Size of remote file: 1.29 MB
app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png DELETED

Git LFS Details

  • SHA256: 864160d73f14a642d0c79c1d5583bc7bbd5783b7fd1a7651cb10b6b404bbc656
  • Pointer size: 132 Bytes
  • Size of remote file: 1.37 MB
app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png DELETED

Git LFS Details

  • SHA256: 434038d40e7c0ea3b9ad891c20f4909537d66bd728dc831dce8fe805390c4669
  • Pointer size: 132 Bytes
  • Size of remote file: 1.33 MB
app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png DELETED

Git LFS Details

  • SHA256: 11364615cb173dfae5739657f8ecca4f1b550e82a8565a8347b52c20fed3c134
  • Pointer size: 132 Bytes
  • Size of remote file: 1.43 MB
app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png DELETED

Git LFS Details

  • SHA256: 9076be5837c8bc5b361a12ada76a5f5088ddab7b5bbed8812e7cf8f6108199b1
  • Pointer size: 132 Bytes
  • Size of remote file: 1.41 MB
app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_result.json DELETED
@@ -1,83 +0,0 @@
1
- {
2
- "filename": "72.jpg",
3
- "damage": {
4
- "severity_score": 0.6467820405960083,
5
- "regions": [
6
- {
7
- "bbox": [
8
- 20.340744018554688,
9
- 93.4926986694336,
10
- 112.2962646484375,
11
- 271.3760986328125
12
- ],
13
- "confidence": 0.6467820405960083
14
- },
15
- {
16
- "bbox": [
17
- 129.6874237060547,
18
- 107.35161590576172,
19
- 238.290771484375,
20
- 377.91192626953125
21
- ],
22
- "confidence": 0.43937909603118896
23
- },
24
- {
25
- "bbox": [
26
- 127.2110595703125,
27
- 23.03559684753418,
28
- 353.25946044921875,
29
- 420.0022888183594
30
- ],
31
- "confidence": 0.3795366883277893
32
- },
33
- {
34
- "bbox": [
35
- 126.8061752319336,
36
- 38.61054611206055,
37
- 242.341064453125,
38
- 433.29034423828125
39
- ],
40
- "confidence": 0.3542208969593048
41
- }
42
- ]
43
- },
44
- "parts": [
45
- {
46
- "part": "hood",
47
- "damaged": true,
48
- "confidence": 0.746218204498291,
49
- "damage_percentage": 0.2284878057871993,
50
- "bbox": [
51
- 1.409912109375,
52
- 666.6517944335938,
53
- 200.2979278564453,
54
- 1020.8939819335938
55
- ]
56
- },
57
- {
58
- "part": "hood",
59
- "damaged": true,
60
- "confidence": 0.5614964962005615,
61
- "damage_percentage": 0.04930226900457474,
62
- "bbox": [
63
- 265.2085876464844,
64
- 582.6806030273438,
65
- 1017.8049926757812,
66
- 1016.5338745117188
67
- ]
68
- },
69
- {
70
- "part": "hood",
71
- "damaged": true,
72
- "confidence": 0.5332340598106384,
73
- "damage_percentage": 0.25070920790460405,
74
- "bbox": [
75
- 0.0,
76
- 501.2841796875,
77
- 358.5841369628906,
78
- 680.34912109375
79
- ]
80
- }
81
- ],
82
- "cost_estimate": null
83
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_damage.png DELETED

Git LFS Details

  • SHA256: 843234317ec8027fee1329fb191590a6523e6645fafa0717f71b267e6d97edf1
  • Pointer size: 132 Bytes
  • Size of remote file: 1.37 MB
app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png DELETED

Git LFS Details

  • SHA256: 7736748ab74a20eb2049e04494b66dc453b0b49d4cd41b3a7990447e6aee37d4
  • Pointer size: 132 Bytes
  • Size of remote file: 1.29 MB
app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png DELETED

Git LFS Details

  • SHA256: 3ce4e1711a1354f0bc289fa81c2fca85f95ba8ab3c97b85dda4977405d7aaadc
  • Pointer size: 132 Bytes
  • Size of remote file: 1.32 MB
app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png DELETED

Git LFS Details

  • SHA256: 5e430bf68e1de01b04208fd84d049e8bebf1199d5f2caa3ae0bebdb2be2938e8
  • Pointer size: 132 Bytes
  • Size of remote file: 1.18 MB
app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png DELETED

Git LFS Details

  • SHA256: a2257fb2db1401111cc2c0d08d0a16ed7d5796983155660c33ea8c895174fbc0
  • Pointer size: 131 Bytes
  • Size of remote file: 625 kB
app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png DELETED

Git LFS Details

  • SHA256: 2193c3f71bc942286682aebacc32ac19458f2431f5347a5d947472fa55b9e0a6
  • Pointer size: 131 Bytes
  • Size of remote file: 629 kB
app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg DELETED

Git LFS Details

  • SHA256: 092aba449d902c7e88a7cd2b4b588f584dbb71ec46d3f1ae6610caced7da72a6
  • Pointer size: 131 Bytes
  • Size of remote file: 111 kB
app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg DELETED

Git LFS Details

  • SHA256: 092aba449d902c7e88a7cd2b4b588f584dbb71ec46d3f1ae6610caced7da72a6
  • Pointer size: 131 Bytes
  • Size of remote file: 111 kB
app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg DELETED

Git LFS Details

  • SHA256: 9727abba3fdd97eeb17e763d77479943b88f9c6d72185d919ae852b5f80e8837
  • Pointer size: 131 Bytes
  • Size of remote file: 105 kB
app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg DELETED

Git LFS Details

  • SHA256: dd09285677be22be4bc70a81ab947db3745875c585b6a9339ad82e07dd733c74
  • Pointer size: 131 Bytes
  • Size of remote file: 107 kB
app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg DELETED

Git LFS Details

  • SHA256: 092aba449d902c7e88a7cd2b4b588f584dbb71ec46d3f1ae6610caced7da72a6
  • Pointer size: 131 Bytes
  • Size of remote file: 111 kB
app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg DELETED

Git LFS Details

  • SHA256: dd09285677be22be4bc70a81ab947db3745875c585b6a9339ad82e07dd733c74
  • Pointer size: 131 Bytes
  • Size of remote file: 107 kB
app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg DELETED

Git LFS Details

  • SHA256: 30936869771053b22752ec10a1c339e40e43061291d8dce60586093dbb87a692
  • Pointer size: 131 Bytes
  • Size of remote file: 116 kB
app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg DELETED

Git LFS Details

  • SHA256: 092aba449d902c7e88a7cd2b4b588f584dbb71ec46d3f1ae6610caced7da72a6
  • Pointer size: 131 Bytes
  • Size of remote file: 111 kB
app/static/uploads/de9bda87-c237-4c6b-8748-1ab245d4ec57.jpg DELETED
Binary file (98.7 kB)
 
app/static/uploads/f0a7c31c-d1cf-460a-abff-d32d76faa384.jpg DELETED
Binary file (75.1 kB)
 
app/templates/index.html CHANGED
@@ -23,7 +23,249 @@
23
  btn.innerText = 'Show JSON';
24
  }
25
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  </head>
28
  <body>
29
  <div class="header" style="position:relative;">
@@ -44,34 +286,308 @@
44
  <div class="result-label">Original</div>
45
  <img src="{{ original_image }}" alt="Original Image" class="result-img">
46
  </div>
47
- {% if result.damage_image is defined and result.damage_image %}
48
- <div>
49
- <div class="result-label">Damage Prediction</div>
50
- <img src="{{ result.damage_image }}" alt="Damage Prediction" class="result-img">
51
- </div>
52
- {% endif %}
53
- {% if result.parts_image is defined and result.parts_image %}
54
  <div>
55
  <div class="result-label">Parts Prediction</div>
56
  <img src="{{ result.parts_image }}" alt="Parts Prediction" class="result-img">
57
  </div>
58
  {% endif %}
59
  </div>
60
- {% if result.json is defined and result.json %}
 
 
 
61
  <h4 style="margin-bottom:8px;">JSON Output</h4>
62
- <button class="copy-btn" onclick="copyJSON()" type="button">Copy JSON</button>
63
- <button class="copy-btn" id="toggle-json-btn" onclick="toggleJSON()" type="button" style="margin-left:10px;">Show JSON</button>
64
  <div id="json-block" style="display:none;">
65
  <pre class="json-output" id="json-pre">{{ result.json | tojson(indent=2) }}</pre>
66
  </div>
67
  <a class="download-btn" href="{{ result.json_download }}" download>Download JSON</a>
68
  {% endif %}
69
- {% if result.damage_image is defined and result.damage_image %}
70
- <a class="download-btn" href="{{ result.damage_image }}" download>Download Damage Image</a>
71
- {% endif %}
72
- {% if result.parts_image is defined and result.parts_image %}
73
  <a class="download-btn" href="{{ result.parts_image }}" download>Download Parts Image</a>
74
  {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  </div>
76
  {% endif %}
77
  </div>
 
23
  btn.innerText = 'Show JSON';
24
  }
25
  }
26
+
27
+ // Add new toggle function for LLM analysis
28
+ function toggleLLM() {
29
+ const block = document.getElementById('llm-block');
30
+ const btn = document.getElementById('toggle-llm-btn');
31
+ if (block.style.display === 'none') {
32
+ block.style.display = 'block';
33
+ btn.innerText = 'Hide Analysis';
34
+ } else {
35
+ block.style.display = 'none';
36
+ btn.innerText = 'Show Analysis';
37
+ }
38
+ }
39
+
40
+ function toggleBlock(blockId, btnId) {
41
+ const block = document.getElementById(blockId);
42
+ const btn = document.getElementById(btnId);
43
+ if (block.style.display === 'none' || !block.style.display) {
44
+ block.style.display = 'block';
45
+ btn.innerText = `Hide ${btnId.includes('json') ? 'JSON' : 'Analysis'}`;
46
+ } else {
47
+ block.style.display = 'none';
48
+ btn.innerText = `Show ${btnId.includes('json') ? 'JSON' : 'Analysis'}`;
49
+ }
50
+ }
51
  </script>
52
+ <style>
53
+ /* Add new LLM analysis styles */
54
+ .llm-section {
55
+ margin-top: 20px;
56
+ padding: 15px;
57
+ background: #f8f9fa;
58
+ border-radius: 8px;
59
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
60
+ }
61
+
62
+ .llm-content {
63
+ width: 100%;
64
+ max-width: 800px;
65
+ margin: 10px auto;
66
+ display: none;
67
+ }
68
+
69
+ .llm-content pre {
70
+ white-space: pre-wrap;
71
+ word-wrap: break-word;
72
+ background: white;
73
+ padding: 15px;
74
+ border-radius: 4px;
75
+ border: 1px solid #dee2e6;
76
+ }
77
+
78
+ .toggle-llm-btn {
79
+ background-color: #007bff;
80
+ color: white;
81
+ border: none;
82
+ padding: 8px 16px;
83
+ border-radius: 4px;
84
+ cursor: pointer;
85
+ margin: 10px 0;
86
+ }
87
+
88
+ .toggle-llm-btn:hover {
89
+ background-color: #0056b3;
90
+ }
91
+
92
+ .warning {
93
+ background-color: #fff3cd;
94
+ color: #856404;
95
+ padding: 1rem;
96
+ margin: 1rem 0;
97
+ border: 1px solid #ffeeba;
98
+ border-radius: 4px;
99
+ }
100
+ .llm-analysis {
101
+ margin-top: 2rem;
102
+ background: white; /* White background */
103
+ border-radius: 12px;
104
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
105
+ overflow: hidden;
106
+ transition: all 0.3s ease;
107
+ }
108
+
109
+ .analysis-header {
110
+ display: flex;
111
+ justify-content: space-between;
112
+ align-items: center;
113
+ padding: 1.5rem;
114
+ background: linear-gradient(135deg, #805ad5 0%, #6b46c1 100%); /* Keep purple gradient header */
115
+ color: white;
116
+ }
117
+
118
+ .analysis-header h3 {
119
+ margin: 0;
120
+ font-size: 1.5rem;
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 10px;
124
+ color: white;
125
+ }
126
+
127
+ .toggle-btn {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 8px;
131
+ background: rgba(255, 255, 255, 0.15);
132
+ color: white;
133
+ border: 1px solid rgba(255, 255, 255, 0.2);
134
+ padding: 8px 16px;
135
+ border-radius: 6px;
136
+ cursor: pointer;
137
+ transition: all 0.2s ease;
138
+ }
139
+
140
+ .toggle-btn:hover {
141
+ background: rgba(255, 255, 255, 0.25);
142
+ }
143
+
144
+ .toggle-btn i {
145
+ transition: transform 0.3s ease;
146
+ }
147
+
148
+ .toggle-btn.active i {
149
+ transform: rotate(180deg);
150
+ }
151
+
152
+ .analysis-content {
153
+ padding: 1.5rem;
154
+ background: white;
155
+ color: #2d3748; /* Dark gray text */
156
+ }
157
+
158
+ .damage-grid {
159
+ display: grid;
160
+ gap: 1.5rem;
161
+ grid-template-columns: 1fr;
162
+ }
163
+
164
+ @media (min-width: 1024px) {
165
+ .damage-grid {
166
+ grid-template-columns: 3fr 2fr;
167
+ }
168
+ }
169
+
170
+ .table-container {
171
+ overflow-x: auto;
172
+ background: white;
173
+ border-radius: 8px;
174
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
175
+ }
176
+
177
+ .damage-table {
178
+ width: 100%;
179
+ border-collapse: collapse;
180
+ margin: 0;
181
+ font-size: 0.95rem;
182
+ color: #2d3748;
183
+ }
184
+
185
+ .damage-table th, .damage-table td {
186
+ padding: 12px 16px;
187
+ text-align: left;
188
+ border-bottom: 1px solid #e2e8f0;
189
+ }
190
+
191
+ .damage-table th {
192
+ background: #f7fafc;
193
+ font-weight: 600;
194
+ color: #4a5568;
195
+ text-transform: uppercase;
196
+ font-size: 0.85rem;
197
+ letter-spacing: 0.05em;
198
+ }
199
+
200
+ .damage-table tr:last-child td {
201
+ border-bottom: none;
202
+ }
203
+
204
+ .damage-table tbody tr {
205
+ transition: background-color 0.2s ease;
206
+ }
207
+
208
+ .damage-table tbody tr:hover {
209
+ background-color: #f7fafc;
210
+ }
211
+
212
+ .summary-section {
213
+ background: white;
214
+ border-radius: 8px;
215
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
216
+ padding: 1.5rem;
217
+ }
218
+
219
+ .summary-header {
220
+ display: flex;
221
+ align-items: center;
222
+ gap: 10px;
223
+ margin-bottom: 1rem;
224
+ color: #4a5568;
225
+ }
226
+
227
+ .summary-header h4 {
228
+ margin: 0;
229
+ font-size: 1.2rem;
230
+ color: #2d3748;
231
+ }
232
+
233
+ .summary-content {
234
+ color: #4a5568;
235
+ line-height: 1.6;
236
+ }
237
+
238
+ .summary-content p {
239
+ margin: 0 0 1rem;
240
+ }
241
+
242
+ .summary-content p:last-child {
243
+ margin-bottom: 0;
244
+ }
245
+
246
+ /* Updated Severity colors for light background */
247
+ .severity-high {
248
+ color: #e53e3e; /* Red */
249
+ }
250
+
251
+ .severity-medium {
252
+ color: #d69e2e; /* Yellow */
253
+ }
254
+
255
+ .severity-low {
256
+ color: #38a169; /* Green */
257
+ }
258
+
259
+ /* Animation for content */
260
+ @keyframes fadeIn {
261
+ from { opacity: 0; transform: translateY(-10px); }
262
+ to { opacity: 1; transform: translateY(0); }
263
+ }
264
+
265
+ .analysis-content.show {
266
+ animation: fadeIn 0.3s ease-out forwards;
267
+ }
268
+ </style>
269
  </head>
270
  <body>
271
  <div class="header" style="position:relative;">
 
286
  <div class="result-label">Original</div>
287
  <img src="{{ original_image }}" alt="Original Image" class="result-img">
288
  </div>
289
+ {% if result.parts_image %}
 
 
 
 
 
 
290
  <div>
291
  <div class="result-label">Parts Prediction</div>
292
  <img src="{{ result.parts_image }}" alt="Parts Prediction" class="result-img">
293
  </div>
294
  {% endif %}
295
  </div>
296
+ {% if result.warning %}
297
+ <div class="warning">{{ result.warning }}</div>
298
+ {% endif %}
299
+ {% if result.json %}
300
  <h4 style="margin-bottom:8px;">JSON Output</h4>
301
+ <button class="copy-btn" onclick="copyJSON()">Copy JSON</button>
302
+ <button class="toggle-btn" id="toggle-json-btn" onclick="toggleBlock('json-block', 'toggle-json-btn')">Show JSON</button>
303
  <div id="json-block" style="display:none;">
304
  <pre class="json-output" id="json-pre">{{ result.json | tojson(indent=2) }}</pre>
305
  </div>
306
  <a class="download-btn" href="{{ result.json_download }}" download>Download JSON</a>
307
  {% endif %}
308
+ {% if result.parts_image %}
 
 
 
309
  <a class="download-btn" href="{{ result.parts_image }}" download>Download Parts Image</a>
310
  {% endif %}
311
+ {% if result.llm_analysis %}
312
+ <div class="llm-analysis">
313
+ <div class="analysis-header">
314
+ <h3><i class="fas fa-robot"></i> AI Damage Assessment Report</h3>
315
+ <button class="toggle-btn" id="toggle-analysis-btn" onclick="toggleBlock('analysis-block', 'toggle-analysis-btn')">
316
+ <span class="btn-text">Show Analysis</span>
317
+ <i class="fas fa-chevron-down"></i>
318
+ </button>
319
+ </div>
320
+ <div id="analysis-block" class="analysis-content" style="display:none;">
321
+ <div class="damage-grid">
322
+ <div class="table-container">
323
+ <table class="damage-table">
324
+ <thead>
325
+ <tr>
326
+ <th>Component</th>
327
+ <th>Status</th>
328
+ <th>Severity</th>
329
+ <th>Action Needed</th>
330
+ </tr>
331
+ </thead>
332
+ <tbody id="damageTbody">
333
+ </tbody>
334
+ </table>
335
+ </div>
336
+ <div class="summary-section">
337
+ <div class="summary-header">
338
+ <i class="fas fa-clipboard-list"></i>
339
+ <h4>Assessment Summary</h4>
340
+ </div>
341
+ <div id="summaryText" class="summary-content"></div>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
348
+
349
+ <style>
350
+ .llm-analysis {
351
+ margin-top: 2rem;
352
+ background: white; /* White background */
353
+ border-radius: 12px;
354
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
355
+ overflow: hidden;
356
+ transition: all 0.3s ease;
357
+ }
358
+
359
+ .analysis-header {
360
+ display: flex;
361
+ justify-content: space-between;
362
+ align-items: center;
363
+ padding: 1.5rem;
364
+ background: linear-gradient(135deg, #805ad5 0%, #6b46c1 100%); /* Keep purple gradient header */
365
+ color: white;
366
+ }
367
+
368
+ .analysis-header h3 {
369
+ margin: 0;
370
+ font-size: 1.5rem;
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 10px;
374
+ color: white;
375
+ }
376
+
377
+ .toggle-btn {
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 8px;
381
+ background: rgba(255, 255, 255, 0.15);
382
+ color: white;
383
+ border: 1px solid rgba(255, 255, 255, 0.2);
384
+ padding: 8px 16px;
385
+ border-radius: 6px;
386
+ cursor: pointer;
387
+ transition: all 0.2s ease;
388
+ }
389
+
390
+ .toggle-btn:hover {
391
+ background: rgba(255, 255, 255, 0.25);
392
+ }
393
+
394
+ .toggle-btn i {
395
+ transition: transform 0.3s ease;
396
+ }
397
+
398
+ .toggle-btn.active i {
399
+ transform: rotate(180deg);
400
+ }
401
+
402
+ .analysis-content {
403
+ padding: 1.5rem;
404
+ background: white;
405
+ color: #2d3748; /* Dark gray text */
406
+ }
407
+
408
+ .damage-grid {
409
+ display: grid;
410
+ gap: 1.5rem;
411
+ grid-template-columns: 1fr;
412
+ }
413
+
414
+ @media (min-width: 1024px) {
415
+ .damage-grid {
416
+ grid-template-columns: 3fr 2fr;
417
+ }
418
+ }
419
+
420
+ .table-container {
421
+ overflow-x: auto;
422
+ background: white;
423
+ border-radius: 8px;
424
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
425
+ }
426
+
427
+ .damage-table {
428
+ width: 100%;
429
+ border-collapse: collapse;
430
+ margin: 0;
431
+ font-size: 0.95rem;
432
+ color: #2d3748;
433
+ }
434
+
435
+ .damage-table th, .damage-table td {
436
+ padding: 12px 16px;
437
+ text-align: left;
438
+ border-bottom: 1px solid #e2e8f0;
439
+ }
440
+
441
+ .damage-table th {
442
+ background: #f7fafc;
443
+ font-weight: 600;
444
+ color: #4a5568;
445
+ text-transform: uppercase;
446
+ font-size: 0.85rem;
447
+ letter-spacing: 0.05em;
448
+ }
449
+
450
+ .damage-table tr:last-child td {
451
+ border-bottom: none;
452
+ }
453
+
454
+ .damage-table tbody tr {
455
+ transition: background-color 0.2s ease;
456
+ }
457
+
458
+ .damage-table tbody tr:hover {
459
+ background-color: #f7fafc;
460
+ }
461
+
462
+ .summary-section {
463
+ background: white;
464
+ border-radius: 8px;
465
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
466
+ padding: 1.5rem;
467
+ }
468
+
469
+ .summary-header {
470
+ display: flex;
471
+ align-items: center;
472
+ gap: 10px;
473
+ margin-bottom: 1rem;
474
+ color: #4a5568;
475
+ }
476
+
477
+ .summary-header h4 {
478
+ margin: 0;
479
+ font-size: 1.2rem;
480
+ color: #2d3748;
481
+ }
482
+
483
+ .summary-content {
484
+ color: #4a5568;
485
+ line-height: 1.6;
486
+ }
487
+
488
+ .summary-content p {
489
+ margin: 0 0 1rem;
490
+ }
491
+
492
+ .summary-content p:last-child {
493
+ margin-bottom: 0;
494
+ }
495
+
496
+ /* Updated Severity colors for light background */
497
+ .severity-high {
498
+ color: #e53e3e; /* Red */
499
+ }
500
+
501
+ .severity-medium {
502
+ color: #d69e2e; /* Yellow */
503
+ }
504
+
505
+ .severity-low {
506
+ color: #38a169; /* Green */
507
+ }
508
+
509
+ /* Animation for content */
510
+ @keyframes fadeIn {
511
+ from { opacity: 0; transform: translateY(-10px); }
512
+ to { opacity: 1; transform: translateY(0); }
513
+ }
514
+
515
+ .analysis-content.show {
516
+ animation: fadeIn 0.3s ease-out forwards;
517
+ }
518
+ </style>
519
+
520
+ <script>
521
+ function parseAndDisplayDamageReport() {
522
+ const block = document.getElementById('analysis-block');
523
+ const tbody = document.getElementById('damageTbody');
524
+ const summaryDiv = document.getElementById('summaryText');
525
+ const analysisText = `{{ result.llm_analysis | safe }}`;
526
+
527
+ tbody.innerHTML = '';
528
+ summaryDiv.innerHTML = '';
529
+
530
+ const lines = analysisText.split('\n');
531
+ let summaryText = [];
532
+
533
+ lines.forEach(line => {
534
+ if (line.includes('|')) {
535
+ const parts = line.split('|').filter(part => part.trim() !== '');
536
+ if (parts.length === 4 && !line.includes('Component') && !line.includes('---')) {
537
+ const row = document.createElement('tr');
538
+ parts.forEach((part, index) => {
539
+ const td = document.createElement('td');
540
+ const text = part.trim();
541
+ td.textContent = text;
542
+
543
+ // Add severity class
544
+ if (index === 2) { // Severity column
545
+ if (text.toLowerCase().includes('high')) {
546
+ td.classList.add('severity-high');
547
+ } else if (text.toLowerCase().includes('medium')) {
548
+ td.classList.add('severity-medium');
549
+ } else if (text.toLowerCase().includes('low')) {
550
+ td.classList.add('severity-low');
551
+ }
552
+ }
553
+ row.appendChild(td);
554
+ });
555
+ tbody.appendChild(row);
556
+ }
557
+ } else if (line.trim() !== '' && !line.includes('**')) {
558
+ if (line.length > 10) {
559
+ summaryText.push(line.trim());
560
+ }
561
+ }
562
+ });
563
+
564
+ summaryText.forEach(text => {
565
+ const p = document.createElement('p');
566
+ p.textContent = text;
567
+ summaryDiv.appendChild(p);
568
+ });
569
+ }
570
+
571
+ function toggleBlock(blockId, btnId) {
572
+ const block = document.getElementById(blockId);
573
+ const btn = document.getElementById(btnId);
574
+ const isHidden = block.style.display === 'none' || !block.style.display;
575
+
576
+ block.style.display = isHidden ? 'block' : 'none';
577
+ btn.classList.toggle('active');
578
+
579
+ const btnText = btn.querySelector('.btn-text');
580
+ btnText.textContent = isHidden ? 'Hide Analysis' : 'Show Analysis';
581
+
582
+ if (isHidden) {
583
+ block.classList.add('show');
584
+ parseAndDisplayDamageReport();
585
+ } else {
586
+ block.classList.remove('show');
587
+ }
588
+ }
589
+ </script>
590
+ {% endif %}
591
  </div>
592
  {% endif %}
593
  </div>
app/utils/config.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ # Simple load of environment variables, works both locally and on Spaces
5
+ load_dotenv()
6
+
7
+ class Config:
8
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
9
+
10
+ @classmethod
11
+ def validate(cls):
12
+ if not cls.GROQ_API_KEY:
13
+ raise ValueError("GROQ_API_KEY environment variable is not set")
app/utils/llm_client.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from groq import Groq
2
+ import base64
3
+ from PIL import Image
4
+ import io
5
+ from .config import Config
6
+
7
+ class GroqAnalyzer:
8
+ def __init__(self):
9
+ Config.validate()
10
+ self.client = Groq(api_key=Config.GROQ_API_KEY)
11
+ self.model = "meta-llama/llama-4-maverick-17b-128e-instruct"
12
+
13
+ def analyze_damage(self, image_path):
14
+ try:
15
+ with open(image_path, "rb") as image_file:
16
+ base64_image = base64.b64encode(image_file.read()).decode()
17
+
18
+ system_message = """You are a professional car damage assessment expert. Your task is to analyze car images and provide structured, consistent damage reports.
19
+
20
+ Please follow these exact guidelines:
21
+ 1. Use only these severity levels: High, Medium, Low
22
+ 2. For status, use these terms: Damaged, Intact, Partially Visible, Potentially damaged
23
+ 3. For actions, use these categories: Replacement/Repair, Inspection, Inspection/Repair
24
+ 4. Always assess these components if visible: Front Bumper, Hood, Grille, Headlights, Fenders
25
+ 5. Provide a consistent summary format focusing on:
26
+ - Main visible damage
27
+ - Recommended immediate actions
28
+ - Secondary inspection points
29
+ """
30
+
31
+ analysis_template = """Please analyze the car damage and provide the assessment in this exact format:
32
+
33
+ [TABLE]
34
+ Component | Status | Severity | Action Needed
35
+ Follow with a structured table using | as separators.
36
+
37
+ [SUMMARY]
38
+ Start with "The image shows..." and describe:
39
+ 1. Primary damage location and severity
40
+ 2. Key components affected
41
+ 3. Required immediate actions
42
+ 4. Additional inspection recommendations
43
+
44
+ Keep the format consistent and use only the predefined terms for severity, status, and actions."""
45
+
46
+ completion = self.client.chat.completions.create(
47
+ model=self.model,
48
+ messages=[
49
+ {
50
+ "role": "system",
51
+ "content": system_message
52
+ },
53
+ {
54
+ "role": "user",
55
+ "content": [
56
+ {
57
+ "type": "text",
58
+ "text": analysis_template
59
+ },
60
+ {
61
+ "type": "image_url",
62
+ "image_url": {
63
+ "url": f"data:image/jpeg;base64,{base64_image}"
64
+ }
65
+ }
66
+ ]
67
+ }
68
+ ],
69
+ temperature=0.3, # Lower temperature for more consistent outputs
70
+ max_completion_tokens=1024,
71
+ top_p=0.9, # Slightly lower top_p for more focused outputs
72
+ stream=True,
73
+ stop=None
74
+ )
75
+
76
+ # Handle streaming response
77
+ full_response = ""
78
+ for chunk in completion:
79
+ if chunk.choices[0].delta.content:
80
+ full_response += chunk.choices[0].delta.content
81
+
82
+ return full_response
83
+
84
+ except Exception as e:
85
+ print(f"Error in Groq analysis: {e}")
86
+ return f"Error analyzing image: {str(e)}"
app/utils/model_loader.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from huggingface_hub import hf_hub_download
3
+
4
+ def download_model_from_hub(repo_id, filename):
5
+ """Download model from Hugging Face Hub"""
6
+ return hf_hub_download(
7
+ repo_id=repo_id,
8
+ filename=filename,
9
+ token=os.getenv("HF_TOKEN") # Optional: if models are private
10
+ )
11
+
12
+ # Model paths
13
+ PARTS_MODEL_PATH = download_model_from_hub(
14
+ "AItoolstack/car_damage_detection",
15
+ "yolov8_models/parts/weights/weights/best.pt"
16
+ )
requirements.txt CHANGED
@@ -10,4 +10,7 @@ jinja2
10
  starlette
11
  requests
12
  aiofiles
13
- python-multipart
 
 
 
 
10
  starlette
11
  requests
12
  aiofiles
13
+ python-multipart
14
+ groq
15
+ python-dotenv
16
+ huggingface-hub