sksameermujahid commited on
Commit
a7987ea
·
verified ·
1 Parent(s): 4c994f4

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +36 -0
  2. app.py +264 -0
  3. requirements.txt +13 -0
  4. templates/index.html +259 -0
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Python slim image
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ build-essential \
10
+ git \
11
+ libglib2.0-0 \
12
+ libsm6 \
13
+ libxrender1 \
14
+ libxext6 \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # Use /data for all Hugging Face cache to avoid permission issues in Spaces
18
+ ENV TRANSFORMERS_CACHE=/data/transformers
19
+ ENV HF_HOME=/data/huggingface
20
+
21
+ # Create writable cache directories
22
+ RUN mkdir -p /data/transformers /data/huggingface && chmod -R 777 /data
23
+
24
+ # Copy requirements and install dependencies
25
+ COPY requirements.txt .
26
+ RUN pip install --no-cache-dir -r requirements.txt
27
+ RUN pip install flask python-dotenv
28
+
29
+ # Copy full app code
30
+ COPY . .
31
+
32
+ # Expose Flask port
33
+ EXPOSE 7860
34
+
35
+ # Run the Flask app
36
+ CMD ["flask", "run", "--host=0.0.0.0", "--port=7860"]
app.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from unicodedata import category
2
+ from flask import Flask, render_template, request, jsonify
3
+ from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
4
+ import os
5
+ import logging
6
+ import requests
7
+ import threading
8
+ from queue import Queue
9
+ from functools import lru_cache
10
+
11
+ app = Flask(__name__)
12
+ logging.basicConfig(level=logging.INFO)
13
+
14
+ CACHE_DIR = "/data"
15
+ model_name = "Qwen/Qwen2.5-0.5B-Instruct"
16
+
17
+ # Load model and tokenizer
18
+ logging.info("Loading tokenizer...")
19
+ tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=CACHE_DIR, trust_remote_code=True)
20
+ logging.info("Loading model...")
21
+ model = AutoModelForCausalLM.from_pretrained(model_name, cache_dir=CACHE_DIR, trust_remote_code=True)
22
+ logging.info("Creating pipeline...")
23
+ generator = pipeline("text-generation", model=model, tokenizer=tokenizer, device=-1)
24
+ logging.info("Model ready.")
25
+
26
+ # Fallback description text
27
+ def get_fallback_description(details):
28
+ category = details.get('parent_category', '').lower()
29
+ address = f"{details.get('address', '')}, {details.get('city', '')}, {details.get('state', '')}"
30
+ price = details.get('price', '')
31
+ rooms = int(details.get('total_rooms', 0)) if details.get('total_rooms') else 0
32
+ area = details.get('area', '')
33
+
34
+ # Get nearby locations
35
+ nearby_info = ""
36
+ lat = details.get('latitude')
37
+ lon = details.get('longitude')
38
+ if lat and lon:
39
+ try:
40
+ lat_f, lon_f = float(lat), float(lon)
41
+ result_queue = Queue()
42
+ thread = threading.Thread(target=fetch_nearby_amenities_thread, args=(lat_f, lon_f, 2500, result_queue))
43
+ thread.start()
44
+ thread.join(timeout=8)
45
+ landmarks = result_queue.get() if not result_queue.empty() else []
46
+
47
+ if landmarks:
48
+ amenities = [x for x in landmarks if any(term in x.lower() for term in ['school', 'hospital', 'restaurant', 'pharmacy', 'bank', 'atm', 'cafe', 'bus', 'fuel', 'supermarket', 'police', 'post'])]
49
+ landmarks = [x for x in landmarks if x not in amenities]
50
+
51
+ if amenities:
52
+ nearby_info += f" The property is conveniently located near {', '.join(amenities[:2])}."
53
+ if landmarks:
54
+ nearby_info += f" Notable landmarks include {', '.join(landmarks[:2])}."
55
+ except Exception as e:
56
+ logging.error(f"Fallback amenity fetch error: {str(e)}")
57
+ pass
58
+
59
+ if category in ['agricultural', 'commercial']:
60
+ return f'''A prime agricultural property located at {address}. This expansive land spans {area} sq. ft., making it ideal for farming or agricultural development. The property is priced at ₹{price} and features {rooms} storage/utility rooms. The land is well-irrigated and offers excellent soil quality, perfect for various agricultural activities.{nearby_info} Whether you're looking to start a farm or expand your agricultural operations, this property provides the perfect foundation for your agricultural ventures.'''
61
+ else: # Commercial
62
+ return f'''A premium commercial property situated in the heart of {address}. This modern commercial space spans {area} sq. ft., offering a versatile layout with {rooms} rooms for optimal business operations. Priced at ₹{price}, this property features excellent visibility and accessibility.{nearby_info} The space is designed to accommodate various business needs, from retail to office use, with modern amenities and professional atmosphere. Whether you're establishing a new business or expanding existing operations, this location provides the perfect blend of convenience and functionality.'''
63
+
64
+ @lru_cache(maxsize=100)
65
+ def fetch_nearby_amenities_thread(lat, lon, radius, result_queue):
66
+ try:
67
+ overpass_url = "https://overpass-api.de/api/interpreter"
68
+ query = f"""
69
+ [out:json][timeout:10];
70
+ (
71
+ node["amenity"~"school|hospital|restaurant|temple|mandir|hotel|pharmacy|bank|atm|cafe|bus_station|fuel|supermarket|police|post_office"](around:{radius},{lat},{lon});
72
+ node["tourism"~"hotel|museum|gallery|viewpoint|attraction"](around:{radius},{lat},{lon});
73
+ node["historic"~"monument|memorial|archaeological_site|castle|ruins"](around:{radius},{lat},{lon});
74
+ node["leisure"~"park|garden|sports_centre|stadium"](around:{radius},{lat},{lon});
75
+ );
76
+ out body;
77
+ """
78
+ response = requests.post(overpass_url, data={'data': query}, timeout=10)
79
+ response.raise_for_status()
80
+ data = response.json()
81
+ landmarks = []
82
+ seen_names = set()
83
+
84
+ for element in data.get('elements', []):
85
+ tags = element.get('tags', {})
86
+ name = tags.get('name')
87
+ if not name or name in seen_names:
88
+ continue
89
+ seen_names.add(name)
90
+
91
+ place_type = None
92
+ if tags.get('tourism'):
93
+ place_type = tags['tourism'].replace('_', ' ').title()
94
+ elif tags.get('historic'):
95
+ place_type = tags['historic'].replace('_', ' ').title()
96
+ elif tags.get('leisure'):
97
+ place_type = tags['leisure'].replace('_', ' ').title()
98
+ elif tags.get('amenity'):
99
+ place_type = tags['amenity'].replace('_', ' ').title()
100
+
101
+ if place_type:
102
+ landmarks.append((f"{place_type} '{name}'", len(name)))
103
+
104
+ # Sort by name length and take top results
105
+ landmarks.sort(key=lambda x: x[1], reverse=True)
106
+ result_queue.put([x[0] for x in landmarks])
107
+ except Exception as e:
108
+ logging.error(f"Overpass API error: {str(e)}")
109
+ result_queue.put([])
110
+
111
+ def generate_description(details):
112
+ try:
113
+ category = details.get('parent_category', '').lower()
114
+
115
+ # Basic info - only include beds/rooms for residential properties
116
+ extra_info = ""
117
+ if category not in ['agricultural', 'commercial']:
118
+ beds = int(details.get('beds', 0)) if details.get('beds') else 0
119
+ rooms = int(details.get('total_rooms', 0)) if details.get('total_rooms') else 0
120
+ baths = int(details.get('baths',0)) if details.get('baths') else 0
121
+ bed_info = f"{beds} bed{'s' if beds != 1 else ''}" if beds > 0 else ""
122
+ room_info = f"{rooms} room{'s' if rooms != 1 else ''}" if rooms > 0 else ""
123
+ bath_info = f"{baths} bath{'s' if baths !=1 else ''}" if baths > 0 else ""
124
+ if beds or rooms or baths:
125
+ parts = []
126
+ if beds: parts.append(bed_info)
127
+ if rooms: parts.append(room_info)
128
+ if baths: parts.append(bath_info)
129
+ extra_info = "This property includes " + " and ".join(parts) + "."
130
+
131
+ # Amenities thread with shorter timeout
132
+ amenities_str = "None found nearby"
133
+ landmarks_str = "None found nearby"
134
+ lat = details.get('latitude')
135
+ lon = details.get('longitude')
136
+ if lat and lon:
137
+ try:
138
+ lat_f, lon_f = float(lat), float(lon)
139
+ result_queue = Queue()
140
+ thread = threading.Thread(target=fetch_nearby_amenities_thread, args=(lat_f, lon_f, 2000, result_queue))
141
+ thread.start()
142
+ thread.join(timeout=8)
143
+ landmarks = result_queue.get() if not result_queue.empty() else []
144
+
145
+ # Split into amenities and landmarks
146
+ amenities = [x for x in landmarks if any(term in x.lower() for term in ['school', 'hospital', 'restaurant', 'pharmacy', 'bank', 'atm', 'cafe', 'bus', 'fuel', 'supermarket', 'police', 'post'])]
147
+ landmarks = [x for x in landmarks if x not in amenities]
148
+
149
+ amenities_str = ", ".join(amenities[:3]) if amenities else "None found nearby"
150
+ landmarks_str = ", ".join(landmarks[:3]) if landmarks else "None found nearby"
151
+ except Exception as e:
152
+ logging.error(f"Amenity fetch error: {e}")
153
+
154
+ # Prompt construction - exclude beds/rooms/baths for agricultural and commercial
155
+ base_fields = [
156
+ f"Property Name: {details.get('property_name', '')}",
157
+ f"Type: {details.get('property_type', '')}",
158
+ f"Category: {details.get('parent_category', '')}",
159
+ f"Address: {details.get('address', '')}, {details.get('city', '')}, {details.get('state', '')}, {details.get('zipcode', '')}, {details.get('country', '')}",
160
+ f"Coordinates: ({details.get('latitude', '')}, {details.get('longitude', '')})",
161
+ f"Year Built: {details.get('year', '')}",
162
+ f"Price: ₹{details.get('price', '')}",
163
+ f"Status: {details.get('property_status', '')}",
164
+ f"Code: {details.get('property_code', '')}",
165
+ f"Area: {details.get('area', '')} sq. ft.",
166
+ f"Features: {details.get('features', '')}",
167
+ f"Nearby Amenities: {amenities_str}",
168
+ f"Nearby Landmarks: {landmarks_str}"
169
+ ]
170
+
171
+ if category not in ['agricultural', 'commercial']:
172
+ base_fields.extend([
173
+ f"Rooms: {details.get('total_rooms', '')}",
174
+ f"Beds: {details.get('beds', '')}",
175
+ f"Baths: {details.get('baths', '')}"
176
+ ])
177
+
178
+ prompt = "Generate a property description between 150 and 200 words. Focus ONLY on property features, location, and amenities. Do NOT include any references to natural features like rivers, lakes, islands, or landscapes. Use ALL fields below. Do NOT skip any:\n- " + "\n- ".join(base_fields)
179
+
180
+ if extra_info:
181
+ prompt += f"\n\n{extra_info}"
182
+ prompt += "\nWrite a natural, complete property description focusing on urban and property characteristics only.\nDescription:"
183
+
184
+ # Run generation in thread with longer timeout
185
+ result_queue = Queue()
186
+ def run_generation(q):
187
+ try:
188
+ out = generator(prompt, max_new_tokens=200, temperature=0.5)[0]['generated_text']
189
+ q.put(out)
190
+ except Exception as e:
191
+ q.put(f"Error: {e}")
192
+
193
+ gen_thread = threading.Thread(target=run_generation, args=(result_queue,))
194
+ gen_thread.start()
195
+ gen_thread.join(timeout=45) # Increased timeout to 45 seconds
196
+
197
+ # Return dynamic fallback if generation takes too long or fails
198
+ if result_queue.empty():
199
+ logging.warning("Generation timeout - using fallback description")
200
+ return get_fallback_description(details)
201
+
202
+ result = result_queue.get()
203
+ if result.startswith("Error:"):
204
+ logging.warning(f"Generation error - using fallback description: {result}")
205
+ return get_fallback_description(details)
206
+
207
+ return result.split('Description:')[-1].strip() if 'Description:' in result else result.strip()
208
+
209
+ except Exception as e:
210
+ logging.error(f"Generation error: {str(e)}")
211
+ return FALLBACK_DESCRIPTION
212
+
213
+ @app.route('/', methods=['GET', 'POST'])
214
+ def index():
215
+ description = None
216
+ if request.method == 'POST':
217
+ fields = ['parent_category', 'property_type', 'property_name', 'property_code', 'property_status',
218
+ 'price', 'area', 'total_rooms', 'beds', 'baths', 'year', 'address', 'country',
219
+ 'state', 'city', 'zipcode', 'latitude', 'longitude', 'features']
220
+ details = {f: request.form.get(f, '') for f in fields}
221
+
222
+ if not details['property_name']:
223
+ description = "Error: Property name is required."
224
+ else:
225
+ description = generate_description(details)
226
+
227
+ return render_template('index.html', description=description)
228
+
229
+ @app.route('/generate', methods=['POST'])
230
+ def generate():
231
+ if not request.is_json:
232
+ return {"error": "Request must be JSON"}, 400
233
+ details = request.get_json()
234
+
235
+ if not details.get('property_name'):
236
+ return {"error": "Missing required field: property_name"}, 400
237
+
238
+ desc = generate_description(details)
239
+ category = details.get('parent_category', '').lower()
240
+
241
+ # Conditionally include information based on property category
242
+ if category not in ['agricultural', 'commercial']:
243
+ info_lines = []
244
+ beds = details.get('beds', '')
245
+ rooms = details.get('total_rooms', '')
246
+ baths = details.get('baths', '')
247
+ if beds and beds != '0':
248
+ info_lines.append(f"Beds: {beds}")
249
+ if rooms and rooms != '0':
250
+ info_lines.append(f"Rooms: {rooms}")
251
+ if baths and baths != '0':
252
+ info_lines.append(f"Baths: {baths}")
253
+ info_lines.append(f"Year: {details.get('year', '')}")
254
+ info_lines.append(f"Price: ₹{details.get('price', '')}")
255
+ extra_info = "\n" + "\n".join(info_lines)
256
+ else:
257
+ extra_info = f"\nYear: {details.get('year', '')}\nPrice: ₹{details.get('price', '')}"
258
+
259
+ return {"description": desc + extra_info}
260
+
261
+ if __name__ == '__main__':
262
+ port = int(os.environ.get('PORT', 7860))
263
+ logging.info(f"Starting app on port {port}")
264
+ app.run(host='0.0.0.0', port=port, threaded=True)
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ langchain
3
+ huggingface_hub
4
+ torch
5
+ transformers
6
+ langchain_community
7
+ flask
8
+ python-dotenv
9
+ torch
10
+ transformers
11
+ accelerate
12
+ sentencepiece
13
+ requests
templates/index.html ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Property Description Generator</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ max-width: 800px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ background-color: #f5f5f5;
14
+ }
15
+ .container {
16
+ background-color: white;
17
+ padding: 30px;
18
+ border-radius: 10px;
19
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
20
+ }
21
+ h1 {
22
+ color: #2c3e50;
23
+ text-align: center;
24
+ margin-bottom: 30px;
25
+ }
26
+ .form-group {
27
+ margin-bottom: 20px;
28
+ }
29
+ label {
30
+ display: block;
31
+ margin-bottom: 5px;
32
+ color: #34495e;
33
+ }
34
+ select, input, textarea {
35
+ width: 100%;
36
+ padding: 8px;
37
+ border: 1px solid #ddd;
38
+ border-radius: 4px;
39
+ box-sizing: border-box;
40
+ }
41
+ button {
42
+ background-color: #3498db;
43
+ color: white;
44
+ padding: 10px 20px;
45
+ border: none;
46
+ border-radius: 4px;
47
+ cursor: pointer;
48
+ width: 100%;
49
+ }
50
+ button:hover {
51
+ background-color: #2980b9;
52
+ }
53
+ .description {
54
+ margin-top: 20px;
55
+ padding: 15px;
56
+ background-color: #f8f9fa;
57
+ border-radius: 4px;
58
+ border-left: 4px solid #3498db;
59
+ }
60
+ .form-row {
61
+ display: flex;
62
+ gap: 20px;
63
+ margin-bottom: 20px;
64
+ }
65
+ .form-group {
66
+ flex: 1;
67
+ }
68
+ .price-input {
69
+ display: flex;
70
+ align-items: center;
71
+ }
72
+ .price-input span {
73
+ padding: 8px;
74
+ background: #f8f9fa;
75
+ border: 1px solid #ddd;
76
+ border-right: none;
77
+ border-radius: 4px 0 0 4px;
78
+ }
79
+ .price-input input {
80
+ border-left: none;
81
+ border-radius: 0 4px 4px 0;
82
+ }
83
+ </style>
84
+ </head>
85
+ <body>
86
+ <div class="container">
87
+ <h1>🏡 AI Property Description Generator</h1>
88
+ <form method="POST">
89
+ <div class="form-group">
90
+ <label for="parent_category">Parent Category:</label>
91
+ <select name="parent_category" id="parent_category" required>
92
+ <option value="">Select Parent Category</option>
93
+ <option value="Residential">Residential</option>
94
+ <option value="Commercial">Commercial</option>
95
+ <option value="Agricultural">Agricultural</option>
96
+ <option value="PG/Co-living">PG/Co-living</option>
97
+ </select>
98
+ </div>
99
+
100
+ <div class="form-group">
101
+ <label for="property_type">Property Type:</label>
102
+ <select name="property_type" id="property_type" required>
103
+ <option value="">Select Property Type</option>
104
+ <option value="Townhouse">Townhouse</option>
105
+ <option value="Flat">Flat</option>
106
+ <option value="Villa">Villa</option>
107
+ <option value="Office Space">Office Space</option>
108
+ <option value="Shop">Shop</option>
109
+ <option value="Farmland">Farmland</option>
110
+ <option value="Commercial Land">Commercial Land</option>
111
+ <option value="Plot/Land">Plot/Land</option>
112
+ <option value="Boys Hostel">Boys Hostel</option>
113
+ <option value="Girls Hostel">Girls Hostel</option>
114
+ <option value="Working Womens Hostel">Working Women's Hostel</option>
115
+ <option value="Boys PG">Boys PG</option>
116
+ <option value="Girls PG">Girls PG</option>
117
+ <option value="Showrooms">Showrooms</option>
118
+ <option value="Co-working">Co-working</option>
119
+ </select>
120
+ </div>
121
+
122
+ <div class="form-row">
123
+ <div class="form-group">
124
+ <label for="property_name">Property Name:</label>
125
+ <input type="text" name="property_name" id="property_name" required>
126
+ </div>
127
+ <div class="form-group">
128
+ <label for="property_code">Property Code:</label>
129
+ <input type="text" name="property_code" id="property_code" value="Pr-00483" readonly>
130
+ </div>
131
+ </div>
132
+
133
+ <div class="form-group">
134
+ <label for="property_status">Property Status:</label>
135
+ <select name="property_status" id="property_status" required>
136
+ <option value="">Select Status</option>
137
+ <option value="Ready to move">Ready to move</option>
138
+ <option value="Available">Available</option>
139
+ <option value="Immediately">Immediately</option>
140
+ <option value="Out of Stock">Out of Stock</option>
141
+ </select>
142
+ </div>
143
+
144
+ <div class="form-row">
145
+ <div class="form-group">
146
+ <label for="price">Property Price:</label>
147
+ <div class="price-input">
148
+ <span>₹</span>
149
+ <input type="number" name="price" id="price" placeholder="Enter property price" required>
150
+ </div>
151
+ </div>
152
+ <div class="form-group">
153
+ <label for="area">Area (sq ft):</label>
154
+ <input type="number" name="area" id="area" placeholder="Enter area" required>
155
+ </div>
156
+ </div>
157
+
158
+ <div class="form-row">
159
+ <div class="form-group">
160
+ <label for="total_rooms">Total Rooms:</label>
161
+ <input type="number" name="total_rooms" id="total_rooms" required>
162
+ </div>
163
+ <div class="form-group">
164
+ <label for="beds">Beds:</label>
165
+ <input type="number" name="beds" id="beds" required>
166
+ </div>
167
+ <div class="form-group">
168
+ <label for="baths">Baths:</label>
169
+ <input type="number" name="baths" id="baths" required>
170
+ </div>
171
+ </div>
172
+
173
+ <div class="form-group">
174
+ <label for="year">Year Established:</label>
175
+ <input type="number" name="year" id="year" min="1900" max="2025" placeholder="Enter year (1900-2025)" required>
176
+ </div>
177
+
178
+ <div class="form-group">
179
+ <label for="address">Address:</label>
180
+ <textarea name="address" id="address" rows="3" placeholder="Address of your property" required></textarea>
181
+ </div>
182
+
183
+ <div class="form-row">
184
+ <div class="form-group">
185
+ <label for="country">Country:</label>
186
+ <input type="text" name="country" id="country" required>
187
+ </div>
188
+ <div class="form-group">
189
+ <label for="state">State:</label>
190
+ <input type="text" name="state" id="state" required>
191
+ </div>
192
+ <div class="form-group">
193
+ <label for="city">City:</label>
194
+ <input type="text" name="city" id="city" required>
195
+ </div>
196
+ </div>
197
+
198
+ <div class="form-row">
199
+ <div class="form-group">
200
+ <label for="zipcode">Zip Code:</label>
201
+ <input type="text" name="zipcode" id="zipcode" value="39702" required>
202
+ </div>
203
+ <div class="form-group">
204
+ <label for="latitude">Latitude:</label>
205
+ <input type="number" name="latitude" id="latitude" value="0" step="any">
206
+ </div>
207
+ <div class="form-group">
208
+ <label for="longitude">Longitude:</label>
209
+ <input type="number" name="longitude" id="longitude" value="0" step="any">
210
+ </div>
211
+ </div>
212
+
213
+ <div class="form-group">
214
+ <label for="features">Key Features:</label>
215
+ <textarea name="features" id="features" rows="4" placeholder="e.g. modern kitchen, parking, garden view" required></textarea>
216
+ </div>
217
+
218
+ <button type="button" id="generate-btn">Generate Description</button>
219
+ </form>
220
+ <div id="loading" style="display:none; color:#2980b9; margin-top:10px;">Generating description...</div>
221
+ <div id="error" style="display:none; color:red; margin-top:10px;"></div>
222
+ <div id="description-container" style="display:none;" class="description">
223
+ <h3>Generated Description:</h3>
224
+ <p id="description-text"></p>
225
+ </div>
226
+ <script>
227
+ document.getElementById('generate-btn').addEventListener('click', async function() {
228
+ const form = document.querySelector('form');
229
+ const formData = new FormData(form);
230
+ const data = {};
231
+ formData.forEach((value, key) => { data[key] = value; });
232
+ document.getElementById('loading').style.display = 'block';
233
+ document.getElementById('error').style.display = 'none';
234
+ document.getElementById('description-container').style.display = 'none';
235
+ try {
236
+ const response = await fetch('/generate', {
237
+ method: 'POST',
238
+ headers: { 'Content-Type': 'application/json' },
239
+ body: JSON.stringify(data)
240
+ });
241
+ const result = await response.json();
242
+ document.getElementById('loading').style.display = 'none';
243
+ if (response.ok && result.description) {
244
+ document.getElementById('description-text').textContent = result.description;
245
+ document.getElementById('description-container').style.display = 'block';
246
+ } else {
247
+ document.getElementById('error').textContent = result.error || 'Failed to generate description.';
248
+ document.getElementById('error').style.display = 'block';
249
+ }
250
+ } catch (e) {
251
+ document.getElementById('loading').style.display = 'none';
252
+ document.getElementById('error').textContent = 'An error occurred.';
253
+ document.getElementById('error').style.display = 'block';
254
+ }
255
+ });
256
+ </script>
257
+ </div>
258
+ </body>
259
+ </html>