nniehaus commited on
Commit
32f3c41
·
verified ·
1 Parent(s): 9dc9f53

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +391 -121
app.py CHANGED
@@ -1,144 +1,414 @@
1
  import streamlit as st
2
- from PIL import Image
3
- from io import BytesIO
4
  import requests
 
 
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="🏡",
14
  layout="wide",
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
- # --- API Configuration ---
32
- API_URL = "https://api.openai.com/v1/chat/completions"
33
-
34
- # --- Caching helpers ---
35
- @st.cache_data
36
- def encode_image_to_b64(image: Image.Image) -> str:
37
- # Convert to RGB if needed
38
- if image.mode == 'RGBA':
39
- bg = Image.new('RGB', image.size, (255, 255, 255))
40
- bg.paste(image, mask=image.split()[3])
41
- image = bg
42
- elif image.mode not in ['RGB', 'L']:
43
- image = image.convert('RGB')
44
- # Resize large images
45
- max_size = (1024, 1024)
46
- if image.width > max_size[0] or image.height > max_size[1]:
47
- image.thumbnail(max_size, Image.LANCZOS)
48
- buf = BytesIO()
49
- image.save(buf, format='JPEG', quality=85)
50
- return base64.b64encode(buf.getvalue()).decode('utf-8')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
 
 
 
 
 
 
 
52
 
53
- def fix_formatting(text: str) -> str:
54
- text = re.sub(r'(\d+)([A-Za-z])', r'\1 \2', text)
55
- text = re.sub(r'([.,])([A-Za-z0-9])', r'\1 \2', text)
56
- text = re.sub(r':([A-Za-z0-9])', r': \1', text)
57
- return text
 
 
 
 
58
 
59
- # --- Core analysis ---
60
- def analyze_home_photos(images, timeframe, details, api_key):
61
- if not api_key:
62
- return "Error: API key is required."
63
  headers = {
64
- "Authorization": f"Bearer {api_key}",
65
- "Content-Type": "application/json"
66
  }
67
- # System prompt
68
- system_prompt = (
69
- f"You are a real estate advisor. Today's date is {datetime.now().strftime('%B %d, %Y')}. "
70
- "Analyze only visible areas and give hyper-specific recommendations."
71
- )
72
- # User content with embedded images
73
- user_content = f"Sell within {timeframe}. {details}\n\n"
74
- for img in images:
75
- b64 = encode_image_to_b64(img)
76
- user_content += f"![photo](data:image/jpeg;base64,{b64})\n\n"
77
- payload = {
78
- "model": "gpt-4o",
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  "messages": [
80
- {"role": "system", "content": system_prompt},
81
- {"role": "user", "content": user_content}
82
  ],
83
- "max_tokens": 2000
84
  }
85
- resp = requests.post(API_URL, headers=headers, json=payload, timeout=90)
86
- if resp.status_code == 200:
87
- data = resp.json()
88
- return fix_formatting(data['choices'][0]['message']['content'])
89
- return f"API Error {resp.status_code}: {resp.text[:200]}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- # --- Cost calculation ---
92
- def calculate_cost(usage):
93
- if not usage:
94
- return 0.0
95
- in_tok = usage.get('prompt_tokens', 0)
96
- out_tok = usage.get('completion_tokens', 0)
97
- return in_tok/1e6*3 + out_tok/1e6*10
 
 
 
 
 
 
98
 
99
- # --- UI layout ---
100
- st.title("Home Value Maximizer 🏡")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- # Sidebar inputs
103
- api_key = st.sidebar.text_input("OpenAI API Key", type="password")
104
- uploaded = st.sidebar.file_uploader(
105
- "Upload home photos", accept_multiple_files=True, type=["jpg", "jpeg", "png"]
106
- )
107
- timeframe = st.sidebar.selectbox(
108
- "Selling timeframe", ["Within 1 month", "1–3 months", "3–6 months", "6–12 months", ">12 months"]
109
- )
110
- budget = st.sidebar.slider("Max improvement budget ($)", 1000, 50000, 10000, 1000)
111
- focus = st.sidebar.multiselect(
112
- "Focus areas",
113
- ["Curb appeal", "Kitchen", "Bathroom", "Living Spaces", "Outdoor", "Storage"],
114
- default=["Kitchen", "Bathroom"]
115
- )
116
 
117
- if st.sidebar.button("🔍 Analyze My Home"):
118
- if not api_key:
119
- st.sidebar.error("Enter API key.")
120
- elif not uploaded:
121
- st.sidebar.error("Upload at least one photo.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  else:
123
- images = []
124
- for f in uploaded:
125
- try:
126
- images.append(Image.open(f))
127
- except:
128
- pass
129
- with st.spinner("Analyzing..."):
130
- result = analyze_home_photos(
131
- images,
132
- timeframe,
133
- f"Budget ${budget}. Focus: {', '.join(focus)}",
134
- api_key
135
- )
136
- # Render result with styled container
137
- st.markdown(
138
- f"<div style='background:#e3f2fd;padding:1rem;border-left:4px solid #1f77b4;'>{result}</div>",
139
- unsafe_allow_html=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  )
141
- # Display cost (usage info not returned in HF)
142
- usage = {}
143
- cost = calculate_cost(usage)
144
- st.metric("💲 Estimated cost", f"${cost:.2f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
 
 
2
  import requests
3
+ import os
4
+ import json
5
  import base64
6
+ from io import BytesIO
7
+ from PIL import Image
8
  import time
 
 
9
 
10
+ # Set page config
11
  st.set_page_config(
12
+ page_title="Marketing Graphic Generator",
13
+ page_icon="🎨",
14
  layout="wide",
15
+ initial_sidebar_state="expanded"
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  )
17
 
18
+ # Custom CSS
19
+ st.markdown("""
20
+ <style>
21
+ .main .block-container {
22
+ padding-top: 2rem;
23
+ padding-bottom: 2rem;
24
+ }
25
+ .stApp {
26
+ background-color: #f8f9fa;
27
+ }
28
+ .title-area {
29
+ text-align: center;
30
+ margin-bottom: 2rem;
31
+ }
32
+ .custom-subheader {
33
+ background-color: #f0f2f6;
34
+ padding: 10px;
35
+ border-radius: 5px;
36
+ margin-bottom: 10px;
37
+ font-weight: 600;
38
+ }
39
+ .generated-image {
40
+ border: 1px solid #ddd;
41
+ border-radius: 5px;
42
+ padding: 10px;
43
+ background-color: white;
44
+ }
45
+ .stButton button {
46
+ width: 100%;
47
+ }
48
+ .footnote {
49
+ font-size: 0.8rem;
50
+ color: #6c757d;
51
+ margin-top: 2rem;
52
+ }
53
+ </style>
54
+ """, unsafe_allow_html=True)
55
 
56
+ # Title and description
57
+ st.markdown("""
58
+ <div class="title-area">
59
+ <h1>Marketing Graphic Generator 🎨</h1>
60
+ <p>Create professional marketing and advertising graphics using AI. Customize text, colors, and style for your business.</p>
61
+ </div>
62
+ """, unsafe_allow_html=True)
63
 
64
+ # Initialize session state
65
+ if "api_key" not in st.session_state:
66
+ st.session_state["api_key"] = ""
67
+ if "generated_image" not in st.session_state:
68
+ st.session_state["generated_image"] = None
69
+ if "prompt_history" not in st.session_state:
70
+ st.session_state["prompt_history"] = []
71
+ if "image_history" not in st.session_state:
72
+ st.session_state["image_history"] = []
73
 
74
+ # Functions
75
+ def generate_image_from_prompt(prompt, api_key, model="gpt-4o"):
 
 
76
  headers = {
77
+ "Content-Type": "application/json",
78
+ "Authorization": f"Bearer {api_key}"
79
  }
80
+
81
+ # Create a system message directing the model to create high-quality marketing graphics
82
+ system_message = """You are an expert graphic designer specialized in creating marketing and advertising materials.
83
+ Create high-quality, professional marketing graphics based on the user's specifications.
84
+ Pay careful attention to font choices, color schemes, layout, and branding elements.
85
+ Ensure text is readable and properly positioned.
86
+ Create images with proper aspect ratios and compositions that would work well for marketing purposes."""
87
+
88
+ # Enhance the user's prompt with specific guidance for marketing images
89
+ enhanced_prompt = f"""Create a professional marketing or advertising graphic with the following specifications:
90
+
91
+ {prompt}
92
+
93
+ Make sure the image has:
94
+ 1. High visual appeal suitable for marketing purposes
95
+ 2. Clear, readable text with proper spacing and alignment
96
+ 3. Professional design elements and composition
97
+ 4. Appropriate color harmony and contrast
98
+ 5. Balanced layout with focal points that draw attention
99
+
100
+ The image should look like it was created by a professional graphic designer and be ready to use for marketing campaigns.
101
+ """
102
+
103
+ data = {
104
+ "model": model,
105
  "messages": [
106
+ {"role": "system", "content": system_message},
107
+ {"role": "user", "content": enhanced_prompt}
108
  ],
109
+ "max_tokens": 4096
110
  }
111
+
112
+ try:
113
+ response = requests.post(
114
+ "https://api.openai.com/v1/chat/completions",
115
+ headers=headers,
116
+ json=data
117
+ )
118
+
119
+ if response.status_code == 200:
120
+ response_data = response.json()
121
+ content = response_data["choices"][0]["message"]["content"]
122
+
123
+ # Extract image URL if present
124
+ if "![" in content and "](" in content:
125
+ image_url = content.split("](")[1].split(")")[0]
126
+ if image_url.startswith("https://"):
127
+ return {"success": True, "image_url": image_url, "message": "Image generated successfully!"}
128
+
129
+ # If using the newer image generation where the image is embedded in the response
130
+ if "image_url" in content or "data:image" in content:
131
+ # For newer versions where image content might be directly embedded
132
+ image_matches = re.findall(r'(https://[^\s]+\.(?:png|jpg|jpeg|gif))', content)
133
+ if image_matches:
134
+ return {"success": True, "image_url": image_matches[0], "message": "Image generated successfully!"}
135
+ elif "data:image" in content:
136
+ # Handle base64 encoded image if present
137
+ base64_match = re.search(r'data:image/[^;]+;base64,([^"]+)', content)
138
+ if base64_match:
139
+ base64_data = base64_match.group(1)
140
+ return {"success": True, "image_data": base64_data, "message": "Image generated successfully!"}
141
+
142
+ return {"success": False, "message": "Could not extract image from response. The model may need additional prompting."}
143
+
144
+ else:
145
+ error_message = f"API Error: {response.status_code}"
146
+ if response.text:
147
+ try:
148
+ error_json = response.json()
149
+ if "error" in error_json:
150
+ error_message += f" - {error_json['error']['message']}"
151
+ except:
152
+ error_message += f" - {response.text[:200]}"
153
+ return {"success": False, "message": error_message}
154
+ except Exception as e:
155
+ return {"success": False, "message": f"Error: {str(e)}"}
156
 
157
+ def save_image(image_data):
158
+ # Create directory if it doesn't exist
159
+ if not os.path.exists("generated_images"):
160
+ os.makedirs("generated_images")
161
+
162
+ # Generate a unique filename
163
+ timestamp = int(time.time())
164
+ filename = f"generated_images/marketing_graphic_{timestamp}.png"
165
+
166
+ # Save the image
167
+ image_data.save(filename)
168
+
169
+ return filename
170
 
171
+ # Sidebar - Configuration
172
+ with st.sidebar:
173
+ st.markdown('<div class="custom-subheader">API Configuration</div>', unsafe_allow_html=True)
174
+
175
+ api_key = st.text_input(
176
+ "Enter your OpenAI API Key",
177
+ value=st.session_state["api_key"],
178
+ type="password",
179
+ help="Your API key is required to generate images. It will not be stored permanently."
180
+ )
181
+ st.session_state["api_key"] = api_key
182
+
183
+ # Display a warning if no API key is provided
184
+ if not api_key:
185
+ st.warning("⚠️ Please enter your OpenAI API key to use this tool")
186
+
187
+ st.markdown('<div class="custom-subheader">Advanced Settings</div>', unsafe_allow_html=True)
188
+
189
+ # Image quality and size settings
190
+ image_quality = st.select_slider(
191
+ "Image Quality",
192
+ options=["Standard", "High", "Maximum"],
193
+ value="High"
194
+ )
195
+
196
+ aspect_ratio = st.selectbox(
197
+ "Aspect Ratio",
198
+ ["Square (1:1)", "Landscape (16:9)", "Portrait (9:16)", "Facebook (1200x628)", "Instagram (1080x1080)", "Twitter (1200x675)"],
199
+ index=0
200
+ )
201
 
202
+ # History
203
+ if st.session_state["prompt_history"]:
204
+ st.markdown('<div class="custom-subheader">Prompt History</div>', unsafe_allow_html=True)
205
+ for i, prompt in enumerate(st.session_state["prompt_history"][-5:]):
206
+ if st.button(f"Reuse: {prompt[:30]}...", key=f"history_{i}"):
207
+ st.session_state["current_prompt"] = prompt
 
 
 
 
 
 
 
 
208
 
209
+ # Main content
210
+ col1, col2 = st.columns([1, 1])
211
+
212
+ with col1:
213
+ st.markdown('<div class="custom-subheader">Design Specifications</div>', unsafe_allow_html=True)
214
+
215
+ # Business Information
216
+ business_name = st.text_input("Business Name", placeholder="e.g., Sunset Cafe")
217
+ business_type = st.text_input("Business Type/Industry", placeholder="e.g., Coffee Shop, Real Estate, Fitness Studio")
218
+ location = st.text_input("Location (City/Region)", placeholder="e.g., San Francisco, CA")
219
+
220
+ # Marketing Content
221
+ st.markdown("##### Marketing Content")
222
+
223
+ marketing_purpose = st.selectbox(
224
+ "Purpose of Graphic",
225
+ ["Advertisement", "Social Media Post", "Flyer", "Banner", "Logo", "Business Card", "Email Header", "Website Hero Image", "Promotional Offer"],
226
+ index=0
227
+ )
228
+
229
+ headline = st.text_input("Headline/Main Text", placeholder="e.g., Grand Opening! 50% Off All Drinks")
230
+ subheading = st.text_input("Subheading/Secondary Text", placeholder="e.g., This weekend only - May 15-17")
231
+ call_to_action = st.text_input("Call to Action", placeholder="e.g., Visit us today! Call 555-123-4567")
232
+
233
+ # Visual Style
234
+ st.markdown("##### Visual Style")
235
+
236
+ color_scheme = st.text_input("Color Scheme", placeholder="e.g., Blue and gold, #FF5733 and #33FF57, Corporate colors")
237
+ font_style = st.text_input("Font Style", placeholder="e.g., Modern sans-serif, Elegant serif, Playful handwritten")
238
+ image_style = st.text_input("Image Style/Mood", placeholder="e.g., Minimalist, Vibrant, Professional, Vintage")
239
+
240
+ # Additional Elements
241
+ st.markdown("##### Additional Elements")
242
+
243
+ visual_elements = st.text_area(
244
+ "Specific Visual Elements to Include",
245
+ placeholder="e.g., Coffee cup with steam, Mountain backdrop, Product showcase, People enjoying the service",
246
+ height=100
247
+ )
248
+
249
+ special_instructions = st.text_area(
250
+ "Special Instructions",
251
+ placeholder="Any additional details or specific requirements for the design",
252
+ height=100
253
+ )
254
+
255
+ # Build the prompt
256
+ if "current_prompt" in st.session_state:
257
+ complete_prompt = st.session_state["current_prompt"]
258
+ del st.session_state["current_prompt"]
259
  else:
260
+ prompt_parts = []
261
+
262
+ # Add business information
263
+ if business_name:
264
+ prompt_parts.append(f"Business Name: {business_name}")
265
+ if business_type:
266
+ prompt_parts.append(f"Business Type: {business_type}")
267
+ if location:
268
+ prompt_parts.append(f"Location: {location}")
269
+
270
+ # Add marketing content
271
+ prompt_parts.append(f"Create a {marketing_purpose.lower()}")
272
+ if headline:
273
+ prompt_parts.append(f"Headline: '{headline}'")
274
+ if subheading:
275
+ prompt_parts.append(f"Subheading: '{subheading}'")
276
+ if call_to_action:
277
+ prompt_parts.append(f"Call to Action: '{call_to_action}'")
278
+
279
+ # Add visual style
280
+ if color_scheme:
281
+ prompt_parts.append(f"Color Scheme: {color_scheme}")
282
+ if font_style:
283
+ prompt_parts.append(f"Font Style: {font_style}")
284
+ if image_style:
285
+ prompt_parts.append(f"Visual Style: {image_style}")
286
+
287
+ # Add aspect ratio
288
+ if "Square" in aspect_ratio:
289
+ prompt_parts.append("Create a square (1:1) image")
290
+ elif "Landscape" in aspect_ratio:
291
+ prompt_parts.append("Create a landscape (16:9) format image")
292
+ elif "Portrait" in aspect_ratio:
293
+ prompt_parts.append("Create a portrait (9:16) format image")
294
+ elif "Facebook" in aspect_ratio:
295
+ prompt_parts.append("Create an image in Facebook post dimensions (1200x628)")
296
+ elif "Instagram" in aspect_ratio:
297
+ prompt_parts.append("Create an image in Instagram post dimensions (1080x1080)")
298
+ elif "Twitter" in aspect_ratio:
299
+ prompt_parts.append("Create an image in Twitter post dimensions (1200x675)")
300
+
301
+ # Add image quality
302
+ prompt_parts.append(f"Generate a {image_quality.lower()} quality image")
303
+
304
+ # Add additional elements
305
+ if visual_elements:
306
+ prompt_parts.append(f"Include these visual elements: {visual_elements}")
307
+ if special_instructions:
308
+ prompt_parts.append(f"Special instructions: {special_instructions}")
309
+
310
+ prompt_parts.append("Make text clear, readable, and properly positioned. Ensure professional design suitable for marketing purposes.")
311
+
312
+ complete_prompt = ". ".join(prompt_parts)
313
+
314
+ # Display the complete prompt with ability to edit
315
+ final_prompt = st.text_area(
316
+ "Complete Prompt (Edit as needed)",
317
+ value=complete_prompt,
318
+ height=150
319
+ )
320
+
321
+ # Generate button
322
+ generate_button = st.button(
323
+ "🎨 Generate Marketing Graphic",
324
+ disabled=not api_key,
325
+ use_container_width=True
326
+ )
327
+
328
+ if generate_button:
329
+ with st.spinner("Generating your marketing graphic... This may take up to 60 seconds"):
330
+ # Save the prompt to history
331
+ if final_prompt not in st.session_state["prompt_history"]:
332
+ st.session_state["prompt_history"].insert(0, final_prompt)
333
+ # Keep only the last 10 prompts
334
+ st.session_state["prompt_history"] = st.session_state["prompt_history"][:10]
335
+
336
+ # Call API to generate image
337
+ result = generate_image_from_prompt(final_prompt, api_key)
338
+
339
+ if result["success"]:
340
+ # Handle image URL or base64 data
341
+ if "image_url" in result:
342
+ try:
343
+ response = requests.get(result["image_url"])
344
+ img = Image.open(BytesIO(response.content))
345
+ st.session_state["generated_image"] = img
346
+ except Exception as e:
347
+ st.error(f"Error loading image: {str(e)}")
348
+ elif "image_data" in result:
349
+ try:
350
+ img_data = base64.b64decode(result["image_data"])
351
+ img = Image.open(BytesIO(img_data))
352
+ st.session_state["generated_image"] = img
353
+ except Exception as e:
354
+ st.error(f"Error decoding image: {str(e)}")
355
+ else:
356
+ st.error(result["message"])
357
+ if "API key" in result["message"]:
358
+ st.warning("Please check that your API key is valid and has access to the GPT-4o model")
359
+
360
+ # Display generated image
361
+ with col2:
362
+ st.markdown('<div class="custom-subheader">Generated Marketing Graphic</div>', unsafe_allow_html=True)
363
+
364
+ if st.session_state["generated_image"] is not None:
365
+ st.markdown('<div class="generated-image">', unsafe_allow_html=True)
366
+ st.image(st.session_state["generated_image"], use_column_width=True)
367
+ st.markdown('</div>', unsafe_allow_html=True)
368
+
369
+ # Download button
370
+ img_buffer = BytesIO()
371
+ st.session_state["generated_image"].save(img_buffer, format="PNG")
372
+ img_bytes = img_buffer.getvalue()
373
+
374
+ st.download_button(
375
+ label="💾 Download Image",
376
+ data=img_bytes,
377
+ file_name=f"marketing_graphic_{int(time.time())}.png",
378
+ mime="image/png",
379
+ use_container_width=True
380
  )
381
+
382
+ # Regenerate button
383
+ if st.button("🔄 Regenerate with Same Prompt", use_container_width=True):
384
+ with st.spinner("Regenerating image... This may take up to 60 seconds"):
385
+ result = generate_image_from_prompt(final_prompt, api_key)
386
+
387
+ if result["success"]:
388
+ if "image_url" in result:
389
+ try:
390
+ response = requests.get(result["image_url"])
391
+ img = Image.open(BytesIO(response.content))
392
+ st.session_state["generated_image"] = img
393
+ except Exception as e:
394
+ st.error(f"Error loading image: {str(e)}")
395
+ elif "image_data" in result:
396
+ try:
397
+ img_data = base64.b64decode(result["image_data"])
398
+ img = Image.open(BytesIO(img_data))
399
+ st.session_state["generated_image"] = img
400
+ except Exception as e:
401
+ st.error(f"Error decoding image: {str(e)}")
402
+ else:
403
+ st.error(result["message"])
404
+ else:
405
+ st.info("Your generated marketing graphic will appear here. Fill in the form and click 'Generate Marketing Graphic' to create your image.")
406
+ st.image("https://via.placeholder.com/800x800.png?text=Marketing+Graphic+Preview", use_column_width=True)
407
+
408
+ # Footer with disclaimer
409
+ st.markdown("""
410
+ <div class="footnote">
411
+ <p><strong>Disclaimer:</strong> This tool uses OpenAI's 4o Image Generation to create marketing graphics. The quality and accuracy of the generated images depend on the model's capabilities and your prompt. For professional marketing materials, consider reviewing and refining the generated images with a graphic designer.</p>
412
+ <p>Images generated using this tool comply with OpenAI's usage policies. You are responsible for ensuring that your use of the generated images complies with applicable laws and regulations.</p>
413
+ </div>
414
+ """, unsafe_allow_html=True)