Spaces:
Runtime error
Runtime error
| import gradio as gr | |
| import google.generativeai as genai | |
| import os | |
| import json | |
| import fitz # PyMuPDF | |
| from PIL import Image | |
| import io | |
| import tempfile | |
| import time | |
| import re | |
| import math | |
| from typing import List, Dict, Any, Union, Tuple | |
| # Configure the Gemini API (will use environment variable in production) | |
| def configure_genai_api(): | |
| api_key = os.environ.get("GEMINI_API_KEY") | |
| if not api_key: | |
| # For local testing, you can set a default key | |
| api_key = os.environ.get("GOOGLE_API_KEY") | |
| if not api_key: | |
| return False | |
| genai.configure(api_key=api_key) | |
| return True | |
| # Function to extract images from PDF | |
| def extract_images_from_pdf(pdf_file): | |
| pdf_document = fitz.open(stream=pdf_file, filetype="pdf") | |
| images = [] | |
| page_images = [] | |
| for page_num in range(len(pdf_document)): | |
| page = pdf_document[page_num] | |
| # First try to get embedded images | |
| image_list = page.get_images(full=True) | |
| # If no embedded images, render the page as an image | |
| if not image_list: | |
| pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) | |
| img_bytes = pix.tobytes("png") | |
| img = Image.open(io.BytesIO(img_bytes)) | |
| images.append(img) | |
| page_images.append((page_num + 1, img)) | |
| else: | |
| # Extract embedded images | |
| for img_index, img_info in enumerate(image_list): | |
| xref = img_info[0] | |
| base_image = pdf_document.extract_image(xref) | |
| image_bytes = base_image["image"] | |
| img = Image.open(io.BytesIO(image_bytes)) | |
| # Filter out very small images (likely icons or decorations) | |
| if img.width > 100 and img.height > 100: | |
| images.append(img) | |
| page_images.append((page_num + 1, img)) | |
| # If no images were found, render all pages as images | |
| if not images: | |
| for page_num in range(len(pdf_document)): | |
| page = pdf_document[page_num] | |
| pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) | |
| img_bytes = pix.tobytes("png") | |
| img = Image.open(io.BytesIO(img_bytes)) | |
| images.append(img) | |
| page_images.append((page_num + 1, img)) | |
| return images, page_images | |
| # Extract text from PDF to find measurement scales and dimensions | |
| def extract_measurement_info(pdf_file): | |
| try: | |
| pdf_document = fitz.open(stream=pdf_file, filetype="pdf") | |
| measurement_info = { | |
| "scale": None, | |
| "ceiling_height": None, | |
| "room_dimensions": {} | |
| } | |
| scale_patterns = [ | |
| r'(?i)scale\s*1\s*:\s*(\d+)', | |
| r'(?i)målestokk\s*1\s*:\s*(\d+)', | |
| r'(?i)skala\s*1\s*:\s*(\d+)', | |
| r'1\s*:\s*(\d+)' | |
| ] | |
| height_patterns = [ | |
| r'(?i)ceiling\s*height\s*[=:]?\s*(\d+[\.,]?\d*)\s*m', | |
| r'(?i)takhøyde\s*[=:]?\s*(\d+[\.,]?\d*)\s*m', | |
| r'(?i)høyde\s*[=:]?\s*(\d+[\.,]?\d*)\s*m', | |
| r'(?i)romhøyde\s*[=:]?\s*(\d+[\.,]?\d*)\s*m', | |
| r'(?i)h\s*[=:]?\s*(\d+[\.,]?\d*)\s*m' | |
| ] | |
| room_dim_patterns = [ | |
| r'(?i)(stue|kjøkken|soverom|bad|gang|entré|kontor|bod).*?(\d+[\.,]?\d*)\s*[xX×]\s*(\d+[\.,]?\d*)', | |
| r'(?i)(living|kitchen|bedroom|bathroom|hallway|entrance|office|storage).*?(\d+[\.,]?\d*)\s*[xX×]\s*(\d+[\.,]?\d*)' | |
| ] | |
| for page_num in range(len(pdf_document)): | |
| page = pdf_document[page_num] | |
| text = page.get_text() | |
| # Look for scale information | |
| if not measurement_info["scale"]: | |
| for pattern in scale_patterns: | |
| matches = re.findall(pattern, text) | |
| if matches: | |
| measurement_info["scale"] = int(matches[0]) | |
| break | |
| # Look for ceiling height | |
| if not measurement_info["ceiling_height"]: | |
| for pattern in height_patterns: | |
| matches = re.findall(pattern, text) | |
| if matches: | |
| height = matches[0].replace(',', '.') | |
| measurement_info["ceiling_height"] = float(height) | |
| break | |
| # Look for room dimensions | |
| for pattern in room_dim_patterns: | |
| matches = re.findall(pattern, text) | |
| for match in matches: | |
| room_name = match[0].lower().strip() | |
| width = float(match[1].replace(',', '.')) | |
| length = float(match[2].replace(',', '.')) | |
| measurement_info["room_dimensions"][room_name] = { | |
| "width": width, | |
| "length": length | |
| } | |
| # Default values if not found | |
| if not measurement_info["scale"]: | |
| measurement_info["scale"] = 100 # Common scale for residential floor plans | |
| if not measurement_info["ceiling_height"]: | |
| measurement_info["ceiling_height"] = 2.4 # Standard ceiling height in Norway | |
| return measurement_info | |
| except Exception as e: | |
| print(f"Error extracting measurement info: {e}") | |
| return { | |
| "scale": 100, | |
| "ceiling_height": 2.4, | |
| "room_dimensions": {} | |
| } | |
| # Norwegian room translations | |
| NORWEGIAN_ROOM_NAMES = { | |
| "stue": "living room", | |
| "kjøkken": "kitchen", | |
| "soverom": "bedroom", | |
| "bad": "bathroom", | |
| "toalett": "toilet", | |
| "gang": "hallway", | |
| "entré": "entrance", | |
| "garderobe": "wardrobe", | |
| "balkong": "balcony", | |
| "terrasse": "terrace", | |
| "kontor": "office", | |
| "vaskerom": "laundry room", | |
| "bod": "storage room", | |
| "garasje": "garage", | |
| "trapp": "stairs", | |
| "spisestue": "dining room", | |
| "arbeidsrom": "study", | |
| "loftstue": "loft", | |
| "kjeller": "basement", | |
| "gjesteværelse": "guest room" | |
| } | |
| # The prompt template for Gemini - with Norwegian context and measurement instructions | |
| def create_prompt(floor_plan_description=None, measurement_info=None): | |
| # Add measurement information to the prompt if available | |
| measurement_context = "" | |
| if measurement_info: | |
| measurement_context = f""" | |
| **Important Measurement Information:** | |
| * The floor plan uses a scale of 1:{measurement_info['scale']} | |
| * The standard ceiling height is {measurement_info['ceiling_height']} meters | |
| """ | |
| if measurement_info["room_dimensions"]: | |
| measurement_context += "* Known room dimensions (width × length in meters):\n" | |
| for room, dims in measurement_info["room_dimensions"].items(): | |
| measurement_context += f" - {room}: {dims['width']} × {dims['length']} meters\n" | |
| prompt = f"""You are an expert architectural assistant. I am providing a PDF floor plan of a house from Norway. The floor plan is likely to contain Norwegian text and terminology. Your job is to analyze the layout and extract **complete room-level information** in **structured JSON format** that I can directly use to build a 3D model. | |
| {measurement_context} | |
| Please return the following **JSON structure**, including estimates and spatial relationships: | |
| ``` | |
| [ | |
| {{ | |
| "name": "Room name", | |
| "name_no": "Norwegian room name (if present)", | |
| "area_m2": 0.0, | |
| "position": "approximate location (e.g., north, south-east corner)", | |
| "dimensions_m": {{ | |
| "width": 0.0, | |
| "length": 0.0 | |
| }}, | |
| "windows": 0, | |
| "window_positions": ["north wall", "east wall"], | |
| "doors": 0, | |
| "door_positions": ["interior", "to terrace"], | |
| "connected_rooms": ["Room A", "Room B"], | |
| "has_external_access": true, | |
| "ceiling_height_m": 2.4, | |
| "furniture": ["sofa", "kitchen island"], | |
| "estimated": false | |
| }} | |
| ] | |
| ``` | |
| **Instructions:** | |
| * Include **all rooms**, including utility spaces (garage, hallway, terrace, storage, laundry, etc.). | |
| * Recognize Norwegian room names and provide both Norwegian (name_no) and English (name) versions. | |
| * Calculate area_m2 accurately as width × length. Double-check this calculation for all rooms. | |
| * If any values are not labeled, **estimate them** based on layout scale and set `"estimated": true`. | |
| * Try to determine the **direction/position** of each room (e.g., "northwest corner", "center"). | |
| * Count and identify **doors and windows**, and specify on which **wall** they are located. | |
| * Include `"has_external_access": true` if a room connects directly outside (e.g., terrace, garage, entrance). | |
| * Optionally include `"furniture"` if visible or labeled in the plan. | |
| * Use the ceiling height of {measurement_info["ceiling_height"] if measurement_info else 2.4}m if not otherwise specified. | |
| * **Only return the JSON array. Do not add explanations or extra text.** | |
| **Common Norwegian architectural terms:** | |
| * Stue = Living room | |
| * Kjøkken = Kitchen | |
| * Soverom = Bedroom | |
| * Bad = Bathroom | |
| * Toalett = Toilet | |
| * Gang = Hallway | |
| * Entré = Entrance | |
| * Garderobe = Wardrobe | |
| * Balkong = Balcony | |
| * Terrasse = Terrace | |
| * Kontor = Office | |
| * Vaskerom = Laundry room | |
| * Bod = Storage room | |
| * Garasje = Garage | |
| * Trapp = Stairs | |
| * Spisestue = Dining room | |
| """ | |
| if floor_plan_description: | |
| prompt += f"\n\nAdditional information about the floor plan: {floor_plan_description}" | |
| return prompt | |
| # Function to post-process and validate the JSON results | |
| def validate_and_fix_measurements(json_data, measurement_info=None): | |
| try: | |
| if isinstance(json_data, str): | |
| data = json.loads(json_data) | |
| else: | |
| data = json_data | |
| if not isinstance(data, list): | |
| return json_data | |
| default_ceiling_height = measurement_info.get("ceiling_height", 2.4) if measurement_info else 2.4 | |
| for room in data: | |
| # Fix ceiling height | |
| if room.get("ceiling_height_m") is None or room.get("ceiling_height_m") <= 0: | |
| room["ceiling_height_m"] = default_ceiling_height | |
| room["estimated"] = True | |
| # Check dimensions and area | |
| if "dimensions_m" in room: | |
| width = room["dimensions_m"].get("width", 0) | |
| length = room["dimensions_m"].get("length", 0) | |
| # If we have dimensions but they're invalid, try to fix them | |
| if width <= 0 or length <= 0: | |
| # Check if we have area to calculate dimensions | |
| if room.get("area_m2", 0) > 0: | |
| # Approximate dimensions using a square root (assuming square-ish room) | |
| side = math.sqrt(room["area_m2"]) | |
| room["dimensions_m"]["width"] = round(side, 1) | |
| room["dimensions_m"]["length"] = round(side, 1) | |
| room["estimated"] = True | |
| else: | |
| # Set reasonable defaults | |
| room["dimensions_m"]["width"] = 3.0 | |
| room["dimensions_m"]["length"] = 3.0 | |
| room["area_m2"] = 9.0 | |
| room["estimated"] = True | |
| else: | |
| # Recalculate area based on dimensions | |
| calculated_area = width * length | |
| current_area = room.get("area_m2", 0) | |
| # If area is missing or significantly different, update it | |
| if current_area <= 0 or abs(current_area - calculated_area) > 0.5: | |
| room["area_m2"] = round(calculated_area, 1) | |
| # If we have area but no dimensions, calculate them | |
| elif "area_m2" in room and room["area_m2"] > 0: | |
| # Approximate dimensions using a square root (assuming square-ish room) | |
| side = math.sqrt(room["area_m2"]) | |
| room["dimensions_m"] = { | |
| "width": round(side, 1), | |
| "length": round(side, 1) | |
| } | |
| room["estimated"] = True | |
| # Check if we have room dimensions in our measurement info | |
| if measurement_info and "room_dimensions" in measurement_info: | |
| room_name_lower = room.get("name", "").lower() | |
| room_name_no_lower = room.get("name_no", "").lower() | |
| # Check both English and Norwegian names | |
| for name in [room_name_lower, room_name_no_lower]: | |
| if name in measurement_info["room_dimensions"]: | |
| known_dims = measurement_info["room_dimensions"][name] | |
| room["dimensions_m"] = { | |
| "width": known_dims["width"], | |
| "length": known_dims["length"] | |
| } | |
| room["area_m2"] = round(known_dims["width"] * known_dims["length"], 1) | |
| room["estimated"] = False | |
| break | |
| return json.dumps(data, indent=2) | |
| except Exception as e: | |
| print(f"Error validating measurements: {e}") | |
| return json_data | |
| # Function to call Gemini model | |
| def analyze_floor_plan(images, description=None, model_name='gemini-1.5-flash', measurement_info=None): | |
| try: | |
| api_configured = configure_genai_api() | |
| if not api_configured: | |
| return json.dumps({ | |
| "error": "Gemini API key not configured. Please set the GEMINI_API_KEY environment variable." | |
| }) | |
| # Use specified Gemini model | |
| model = genai.GenerativeModel(model_name) | |
| # Create prompt with optional description and measurement info | |
| prompt = create_prompt(description, measurement_info) | |
| # Call Gemini with the images and prompt | |
| response = model.generate_content([prompt, *images]) | |
| # Extract JSON from the response | |
| response_text = response.text | |
| # Find JSON content between triple backticks if present | |
| json_text = None | |
| if "```" in response_text: | |
| parts = response_text.split("```") | |
| for i, part in enumerate(parts): | |
| part = part.strip() | |
| # Skip empty parts and parts that are just language identifiers | |
| if not part or part.lower() in ["json", "javascript"]: | |
| continue | |
| # Try to parse this part as JSON | |
| try: | |
| json.loads(part) | |
| json_text = part | |
| break | |
| except: | |
| # If this part is followed by another part and it might be a code block | |
| if i + 1 < len(parts): | |
| try: | |
| # Sometimes the JSON content is split across parts strangely | |
| combined = part + parts[i+1].strip() | |
| json.loads(combined) | |
| json_text = combined | |
| break | |
| except: | |
| pass | |
| # If we still didn't find valid JSON, try the second part with common fixes | |
| if not json_text and len(parts) > 1: | |
| potential_json = parts[1].strip() | |
| # Remove "json" language identifier if present | |
| if potential_json.lower().startswith("json"): | |
| potential_json = potential_json[4:].strip() | |
| # Try to fix common JSON issues | |
| potential_json = potential_json.replace("'", '"') # Replace single quotes with double quotes | |
| try: | |
| json.loads(potential_json) | |
| json_text = potential_json | |
| except: | |
| pass | |
| else: | |
| # If no backticks, try the entire response | |
| try: | |
| json.loads(response_text.strip()) | |
| json_text = response_text.strip() | |
| except: | |
| pass | |
| # If we found and validated JSON, return it | |
| if json_text: | |
| try: | |
| parsed_json = json.loads(json_text) | |
| # Validate and fix measurements | |
| validated_json = validate_and_fix_measurements(parsed_json, measurement_info) | |
| return validated_json | |
| except: | |
| # Final fallback - return the response as an error | |
| return json.dumps({ | |
| "error": "Could not parse JSON from model response", | |
| "raw_response": response_text | |
| }) | |
| else: | |
| # No valid JSON found | |
| return json.dumps({ | |
| "error": "Could not extract valid JSON from model response", | |
| "raw_response": response_text | |
| }) | |
| except Exception as e: | |
| # Return a valid JSON with error message | |
| return json.dumps({ | |
| "error": f"Error analyzing floor plan: {str(e)}" | |
| }) | |
| # Function to display the parsed JSON in a more user-friendly view | |
| def create_json_visualization(json_data): | |
| try: | |
| # Try to parse the JSON | |
| data = json.loads(json_data) | |
| # Check if it's an error message | |
| if isinstance(data, dict) and "error" in data: | |
| return f""" | |
| <div style="border: 1px solid #f44336; border-radius: 8px; padding: 16px; background-color: #ffebee;"> | |
| <h3 style="color: #d32f2f; margin-top: 0;">Error</h3> | |
| <p>{data["error"]}</p> | |
| {f'<pre style="background-color: #f5f5f5; padding: 8px; border-radius: 4px; overflow: auto;">{data.get("raw_response", "")}</pre>' if "raw_response" in data else ""} | |
| </div> | |
| """ | |
| # If it's an empty list, show message | |
| if isinstance(data, list) and len(data) == 0: | |
| return """ | |
| <div style="border: 1px solid #ff9800; border-radius: 8px; padding: 16px; background-color: #fff8e1;"> | |
| <h3 style="color: #ef6c00; margin-top: 0;">Ingen rom oppdaget / No Rooms Detected</h3> | |
| <p>Analysen oppdaget ingen rom i plantegningen. Dette kan skyldes: / The analysis did not detect any rooms in the provided floor plan. This might be due to:</p> | |
| <ul> | |
| <li>Dårlig bildekvalitet / Low image quality in the floor plan</li> | |
| <li>Uvanlig plantegningsformat / Non-standard floor plan format</li> | |
| <li>Manglende romnavn eller grenser / Missing room labels or boundaries</li> | |
| </ul> | |
| <p>Prøv å laste opp en tydeligere plantegning eller gi mer kontekst i beskrivelsen. / Try uploading a clearer floor plan or providing more context in the description.</p> | |
| </div> | |
| """ | |
| # If we have valid room data, create the visualization | |
| html = """ | |
| <style> | |
| .room-card { | |
| border: 1px solid #ddd; | |
| border-radius: 8px; | |
| padding: 15px; | |
| margin-bottom: 15px; | |
| background-color: #f9f9f9; | |
| } | |
| .room-name { | |
| font-size: 18px; | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| color: #333; | |
| } | |
| .norwegian-name { | |
| font-style: italic; | |
| color: #555; | |
| font-size: 14px; | |
| margin-left: 8px; | |
| } | |
| .room-details { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 10px; | |
| } | |
| .detail-group { | |
| margin-bottom: 8px; | |
| } | |
| .detail-label { | |
| font-weight: 600; | |
| color: #555; | |
| } | |
| .detail-value { | |
| color: #333; | |
| } | |
| .estimated-badge { | |
| display: inline-block; | |
| background-color: #ffcc00; | |
| color: #333; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| margin-left: 8px; | |
| } | |
| </style> | |
| """ | |
| # Add summary information at the top | |
| total_area = sum(room.get("area_m2", 0) for room in data) | |
| html += f""" | |
| <div style="margin-bottom: 20px; padding: 12px; background-color: #e3f2fd; border-radius: 8px;"> | |
| <h3 style="margin-top: 0; color: #0d47a1;">Romoppsummering / Room Summary</h3> | |
| <p>Totalt antall rom / Total rooms: <strong>{len(data)}</strong></p> | |
| <p>Total areal / Total area: <strong>{total_area:.1f} m²</strong></p> | |
| </div> | |
| """ | |
| for room in data: | |
| estimated = room.get("estimated", False) | |
| estimated_badge = '<span class="estimated-badge">Estimated</span>' if estimated else '' | |
| norwegian_name = f'<span class="norwegian-name">({room.get("name_no", "")})</span>' if "name_no" in room and room["name_no"] else '' | |
| html += f""" | |
| <div class="room-card"> | |
| <div class="room-name">{room.get("name", "Unnamed Room")} {norwegian_name} {estimated_badge}</div> | |
| <div class="room-details"> | |
| <div class="detail-group"> | |
| <div class="detail-label">Area:</div> | |
| <div class="detail-value">{room.get("area_m2", "N/A")} m²</div> | |
| </div> | |
| <div class="detail-group"> | |
| <div class="detail-label">Position:</div> | |
| <div class="detail-value">{room.get("position", "N/A")}</div> | |
| </div> | |
| <div class="detail-group"> | |
| <div class="detail-label">Dimensions:</div> | |
| <div class="detail-value"> | |
| {room.get("dimensions_m", {}).get("width", "N/A")} × {room.get("dimensions_m", {}).get("length", "N/A")} m | |
| </div> | |
| </div> | |
| <div class="detail-group"> | |
| <div class="detail-label">Windows:</div> | |
| <div class="detail-value">{room.get("windows", "N/A")}</div> | |
| </div> | |
| <div class="detail-group"> | |
| <div class="detail-label">Doors:</div> | |
| <div class="detail-value">{room.get("doors", "N/A")}</div> | |
| </div> | |
| <div class="detail-group"> | |
| <div class="detail-label">Ceiling Height:</div> | |
| <div class="detail-value">{room.get("ceiling_height_m", "N/A")} m</div> | |
| </div> | |
| </div> | |
| <div style="margin-top: 10px;"> | |
| <div class="detail-label">Window Positions:</div> | |
| <div class="detail-value">{", ".join(room.get("window_positions", ["None"]))}</div> | |
| </div> | |
| <div style="margin-top: 10px;"> | |
| <div class="detail-label">Door Positions:</div> | |
| <div class="detail-value">{", ".join(room.get("door_positions", ["None"]))}</div> | |
| </div> | |
| <div style="margin-top: 10px;"> | |
| <div class="detail-label">Connected Rooms:</div> | |
| <div class="detail-value">{", ".join(room.get("connected_rooms", ["None"]))}</div> | |
| </div> | |
| <div style="margin-top: 10px;"> | |
| <div class="detail-label">External Access:</div> | |
| <div class="detail-value">{"Yes" if room.get("has_external_access", False) else "No"}</div> | |
| </div> | |
| <div style="margin-top: 10px;"> | |
| <div class="detail-label">Furniture:</div> | |
| <div class="detail-value">{", ".join(room.get("furniture", ["None"]))}</div> | |
| </div> | |
| </div> | |
| """ | |
| return html | |
| except Exception as e: | |
| return f""" | |
| <div style="border: 1px solid #f44336; border-radius: 8px; padding: 16px; background-color: #ffebee;"> | |
| <h3 style="color: #d32f2f; margin-top: 0;">Error Visualizing Data</h3> | |
| <p>{str(e)}</p> | |
| <pre style="background-color: #f5f5f5; padding: 8px; border-radius: 4px; overflow: auto;">{json_data}</pre> | |
| </div> | |
| """ | |
| # Function to save JSON to file | |
| def save_json_to_file(json_data, prefix="floor_plan_analysis"): | |
| if not json_data: | |
| return None | |
| try: | |
| # Create temporary file | |
| timestamp = time.strftime("%Y%m%d-%H%M%S") | |
| filename = f"{prefix}_{timestamp}.json" | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".json", mode="w") as f: | |
| f.write(json_data) | |
| return f.name | |
| except Exception as e: | |
| print(f"Error saving JSON to file: {str(e)}") | |
| return None | |
| # Gradio interface | |
| def create_interface(): | |
| with gr.Blocks(title="Floor Plan Analyzer", theme=gr.themes.Soft()) as app: | |
| # Header | |
| gr.Markdown(""" | |
| # 🏠 Arkitektonisk Plantegningsanalysator / Architectural Floor Plan Analyzer | |
| Last opp en PDF av en arkitektonisk plantegning for å få detaljert rominformasjon i JSON-format. | |
| Upload a PDF of an architectural floor plan to get detailed room-level information in JSON format. | |
| Dette verktøyet bruker Google's Gemini Pro Vision-modell for å analysere plantegninger og ekstrahere strukturerte data. | |
| This tool uses Google's Gemini Pro Vision model to analyze floor plans and extract structured data. | |
| """) | |
| # Main content area | |
| with gr.Tabs(): | |
| # Upload and analyze tab | |
| with gr.TabItem("Analyser Plantegning / Analyze Floor Plan"): | |
| with gr.Row(): | |
| # Left column - Inputs | |
| with gr.Column(scale=2): | |
| # Model selection | |
| model_dropdown = gr.Dropdown( | |
| choices=["gemini-1.5-flash", "gemini-1.5-pro-latest"], | |
| label="Velg Gemini-modell / Select Gemini Model", | |
| value="gemini-1.5-flash", | |
| interactive=True | |
| ) | |
| # File upload | |
| pdf_input = gr.File( | |
| label="Last opp plantegning PDF / Upload Floor Plan PDF", | |
| file_types=[".pdf"], | |
| type="binary" | |
| ) | |
| # Description field | |
| description_input = gr.Textbox( | |
| label="Valgfri beskrivelse / Optional Description", | |
| placeholder="F.eks. 'Dette er et hus med 3 soverom' / E.g., 'This is a house with 3 bedrooms'", | |
| lines=3 | |
| ) | |
| # Analysis button | |
| with gr.Row(): | |
| analyze_button = gr.Button("Analyser Plantegning / Analyze Floor Plan", variant="primary", scale=2) | |
| clear_button = gr.Button("Tøm / Clear", variant="secondary", scale=1) | |
| # Right column - Outputs | |
| with gr.Column(scale=3): | |
| # Status message | |
| status_message = gr.Markdown("Last opp en plantegning PDF og klikk 'Analyser' for å starte / Upload a PDF floor plan and click 'Analyze' to start") | |
| # Progress indicator | |
| with gr.Row(visible=False) as progress_row: | |
| progress = gr.Textbox(value="Analyserer plantegning... / Analyzing floor plan...", label="Status") | |
| # Image gallery | |
| image_output = gr.Gallery( | |
| label="Ekstraherte plantegningsbilder / Extracted Floor Plan Images", | |
| columns=2, | |
| rows=2, | |
| height=400, | |
| object_fit="contain" | |
| ) | |
| # JSON results area (tabbed output) | |
| with gr.Tabs(): | |
| with gr.TabItem("JSON Output"): | |
| output_json = gr.JSON(label="Romanalyseresultat / Room Analysis Result") | |
| download_button = gr.Button("Last ned JSON / Download JSON", variant="secondary") | |
| json_file_output = gr.File(label="Last ned JSON-fil / Download JSON File", visible=False) | |
| with gr.TabItem("Visuell oppsummering / Visual Summary"): | |
| html_output = gr.HTML(label="Romanalysevisualisering / Room Analysis Visualization") | |
| # About tab | |
| with gr.TabItem("Om verktøyet / About & Help"): | |
| gr.Markdown(""" | |
| ## Om dette verktøyet / About This Tool | |
| Denne Arkitektoniske Plantegningsanalysatoren bruker Google's Gemini AI for å ekstrahere strukturert informasjon fra plantegninger i PDF-format. | |
| This Architectural Floor Plan Analyzer uses Google's Gemini AI to extract structured information from floor plan PDFs. | |
| ### Hvordan det fungerer / How it Works | |
| 1. **Last opp en PDF / Upload a PDF**: Verktøyet ekstraherer bilder fra din plantegnings-PDF | |
| 2. **Analyse / Analysis**: Gemini AI-modellen undersøker plantegningen og identifiserer rom, dimensjoner og funksjoner | |
| 3. **Resultater / Results**: Du mottar en strukturert JSON-utgang med detaljert informasjon om hvert rom | |
| ### Utgangsformat / Output Format | |
| JSON-utdataene inkluderer / The JSON output includes: | |
| - Romnavn og plasseringer / Room names and locations | |
| - Areal og dimensjoner / Area and dimensions | |
| - Vinduer og dører (antall og posisjoner) / Windows and doors (count and positions) | |
| - Tilkoblede rom / Connected rooms | |
| - Informasjon om ekstern tilgang / External access information | |
| - Møbler (hvis synlig i planen) / Furniture (if visible in the plan) | |
| - Takhøyde (standard: 2,4 m) / Ceiling height (default: 2.4m) | |
| ### Tips for beste resultater / Tips for Best Results | |
| - Bruk klare plantegninger av høy kvalitet / Use clear, high-quality floor plans | |
| - Sørg for at romnavnene er synlige / Make sure room labels are visible | |
| - Gi ytterligere kontekst i beskrivelsefeltet om nødvendig / Provide additional context in the description field if needed | |
| - Prøv forskjellige modeller hvis resultatene ikke er tilfredsstillende / Try different models if results are not satisfactory | |
| ### Krav / Requirements | |
| Dette verktøyet krever en gyldig Gemini API-nøkkel som er satt som en miljøvariabel. | |
| This tool requires a valid Gemini API key to be set as an environment variable. | |
| For spørsmål eller problemer, vennligst kontakt utvikleren. | |
| For questions or issues, please contact the developer. | |
| """) | |
| # Event handlers | |
| def process_pdf(pdf_file, description, model_name): | |
| if pdf_file is None: | |
| return ( | |
| gr.update(visible=False), | |
| "Vennligst last opp en PDF-fil. / Please upload a PDF file.", | |
| None, | |
| json.dumps({"error": "No PDF file uploaded"}), | |
| "<div>Error: No PDF file uploaded</div>", | |
| gr.update(value=None) | |
| ) | |
| # Show progress | |
| yield ( | |
| gr.update(visible=True), | |
| "Ekstraherer bilder fra PDF... / Extracting images from PDF...", | |
| None, | |
| None, | |
| None, | |
| gr.update(value=None) | |
| ) | |
| try: | |
| # Extract images from PDF | |
| images, page_images = extract_images_from_pdf(pdf_file) | |
| if not images: | |
| error_json = json.dumps({"error": "Could not extract any images from the PDF"}) | |
| yield ( | |
| gr.update(visible=False), | |
| "Feil: Kunne ikke ekstrahere bilder fra PDF. / Error: Could not extract any images from the PDF.", | |
| None, | |
| error_json, | |
| create_json_visualization(error_json), | |
| gr.update(value=None) | |
| ) | |
| return | |
| # Update progress | |
| yield ( | |
| gr.update(visible=True), | |
| "Analyserer plantegning med Gemini AI... / Analyzing floor plan with Gemini AI...", | |
| [img[1] for img in page_images], | |
| None, | |
| None, | |
| gr.update(value=None) | |
| ) | |
| # Call Gemini for analysis | |
| json_result = analyze_floor_plan(images, description, model_name) | |
| # Create visualization | |
| html_viz = create_json_visualization(json_result) | |
| # Save JSON to file | |
| json_file_path = save_json_to_file(json_result) | |
| # Hide progress | |
| yield ( | |
| gr.update(visible=False), | |
| "Analyse fullført! / Analysis complete!", | |
| [img[1] for img in page_images], | |
| json_result, | |
| html_viz, | |
| gr.update(value=json_file_path) | |
| ) | |
| except Exception as e: | |
| error_msg = str(e) | |
| error_json = json.dumps({"error": f"Error processing PDF: {error_msg}"}) | |
| error_html = create_json_visualization(error_json) | |
| yield ( | |
| gr.update(visible=False), | |
| f"Feil: {error_msg} / Error: {error_msg}", | |
| None, | |
| error_json, | |
| error_html, | |
| gr.update(value=None) | |
| ) | |
| def clear_outputs(): | |
| return ( | |
| gr.update(visible=False), | |
| "Last opp en plantegning PDF og klikk 'Analyser' for å starte / Upload a PDF floor plan and click 'Analyze' to start", | |
| None, | |
| None, | |
| None, | |
| gr.update(value=None) | |
| ) | |
| analyze_button.click( | |
| fn=process_pdf, | |
| inputs=[pdf_input, description_input, model_dropdown], | |
| outputs=[progress_row, status_message, image_output, output_json, html_output, json_file_output] | |
| ) | |
| clear_button.click( | |
| fn=clear_outputs, | |
| inputs=[], | |
| outputs=[progress_row, status_message, image_output, output_json, html_output, json_file_output] | |
| ) | |
| download_button.click( | |
| fn=lambda x: x, | |
| inputs=[json_file_output], | |
| outputs=[json_file_output] | |
| ) | |
| return app | |
| # Create and launch the interface | |
| demo = create_interface() | |
| # For local testing | |
| if __name__ == "__main__": | |
| demo.launch() | |
| else: | |
| # For Hugging Face Spaces | |
| demo.launch(share=False) |