Spaces:
Sleeping
Sleeping
Commit
·
98bf2c9
1
Parent(s):
dec4c30
Update: Remove damage model and add Docker configuration
Browse files- .gitignore +39 -0
- Dockerfile +29 -11
- app/.gitignore +4 -0
- app/__pycache__/main.cpython-310.pyc +0 -0
- app/main.py +17 -4
- app/routers/__pycache__/inference.cpython-310.pyc +0 -0
- app/routers/inference.py +118 -95
- app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_damage.png +0 -3
- app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png +0 -3
- app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png +0 -3
- app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png +0 -3
- app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png +0 -3
- app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png +0 -3
- app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_result.json +0 -86
- app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_damage.png +0 -3
- app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png +0 -3
- app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png +0 -3
- app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png +0 -3
- app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png +0 -3
- app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png +0 -3
- app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png +0 -3
- app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png +0 -3
- app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_result.json +0 -83
- app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_damage.png +0 -3
- app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png +0 -3
- app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png +0 -3
- app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png +0 -3
- app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png +0 -3
- app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png +0 -3
- app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg +0 -3
- app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg +0 -3
- app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg +0 -3
- app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg +0 -3
- app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg +0 -3
- app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg +0 -3
- app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg +0 -3
- app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg +0 -3
- app/static/uploads/de9bda87-c237-4c6b-8748-1ab245d4ec57.jpg +0 -0
- app/static/uploads/f0a7c31c-d1cf-460a-abff-d32d76faa384.jpg +0 -0
- app/templates/index.html +530 -14
- app/utils/config.py +13 -0
- app/utils/llm_client.py +86 -0
- app/utils/model_loader.py +16 -0
- 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 |
-
#
|
6 |
-
|
|
|
|
|
|
|
|
|
7 |
build-essential \
|
8 |
libgl1-mesa-glx \
|
9 |
libglib2.0-0 \
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
11 |
|
12 |
-
#
|
13 |
COPY requirements.txt .
|
14 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
15 |
|
16 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
COPY . .
|
18 |
|
19 |
-
#
|
20 |
-
|
|
|
21 |
|
22 |
-
|
23 |
|
24 |
-
#
|
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 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
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 |
-
|
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 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
205 |
try:
|
206 |
-
os.remove(
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png
DELETED
Git LFS Details
|
app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png
DELETED
Git LFS Details
|
app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png
DELETED
Git LFS Details
|
app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png
DELETED
Git LFS Details
|
app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png
DELETED
Git LFS Details
|
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
|
app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png
DELETED
Git LFS Details
|
app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png
DELETED
Git LFS Details
|
app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png
DELETED
Git LFS Details
|
app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png
DELETED
Git LFS Details
|
app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png
DELETED
Git LFS Details
|
app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png
DELETED
Git LFS Details
|
app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png
DELETED
Git LFS Details
|
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
|
app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png
DELETED
Git LFS Details
|
app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png
DELETED
Git LFS Details
|
app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png
DELETED
Git LFS Details
|
app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png
DELETED
Git LFS Details
|
app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png
DELETED
Git LFS Details
|
app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg
DELETED
Git LFS Details
|
app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg
DELETED
Git LFS Details
|
app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg
DELETED
Git LFS Details
|
app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg
DELETED
Git LFS Details
|
app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg
DELETED
Git LFS Details
|
app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg
DELETED
Git LFS Details
|
app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg
DELETED
Git LFS Details
|
app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg
DELETED
Git LFS Details
|
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.
|
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.
|
|
|
|
|
|
|
61 |
<h4 style="margin-bottom:8px;">JSON Output</h4>
|
62 |
-
<button class="copy-btn" onclick="copyJSON()"
|
63 |
-
<button class="
|
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.
|
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
|