Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +6 -4
- app/__pycache__/app.cpython-310.pyc +0 -0
- app/__pycache__/integration.cpython-310.pyc +0 -0
- app/__pycache__/vehicle_recognition.cpython-310.pyc +0 -0
- app/app.py +149 -0
- app/vehicle_recognition.py +151 -0
- documentation.md +23 -4
- reports/screenshots/README.md +1 -0
README.md
CHANGED
|
@@ -31,10 +31,11 @@ The trained numeric model uses an AutoScout24 dataset with EUR prices. For the S
|
|
| 31 |
|
| 32 |
1. Enter vehicle details such as make, model, production year, mileage, fuel type, transmission, and power.
|
| 33 |
2. Upload front, side, rear, and interior vehicle images.
|
| 34 |
-
3.
|
| 35 |
-
4. Receive
|
| 36 |
-
5. Receive
|
| 37 |
-
6.
|
|
|
|
| 38 |
|
| 39 |
## Repository Structure
|
| 40 |
|
|
@@ -58,6 +59,7 @@ Current implemented parts:
|
|
| 58 |
- price preprocessing and feature engineering,
|
| 59 |
- numeric price model training and evaluation,
|
| 60 |
- OpenAI Vision damage analysis with manual fallback,
|
|
|
|
| 61 |
- integrated Gradio app for price recommendation, damage adjustment, and listing generation.
|
| 62 |
|
| 63 |
## Local Commands
|
|
|
|
| 31 |
|
| 32 |
1. Enter vehicle details such as make, model, production year, mileage, fuel type, transmission, and power.
|
| 33 |
2. Upload front, side, rear, and interior vehicle images.
|
| 34 |
+
3. Optionally use AI prefill to suggest make, model, year, and body type from the uploaded images.
|
| 35 |
+
4. Receive a base market price prediction.
|
| 36 |
+
5. Receive detected damage categories, image evidence, image-quality warnings, and a transparent damage adjustment.
|
| 37 |
+
6. Receive a recommended listing price range.
|
| 38 |
+
7. Generate an honest used car advertisement text.
|
| 39 |
|
| 40 |
## Repository Structure
|
| 41 |
|
|
|
|
| 59 |
- price preprocessing and feature engineering,
|
| 60 |
- numeric price model training and evaluation,
|
| 61 |
- OpenAI Vision damage analysis with manual fallback,
|
| 62 |
+
- OpenAI Vision vehicle-field prefill with manual verification,
|
| 63 |
- integrated Gradio app for price recommendation, damage adjustment, and listing generation.
|
| 64 |
|
| 65 |
## Local Commands
|
app/__pycache__/app.cpython-310.pyc
CHANGED
|
Binary files a/app/__pycache__/app.cpython-310.pyc and b/app/__pycache__/app.cpython-310.pyc differ
|
|
|
app/__pycache__/integration.cpython-310.pyc
CHANGED
|
Binary files a/app/__pycache__/integration.cpython-310.pyc and b/app/__pycache__/integration.cpython-310.pyc differ
|
|
|
app/__pycache__/vehicle_recognition.cpython-310.pyc
ADDED
|
Binary file (5.47 kB). View file
|
|
|
app/app.py
CHANGED
|
@@ -12,6 +12,7 @@ from app.integration import IntegratedAnalysis, run_integrated_analysis
|
|
| 12 |
from app.nlp_generator import PromptStrategy
|
| 13 |
from app.price_model import PricePrediction, VehicleFeatures
|
| 14 |
from app.utils import EUR_TO_CHF_RATE, SWISS_MARKET_FACTOR, eur_to_chf, format_chf
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
DAMAGE_CHOICES = ["scratch", "dent", "crack", "lamp broken", "glass shatter", "tire flat"]
|
|
@@ -35,6 +36,117 @@ TRANSMISSION_CHOICES = ["Automatic", "Manual", "Semi-automatic", "Unknown"]
|
|
| 35 |
BODY_CHOICES = ["SUV", "Sedan", "Hatchback", "Station Wagon", "Coupe", "Convertible", "Van", "Other"]
|
| 36 |
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
def fallback_price_prediction(features: VehicleFeatures) -> PricePrediction:
|
| 39 |
"""Provide a conservative fallback if the trained model artifact is missing."""
|
| 40 |
age = max(0, 2026 - features.production_year)
|
|
@@ -365,6 +477,23 @@ CUSTOM_CSS = """
|
|
| 365 |
margin: 6px 0 0 18px;
|
| 366 |
padding: 0;
|
| 367 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
.flow-line {
|
| 369 |
display: grid;
|
| 370 |
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
@@ -489,6 +618,10 @@ def build_demo() -> gr.Blocks:
|
|
| 489 |
label="Manual fallback damage labels",
|
| 490 |
)
|
| 491 |
gr.HTML(damage_weights_html())
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
prompt_strategy = gr.Radio(
|
| 494 |
["Sales-optimized but honest", "Neutral factual"],
|
|
@@ -531,6 +664,22 @@ def build_demo() -> gr.Blocks:
|
|
| 531 |
prompt_strategy,
|
| 532 |
]
|
| 533 |
outputs = [metrics, flow, vision_evidence, listing, explanation, damage_json, prompt_trace]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
run_button.click(predict_listing, inputs=inputs, outputs=outputs, api_name="predict_listing")
|
| 535 |
return demo
|
| 536 |
|
|
|
|
| 12 |
from app.nlp_generator import PromptStrategy
|
| 13 |
from app.price_model import PricePrediction, VehicleFeatures
|
| 14 |
from app.utils import EUR_TO_CHF_RATE, SWISS_MARKET_FACTOR, eur_to_chf, format_chf
|
| 15 |
+
from app.vehicle_recognition import VehicleRecognition, recognize_vehicle_from_images
|
| 16 |
|
| 17 |
|
| 18 |
DAMAGE_CHOICES = ["scratch", "dent", "crack", "lamp broken", "glass shatter", "tire flat"]
|
|
|
|
| 36 |
BODY_CHOICES = ["SUV", "Sedan", "Hatchback", "Station Wagon", "Coupe", "Convertible", "Van", "Other"]
|
| 37 |
|
| 38 |
|
| 39 |
+
def map_make_to_ui(make: str | None) -> tuple[str, str]:
|
| 40 |
+
"""Map recognized make to dropdown/custom make fields."""
|
| 41 |
+
if not make:
|
| 42 |
+
return "Other", ""
|
| 43 |
+
for choice in MAKE_CHOICES:
|
| 44 |
+
if choice.lower() == make.lower():
|
| 45 |
+
return choice, ""
|
| 46 |
+
return "Other", make
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def map_body_type_to_ui(body_type: str | None) -> str:
|
| 50 |
+
"""Map recognized body type to dropdown value."""
|
| 51 |
+
if not body_type:
|
| 52 |
+
return "Other"
|
| 53 |
+
normalized = body_type.strip().lower()
|
| 54 |
+
aliases = {
|
| 55 |
+
"estate": "Station Wagon",
|
| 56 |
+
"wagon": "Station Wagon",
|
| 57 |
+
"combi": "Station Wagon",
|
| 58 |
+
"saloon": "Sedan",
|
| 59 |
+
"convertible": "Convertible",
|
| 60 |
+
"cabriolet": "Convertible",
|
| 61 |
+
"hatch": "Hatchback",
|
| 62 |
+
}
|
| 63 |
+
if normalized in aliases:
|
| 64 |
+
return aliases[normalized]
|
| 65 |
+
for choice in BODY_CHOICES:
|
| 66 |
+
if choice.lower() == normalized:
|
| 67 |
+
return choice
|
| 68 |
+
return "Other"
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def recognition_html(recognition: VehicleRecognition | None, error: str | None = None) -> str:
|
| 72 |
+
"""Render vehicle-recognition status for the app."""
|
| 73 |
+
if error:
|
| 74 |
+
return f"<div class='evidence-warning'><b>AI prefill failed</b><p>{error}</p></div>"
|
| 75 |
+
if recognition is None:
|
| 76 |
+
return "<div class='empty-state'>Upload a vehicle image and click AI prefill to suggest vehicle fields.</div>"
|
| 77 |
+
|
| 78 |
+
warnings = "".join(f"<li>{warning}</li>" for warning in recognition.warnings)
|
| 79 |
+
warning_block = f"<ul>{warnings}</ul>" if warnings else "<span>No recognition warnings.</span>"
|
| 80 |
+
fields = [
|
| 81 |
+
f"Make: {recognition.make or 'unknown'}",
|
| 82 |
+
f"Model: {recognition.model or 'unknown'}",
|
| 83 |
+
f"Year: {recognition.production_year or 'unknown'}",
|
| 84 |
+
f"Body: {recognition.body_type or 'unknown'}",
|
| 85 |
+
]
|
| 86 |
+
return f"""
|
| 87 |
+
<div class="prefill-panel">
|
| 88 |
+
<div class="result-label">AI vehicle prefill</div>
|
| 89 |
+
<p><b>{' | '.join(fields)}</b></p>
|
| 90 |
+
<p>Confidence: {recognition.confidence:.2f}</p>
|
| 91 |
+
<p>{recognition.evidence or 'No detailed recognition evidence returned.'}</p>
|
| 92 |
+
<div class="result-sub">Please verify before valuation.</div>
|
| 93 |
+
<div class="prefill-warnings">{warning_block}</div>
|
| 94 |
+
</div>
|
| 95 |
+
""".strip()
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def prefill_vehicle_fields(
|
| 99 |
+
front_image: str | None,
|
| 100 |
+
side_image: str | None,
|
| 101 |
+
rear_image: str | None,
|
| 102 |
+
interior_image: str | None,
|
| 103 |
+
current_make: str,
|
| 104 |
+
current_custom_make: str,
|
| 105 |
+
current_model: str,
|
| 106 |
+
current_year: float,
|
| 107 |
+
current_body_type: str,
|
| 108 |
+
) -> tuple[str, str, str, int, str, str]:
|
| 109 |
+
"""Recognize vehicle identity and update form fields."""
|
| 110 |
+
image_paths = [
|
| 111 |
+
("front", front_image),
|
| 112 |
+
("side", side_image),
|
| 113 |
+
("rear", rear_image),
|
| 114 |
+
("interior", interior_image),
|
| 115 |
+
]
|
| 116 |
+
image_paths = [(view, path) for view, path in image_paths if path]
|
| 117 |
+
try:
|
| 118 |
+
recognition = recognize_vehicle_from_images(image_paths)
|
| 119 |
+
except Exception as exc: # noqa: BLE001 - user-facing status
|
| 120 |
+
return (
|
| 121 |
+
current_make,
|
| 122 |
+
current_custom_make,
|
| 123 |
+
current_model,
|
| 124 |
+
int(current_year),
|
| 125 |
+
current_body_type,
|
| 126 |
+
recognition_html(None, str(exc)),
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
make_value, custom_make = map_make_to_ui(recognition.make)
|
| 130 |
+
if not recognition.make:
|
| 131 |
+
make_value = current_make
|
| 132 |
+
custom_make = current_custom_make
|
| 133 |
+
model_value = recognition.model or current_model
|
| 134 |
+
year_value = recognition.production_year or int(current_year)
|
| 135 |
+
body_value = (
|
| 136 |
+
map_body_type_to_ui(recognition.body_type)
|
| 137 |
+
if recognition.body_type
|
| 138 |
+
else current_body_type
|
| 139 |
+
)
|
| 140 |
+
return (
|
| 141 |
+
make_value,
|
| 142 |
+
custom_make,
|
| 143 |
+
model_value,
|
| 144 |
+
year_value,
|
| 145 |
+
body_value,
|
| 146 |
+
recognition_html(recognition),
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
def fallback_price_prediction(features: VehicleFeatures) -> PricePrediction:
|
| 151 |
"""Provide a conservative fallback if the trained model artifact is missing."""
|
| 152 |
age = max(0, 2026 - features.production_year)
|
|
|
|
| 477 |
margin: 6px 0 0 18px;
|
| 478 |
padding: 0;
|
| 479 |
}
|
| 480 |
+
.prefill-panel {
|
| 481 |
+
border: 1px solid #c7d2fe;
|
| 482 |
+
border-radius: 8px;
|
| 483 |
+
background: #f8fafc;
|
| 484 |
+
padding: 12px;
|
| 485 |
+
margin: 10px 0 0;
|
| 486 |
+
}
|
| 487 |
+
.prefill-panel p {
|
| 488 |
+
margin: 4px 0;
|
| 489 |
+
color: #17202a;
|
| 490 |
+
}
|
| 491 |
+
.prefill-warnings ul {
|
| 492 |
+
margin: 8px 0 0 18px;
|
| 493 |
+
padding: 0;
|
| 494 |
+
color: #475569;
|
| 495 |
+
font-size: 13px;
|
| 496 |
+
}
|
| 497 |
.flow-line {
|
| 498 |
display: grid;
|
| 499 |
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
| 618 |
label="Manual fallback damage labels",
|
| 619 |
)
|
| 620 |
gr.HTML(damage_weights_html())
|
| 621 |
+
prefill_button = gr.Button("AI prefill vehicle fields", variant="secondary")
|
| 622 |
+
prefill_status = gr.HTML(
|
| 623 |
+
"<div class='empty-state'>Upload at least one vehicle image to suggest make, model, year, and body type.</div>"
|
| 624 |
+
)
|
| 625 |
|
| 626 |
prompt_strategy = gr.Radio(
|
| 627 |
["Sales-optimized but honest", "Neutral factual"],
|
|
|
|
| 664 |
prompt_strategy,
|
| 665 |
]
|
| 666 |
outputs = [metrics, flow, vision_evidence, listing, explanation, damage_json, prompt_trace]
|
| 667 |
+
prefill_button.click(
|
| 668 |
+
prefill_vehicle_fields,
|
| 669 |
+
inputs=[
|
| 670 |
+
front_image,
|
| 671 |
+
side_image,
|
| 672 |
+
rear_image,
|
| 673 |
+
interior_image,
|
| 674 |
+
make,
|
| 675 |
+
custom_make,
|
| 676 |
+
model,
|
| 677 |
+
production_year,
|
| 678 |
+
body_type,
|
| 679 |
+
],
|
| 680 |
+
outputs=[make, custom_make, model, production_year, body_type, prefill_status],
|
| 681 |
+
api_name="prefill_vehicle_fields",
|
| 682 |
+
)
|
| 683 |
run_button.click(predict_listing, inputs=inputs, outputs=outputs, api_name="predict_listing")
|
| 684 |
return demo
|
| 685 |
|
app/vehicle_recognition.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vehicle identification from uploaded images."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import base64
|
| 6 |
+
import json
|
| 7 |
+
import mimetypes
|
| 8 |
+
import os
|
| 9 |
+
import re
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
from app.damage_model import DEFAULT_OPENAI_VISION_MODEL
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass(frozen=True)
|
| 18 |
+
class VehicleRecognition:
|
| 19 |
+
"""Structured vehicle identification suggestion from image evidence."""
|
| 20 |
+
|
| 21 |
+
make: str | None = None
|
| 22 |
+
model: str | None = None
|
| 23 |
+
production_year: int | None = None
|
| 24 |
+
body_type: str | None = None
|
| 25 |
+
confidence: float = 0.0
|
| 26 |
+
evidence: str = ""
|
| 27 |
+
warnings: list[str] = field(default_factory=list)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _image_to_data_url(image_path: Path) -> str:
|
| 31 |
+
"""Encode an image as a data URL for OpenAI vision input."""
|
| 32 |
+
mime_type = mimetypes.guess_type(str(image_path))[0] or "image/jpeg"
|
| 33 |
+
encoded = base64.b64encode(image_path.read_bytes()).decode("ascii")
|
| 34 |
+
return f"data:{mime_type};base64,{encoded}"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _extract_json_object(text: str) -> dict[str, Any]:
|
| 38 |
+
"""Parse a JSON object from model output, even if wrapped in text."""
|
| 39 |
+
try:
|
| 40 |
+
parsed = json.loads(text)
|
| 41 |
+
if isinstance(parsed, dict):
|
| 42 |
+
return parsed
|
| 43 |
+
except json.JSONDecodeError:
|
| 44 |
+
pass
|
| 45 |
+
|
| 46 |
+
match = re.search(r"\{.*\}", text, flags=re.DOTALL)
|
| 47 |
+
if not match:
|
| 48 |
+
raise ValueError("Vehicle recognition response did not contain a JSON object.")
|
| 49 |
+
parsed = json.loads(match.group(0))
|
| 50 |
+
if not isinstance(parsed, dict):
|
| 51 |
+
raise ValueError("Vehicle recognition response JSON was not an object.")
|
| 52 |
+
return parsed
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _clean_optional_text(value: Any) -> str | None:
|
| 56 |
+
"""Normalize optional text values from a model response."""
|
| 57 |
+
if value is None:
|
| 58 |
+
return None
|
| 59 |
+
text = str(value).strip()
|
| 60 |
+
if not text or text.lower() in {"unknown", "null", "none", "not visible"}:
|
| 61 |
+
return None
|
| 62 |
+
return text
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _parse_year(value: Any) -> int | None:
|
| 66 |
+
"""Parse a plausible production-year estimate."""
|
| 67 |
+
if value is None:
|
| 68 |
+
return None
|
| 69 |
+
match = re.search(r"(19|20)\d{2}", str(value))
|
| 70 |
+
if not match:
|
| 71 |
+
return None
|
| 72 |
+
year = int(match.group(0))
|
| 73 |
+
if 1990 <= year <= 2026:
|
| 74 |
+
return year
|
| 75 |
+
return None
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def recognize_vehicle_from_images(
|
| 79 |
+
image_paths: list[tuple[str, str | Path]],
|
| 80 |
+
model: str | None = None,
|
| 81 |
+
) -> VehicleRecognition:
|
| 82 |
+
"""Suggest make, model, approximate year, and body type from vehicle images."""
|
| 83 |
+
existing_paths = [
|
| 84 |
+
(view, Path(path))
|
| 85 |
+
for view, path in image_paths
|
| 86 |
+
if path and Path(path).exists()
|
| 87 |
+
]
|
| 88 |
+
if not existing_paths:
|
| 89 |
+
raise FileNotFoundError("Upload at least one vehicle image before using AI prefill.")
|
| 90 |
+
|
| 91 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 92 |
+
if not api_key:
|
| 93 |
+
raise RuntimeError("OPENAI_API_KEY is not configured.")
|
| 94 |
+
|
| 95 |
+
from openai import OpenAI
|
| 96 |
+
|
| 97 |
+
model_name = model or os.getenv("OPENAI_VISION_MODEL", DEFAULT_OPENAI_VISION_MODEL)
|
| 98 |
+
prompt = """
|
| 99 |
+
You are a careful used-car identification assistant. Inspect the uploaded car images and infer only visible or strongly likely vehicle facts.
|
| 100 |
+
Return only valid JSON with this schema:
|
| 101 |
+
{
|
| 102 |
+
"make": "manufacturer or null",
|
| 103 |
+
"model": "model name or null",
|
| 104 |
+
"production_year": 2018,
|
| 105 |
+
"body_type": "SUV|Sedan|Hatchback|Station Wagon|Coupe|Convertible|Van|Other|null",
|
| 106 |
+
"confidence": 0.0,
|
| 107 |
+
"evidence": "short visual evidence, such as badges, grille, shape, lights, or interior clues",
|
| 108 |
+
"warnings": ["uncertainties such as no badge visible, partial image, similar generations, model year approximate"]
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
Rules:
|
| 112 |
+
- Be conservative. If make/model/year is uncertain, use null or add a warning.
|
| 113 |
+
- Estimate production_year as the most likely visual model year, not registration year.
|
| 114 |
+
- Do not infer mileage, service history, transmission, power, accident history, or fuel type from the image.
|
| 115 |
+
- Prefer broad body_type over exact trim.
|
| 116 |
+
""".strip()
|
| 117 |
+
|
| 118 |
+
content: list[dict[str, str]] = [{"type": "input_text", "text": prompt}]
|
| 119 |
+
for view, path in existing_paths:
|
| 120 |
+
content.append({"type": "input_text", "text": f"View: {view}"})
|
| 121 |
+
content.append({"type": "input_image", "image_url": _image_to_data_url(path)})
|
| 122 |
+
|
| 123 |
+
client = OpenAI(api_key=api_key)
|
| 124 |
+
response = client.responses.create(
|
| 125 |
+
model=model_name,
|
| 126 |
+
input=[
|
| 127 |
+
{
|
| 128 |
+
"role": "user",
|
| 129 |
+
"content": content,
|
| 130 |
+
}
|
| 131 |
+
],
|
| 132 |
+
)
|
| 133 |
+
payload = _extract_json_object(response.output_text)
|
| 134 |
+
warnings = payload.get("warnings", [])
|
| 135 |
+
if isinstance(warnings, str):
|
| 136 |
+
warnings = [warnings]
|
| 137 |
+
|
| 138 |
+
confidence = float(payload.get("confidence", 0.0) or 0.0)
|
| 139 |
+
confidence = max(0.0, min(confidence, 1.0))
|
| 140 |
+
if confidence < 0.55:
|
| 141 |
+
warnings.append("Vehicle recognition confidence is low; please verify the fields manually.")
|
| 142 |
+
|
| 143 |
+
return VehicleRecognition(
|
| 144 |
+
make=_clean_optional_text(payload.get("make")),
|
| 145 |
+
model=_clean_optional_text(payload.get("model")),
|
| 146 |
+
production_year=_parse_year(payload.get("production_year")),
|
| 147 |
+
body_type=_clean_optional_text(payload.get("body_type")),
|
| 148 |
+
confidence=confidence,
|
| 149 |
+
evidence=str(payload.get("evidence", "")).strip(),
|
| 150 |
+
warnings=[str(item) for item in warnings if str(item).strip()],
|
| 151 |
+
)
|
documentation.md
CHANGED
|
@@ -35,9 +35,10 @@ The three AI blocks are integrated into one decision flow rather than executed i
|
|
| 35 |
Vehicle attributes -> Numeric ML model -> Base market price
|
| 36 |
Vehicle images -> Computer vision model -> Damage categories + evidence + quality warnings + damage severity score
|
| 37 |
Base price + damage score + user notes -> NLP component -> Explanation + sales listing
|
|
|
|
| 38 |
```
|
| 39 |
|
| 40 |
-
The numeric model provides the base price. The computer vision model provides additional information about the vehicle's visible condition. The damage score is then used as a transparent adjustment factor. Finally, the NLP component combines the structured vehicle information, the base price, the damage findings, and the adjusted price into a user-facing explanation and listing text.
|
| 41 |
|
| 42 |
### Planned Application Output
|
| 43 |
|
|
@@ -50,7 +51,8 @@ For a given vehicle, the application should return:
|
|
| 50 |
- final recommended listing price range,
|
| 51 |
- generated listing text,
|
| 52 |
- short explanation of the recommendation,
|
| 53 |
-
- limitations and confidence notes
|
|
|
|
| 54 |
|
| 55 |
### Scope
|
| 56 |
|
|
@@ -330,6 +332,17 @@ The vision prompt asks the model to return structured JSON with visible damage l
|
|
| 330 |
|
| 331 |
The app shows the visual evidence directly in the normal result area, not only in the technical details. If images are blurry, dark, cropped, missing important views, or if model confidence is low, the app adds a warning such as "Damage analysis uncertain". This prevents the interface from overclaiming visual certainty.
|
| 332 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
Damage-score weights:
|
| 334 |
|
| 335 |
| Damage label | Weight |
|
|
@@ -378,6 +391,7 @@ Vehicle data -> numeric ML model -> base market price
|
|
| 378 |
Image/manual damage labels -> CV damage component -> evidence + damage score
|
| 379 |
Base price + damage score -> adjusted listing price
|
| 380 |
Vehicle data + price outputs + damage findings -> NLP -> listing + explanation
|
|
|
|
| 381 |
```
|
| 382 |
|
| 383 |
The integration is implemented in `app/integration.py` and exposed through the Gradio interface in `app/app.py`.
|
|
@@ -386,6 +400,7 @@ Visible interaction between the blocks:
|
|
| 386 |
|
| 387 |
- the numeric model provides the base market price,
|
| 388 |
- the damage component changes the price through the damage score,
|
|
|
|
| 389 |
- the NLP component receives both the numeric model output and the damage output,
|
| 390 |
- the final listing explicitly explains the base price, damage adjustment, visual evidence, Swiss calibration, and recommended listing price.
|
| 391 |
|
|
@@ -561,6 +576,7 @@ The deployed Hugging Face Space should be tested with at least three cases:
|
|
| 561 |
| Clean vehicle | normal mileage, clear exterior images, no visible damage | damage score close to 0, no damage discount, listing mentions no visible damage |
|
| 562 |
| Minor visible damage | front/side images with scratch or dent | damage label appears, CHF price is adjusted downward, listing mentions the damage transparently |
|
| 563 |
| Low-quality images | dark, blurry, cropped, or interior-only images | app shows image-quality warning and avoids overconfident damage claims |
|
|
|
|
| 564 |
|
| 565 |
### Screenshots
|
| 566 |
|
|
@@ -569,7 +585,8 @@ Screenshots for the final report should be stored in `reports/screenshots/`:
|
|
| 569 |
- `01_app_input_multi_image.png`: vehicle form with front, side, rear, and interior upload fields,
|
| 570 |
- `02_app_result_clean_vehicle.png`: result with CHF recommendation and no visible damage,
|
| 571 |
- `03_app_result_damage_detected.png`: result with damage score, visual evidence, and adjusted price,
|
| 572 |
-
- `04_app_quality_warning.png`: result showing an uncertain image-quality warning
|
|
|
|
| 573 |
|
| 574 |
## 7. Ethical Considerations
|
| 575 |
|
|
@@ -579,6 +596,7 @@ To reduce misleading outputs, the app:
|
|
| 579 |
|
| 580 |
- explicitly states that the damage score is heuristic,
|
| 581 |
- shows visual evidence and image-quality warnings,
|
|
|
|
| 582 |
- keeps manual fallback labels separate from AI image analysis,
|
| 583 |
- avoids claiming exact repair costs,
|
| 584 |
- generates listing text that mentions visible damage instead of hiding it.
|
|
@@ -593,7 +611,8 @@ Current limitations:
|
|
| 593 |
- the Swiss-market factor is transparent but not learned from a Swiss transaction dataset,
|
| 594 |
- OpenAI Vision provides image-level visual assessment, not bounding-box localization,
|
| 595 |
- image analysis cannot detect hidden mechanical or structural issues,
|
| 596 |
-
- generated listings are deterministic templates and not full LLM-generated marketplace copy
|
|
|
|
| 597 |
|
| 598 |
Future work:
|
| 599 |
|
|
|
|
| 35 |
Vehicle attributes -> Numeric ML model -> Base market price
|
| 36 |
Vehicle images -> Computer vision model -> Damage categories + evidence + quality warnings + damage severity score
|
| 37 |
Base price + damage score + user notes -> NLP component -> Explanation + sales listing
|
| 38 |
+
Vehicle images -> OpenAI Vision prefill -> Suggested make/model/year/body type -> User verification -> Numeric ML model
|
| 39 |
```
|
| 40 |
|
| 41 |
+
The numeric model provides the base price. The computer vision model provides additional information about the vehicle's visible condition. In addition, an optional OpenAI Vision prefill step can suggest make, model, approximate production year, and body type from uploaded images. The user can verify or edit these fields before valuation. The damage score is then used as a transparent adjustment factor. Finally, the NLP component combines the structured vehicle information, the base price, the damage findings, and the adjusted price into a user-facing explanation and listing text.
|
| 42 |
|
| 43 |
### Planned Application Output
|
| 44 |
|
|
|
|
| 51 |
- final recommended listing price range,
|
| 52 |
- generated listing text,
|
| 53 |
- short explanation of the recommendation,
|
| 54 |
+
- limitations and confidence notes,
|
| 55 |
+
- optional AI vehicle-field suggestions with confidence and warnings.
|
| 56 |
|
| 57 |
### Scope
|
| 58 |
|
|
|
|
| 332 |
|
| 333 |
The app shows the visual evidence directly in the normal result area, not only in the technical details. If images are blurry, dark, cropped, missing important views, or if model confidence is low, the app adds a warning such as "Damage analysis uncertain". This prevents the interface from overclaiming visual certainty.
|
| 334 |
|
| 335 |
+
### Computer Vision: Vehicle Field Prefill
|
| 336 |
+
|
| 337 |
+
The optional vehicle-prefill component is implemented in `app/vehicle_recognition.py`. It uses uploaded vehicle images to suggest:
|
| 338 |
+
|
| 339 |
+
- make,
|
| 340 |
+
- model,
|
| 341 |
+
- approximate production year,
|
| 342 |
+
- body type.
|
| 343 |
+
|
| 344 |
+
This feature improves usability but does not replace manual input. The app explicitly shows confidence, evidence, and warnings, and the user can correct all fields before the numeric price model is executed. The prefill prompt is conservative: it may return unknown values if the badge, lights, body shape, or model generation are not clear.
|
| 345 |
+
|
| 346 |
Damage-score weights:
|
| 347 |
|
| 348 |
| Damage label | Weight |
|
|
|
|
| 391 |
Image/manual damage labels -> CV damage component -> evidence + damage score
|
| 392 |
Base price + damage score -> adjusted listing price
|
| 393 |
Vehicle data + price outputs + damage findings -> NLP -> listing + explanation
|
| 394 |
+
Vehicle images -> optional prefill -> editable vehicle fields -> numeric ML model
|
| 395 |
```
|
| 396 |
|
| 397 |
The integration is implemented in `app/integration.py` and exposed through the Gradio interface in `app/app.py`.
|
|
|
|
| 400 |
|
| 401 |
- the numeric model provides the base market price,
|
| 402 |
- the damage component changes the price through the damage score,
|
| 403 |
+
- the optional vehicle-prefill component can populate structured fields from visual evidence,
|
| 404 |
- the NLP component receives both the numeric model output and the damage output,
|
| 405 |
- the final listing explicitly explains the base price, damage adjustment, visual evidence, Swiss calibration, and recommended listing price.
|
| 406 |
|
|
|
|
| 576 |
| Clean vehicle | normal mileage, clear exterior images, no visible damage | damage score close to 0, no damage discount, listing mentions no visible damage |
|
| 577 |
| Minor visible damage | front/side images with scratch or dent | damage label appears, CHF price is adjusted downward, listing mentions the damage transparently |
|
| 578 |
| Low-quality images | dark, blurry, cropped, or interior-only images | app shows image-quality warning and avoids overconfident damage claims |
|
| 579 |
+
| Vehicle prefill | clear front/side image with visible badge/body shape | make/model/year/body suggestions appear and remain editable before final analysis |
|
| 580 |
|
| 581 |
### Screenshots
|
| 582 |
|
|
|
|
| 585 |
- `01_app_input_multi_image.png`: vehicle form with front, side, rear, and interior upload fields,
|
| 586 |
- `02_app_result_clean_vehicle.png`: result with CHF recommendation and no visible damage,
|
| 587 |
- `03_app_result_damage_detected.png`: result with damage score, visual evidence, and adjusted price,
|
| 588 |
+
- `04_app_quality_warning.png`: result showing an uncertain image-quality warning,
|
| 589 |
+
- `05_app_ai_prefill.png`: AI prefill result with suggested vehicle fields and confidence.
|
| 590 |
|
| 591 |
## 7. Ethical Considerations
|
| 592 |
|
|
|
|
| 596 |
|
| 597 |
- explicitly states that the damage score is heuristic,
|
| 598 |
- shows visual evidence and image-quality warnings,
|
| 599 |
+
- treats image-based vehicle recognition as editable suggestions rather than verified facts,
|
| 600 |
- keeps manual fallback labels separate from AI image analysis,
|
| 601 |
- avoids claiming exact repair costs,
|
| 602 |
- generates listing text that mentions visible damage instead of hiding it.
|
|
|
|
| 611 |
- the Swiss-market factor is transparent but not learned from a Swiss transaction dataset,
|
| 612 |
- OpenAI Vision provides image-level visual assessment, not bounding-box localization,
|
| 613 |
- image analysis cannot detect hidden mechanical or structural issues,
|
| 614 |
+
- generated listings are deterministic templates and not full LLM-generated marketplace copy,
|
| 615 |
+
- image-based make/model/year recognition can be wrong for visually similar generations or modified vehicles.
|
| 616 |
|
| 617 |
Future work:
|
| 618 |
|
reports/screenshots/README.md
CHANGED
|
@@ -6,5 +6,6 @@ Use this folder for final documentation screenshots from the deployed Hugging Fa
|
|
| 6 |
- `02_app_result_clean_vehicle.png`: CHF recommendation with no visible damage.
|
| 7 |
- `03_app_result_damage_detected.png`: damage score, visual evidence, and adjusted CHF price.
|
| 8 |
- `04_app_quality_warning.png`: uncertain image-quality warning for blurry, dark, cropped, or incomplete images.
|
|
|
|
| 9 |
|
| 10 |
These screenshots demonstrate the deployed user workflow required in the semester project documentation.
|
|
|
|
| 6 |
- `02_app_result_clean_vehicle.png`: CHF recommendation with no visible damage.
|
| 7 |
- `03_app_result_damage_detected.png`: damage score, visual evidence, and adjusted CHF price.
|
| 8 |
- `04_app_quality_warning.png`: uncertain image-quality warning for blurry, dark, cropped, or incomplete images.
|
| 9 |
+
- `05_app_ai_prefill.png`: AI vehicle-prefill result with suggested make, model, year, body type, confidence, and warnings.
|
| 10 |
|
| 11 |
These screenshots demonstrate the deployed user workflow required in the semester project documentation.
|