Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -126,44 +126,7 @@ class FloorPlanProcessor:
|
|
| 126 |
logger.error(f"Image error: {str(e)}")
|
| 127 |
pdf.error = str(e)
|
| 128 |
return False
|
| 129 |
-
|
| 130 |
-
def _preprocess_floor_plan(self, image):
|
| 131 |
-
"""
|
| 132 |
-
Enhance floor plan image for better analysis
|
| 133 |
-
"""
|
| 134 |
-
try:
|
| 135 |
-
logger.info(f"Preprocessing image: {image.size}")
|
| 136 |
-
|
| 137 |
-
# Convert to grayscale
|
| 138 |
-
if image.mode != 'L':
|
| 139 |
-
gray = image.convert('L')
|
| 140 |
-
else:
|
| 141 |
-
gray = image
|
| 142 |
-
|
| 143 |
-
# Enhance contrast
|
| 144 |
-
enhancer = ImageEnhance.Contrast(gray)
|
| 145 |
-
gray = enhancer.enhance(1.5)
|
| 146 |
-
logger.info("β Contrast enhanced")
|
| 147 |
-
|
| 148 |
-
# Sharpen
|
| 149 |
-
gray = gray.filter(ImageFilter.SHARPEN)
|
| 150 |
-
logger.info("β Sharpened")
|
| 151 |
-
|
| 152 |
-
# Remove noise
|
| 153 |
-
gray = gray.filter(ImageFilter.MedianFilter(size=3))
|
| 154 |
-
logger.info("β Noise removed")
|
| 155 |
-
|
| 156 |
-
# Convert back to RGB
|
| 157 |
-
result = gray.convert('RGB')
|
| 158 |
-
logger.info(f"β Preprocessing complete: {result.size}")
|
| 159 |
-
|
| 160 |
-
return result
|
| 161 |
-
|
| 162 |
-
except Exception as e:
|
| 163 |
-
logger.error(f"Preprocessing error: {str(e)}")
|
| 164 |
-
return image
|
| 165 |
-
|
| 166 |
-
|
| 167 |
async def extract_images_from_pdf(self, pdf, file_content):
|
| 168 |
try:
|
| 169 |
pdf_document = fitz.open(stream=file_content, filetype="pdf")
|
|
@@ -195,6 +158,74 @@ class FloorPlanProcessor:
|
|
| 195 |
pdf.error = str(e)
|
| 196 |
return False
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
async def analyze_floor_plan(self, pdf_id, description=None):
|
| 199 |
pdf = self.pdfs.get(pdf_id)
|
| 200 |
if not pdf:
|
|
@@ -211,7 +242,6 @@ class FloorPlanProcessor:
|
|
| 211 |
# Use ONLY the first/best image for single file analysis
|
| 212 |
best_image = self._select_single_best_image(pdf.images)
|
| 213 |
best_image = self._preprocess_floor_plan(best_image)
|
| 214 |
-
|
| 215 |
optimized_image = self._optimize_image(best_image, target_size=2048)
|
| 216 |
|
| 217 |
logger.info(f"Using single image: {optimized_image.size[0]}x{optimized_image.size[1]}px")
|
|
@@ -259,35 +289,6 @@ class FloorPlanProcessor:
|
|
| 259 |
logger.warning("All attempts failed, using fallback")
|
| 260 |
return self._generate_fallback(pdf.measurement_info)
|
| 261 |
|
| 262 |
-
def _select_single_best_image(self, images):
|
| 263 |
-
"""Select the single best image"""
|
| 264 |
-
if len(images) == 1:
|
| 265 |
-
return images[0]
|
| 266 |
-
|
| 267 |
-
# Score by area (largest = best for floor plans)
|
| 268 |
-
scored = [(img.size[0] * img.size[1], img) for img in images]
|
| 269 |
-
scored.sort(reverse=True, key=lambda x: x[0])
|
| 270 |
-
|
| 271 |
-
best = scored[0][1]
|
| 272 |
-
logger.info(f"Selected best from {len(images)} images")
|
| 273 |
-
return best
|
| 274 |
-
|
| 275 |
-
def _optimize_image(self, image, target_size=2048):
|
| 276 |
-
"""Optimize image for analysis"""
|
| 277 |
-
if image.mode not in ('RGB', 'L'):
|
| 278 |
-
image = image.convert('RGB')
|
| 279 |
-
|
| 280 |
-
width, height = image.size
|
| 281 |
-
|
| 282 |
-
if width > target_size or height > target_size:
|
| 283 |
-
ratio = target_size / max(width, height)
|
| 284 |
-
new_width = int(width * ratio)
|
| 285 |
-
new_height = int(height * ratio)
|
| 286 |
-
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
| 287 |
-
logger.info(f"Resized: {width}x{height} β {new_width}x{new_height}")
|
| 288 |
-
|
| 289 |
-
return image
|
| 290 |
-
|
| 291 |
async def _analyze_with_gemini(self, image, measurement_info, description, timeout, attempt=0):
|
| 292 |
"""Analyze with Gemini API"""
|
| 293 |
prompt = self._create_detailed_prompt(description, measurement_info)
|
|
@@ -516,7 +517,7 @@ Standard takhΓΈyde: {measurement_info['ceiling_height']}m
|
|
| 516 |
|
| 517 |
app = FastAPI(
|
| 518 |
title="Floor Plan API",
|
| 519 |
-
version="1.0.
|
| 520 |
docs_url="/"
|
| 521 |
)
|
| 522 |
|
|
@@ -673,8 +674,9 @@ async def startup_event():
|
|
| 673 |
os.makedirs("logs", exist_ok=True)
|
| 674 |
|
| 675 |
logger.info("\n" + "="*60)
|
| 676 |
-
logger.info("Floor Plan API -
|
| 677 |
logger.info(f"Model: gemini-2.5-pro")
|
|
|
|
| 678 |
logger.info(f"API Key: {'SET' if GOOGLE_API_KEY else 'NOT SET'}")
|
| 679 |
logger.info(f"Port: 7860")
|
| 680 |
logger.info("="*60 + "\n")
|
|
|
|
| 126 |
logger.error(f"Image error: {str(e)}")
|
| 127 |
pdf.error = str(e)
|
| 128 |
return False
|
| 129 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
async def extract_images_from_pdf(self, pdf, file_content):
|
| 131 |
try:
|
| 132 |
pdf_document = fitz.open(stream=file_content, filetype="pdf")
|
|
|
|
| 158 |
pdf.error = str(e)
|
| 159 |
return False
|
| 160 |
|
| 161 |
+
def _select_single_best_image(self, images):
|
| 162 |
+
"""Select the single best image"""
|
| 163 |
+
if len(images) == 1:
|
| 164 |
+
return images[0]
|
| 165 |
+
|
| 166 |
+
# Score by area (largest = best for floor plans)
|
| 167 |
+
scored = [(img.size[0] * img.size[1], img) for img in images]
|
| 168 |
+
scored.sort(reverse=True, key=lambda x: x[0])
|
| 169 |
+
|
| 170 |
+
best = scored[0][1]
|
| 171 |
+
logger.info(f"Selected best from {len(images)} images")
|
| 172 |
+
return best
|
| 173 |
+
|
| 174 |
+
def _preprocess_floor_plan(self, image):
|
| 175 |
+
"""
|
| 176 |
+
Enhance floor plan image for better analysis
|
| 177 |
+
- Improves contrast (helps model see room boundaries better)
|
| 178 |
+
- Sharpens edges (makes text/dimensions clearer)
|
| 179 |
+
- Removes noise (reduces confusion from scan artifacts)
|
| 180 |
+
"""
|
| 181 |
+
try:
|
| 182 |
+
logger.info(f"Preprocessing image: {image.size}")
|
| 183 |
+
|
| 184 |
+
# Step 1: Convert to grayscale for processing
|
| 185 |
+
if image.mode != 'L':
|
| 186 |
+
gray = image.convert('L')
|
| 187 |
+
else:
|
| 188 |
+
gray = image
|
| 189 |
+
|
| 190 |
+
# Step 2: Enhance contrast (1.5x = moderate boost)
|
| 191 |
+
enhancer = ImageEnhance.Contrast(gray)
|
| 192 |
+
gray = enhancer.enhance(1.5)
|
| 193 |
+
logger.info("β Contrast enhanced")
|
| 194 |
+
|
| 195 |
+
# Step 3: Sharpen to make text/lines clearer
|
| 196 |
+
gray = gray.filter(ImageFilter.SHARPEN)
|
| 197 |
+
logger.info("β Sharpened")
|
| 198 |
+
|
| 199 |
+
# Step 4: Remove noise with median filter
|
| 200 |
+
gray = gray.filter(ImageFilter.MedianFilter(size=3))
|
| 201 |
+
logger.info("β Noise removed")
|
| 202 |
+
|
| 203 |
+
# Step 5: Convert back to RGB for Gemini
|
| 204 |
+
result = gray.convert('RGB')
|
| 205 |
+
logger.info(f"β Preprocessing complete: {result.size}")
|
| 206 |
+
|
| 207 |
+
return result
|
| 208 |
+
|
| 209 |
+
except Exception as e:
|
| 210 |
+
logger.error(f"Preprocessing error: {str(e)}")
|
| 211 |
+
return image # Return original if preprocessing fails
|
| 212 |
+
|
| 213 |
+
def _optimize_image(self, image, target_size=2048):
|
| 214 |
+
"""Optimize image for analysis"""
|
| 215 |
+
if image.mode not in ('RGB', 'L'):
|
| 216 |
+
image = image.convert('RGB')
|
| 217 |
+
|
| 218 |
+
width, height = image.size
|
| 219 |
+
|
| 220 |
+
if width > target_size or height > target_size:
|
| 221 |
+
ratio = target_size / max(width, height)
|
| 222 |
+
new_width = int(width * ratio)
|
| 223 |
+
new_height = int(height * ratio)
|
| 224 |
+
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
| 225 |
+
logger.info(f"Resized: {width}x{height} β {new_width}x{new_height}")
|
| 226 |
+
|
| 227 |
+
return image
|
| 228 |
+
|
| 229 |
async def analyze_floor_plan(self, pdf_id, description=None):
|
| 230 |
pdf = self.pdfs.get(pdf_id)
|
| 231 |
if not pdf:
|
|
|
|
| 242 |
# Use ONLY the first/best image for single file analysis
|
| 243 |
best_image = self._select_single_best_image(pdf.images)
|
| 244 |
best_image = self._preprocess_floor_plan(best_image)
|
|
|
|
| 245 |
optimized_image = self._optimize_image(best_image, target_size=2048)
|
| 246 |
|
| 247 |
logger.info(f"Using single image: {optimized_image.size[0]}x{optimized_image.size[1]}px")
|
|
|
|
| 289 |
logger.warning("All attempts failed, using fallback")
|
| 290 |
return self._generate_fallback(pdf.measurement_info)
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
async def _analyze_with_gemini(self, image, measurement_info, description, timeout, attempt=0):
|
| 293 |
"""Analyze with Gemini API"""
|
| 294 |
prompt = self._create_detailed_prompt(description, measurement_info)
|
|
|
|
| 517 |
|
| 518 |
app = FastAPI(
|
| 519 |
title="Floor Plan API",
|
| 520 |
+
version="1.0.7",
|
| 521 |
docs_url="/"
|
| 522 |
)
|
| 523 |
|
|
|
|
| 674 |
os.makedirs("logs", exist_ok=True)
|
| 675 |
|
| 676 |
logger.info("\n" + "="*60)
|
| 677 |
+
logger.info("Floor Plan API - Optimized Version")
|
| 678 |
logger.info(f"Model: gemini-2.5-pro")
|
| 679 |
+
logger.info(f"With Image Preprocessing: YES")
|
| 680 |
logger.info(f"API Key: {'SET' if GOOGLE_API_KEY else 'NOT SET'}")
|
| 681 |
logger.info(f"Port: 7860")
|
| 682 |
logger.info("="*60 + "\n")
|