Spaces:
Sleeping
Sleeping
| """Gradio frontend for AI Car Purchase Advisor.""" | |
| from __future__ import annotations | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Any | |
| import gradio as gr | |
| from src.price_predictor import predict_price | |
| from src import price_predictor | |
| from src.recommendation_engine import generate_recommendation | |
| from src.utils import format_currency_chf, to_float | |
| from src.vision_analyzer import analyze_car_image | |
| def _normalize_age(year_or_age_value: float | int | None, mode: str) -> float: | |
| value = to_float(year_or_age_value, 0) | |
| if value <= 0: | |
| return 6.0 | |
| if str(mode).strip().lower() == "year": | |
| current_year = datetime.now().year | |
| age = current_year - value | |
| if age < 0: | |
| return 0.0 | |
| return float(age) | |
| return float(value) | |
| def _build_vehicle_recognition_markdown(vision_results: dict[str, Any]) -> str: | |
| predicted_class = vision_results.get("predicted_class", "Unknown") | |
| confidence = float(vision_results.get("confidence", 0.0) or 0.0) | |
| method = vision_results.get("method", "fallback") | |
| # Case A: Image analyzed by vision model | |
| if method == "local_transfer_model": | |
| confidence_warning = "" | |
| if confidence < 0.5: | |
| confidence_warning = "\n\nThe image model is uncertain. Please verify the result manually." | |
| return f""" | |
| ### 1. Vehicle recognition – Computer Vision | |
| Detected brand/model group: **{predicted_class}** | |
| The image analysis provides a coarse vehicle brand/model prediction based on deep learning. It is not exact technical vehicle identification.{confidence_warning} | |
| """.strip() | |
| # Case B: Manual input without image | |
| elif method == "manual_input": | |
| return f""" | |
| ### 1. Vehicle recognition – Manual Input | |
| Vehicle used for the estimate: **{predicted_class}** | |
| The price estimate is based on your manual vehicle input and structured used-car listing data. | |
| """.strip() | |
| # Case C: Unknown or fallback | |
| else: | |
| return f""" | |
| ### 1. Vehicle recognition | |
| Vehicle: **Unknown** | |
| Please upload a car image or enter a known make/model to get a price estimate. | |
| """.strip() | |
| def _build_price_estimate_markdown(price_prediction: dict[str, Any]) -> str: | |
| estimated_price = format_currency_chf(price_prediction.get("estimated_price")) | |
| lower_bound = format_currency_chf(price_prediction.get("lower_bound")) | |
| upper_bound = format_currency_chf(price_prediction.get("upper_bound")) | |
| return f""" | |
| ### 2. Price estimate – ML Numeric Data | |
| - Estimated market price: **{estimated_price}** | |
| - Expected price range: **{lower_bound} – {upper_bound}** | |
| This estimate is based on structured used-car listing data. | |
| """.strip() | |
| def _build_purchase_assessment_markdown(recommendation: dict[str, Any]) -> str: | |
| rough_months_needed = recommendation.get("rough_months_needed") | |
| financing_gap = recommendation.get("financing_gap") | |
| months_text = "n/a" if rough_months_needed in {None, "", 0} else f"{float(rough_months_needed):.1f} months" | |
| gap_text = "n/a" if financing_gap in {None, ""} else format_currency_chf(financing_gap) | |
| budget_assessment = recommendation.get('price_budget_assessment', 'n/a') | |
| budget_reason = recommendation.get('price_budget_reason', '') | |
| financing_orientation = recommendation.get('financing_orientation', 'n/a') | |
| financing_reason = recommendation.get('financing_reason', '') | |
| explanation = recommendation.get('full_explanation', 'n/a') | |
| return f""" | |
| ### 3. Purchase assessment – NLP Explanation | |
| **Budget assessment:** {budget_assessment} | |
| {budget_reason} | |
| **Simple financing orientation:** {financing_orientation} | |
| - Financing gap: {gap_text} | |
| - Rough months needed: {months_text} | |
| {financing_reason} | |
| **Assessment:** | |
| {explanation} | |
| """.strip() | |
| def _build_debug_payload( | |
| vision_results: dict[str, Any], | |
| price_prediction: dict[str, Any], | |
| recommendation: dict[str, Any], | |
| ) -> dict[str, Any]: | |
| return { | |
| "vision": vision_results, | |
| "price": price_prediction, | |
| "recommendation": recommendation, | |
| } | |
| def _collect_example_images(limit: int = 3) -> list[list[str]]: | |
| project_root = Path(__file__).resolve().parent | |
| examples_dir = project_root / "example_images" | |
| allowed_ext = {".jpg", ".jpeg", ".png", ".webp"} | |
| if not examples_dir.exists(): | |
| print(f"Example image folder not found: {examples_dir}") | |
| return [] | |
| image_paths = sorted( | |
| [ | |
| path | |
| for path in examples_dir.rglob("*") | |
| if path.is_file() and path.suffix.lower() in allowed_ext | |
| ] | |
| ) | |
| examples = [[str(path)] for path in image_paths[:limit]] | |
| print("Loaded example images:", examples) | |
| return examples | |
| def _run_advisor( | |
| image, | |
| make_model, | |
| mileage_km, | |
| car_age_years, | |
| budget_chf, | |
| max_monthly_rate_chf, | |
| ): | |
| # Allow either image OR manual make_model input, but require at least one | |
| if image is None and not (make_model and make_model.strip()): | |
| return ( | |
| "Please upload a car image OR enter a known make/model to start the analysis.", | |
| "", | |
| "", | |
| {}, | |
| ) | |
| # If image is provided, use vision analyzer; otherwise use manual input | |
| if image is not None: | |
| vision_results = analyze_car_image(image) | |
| predicted_class = vision_results.get("predicted_class", "Unknown") | |
| else: | |
| # No image, but make_model was entered manually | |
| predicted_class = make_model.strip() if make_model and make_model.strip() else "Unknown" | |
| vision_results = { | |
| "predicted_class": predicted_class, | |
| "confidence": 0.0, | |
| "method": "manual_input", | |
| "notes": ["Car brand/model entered manually without image."], | |
| } | |
| age = _normalize_age(car_age_years, "Age") | |
| make_model_input = make_model.strip() if make_model and make_model.strip() else predicted_class | |
| # For manual input without image, use heuristic (ML model needs too many features we don't have) | |
| # For image-based input, use ML model if available | |
| if vision_results.get("method") == "manual_input": | |
| # Manual input: use heuristic with make_model for brand detection | |
| price_input = { | |
| "make_model": make_model_input, | |
| "hp_kW": None, | |
| "Fuel": None, | |
| } | |
| price_prediction = price_predictor._heuristic_estimate(price_input) | |
| else: | |
| # Image-based: try ML model with available features | |
| price_input = { | |
| "make_model": make_model_input, | |
| "hp_kW": None, | |
| "Fuel": None, | |
| } | |
| price_prediction = predict_price(price_input) | |
| user_inputs = { | |
| "budget_chf": to_float(budget_chf, 0), | |
| "max_monthly_rate_chf": to_float(max_monthly_rate_chf, 0), | |
| "car_age": age, | |
| "km": to_float(mileage_km, 0), | |
| } | |
| recommendation = generate_recommendation( | |
| user_inputs=user_inputs, | |
| vision_results=vision_results, | |
| price_prediction=price_prediction, | |
| ) | |
| return ( | |
| _build_vehicle_recognition_markdown(vision_results), | |
| _build_price_estimate_markdown(price_prediction), | |
| _build_purchase_assessment_markdown(recommendation), | |
| _build_debug_payload(vision_results, price_prediction, recommendation), | |
| ) | |
| def _clear_outputs(): | |
| return "", "", "", {} | |
| def build_interface() -> gr.Blocks: | |
| with gr.Blocks(title="AI Car Purchase Advisor") as demo: | |
| gr.Markdown( | |
| """ | |
| # AI Car Purchase Advisor | |
| Upload a car image to estimate the vehicle class, market price range and receive a short AI-generated purchase assessment. | |
| Image upload -> analyze_car_image() -> predicted_class -> predict_price() -> estimated_price/range -> generate_recommendation() -> final explanation | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| image_input = gr.Image(type="pil", label="Upload car image") | |
| make_model = gr.Textbox(label="Known make/model", placeholder="e.g. Audi A3") | |
| mileage_km = gr.Number(label="Mileage in km", value=80000) | |
| car_age_years = gr.Number(label="Car age in years", value=6) | |
| budget_chf = gr.Number(label="Budget in CHF", value=25000) | |
| max_monthly_rate_chf = gr.Number(label="Maximum monthly rate in CHF", value=450) | |
| with gr.Row(): | |
| submit_btn = gr.Button("Generate assessment", variant="primary") | |
| clear_btn = gr.Button("Clear") | |
| with gr.Accordion("Example images", open=False): | |
| examples = _collect_example_images() | |
| if examples: | |
| gr.Examples(examples=examples, inputs=image_input, outputs=None, label="Click an example to load it") | |
| else: | |
| gr.Markdown("No example images found in the training dataset.") | |
| with gr.Column(scale=1): | |
| vehicle_recognition = gr.Markdown(label="Vehicle recognition") | |
| price_estimate = gr.Markdown(label="Price estimate") | |
| purchase_assessment = gr.Markdown(label="Purchase assessment") | |
| with gr.Accordion("Debug details", open=False): | |
| debug_output = gr.JSON(label="Raw pipeline output") | |
| submit_btn.click( | |
| fn=_run_advisor, | |
| inputs=[image_input, make_model, mileage_km, car_age_years, budget_chf, max_monthly_rate_chf], | |
| outputs=[ | |
| vehicle_recognition, | |
| price_estimate, | |
| purchase_assessment, | |
| debug_output, | |
| ], | |
| ) | |
| clear_btn.click( | |
| fn=_clear_outputs, | |
| inputs=[], | |
| outputs=[ | |
| vehicle_recognition, | |
| price_estimate, | |
| purchase_assessment, | |
| debug_output, | |
| ], | |
| ) | |
| return demo | |
| def main() -> None: | |
| demo = build_interface() | |
| demo.launch(show_error=True) | |
| if __name__ == "__main__": | |
| main() | |