Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,13 @@
|
|
| 1 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
#
|
| 4 |
st.set_page_config(
|
| 5 |
page_title="Home Value Maximizer",
|
| 6 |
page_icon="🏡",
|
|
@@ -8,703 +15,131 @@ st.set_page_config(
|
|
| 8 |
initial_sidebar_state="collapsed"
|
| 9 |
)
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
# Get current date
|
| 24 |
-
current_date = datetime.now().strftime("%B %d, %Y")
|
| 25 |
-
|
| 26 |
-
# Set API key from Hugging Face secrets
|
| 27 |
-
try:
|
| 28 |
-
OPENAI_API_KEY = st.secrets["OPENAI_API_KEY"]
|
| 29 |
-
except:
|
| 30 |
-
OPENAI_API_KEY = "" # Will be prompted to enter API key if not in secrets
|
| 31 |
-
|
| 32 |
-
# Session state initialization
|
| 33 |
-
if "uploaded_images" not in st.session_state:
|
| 34 |
-
st.session_state["uploaded_images"] = []
|
| 35 |
-
if "api_key" not in st.session_state:
|
| 36 |
-
st.session_state["api_key"] = OPENAI_API_KEY
|
| 37 |
-
if "token_usage" not in st.session_state:
|
| 38 |
-
st.session_state["token_usage"] = {}
|
| 39 |
-
|
| 40 |
-
# Function to encode image to base64
|
| 41 |
-
def encode_image(image):
|
| 42 |
-
try:
|
| 43 |
-
# Convert RGBA to RGB if needed
|
| 44 |
-
if image.mode == 'RGBA':
|
| 45 |
-
background = Image.new('RGB', image.size, (255, 255, 255))
|
| 46 |
-
background.paste(image, (0, 0), image)
|
| 47 |
-
image = background
|
| 48 |
-
elif image.mode not in ['RGB', 'L']:
|
| 49 |
-
# Convert any other mode to RGB
|
| 50 |
-
image = image.convert('RGB')
|
| 51 |
-
|
| 52 |
-
# Verify the image has valid dimensions
|
| 53 |
-
if image.width <= 0 or image.height <= 0:
|
| 54 |
-
raise ValueError("Image has invalid dimensions: width or height ≤ 0")
|
| 55 |
-
|
| 56 |
-
# Resize large images to reduce token usage
|
| 57 |
-
max_size = (1024, 1024)
|
| 58 |
-
if image.width > max_size[0] or image.height > max_size[1]:
|
| 59 |
-
image.thumbnail(max_size, Image.LANCZOS)
|
| 60 |
-
|
| 61 |
-
# Save to buffer with error handling
|
| 62 |
-
buffered = BytesIO()
|
| 63 |
-
image.save(buffered, format="JPEG", quality=85)
|
| 64 |
-
if buffered.getvalue() == b'':
|
| 65 |
-
raise ValueError("Generated empty image data")
|
| 66 |
-
|
| 67 |
-
# Encode to base64
|
| 68 |
-
encoded = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
| 69 |
-
if not encoded:
|
| 70 |
-
raise ValueError("Base64 encoding produced empty result")
|
| 71 |
-
|
| 72 |
-
return encoded
|
| 73 |
-
except Exception as e:
|
| 74 |
-
raise Exception(f"Image encoding failed: {str(e)}")
|
| 75 |
|
| 76 |
-
#
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
return text
|
| 88 |
|
| 89 |
-
#
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
return "Error: API Key is required for analysis."
|
| 93 |
-
|
| 94 |
-
if not images or len(images) == 0:
|
| 95 |
-
return "Error: No valid images provided for analysis. Please upload at least one image of your home."
|
| 96 |
-
|
| 97 |
-
headers = {
|
| 98 |
-
"Content-Type": "application/json",
|
| 99 |
-
"Authorization": f"Bearer {api_key}"
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
# Construct the message content
|
| 103 |
-
content = [
|
| 104 |
-
{"type": "text", "text": f"""Analyze these home photos and provide EXTREMELY SPECIFIC strategies to maximize the selling price.
|
| 105 |
-
The owner plans to sell within {timeframe}. Today's date is {current_date}. {additional_details}
|
| 106 |
-
|
| 107 |
-
Be incredibly detailed and specific about what you see in each image. Reference exact features, colors, materials, and conditions.
|
| 108 |
-
Provide precise recommendations with specific products, materials, colors, and techniques.
|
| 109 |
-
|
| 110 |
-
IMPORTANT: Ensure all your text is properly formatted with spaces between numbers and words, and proper spacing after punctuation.
|
| 111 |
-
"""}
|
| 112 |
-
]
|
| 113 |
-
|
| 114 |
-
# Add images to the message with proper format for vision API
|
| 115 |
-
try:
|
| 116 |
-
for i, img in enumerate(images):
|
| 117 |
-
try:
|
| 118 |
-
base64_img = encode_image(img)
|
| 119 |
-
content.append({
|
| 120 |
-
"type": "image_url",
|
| 121 |
-
"image_url": {
|
| 122 |
-
"url": f"data:image/jpeg;base64,{base64_img}",
|
| 123 |
-
"detail": "low" # Use low detail to reduce token usage
|
| 124 |
-
}
|
| 125 |
-
})
|
| 126 |
-
except Exception as img_error:
|
| 127 |
-
# Log error but continue with other images
|
| 128 |
-
print(f"Error processing image {i}: {str(img_error)}")
|
| 129 |
-
except Exception as e:
|
| 130 |
-
return f"Error preparing images for analysis: {str(e)}"
|
| 131 |
-
|
| 132 |
-
# Enhanced system prompt for more specific and actionable recommendations
|
| 133 |
-
system_prompt = """You are an expert real estate advisor with deep knowledge of home selling strategies. You analyze home photos to provide EXTREMELY SPECIFIC, actionable recommendations that will maximize the property's selling price.
|
| 134 |
-
|
| 135 |
-
## IMPORTANT: ONLY make recommendations based on what you can actually see in the photos. DO NOT make assumptions about areas not shown (like exterior if only interior is shown or vice versa).
|
| 136 |
-
|
| 137 |
-
## FORMAT YOUR RESPONSE LIKE THIS:
|
| 138 |
-
|
| 139 |
-
### 📋 KEY RECOMMENDATIONS SUMMARY
|
| 140 |
-
Begin with this exact text: "**HOME VALUE MAXIMIZER SUMMARY**"
|
| 141 |
|
| 142 |
-
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
-
|
| 147 |
-
-
|
| 148 |
-
-
|
| 149 |
|
| 150 |
### 👁️ AREAS ANALYZED
|
| 151 |
-
|
| 152 |
|
| 153 |
### 🌟 TOP PRICE-MAXIMIZING PRIORITIES
|
| 154 |
-
|
| 155 |
-
- **Exact issue you see**: Be incredibly specific about what you observe in the photo (e.g., "The beige laminate countertops in the kitchen are visibly worn with scratches near the sink area")
|
| 156 |
-
- **Precise recommendation**: Specify EXACT materials, colors, products, or contractors (e.g., "Replace with Calacatta Quartz countertops in white with subtle gray veining")
|
| 157 |
-
- **Specific cost estimate**: Give narrow ranges (e.g., "$2,800-$3,200")
|
| 158 |
-
- **Precise value impact**: Quantify the increase (e.g., "Estimated to add $8,000-$10,000 to home value")
|
| 159 |
-
- **Timeline**: Exact number of days needed and mention if permits are required
|
| 160 |
|
| 161 |
-
### 🔨 QUICK WINS (1
|
| 162 |
-
List 5
|
| 163 |
-
- **Kitchen**: Name specific issues, specific solutions, specific products/colors/materials
|
| 164 |
-
- **Living Spaces**: Name specific issues, specific solutions, specific products/colors/materials
|
| 165 |
-
- **Bathroom**: Name specific issues, specific solutions, specific products/colors/materials
|
| 166 |
-
Only include categories that are actually visible in the photos.
|
| 167 |
-
Include PRECISE cost estimates, DIY or contractor recommendations, and EXACT product suggestions.
|
| 168 |
|
| 169 |
### 📊 SPECIFIC PRICING STRATEGY
|
| 170 |
-
|
| 171 |
-
-
|
| 172 |
-
- **Specific pricing psychology**: e.g., "$399,900 rather than $400,000" - MAKE SURE TO INCLUDE SPACES BETWEEN NUMBERS AND TEXT
|
| 173 |
-
- **Exact timing**: Specific days of week and dates based on current date and selling timeframe
|
| 174 |
-
- **Market-specific advice**: Relate to current market conditions with specific details
|
| 175 |
-
|
| 176 |
-
IMPORTANT: ALWAYS use proper spacing in your text. For example:
|
| 177 |
-
- CORRECT: "List at $399,900 to appeal psychologically to buyers under $400k."
|
| 178 |
-
- INCORRECT: "List at $399,900toappealpsychologicallytobuyers."
|
| 179 |
|
| 180 |
### 📸 DETAILED MARKETING PLAN
|
| 181 |
-
|
| 182 |
-
- **Exactly which room features**: Name specific architectural elements or features to highlight
|
| 183 |
-
- **Specific photography angles**: Exact camera positions and times of day for optimal lighting
|
| 184 |
-
- **Specific staging items**: Name exact furniture pieces, decor items, or color accents to add/remove
|
| 185 |
-
- **Exact virtual tools**: Name specific apps or services for virtual tours/staging
|
| 186 |
|
| 187 |
### 📝 SPECIFIC NEGOTIATION TACTICS
|
| 188 |
-
|
| 189 |
-
- **Exact language**: Provide specific scripts for countering common objections
|
| 190 |
-
- **Specific inspection strategy**: Name exact items to fix pre-inspection
|
| 191 |
-
- **Precise contingency planning**: Specific strategies for common issues based on the home's visible condition
|
| 192 |
|
| 193 |
### ⚠️ CRITICAL ISSUES TO ADDRESS
|
| 194 |
-
List 3
|
| 195 |
|
| 196 |
### 📅 DETAILED TIMELINE WITH DATES
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
For example, if selling within 1-3 months:
|
| 200 |
-
- By [EXACT DATE]: Complete specific kitchen updates (list precisely what)
|
| 201 |
-
- By [EXACT DATE]: Finish specific bathroom improvements (list precisely what)
|
| 202 |
-
- By [EXACT DATE]: Address specific interior issues (list precisely what)
|
| 203 |
-
- [EXACT DATE]: Schedule professional photography
|
| 204 |
-
- [EXACT DATE]: List the home on the market
|
| 205 |
|
| 206 |
### 🚫 AREAS NOT VISIBLE
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
## IMPORTANT GUIDELINES:
|
| 210 |
-
1. ONLY make recommendations based on what you can ACTUALLY SEE in the photos. Never make recommendations for areas not shown.
|
| 211 |
-
2. Be EXTREMELY SPECIFIC about what you see in each photo - reference exact details, colors, materials
|
| 212 |
-
3. Give PRECISE product recommendations when possible - name brands, models, colors
|
| 213 |
-
4. Provide EXACT cost estimates in narrow ranges
|
| 214 |
-
5. Specify CLEAR timelines with CALENDAR DATES based on today's date (${current_date})
|
| 215 |
-
6. Make all advice HYPER-SPECIFIC to the actual property in the photos
|
| 216 |
-
7. NEVER give generic advice - every recommendation should directly reference visible elements
|
| 217 |
-
8. ALWAYS use proper spacing in text - add spaces between numbers and words, after punctuation, etc.
|
| 218 |
"""
|
| 219 |
-
|
| 220 |
-
# Replace placeholders in the system prompt
|
| 221 |
-
formatted_system_prompt = system_prompt.replace("${current_date}", current_date).replace("${timeframe}", timeframe)
|
| 222 |
-
|
| 223 |
-
# Create the API payload with enhanced system prompt
|
| 224 |
-
payload = {
|
| 225 |
-
"model": MODEL_NAME,
|
| 226 |
-
"messages": [
|
| 227 |
-
{
|
| 228 |
-
"role": "system",
|
| 229 |
-
"content": formatted_system_prompt
|
| 230 |
-
},
|
| 231 |
-
{
|
| 232 |
-
"role": "user",
|
| 233 |
-
"content": content
|
| 234 |
-
}
|
| 235 |
-
],
|
| 236 |
-
"max_tokens": 2500
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
-
try:
|
| 240 |
-
response = requests.post(API_URL, headers=headers, json=payload, timeout=90)
|
| 241 |
-
|
| 242 |
-
if response.status_code == 200:
|
| 243 |
-
response_data = response.json()
|
| 244 |
-
|
| 245 |
-
# Store token usage data
|
| 246 |
-
if "usage" in response_data:
|
| 247 |
-
st.session_state["token_usage"] = response_data["usage"]
|
| 248 |
-
|
| 249 |
-
# Get the content and fix any formatting issues
|
| 250 |
-
content = response_data["choices"][0]["message"]["content"]
|
| 251 |
-
content = fix_formatting(content)
|
| 252 |
-
|
| 253 |
-
return content
|
| 254 |
-
else:
|
| 255 |
-
error_text = f"API Error: {response.status_code}"
|
| 256 |
-
if response.text:
|
| 257 |
-
try:
|
| 258 |
-
error_json = response.json()
|
| 259 |
-
if "error" in error_json:
|
| 260 |
-
error_text += f" - {error_json['error']['message']}"
|
| 261 |
-
except:
|
| 262 |
-
error_text += f" - {response.text[:200]}"
|
| 263 |
-
return f"Error: {error_text}. Please check your API key and try again."
|
| 264 |
-
except Exception as e:
|
| 265 |
-
return f"Error communicating with OpenAI API: {str(e)}. Please check your internet connection and try again."
|
| 266 |
|
| 267 |
-
#
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
return {"total": "Unknown"}
|
| 271 |
-
|
| 272 |
-
# Latest pricing as of April 2024 for GPT-4o
|
| 273 |
-
input_cost_per_1M = 3 # $3 per 1M input tokens for GPT-4o
|
| 274 |
-
output_cost_per_1M = 10 # $10 per 1M output tokens for GPT-4o
|
| 275 |
-
|
| 276 |
-
prompt_tokens = token_usage.get("prompt_tokens", 0)
|
| 277 |
-
completion_tokens = token_usage.get("completion_tokens", 0)
|
| 278 |
-
|
| 279 |
-
input_cost = (prompt_tokens / 1000000) * input_cost_per_1M
|
| 280 |
-
output_cost = (completion_tokens / 1000000) * output_cost_per_1M
|
| 281 |
-
total_cost = input_cost + output_cost
|
| 282 |
-
|
| 283 |
-
return {
|
| 284 |
-
"input_tokens": prompt_tokens,
|
| 285 |
-
"output_tokens": completion_tokens,
|
| 286 |
-
"input_cost": input_cost,
|
| 287 |
-
"output_cost": output_cost,
|
| 288 |
-
"total_cost": total_cost
|
| 289 |
-
}
|
| 290 |
|
| 291 |
-
#
|
| 292 |
-
st.
|
| 293 |
-
|
| 294 |
-
/* Override streamlit's default padding to prevent header cutoff */
|
| 295 |
-
.main .block-container {
|
| 296 |
-
padding-top: 2rem !important; /* Increased from 1rem to give more space */
|
| 297 |
-
padding-bottom: 1rem !important;
|
| 298 |
-
max-width: 100% !important;
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
/* Fix for the header area to ensure it's fully visible in Hugging Face Spaces */
|
| 302 |
-
.stApp {
|
| 303 |
-
margin-top: 0.5rem !important;
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
/* Fix text visibility issues - ensure good contrast */
|
| 307 |
-
.upload-text {
|
| 308 |
-
color: #262730 !important;
|
| 309 |
-
background-color: transparent !important;
|
| 310 |
-
font-weight: 500 !important;
|
| 311 |
-
}
|
| 312 |
-
|
| 313 |
-
/* Make the title and header area more compact but visible */
|
| 314 |
-
.title-area {
|
| 315 |
-
margin: 0 !important;
|
| 316 |
-
padding: 0.5rem 0 !important; /* Added top/bottom padding */
|
| 317 |
-
margin-bottom: 0.5rem !important;
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
/* Make header more compact but ensure visibility */
|
| 321 |
-
.stMarkdown h1 {
|
| 322 |
-
margin-top: 0.5rem !important; /* Added margin to prevent cutoff */
|
| 323 |
-
margin-bottom: 0.5rem !important;
|
| 324 |
-
font-size: 1.8rem !important;
|
| 325 |
-
padding: 0 !important;
|
| 326 |
-
line-height: 1.3 !important; /* Improved line height */
|
| 327 |
-
}
|
| 328 |
-
|
| 329 |
-
/* Ensure the logo and title are visible */
|
| 330 |
-
.title-section {
|
| 331 |
-
display: block !important;
|
| 332 |
-
padding-top: 0.5rem !important;
|
| 333 |
-
margin-bottom: 0.75rem !important;
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
/* Steps styling - improved visibility and spacing */
|
| 337 |
-
.steps-container {
|
| 338 |
-
display: flex;
|
| 339 |
-
justify-content: space-between;
|
| 340 |
-
margin: 0.75rem 0 1rem 0 !important; /* Added top margin */
|
| 341 |
-
padding: 0 !important;
|
| 342 |
-
}
|
| 343 |
-
|
| 344 |
-
.step-item {
|
| 345 |
-
flex: 1;
|
| 346 |
-
padding: 0.5rem !important; /* Increased padding */
|
| 347 |
-
font-size: 0.85rem;
|
| 348 |
-
color: #262730;
|
| 349 |
-
background-color: #f0f2f6;
|
| 350 |
-
border-radius: 4px;
|
| 351 |
-
margin-right: 0.5rem;
|
| 352 |
-
text-align: center;
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
/* Enhanced section styling */
|
| 356 |
-
.stMarkdown h3 {
|
| 357 |
-
margin-top: 1.5rem;
|
| 358 |
-
padding: 0.5rem;
|
| 359 |
-
border-radius: 0.5rem;
|
| 360 |
-
font-weight: 600;
|
| 361 |
-
color: white;
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
/* Style for summary section */
|
| 365 |
-
.stMarkdown h3:contains("KEY RECOMMENDATIONS SUMMARY") {
|
| 366 |
-
background-color: #2196f3;
|
| 367 |
-
}
|
| 368 |
-
|
| 369 |
-
/* Summary content styling */
|
| 370 |
-
.summary-title {
|
| 371 |
-
font-size: 1.2rem;
|
| 372 |
-
font-weight: bold;
|
| 373 |
-
margin-bottom: 0.5rem;
|
| 374 |
-
color: #2196f3;
|
| 375 |
-
}
|
| 376 |
-
|
| 377 |
-
.summary-subtitle {
|
| 378 |
-
font-size: 0.9rem;
|
| 379 |
-
font-weight: 500;
|
| 380 |
-
margin-bottom: 1rem;
|
| 381 |
-
color: #424242;
|
| 382 |
-
}
|
| 383 |
-
|
| 384 |
-
/* Priority recommendations */
|
| 385 |
-
.stMarkdown h3:contains("TOP PRICE") {
|
| 386 |
-
background-color: #1e88e5;
|
| 387 |
-
}
|
| 388 |
-
|
| 389 |
-
/* Quick improvements section */
|
| 390 |
-
.stMarkdown h3:contains("QUICK WINS") {
|
| 391 |
-
background-color: #43a047;
|
| 392 |
-
}
|
| 393 |
-
|
| 394 |
-
/* Strategic pricing section */
|
| 395 |
-
.stMarkdown h3:contains("SPECIFIC PRICING") {
|
| 396 |
-
background-color: #5e35b1;
|
| 397 |
-
}
|
| 398 |
-
|
| 399 |
-
/* Marketing section */
|
| 400 |
-
.stMarkdown h3:contains("DETAILED MARKETING") {
|
| 401 |
-
background-color: #fb8c00;
|
| 402 |
-
}
|
| 403 |
-
|
| 404 |
-
/* Negotiation section */
|
| 405 |
-
.stMarkdown h3:contains("SPECIFIC NEGOTIATION") {
|
| 406 |
-
background-color: #00897b;
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
/* Critical issues section */
|
| 410 |
-
.stMarkdown h3:contains("CRITICAL ISSUES") {
|
| 411 |
-
background-color: #e53935;
|
| 412 |
-
}
|
| 413 |
-
|
| 414 |
-
/* Timeline section */
|
| 415 |
-
.stMarkdown h3:contains("DETAILED TIMELINE") {
|
| 416 |
-
background-color: #8e24aa;
|
| 417 |
-
}
|
| 418 |
-
|
| 419 |
-
/* Style for bullet points in summary */
|
| 420 |
-
.stMarkdown ul li {
|
| 421 |
-
margin-bottom: 0.75rem;
|
| 422 |
-
line-height: 1.6;
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
/* Bold text */
|
| 426 |
-
.stMarkdown strong {
|
| 427 |
-
color: #262730;
|
| 428 |
-
font-weight: 700;
|
| 429 |
-
}
|
| 430 |
-
|
| 431 |
-
/* Results container */
|
| 432 |
-
.results-container {
|
| 433 |
-
padding: 1.5rem;
|
| 434 |
-
background-color: white;
|
| 435 |
-
border-radius: 0.5rem;
|
| 436 |
-
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
| 437 |
-
margin-bottom: 1.5rem;
|
| 438 |
-
}
|
| 439 |
-
|
| 440 |
-
/* Cost information */
|
| 441 |
-
.cost-info {
|
| 442 |
-
background-color: #f8f9fa;
|
| 443 |
-
padding: 0.75rem;
|
| 444 |
-
border-radius: 0.5rem;
|
| 445 |
-
font-size: 0.85rem;
|
| 446 |
-
margin-top: 1rem;
|
| 447 |
-
}
|
| 448 |
-
|
| 449 |
-
/* Blue background for summary section */
|
| 450 |
-
.summary-section {
|
| 451 |
-
background-color: #e3f2fd;
|
| 452 |
-
border-radius: 8px;
|
| 453 |
-
padding: 1rem;
|
| 454 |
-
margin-bottom: 1.5rem;
|
| 455 |
-
border-left: 4px solid #2196f3;
|
| 456 |
-
}
|
| 457 |
-
|
| 458 |
-
/* Custom header style for subheaders */
|
| 459 |
-
.custom-subheader {
|
| 460 |
-
background-color: #f5f5f5;
|
| 461 |
-
padding: 10px;
|
| 462 |
-
border-radius: 5px;
|
| 463 |
-
margin-bottom: 10px;
|
| 464 |
-
color: #262730;
|
| 465 |
-
font-weight: 600;
|
| 466 |
-
}
|
| 467 |
-
|
| 468 |
-
/* Fix for code blocks and pre-formatted text */
|
| 469 |
-
pre, code {
|
| 470 |
-
white-space: pre-wrap !important;
|
| 471 |
-
word-break: break-word !important;
|
| 472 |
-
}
|
| 473 |
-
|
| 474 |
-
/* Fix for Hugging Face Spaces iframe header issues */
|
| 475 |
-
@media screen and (max-width: 1200px) {
|
| 476 |
-
.main .block-container {
|
| 477 |
-
padding-top: 3rem !important; /* Even more padding for smaller screens */
|
| 478 |
-
}
|
| 479 |
-
}
|
| 480 |
-
</style>
|
| 481 |
-
""", unsafe_allow_html=True)
|
| 482 |
|
| 483 |
-
#
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
""",
|
| 498 |
-
|
| 499 |
-
#
|
| 500 |
-
if
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
col1, col2 = st.columns(2)
|
| 506 |
-
|
| 507 |
-
with col1:
|
| 508 |
-
# Custom subheader with background
|
| 509 |
-
st.markdown('<div class="custom-subheader">📸 Upload Home Photos</div>', unsafe_allow_html=True)
|
| 510 |
-
uploaded_files = st.file_uploader("Upload photos of different areas of your home",
|
| 511 |
-
accept_multiple_files=True,
|
| 512 |
-
type=["jpg", "jpeg", "png"])
|
| 513 |
-
|
| 514 |
-
# Process uploaded images
|
| 515 |
-
if uploaded_files:
|
| 516 |
-
images = []
|
| 517 |
-
valid_images = []
|
| 518 |
-
error_messages = []
|
| 519 |
-
|
| 520 |
-
for file in uploaded_files:
|
| 521 |
-
try:
|
| 522 |
-
# Read file content first to check if it's valid
|
| 523 |
-
file_bytes = file.getvalue()
|
| 524 |
-
if len(file_bytes) == 0:
|
| 525 |
-
error_messages.append(f"Error: {file.name} appears to be empty.")
|
| 526 |
-
continue
|
| 527 |
-
|
| 528 |
-
# Try to open the image with PIL
|
| 529 |
-
image = Image.open(BytesIO(file_bytes))
|
| 530 |
-
|
| 531 |
-
# Validate image by trying to get its format
|
| 532 |
-
image_format = image.format
|
| 533 |
-
if not image_format:
|
| 534 |
-
error_messages.append(f"Error: {file.name} doesn't appear to be a valid image format.")
|
| 535 |
-
continue
|
| 536 |
-
|
| 537 |
-
# Resize large images to reduce token usage
|
| 538 |
-
max_size = (1024, 1024)
|
| 539 |
-
if image.width > max_size[0] or image.height > max_size[1]:
|
| 540 |
-
image.thumbnail(max_size, Image.LANCZOS)
|
| 541 |
-
|
| 542 |
-
images.append(image)
|
| 543 |
-
valid_images.append(file.name)
|
| 544 |
-
except Exception as e:
|
| 545 |
-
error_messages.append(f"Error processing {file.name}: {str(e)}")
|
| 546 |
-
|
| 547 |
-
# Update session state with valid images
|
| 548 |
-
st.session_state["uploaded_images"] = images
|
| 549 |
-
|
| 550 |
-
# Display uploaded images and errors
|
| 551 |
-
if images:
|
| 552 |
-
st.write(f"**{len(images)} valid images uploaded**")
|
| 553 |
-
image_cols = st.columns(min(3, len(images)))
|
| 554 |
-
for i, img in enumerate(images):
|
| 555 |
-
with image_cols[i % min(3, len(images))]:
|
| 556 |
-
st.image(img, width=150, caption=f"Image {i+1}: {valid_images[i]}")
|
| 557 |
-
|
| 558 |
-
# Display error messages if any
|
| 559 |
-
if error_messages:
|
| 560 |
-
with st.expander(f"⚠️ {len(error_messages)} image upload issues detected. Click to view details."):
|
| 561 |
-
for error in error_messages:
|
| 562 |
-
st.error(error)
|
| 563 |
-
|
| 564 |
-
# Custom subheader with background
|
| 565 |
-
st.markdown('<div class="custom-subheader">🏠 Selling Timeline & Details</div>', unsafe_allow_html=True)
|
| 566 |
-
|
| 567 |
-
# Expanded selling timeline options
|
| 568 |
-
timeframe = st.radio("When do you plan to sell your home?",
|
| 569 |
-
["Within 1 month", "1-3 months", "3-6 months", "6-12 months", "More than 12 months"])
|
| 570 |
-
|
| 571 |
-
additional_details = st.text_area("Additional Details",
|
| 572 |
-
placeholder="Describe your home, local market conditions, target buyers, or any specific concerns you have about the selling process.",
|
| 573 |
-
height=100)
|
| 574 |
-
|
| 575 |
-
# Additional preferences with custom subheader
|
| 576 |
-
st.markdown('<div class="custom-subheader">💰 Budget & Preferences</div>', unsafe_allow_html=True)
|
| 577 |
-
|
| 578 |
-
max_budget = st.slider("Maximum budget for improvements ($)",
|
| 579 |
-
min_value=1000,
|
| 580 |
-
max_value=50000,
|
| 581 |
-
value=10000,
|
| 582 |
-
step=1000)
|
| 583 |
-
|
| 584 |
-
improvement_focus = st.multiselect(
|
| 585 |
-
"Areas to focus on (optional)",
|
| 586 |
-
["Curb appeal", "Kitchen", "Bathroom", "Living spaces", "Outdoor areas", "Storage solutions", "Energy efficiency", "Marketing", "Pricing strategy", "Negotiation", "Staging"],
|
| 587 |
-
default=["Curb appeal", "Kitchen", "Bathroom", "Pricing strategy"]
|
| 588 |
-
)
|
| 589 |
-
|
| 590 |
-
diy_preference = st.select_slider(
|
| 591 |
-
"DIY Preference",
|
| 592 |
-
options=["Professional work only", "Mix of DIY and professional", "Mostly DIY if possible"],
|
| 593 |
-
value="Mix of DIY and professional"
|
| 594 |
-
)
|
| 595 |
-
|
| 596 |
-
# Analysis button with improved error handling
|
| 597 |
-
analyze_button = st.button('🔍 Analyze My Home',
|
| 598 |
-
use_container_width=True,
|
| 599 |
-
disabled=len(st.session_state["uploaded_images"]) == 0 or not st.session_state["api_key"])
|
| 600 |
-
|
| 601 |
-
# Clear image error message placement
|
| 602 |
-
image_error_placeholder = st.empty()
|
| 603 |
-
|
| 604 |
-
if len(st.session_state["uploaded_images"]) == 0:
|
| 605 |
-
image_error_placeholder.warning("⚠️ Please upload at least one valid photo of your home to receive recommendations.")
|
| 606 |
-
|
| 607 |
-
if not st.session_state["api_key"]:
|
| 608 |
-
st.warning("⚠️ Please enter your OpenAI API key to enable analysis.")
|
| 609 |
-
|
| 610 |
-
with col2:
|
| 611 |
-
# Custom subheader with background
|
| 612 |
-
st.markdown('<div class="custom-subheader">💡 Comprehensive Selling Strategies</div>', unsafe_allow_html=True)
|
| 613 |
-
|
| 614 |
-
# Process and display analysis results
|
| 615 |
-
if analyze_button or "analysis_result" in st.session_state:
|
| 616 |
-
# Process analysis if button pressed
|
| 617 |
-
if analyze_button:
|
| 618 |
-
# Check if there are valid images
|
| 619 |
-
if len(st.session_state["uploaded_images"]) == 0:
|
| 620 |
-
st.error("No valid images to analyze. Please upload at least one home photo.")
|
| 621 |
-
else:
|
| 622 |
-
# Simple loading indicator using pure Streamlit - no HTML, JS or CSS
|
| 623 |
-
with st.spinner("🏡 Analyzing your home photos..."):
|
| 624 |
-
# Show a progress bar
|
| 625 |
-
progress_bar = st.progress(0)
|
| 626 |
-
|
| 627 |
-
# Show the analysis steps with native Streamlit components
|
| 628 |
-
steps_placeholder = st.empty()
|
| 629 |
-
steps_placeholder.info("Step 1: Identifying property features and conditions...")
|
| 630 |
-
progress_bar.progress(25)
|
| 631 |
-
time.sleep(0.5) # Reduced delay for faster response
|
| 632 |
-
|
| 633 |
-
steps_placeholder.info("Step 2: Evaluating improvement opportunities and researching specific recommendations...")
|
| 634 |
-
progress_bar.progress(50)
|
| 635 |
-
time.sleep(0.5) # Reduced delay for faster response
|
| 636 |
-
|
| 637 |
-
steps_placeholder.info("Step 3: Calculating ROI potential and precise cost estimates...")
|
| 638 |
-
progress_bar.progress(75)
|
| 639 |
-
time.sleep(0.5) # Reduced delay for faster response
|
| 640 |
-
|
| 641 |
-
steps_placeholder.info("Step 4: Creating detailed timeline and finalizing recommendations...")
|
| 642 |
-
|
| 643 |
-
# Make the actual API call with enhanced error handling
|
| 644 |
-
try:
|
| 645 |
-
analysis_text = analyze_home_photos(
|
| 646 |
-
st.session_state["uploaded_images"],
|
| 647 |
-
timeframe,
|
| 648 |
-
f"Budget: ${max_budget}. Focus areas: {', '.join(improvement_focus)}. DIY preference: {diy_preference}. {additional_details}",
|
| 649 |
-
st.session_state["api_key"]
|
| 650 |
-
)
|
| 651 |
-
|
| 652 |
-
# Store the result
|
| 653 |
-
st.session_state["analysis_result"] = analysis_text
|
| 654 |
-
except Exception as e:
|
| 655 |
-
st.error(f"Error during analysis: {str(e)}")
|
| 656 |
-
st.session_state["analysis_result"] = f"An error occurred during analysis: {str(e)}. Please try again with different images or check your API key."
|
| 657 |
-
|
| 658 |
-
# Complete the progress bar and clear the loading indicators
|
| 659 |
-
progress_bar.progress(100)
|
| 660 |
-
steps_placeholder.empty()
|
| 661 |
-
progress_bar.empty()
|
| 662 |
-
|
| 663 |
-
# Display results
|
| 664 |
-
if "analysis_result" in st.session_state:
|
| 665 |
-
# Process the output to enhance summary formatting
|
| 666 |
-
result = st.session_state["analysis_result"]
|
| 667 |
-
|
| 668 |
-
# Check if we need to style the summary section
|
| 669 |
-
if "**HOME VALUE MAXIMIZER SUMMARY**" in result:
|
| 670 |
-
# Split the result to find the summary section
|
| 671 |
-
parts = result.split("### 📋 KEY RECOMMENDATIONS SUMMARY", 1)
|
| 672 |
-
if len(parts) > 1:
|
| 673 |
-
before_summary = parts[0]
|
| 674 |
-
rest_parts = parts[1].split("### 🌟", 1)
|
| 675 |
-
if len(rest_parts) > 1:
|
| 676 |
-
summary_content = rest_parts[0]
|
| 677 |
-
after_summary = "### 🌟" + rest_parts[1]
|
| 678 |
-
|
| 679 |
-
# Apply custom formatting to the summary
|
| 680 |
-
result = before_summary + '<div class="summary-section">' + summary_content + '</div>' + after_summary
|
| 681 |
-
|
| 682 |
-
# Display the enhanced result
|
| 683 |
-
st.markdown(f'<div class="results-container">{result}</div>', unsafe_allow_html=True)
|
| 684 |
-
|
| 685 |
-
# Add a download button for the report
|
| 686 |
-
st.download_button(
|
| 687 |
-
label="Download Selling Strategies as Text",
|
| 688 |
-
data=st.session_state["analysis_result"],
|
| 689 |
-
file_name="home_selling_strategies.txt",
|
| 690 |
-
mime="text/plain",
|
| 691 |
-
use_container_width=True
|
| 692 |
-
)
|
| 693 |
else:
|
| 694 |
-
st.
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
st.
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
- **Improvement recommendations** and costs may vary based on your location and local labor costs
|
| 707 |
-
- **Product recommendations** are suggestions only; please research compatibility with your home
|
| 708 |
-
|
| 709 |
-
For the best results, consult with experienced real estate professionals in your area before making significant decisions.
|
| 710 |
-
""")
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
+
from PIL import Image
|
| 3 |
+
from io import BytesIO
|
| 4 |
+
import openai
|
| 5 |
+
import base64
|
| 6 |
+
import time
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
import re
|
| 9 |
|
| 10 |
+
# --- Page config (MUST be first Streamlit command) ---
|
| 11 |
st.set_page_config(
|
| 12 |
page_title="Home Value Maximizer",
|
| 13 |
page_icon="🏡",
|
|
|
|
| 15 |
initial_sidebar_state="collapsed"
|
| 16 |
)
|
| 17 |
|
| 18 |
+
# --- Global CSS for consistent fonts & spacing ---
|
| 19 |
+
st.markdown(
|
| 20 |
+
"""
|
| 21 |
+
<style>
|
| 22 |
+
body, .stApp { font-family: 'Helvetica Neue', Arial, sans-serif !important; }
|
| 23 |
+
h1, h2, h3, h4, h5, h6 { font-family: inherit !important; font-weight: 600 !important; }
|
| 24 |
+
pre, code { font-family: 'Courier New', monospace !important; white-space: pre-wrap !important; word-break: break-word !important; }
|
| 25 |
+
.stMarkdown ul > li { margin-bottom: 0.5rem; line-height: 1.4; }
|
| 26 |
+
</style>
|
| 27 |
+
""",
|
| 28 |
+
unsafe_allow_html=True
|
| 29 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
# --- OpenAI SDK client caching ---
|
| 32 |
+
@st.cache_resource
|
| 33 |
+
def get_openai_client(api_key: str):
|
| 34 |
+
openai.api_key = api_key
|
| 35 |
+
return openai
|
| 36 |
+
|
| 37 |
+
# --- Image encoding helper ---
|
| 38 |
+
@st.cache_data
|
| 39 |
+
def encode_image_to_b64(image: Image.Image) -> str:
|
| 40 |
+
# Convert to RGB if needed
|
| 41 |
+
if image.mode == 'RGBA':
|
| 42 |
+
bg = Image.new('RGB', image.size, (255,255,255))
|
| 43 |
+
bg.paste(image, mask=image.split()[3])
|
| 44 |
+
image = bg
|
| 45 |
+
elif image.mode not in ['RGB', 'L']:
|
| 46 |
+
image = image.convert('RGB')
|
| 47 |
+
# Resize to limit token usage
|
| 48 |
+
max_size = (1024, 1024)
|
| 49 |
+
if image.width > max_size[0] or image.height > max_size[1]:
|
| 50 |
+
image.thumbnail(max_size, Image.LANCZOS)
|
| 51 |
+
buf = BytesIO()
|
| 52 |
+
image.save(buf, format='JPEG', quality=85)
|
| 53 |
+
return base64.b64encode(buf.getvalue()).decode('utf-8')
|
| 54 |
+
|
| 55 |
+
# --- Simple formatting fixes ---
|
| 56 |
+
def fix_formatting(text: str) -> str:
|
| 57 |
+
text = re.sub(r'(\d+)([A-Za-z])', r'\1 \2', text)
|
| 58 |
+
text = re.sub(r'([.,])([A-Za-z0-9])', r'\1 \2', text)
|
| 59 |
+
text = re.sub(r':([A-Za-z0-9])', r': \1', text)
|
| 60 |
return text
|
| 61 |
|
| 62 |
+
# --- System prompt ---
|
| 63 |
+
SYSTEM_PROMPT = f"""
|
| 64 |
+
You are an expert real estate advisor with deep knowledge of home selling strategies. You analyze home photos to provide EXTREMELY SPECIFIC, actionable recommendations that will maximize the property's selling price.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
ONLY make recommendations based on visible areas in the photos. Reference exact features, colors, materials, and conditions you can see.
|
| 67 |
|
| 68 |
+
FORMAT:
|
| 69 |
+
### 📋 HOME VALUE MAXIMIZER SUMMARY
|
| 70 |
+
- Start with exactly this header.
|
| 71 |
+
- Then: "Below are the top improvements to maximize your home’s value..."
|
| 72 |
+
- List 5 bullet points: **bold issue**, specific solution, estimated ROI.
|
| 73 |
|
| 74 |
### 👁️ AREAS ANALYZED
|
| 75 |
+
List which areas you see (e.g., Kitchen, Living Room).
|
| 76 |
|
| 77 |
### 🌟 TOP PRICE-MAXIMIZING PRIORITIES
|
| 78 |
+
For the 3–4 biggest improvements: exact issue, precise recommendation, cost estimate, value impact, timeline.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
+
### 🔨 QUICK WINS (1–3 days)
|
| 81 |
+
List 5–6 fast, high-ROI fixes for visible areas; include product names/colors/costs.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
### 📊 SPECIFIC PRICING STRATEGY
|
| 84 |
+
- Precise price point and pricing psychology (spaces between numbers and text).
|
| 85 |
+
- Exact timing: days/dates based on current date ({datetime.now().strftime('%B %d, %Y')}).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
### 📸 DETAILED MARKETING PLAN
|
| 88 |
+
List specific features to highlight, angles, staging items, virtual tools.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
### 📝 SPECIFIC NEGOTIATION TACTICS
|
| 91 |
+
Exact scripts, inspection strategies, contingency planning.
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
### ⚠️ CRITICAL ISSUES TO ADDRESS
|
| 94 |
+
List 3 visible problems with exact fixes.
|
| 95 |
|
| 96 |
### 📅 DETAILED TIMELINE WITH DATES
|
| 97 |
+
Provide calendar dates for each step, from today.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
### 🚫 AREAS NOT VISIBLE
|
| 100 |
+
Note any important areas you couldn’t see.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
+
# --- Streamlit layout ---
|
| 104 |
+
st.title("Home Value Maximizer 🏡")
|
| 105 |
+
st.write("Upload photos, enter your timeline & budget, then click Analyze for detailed, hyper-specific advice.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
+
# API key
|
| 108 |
+
api_key = st.sidebar.text_input("OpenAI API Key", type="password")
|
| 109 |
+
client = get_openai_client(api_key) if api_key else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
# Upload images
|
| 112 |
+
st.sidebar.header("📸 Upload Home Photos")
|
| 113 |
+
uploaded = st.sidebar.file_uploader("Images", type=["jpg","png","jpeg"], accept_multiple_files=True)
|
| 114 |
+
images = []
|
| 115 |
+
for file in uploaded:
|
| 116 |
+
try:
|
| 117 |
+
img = Image.open(file)
|
| 118 |
+
images.append(img)
|
| 119 |
+
except:
|
| 120 |
+
st.sidebar.error(f"Failed to load {file.name}")
|
| 121 |
+
|
| 122 |
+
# Inputs
|
| 123 |
+
timeframe = st.sidebar.selectbox("Selling timeframe", ["Within 1 month","1–3 months","3–6 months","6–12 months",">12 months"])
|
| 124 |
+
budget = st.sidebar.slider("Max improvement budget ($)", 1000, 50000, 10000, 1000)
|
| 125 |
+
improve_focus = st.sidebar.multiselect("Focus areas (opt)", ["Curb appeal","Kitchen","Bathroom","Living Spaces","Outdoor","Storage","Energy","Marketing","Negotiation","Staging"], default=["Kitchen","Bathroom"])
|
| 126 |
+
|
| 127 |
+
# Analyze button
|
| 128 |
+
if st.button("🔍 Analyze My Home"):
|
| 129 |
+
if not client:
|
| 130 |
+
st.error("Enter API key to analyze.")
|
| 131 |
+
elif not images:
|
| 132 |
+
st.error("Upload at least one photo.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
else:
|
| 134 |
+
with st.spinner("Analyzing..."):
|
| 135 |
+
user_details = f"Budget: ${budget}. Focus: {', '.join(improve_focus)}"
|
| 136 |
+
analysis = analyze_home_photos(images, timeframe, user_details, client)
|
| 137 |
+
st.success("Analysis complete!")
|
| 138 |
+
st.markdown(f"<div style='background:#e3f2fd;padding:1rem;border-left:4px solid #1f77b4;'>{analysis}</div>", unsafe_allow_html=True)
|
| 139 |
+
# Cost
|
| 140 |
+
usage = client.api_usage if hasattr(client, 'api_usage') else {}
|
| 141 |
+
cost = calculate_cost(usage)
|
| 142 |
+
st.metric("💲 Estimated Cost", f"${cost:.2f}")
|
| 143 |
+
|
| 144 |
+
else:
|
| 145 |
+
st.info("Submit images & API key to generate recommendations.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|