mroccuper commited on
Commit
39b3133
·
verified ·
1 Parent(s): b455c9b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -73
app.py CHANGED
@@ -1,8 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import google.generativeai as genai
3
  import re
4
  import json
5
- import os # Good practice to import os if using env variables later
6
 
7
  # --- Constants for Limits ---
8
  TITLE_MAX_LEN = 60
@@ -95,19 +107,27 @@ REMINDER: Make sure each title aims for EXACTLY {TITLE_MAX_LEN} characters. Coun
95
 
96
  return combined_prompt
97
 
98
- def generate_amazon_listing(api_key, quote, niche, target, keywords):
 
99
  """Generate Amazon listing using Gemini API, enforcing character limits."""
100
- # Input validation
101
- if not api_key:
102
- return "Error: Please enter a valid Gemini API key"
 
 
 
 
103
  if not quote or not niche or not target:
104
- return "Error: Please fill in all required fields (Quote, Holiday/Event, and Target Audience)"
105
 
106
  try:
107
- # Configure the Gemini API with the provided key
108
- # Consider using environment variables for production:
109
- # api_key = os.getenv("GEMINI_API_KEY") or api_key_input
110
- genai.configure(api_key=api_key)
 
 
 
111
 
112
  # Create model with optimized settings
113
  model = genai.GenerativeModel(
@@ -116,7 +136,6 @@ def generate_amazon_listing(api_key, quote, niche, target, keywords):
116
  "temperature": 0.3, # Lower temperature for more predictable output
117
  "top_p": 0.8,
118
  "max_output_tokens": 1024, # Maximum tokens the API can *return*
119
- # Note: 'max_output_tokens' doesn't directly control character count precisely
120
  "response_mime_type": "application/json" # Request JSON directly
121
  }
122
  )
@@ -128,9 +147,8 @@ def generate_amazon_listing(api_key, quote, niche, target, keywords):
128
  # --- Generate Main Listing Content ---
129
  response = model.generate_content(prompt)
130
 
131
- # Since we requested JSON, parse it directly. Add error handling.
132
  try:
133
- # Access the text part and load as JSON
134
  response_text = response.text
135
  # Sometimes the API might still wrap it in markdown, try to strip it
136
  json_match = re.search(r'```json\s*({.*?})\s*```', response_text, re.DOTALL | re.IGNORECASE)
@@ -143,37 +161,30 @@ def generate_amazon_listing(api_key, quote, niche, target, keywords):
143
  result = json.loads(json_str)
144
 
145
  except (json.JSONDecodeError, AttributeError, IndexError) as json_err:
146
- # Handle cases where response.text is empty, not valid JSON, or API returned unexpected format
147
  print(f"JSON Parsing Error: {json_err}")
148
- print(f"Raw response text: {getattr(response, 'text', 'N/A')}") # Log raw response for debugging
149
- # Check for safety blocks
150
- if response.prompt_feedback.block_reason:
151
  return f"Error: Generation blocked due to: {response.prompt_feedback.block_reason}. Content filters may have been triggered."
152
  return "Error: Could not parse JSON response from Gemini API. The API might have returned an unexpected format or an empty response. Please try again."
153
 
154
-
155
  # --- ENFORCE CHARACTER LIMITS ---
156
- # Get raw values and immediately truncate if they exceed the MAX limit
157
  title = result.get("title", "")[:TITLE_MAX_LEN]
158
  brand_name = result.get("brand_name", "")[:BRAND_NAME_MAX_LEN]
159
  bullet1 = result.get("bullet_point_1", "")[:BULLET_POINT_MAX_LEN]
160
  bullet2 = result.get("bullet_point_2", "")[:BULLET_POINT_MAX_LEN]
161
- suggested_keywords = result.get("suggested_keywords", "Error generating suggested keywords") # Keywords don't usually need truncation
162
 
163
  # --- VALIDATION (using potentially truncated values) ---
164
-
165
- # Validate that the output actually matches the input criteria (optional but good)
166
- # Check if *any* part of the target audience is in the title if it's comma-separated
167
  target_parts = [t.strip().lower() for t in target.split(',')]
168
  title_lower = title.lower()
169
  if not (quote.lower() in title_lower or
170
  niche.lower() in title_lower or
171
  any(t_part in title_lower for t_part in target_parts)):
172
- return f"Error: Generated title ('{title}') doesn't seem to strongly match the requested theme: '{quote}', '{niche}', or '{target}'. Please try again or adjust input."
 
 
173
 
174
  # --- Optional: Validate the *lower* bound for bullet points ---
175
- # Uncomment these lines if you strictly need the bullets to be AT LEAST min_len characters
176
- # Note: This check happens *after* truncation, so if truncation occurred, it might pass this check.
177
  # if len(bullet1) < BULLET_POINT_MIN_LEN:
178
  # return f"Error: Bullet point 1 length ({len(bullet1)}) is less than the required minimum {BULLET_POINT_MIN_LEN} characters after generation/truncation. Please try again."
179
  # if len(bullet2) < BULLET_POINT_MIN_LEN:
@@ -188,7 +199,7 @@ def generate_amazon_listing(api_key, quote, niche, target, keywords):
188
  if phrase in bullet1_lower or phrase in bullet2_lower:
189
  return f"Error: Generated bullet points contain disallowed generic phrase '{phrase}'. Please try again."
190
 
191
- # Format main output first - using the enforced length values
192
  main_output = format_output(
193
  title,
194
  brand_name,
@@ -200,12 +211,10 @@ def generate_amazon_listing(api_key, quote, niche, target, keywords):
200
  # --- Generate Variations (Optional Second Call) ---
201
  try:
202
  variations_prompt = generate_multiple_variations_prompt(quote, niche, target, keywords)
203
- # Use a separate model instance or reuse if configuration is the same
204
- # Using the same model instance here
205
  response_var = model.generate_content(
206
  variations_prompt,
207
- generation_config={ # Can reuse or adjust config for variations
208
- "temperature": 0.4, # Slightly higher temp for more variety
209
  "top_p": 0.8,
210
  "max_output_tokens": 1024,
211
  "response_mime_type": "application/json"
@@ -215,7 +224,6 @@ def generate_amazon_listing(api_key, quote, niche, target, keywords):
215
  # Parse variations JSON
216
  try:
217
  response_var_text = response_var.text
218
- # Try stripping markdown again
219
  json_match_var = re.search(r'```json\s*({.*?})\s*```', response_var_text, re.DOTALL | re.IGNORECASE)
220
  if json_match_var:
221
  json_str_var = json_match_var.group(1)
@@ -224,52 +232,58 @@ def generate_amazon_listing(api_key, quote, niche, target, keywords):
224
 
225
  variations = json.loads(json_str_var)
226
 
227
- # Format variations output, enforcing limits here too
228
  variations_output = "\n\nADDITIONAL VARIATIONS:\n\n"
229
  variations_output += "Title Variations:\n"
230
  for i, var in enumerate(variations.get("title_variations", []), 1):
231
- truncated_var = var[:TITLE_MAX_LEN] # Enforce limit
232
  variations_output += f"{i}. {truncated_var} ({count_characters(truncated_var)}/{TITLE_MAX_LEN} characters)\n"
233
 
234
  variations_output += "\nBrand Name Variations:\n"
235
  for i, var in enumerate(variations.get("brand_name_variations", []), 1):
236
- truncated_var = var[:BRAND_NAME_MAX_LEN] # Enforce limit
237
  variations_output += f"{i}. {truncated_var} ({count_characters(truncated_var)}/{BRAND_NAME_MAX_LEN} characters)\n"
238
 
239
- # Combine main output with variations
240
  return main_output + variations_output
241
 
242
  except (json.JSONDecodeError, AttributeError, IndexError) as json_var_err:
243
  print(f"JSON Parsing Error (Variations): {json_var_err}")
244
  print(f"Raw variations response text: {getattr(response_var, 'text', 'N/A')}")
245
- # Check for safety blocks on variations
246
- if response_var.prompt_feedback.block_reason:
247
  return main_output + f"\n\n(Could not generate variations: Blocked - {response_var.prompt_feedback.block_reason})"
248
  return main_output + "\n\n(Could not parse variations response)"
249
 
250
  except genai.types.generation_types.BlockedPromptException as var_block_error:
251
  return main_output + f"\n\n(Variations prompt blocked: {var_block_error})"
252
  except Exception as var_error:
253
- # Catch other errors during variation generation
254
- print(f"Error generating variations: {var_error}") # Log the error
255
  return main_output + f"\n\n(Could not generate variations due to an error)"
256
 
257
  except genai.types.generation_types.BlockedPromptException as block_error:
258
- # Catch blocked prompts specifically for better feedback
259
  return f"Error: The main prompt was blocked by Gemini API safety filters: {block_error}. Please modify your input and try again."
260
  except Exception as e:
261
- # Catch other potential errors during the main API call
262
- print(f"Error during main listing generation: {e}") # Log the error
263
- # You might want to check response.candidates[0].finish_reason if available
264
- # finish_reason = getattr(response.candidates[0], 'finish_reason', 'UNKNOWN')
265
- # safety_ratings = getattr(response.candidates[0].safety_ratings, 'name', 'UNKNOWN')
266
- return f"Error generating main listing. Please check logs or try again."
267
-
 
 
 
 
 
 
 
268
  except Exception as e:
269
- # Catch configuration errors or other unexpected issues
270
- print(f"General Error: {e}") # Log the error
 
 
271
  return f"An unexpected error occurred: {str(e)}"
272
 
 
273
  # --- Create the Gradio Interface ---
274
  def create_interface():
275
  with gr.Blocks(title="Amazon Merch on Demand Listing Generator", theme=gr.themes.Soft()) as app:
@@ -278,12 +292,12 @@ def create_interface():
278
 
279
  with gr.Row():
280
  with gr.Column(scale=1):
281
- # Recommend using environment variable for API key in real deployments
282
- api_key = gr.Textbox(
283
  label="Gemini API Key",
284
- placeholder="Enter your Gemini API key (or leave blank if set as environment variable)",
285
  type="password",
286
- # value=os.getenv("GEMINI_API_KEY", "") # Pre-fill if env var exists
287
  )
288
  quote = gr.Textbox(label="Quote/Design/Idea", placeholder="e.g., Lucky To Be A Teacher", value="Rainbow with a quote \"Lucky To Be A Teacher\"")
289
  niche = gr.Textbox(label="Niche/Holiday/Event", placeholder="e.g., St Patrick's Day", value="St Patricks Day")
@@ -300,21 +314,23 @@ def create_interface():
300
  status = gr.Textbox(label="Status", value="Ready", interactive=False, lines=1)
301
  output = gr.Textbox(label="Generated Amazon Listing", lines=25, interactive=True) # Make output selectable
302
 
303
- def on_submit(api_key_input, quote, niche, target, keywords):
304
- # Use environment variable if input is blank (optional but good practice)
305
- # final_api_key = os.getenv("GEMINI_API_KEY") or api_key_input
306
- final_api_key = api_key_input # Keep it simple for now
307
-
308
- if not final_api_key:
309
- # Update status first before returning error message
310
  yield "Error: Gemini API Key is required.", ""
311
- return # Stop execution
 
 
 
 
312
 
313
  # Update status to indicate processing
314
  yield "Generating listing... This may take a moment.", "Processing..."
315
 
316
- # Generate the listing
317
- result = generate_amazon_listing(final_api_key, quote, niche, target, keywords)
318
 
319
  # Update status and output based on the result
320
  if "Error:" in result:
@@ -322,11 +338,13 @@ def create_interface():
322
  else:
323
  yield "Listing generated successfully!", result
324
 
 
325
  submit_btn.click(
326
  fn=on_submit,
327
- inputs=[api_key, quote, niche, target, keywords],
 
328
  outputs=[status, output],
329
- show_progress="full" # Show more detailed progress
330
  )
331
 
332
  gr.Markdown("## Example Input")
@@ -340,11 +358,11 @@ Use the pre-filled example above or enter your own details:
340
 
341
  gr.Markdown("""
342
  ## Notes & Troubleshooting
 
343
  * **Character Limits:** The app attempts to generate text close to the limits requested in the prompt, but **strictly enforces maximum lengths** by truncating if necessary (Title: 60, Brand: 50, Bullets: 256). The displayed count reflects the final length.
344
- * **API Key:** For security, consider setting your Gemini API Key as an environment variable (`GEMINI_API_KEY`) instead of pasting it directly, especially if deploying publicly.
345
- * **Errors:** If you see errors related to 'BlockedPromptException' or 'Safety', your input might have triggered content filters. Try rephrasing. Other errors might relate to API connectivity or quota.
346
  * **Variations:** The app generates the main listing first, then attempts to generate title/brand variations. Variation generation might fail separately without affecting the main listing.
347
- * **JSON Request:** The app now explicitly requests JSON output from the API (`response_mime_type`).
348
  """)
349
 
350
  return app
@@ -352,8 +370,8 @@ Use the pre-filled example above or enter your own details:
352
  # --- Create and Launch the Gradio App ---
353
  app = create_interface()
354
 
355
- # Launch the app (remove debug=True for production)
 
356
  if __name__ == "__main__":
357
- # Set share=True to get a public link (useful for temporary sharing)
358
- app.launch(debug=True)
359
- # app.launch() # For deployment (e.g., Hugging Face Spaces)
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ################################################################################
4
+ # WARNING: API Key Security
5
+ # This application requires you to enter your Gemini API key manually in the UI.
6
+ # DO NOT paste your API key directly into this code if you plan to share it.
7
+ # Ensure your API key is kept confidential. Sharing your key can lead to
8
+ # unauthorized use and potential charges to your account.
9
+ # Consider using secure methods like environment variables or secrets management
10
+ # for deployed applications.
11
+ ################################################################################
12
+
13
  import gradio as gr
14
  import google.generativeai as genai
15
  import re
16
  import json
17
+ import os # Still useful for potential future path operations, etc.
18
 
19
  # --- Constants for Limits ---
20
  TITLE_MAX_LEN = 60
 
107
 
108
  return combined_prompt
109
 
110
+ # Takes the API Key directly as an argument
111
+ def generate_amazon_listing(api_key_input, quote, niche, target, keywords):
112
  """Generate Amazon listing using Gemini API, enforcing character limits."""
113
+
114
+ # --- Input Validation ---
115
+ # Clean the API key first to handle potential whitespace issues
116
+ cleaned_api_key = api_key_input.strip() if api_key_input else ""
117
+
118
+ if not cleaned_api_key:
119
+ return "Error: Please enter a valid Gemini API key."
120
  if not quote or not niche or not target:
121
+ return "Error: Please fill in all required fields (Quote, Holiday/Event, and Target Audience)."
122
 
123
  try:
124
+ # --- Configure API ---
125
+ # Use the cleaned API key from the input
126
+ print(f"DEBUG: Configuring with API Key (length {len(cleaned_api_key)}): '{cleaned_api_key[:5]}...{cleaned_api_key[-5:]}'") # Log sanitized key
127
+ if '\n' in cleaned_api_key or '\r' in cleaned_api_key:
128
+ print("WARNING: API Key may contain newline characters!")
129
+
130
+ genai.configure(api_key=cleaned_api_key)
131
 
132
  # Create model with optimized settings
133
  model = genai.GenerativeModel(
 
136
  "temperature": 0.3, # Lower temperature for more predictable output
137
  "top_p": 0.8,
138
  "max_output_tokens": 1024, # Maximum tokens the API can *return*
 
139
  "response_mime_type": "application/json" # Request JSON directly
140
  }
141
  )
 
147
  # --- Generate Main Listing Content ---
148
  response = model.generate_content(prompt)
149
 
150
+ # Parse JSON response
151
  try:
 
152
  response_text = response.text
153
  # Sometimes the API might still wrap it in markdown, try to strip it
154
  json_match = re.search(r'```json\s*({.*?})\s*```', response_text, re.DOTALL | re.IGNORECASE)
 
161
  result = json.loads(json_str)
162
 
163
  except (json.JSONDecodeError, AttributeError, IndexError) as json_err:
 
164
  print(f"JSON Parsing Error: {json_err}")
165
+ print(f"Raw response text: {getattr(response, 'text', 'N/A')}")
166
+ if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
 
167
  return f"Error: Generation blocked due to: {response.prompt_feedback.block_reason}. Content filters may have been triggered."
168
  return "Error: Could not parse JSON response from Gemini API. The API might have returned an unexpected format or an empty response. Please try again."
169
 
 
170
  # --- ENFORCE CHARACTER LIMITS ---
 
171
  title = result.get("title", "")[:TITLE_MAX_LEN]
172
  brand_name = result.get("brand_name", "")[:BRAND_NAME_MAX_LEN]
173
  bullet1 = result.get("bullet_point_1", "")[:BULLET_POINT_MAX_LEN]
174
  bullet2 = result.get("bullet_point_2", "")[:BULLET_POINT_MAX_LEN]
175
+ suggested_keywords = result.get("suggested_keywords", "Error generating suggested keywords")
176
 
177
  # --- VALIDATION (using potentially truncated values) ---
 
 
 
178
  target_parts = [t.strip().lower() for t in target.split(',')]
179
  title_lower = title.lower()
180
  if not (quote.lower() in title_lower or
181
  niche.lower() in title_lower or
182
  any(t_part in title_lower for t_part in target_parts)):
183
+ # This is a soft warning, not necessarily a hard error
184
+ print(f"Warning: Generated title ('{title}') might not strongly match the requested theme: '{quote}', '{niche}', or '{target}'.")
185
+
186
 
187
  # --- Optional: Validate the *lower* bound for bullet points ---
 
 
188
  # if len(bullet1) < BULLET_POINT_MIN_LEN:
189
  # return f"Error: Bullet point 1 length ({len(bullet1)}) is less than the required minimum {BULLET_POINT_MIN_LEN} characters after generation/truncation. Please try again."
190
  # if len(bullet2) < BULLET_POINT_MIN_LEN:
 
199
  if phrase in bullet1_lower or phrase in bullet2_lower:
200
  return f"Error: Generated bullet points contain disallowed generic phrase '{phrase}'. Please try again."
201
 
202
+ # Format main output first
203
  main_output = format_output(
204
  title,
205
  brand_name,
 
211
  # --- Generate Variations (Optional Second Call) ---
212
  try:
213
  variations_prompt = generate_multiple_variations_prompt(quote, niche, target, keywords)
 
 
214
  response_var = model.generate_content(
215
  variations_prompt,
216
+ generation_config={
217
+ "temperature": 0.4,
218
  "top_p": 0.8,
219
  "max_output_tokens": 1024,
220
  "response_mime_type": "application/json"
 
224
  # Parse variations JSON
225
  try:
226
  response_var_text = response_var.text
 
227
  json_match_var = re.search(r'```json\s*({.*?})\s*```', response_var_text, re.DOTALL | re.IGNORECASE)
228
  if json_match_var:
229
  json_str_var = json_match_var.group(1)
 
232
 
233
  variations = json.loads(json_str_var)
234
 
235
+ # Format variations output, enforcing limits
236
  variations_output = "\n\nADDITIONAL VARIATIONS:\n\n"
237
  variations_output += "Title Variations:\n"
238
  for i, var in enumerate(variations.get("title_variations", []), 1):
239
+ truncated_var = var[:TITLE_MAX_LEN]
240
  variations_output += f"{i}. {truncated_var} ({count_characters(truncated_var)}/{TITLE_MAX_LEN} characters)\n"
241
 
242
  variations_output += "\nBrand Name Variations:\n"
243
  for i, var in enumerate(variations.get("brand_name_variations", []), 1):
244
+ truncated_var = var[:BRAND_NAME_MAX_LEN]
245
  variations_output += f"{i}. {truncated_var} ({count_characters(truncated_var)}/{BRAND_NAME_MAX_LEN} characters)\n"
246
 
 
247
  return main_output + variations_output
248
 
249
  except (json.JSONDecodeError, AttributeError, IndexError) as json_var_err:
250
  print(f"JSON Parsing Error (Variations): {json_var_err}")
251
  print(f"Raw variations response text: {getattr(response_var, 'text', 'N/A')}")
252
+ if hasattr(response_var, 'prompt_feedback') and response_var.prompt_feedback.block_reason:
 
253
  return main_output + f"\n\n(Could not generate variations: Blocked - {response_var.prompt_feedback.block_reason})"
254
  return main_output + "\n\n(Could not parse variations response)"
255
 
256
  except genai.types.generation_types.BlockedPromptException as var_block_error:
257
  return main_output + f"\n\n(Variations prompt blocked: {var_block_error})"
258
  except Exception as var_error:
259
+ print(f"Error generating variations: {var_error}")
 
260
  return main_output + f"\n\n(Could not generate variations due to an error)"
261
 
262
  except genai.types.generation_types.BlockedPromptException as block_error:
 
263
  return f"Error: The main prompt was blocked by Gemini API safety filters: {block_error}. Please modify your input and try again."
264
  except Exception as e:
265
+ print(f"Error during main listing generation: {e}")
266
+ # Attempt to get more specific feedback if available
267
+ finish_reason = "Unknown"
268
+ safety_ratings = "Unknown"
269
+ if hasattr(response, 'candidates') and response.candidates:
270
+ finish_reason = getattr(response.candidates[0], 'finish_reason', 'UNKNOWN')
271
+ safety_ratings = getattr(response.candidates[0], 'safety_ratings', 'UNKNOWN')
272
+ print(f"Finish Reason: {finish_reason}, Safety Ratings: {safety_ratings}")
273
+ return f"Error generating main listing (Reason: {finish_reason}). Please check logs or try again."
274
+
275
+ # Catch configuration errors or other unexpected issues outside the API call block
276
+ except google.api_core.exceptions.PermissionDenied as perm_denied:
277
+ print(f"Permission Denied Error: {perm_denied}")
278
+ return "Error: Permission Denied. Please check if your API key is valid, correctly entered, and has the necessary permissions enabled."
279
  except Exception as e:
280
+ print(f"General Error: {e}")
281
+ # Check if it's an authentication error which often manifests differently
282
+ if "authentication" in str(e).lower() or "credentials" in str(e).lower():
283
+ return "Error: Authentication failed. Please double-check your API key."
284
  return f"An unexpected error occurred: {str(e)}"
285
 
286
+
287
  # --- Create the Gradio Interface ---
288
  def create_interface():
289
  with gr.Blocks(title="Amazon Merch on Demand Listing Generator", theme=gr.themes.Soft()) as app:
 
292
 
293
  with gr.Row():
294
  with gr.Column(scale=1):
295
+ # API Key input is now mandatory here
296
+ api_key_ui = gr.Textbox(
297
  label="Gemini API Key",
298
+ placeholder="Enter your Gemini API key here",
299
  type="password",
300
+ # Make sure this input component is mandatory in the logic below
301
  )
302
  quote = gr.Textbox(label="Quote/Design/Idea", placeholder="e.g., Lucky To Be A Teacher", value="Rainbow with a quote \"Lucky To Be A Teacher\"")
303
  niche = gr.Textbox(label="Niche/Holiday/Event", placeholder="e.g., St Patrick's Day", value="St Patricks Day")
 
314
  status = gr.Textbox(label="Status", value="Ready", interactive=False, lines=1)
315
  output = gr.Textbox(label="Generated Amazon Listing", lines=25, interactive=True) # Make output selectable
316
 
317
+ # This function is called when the button is clicked
318
+ def on_submit(api_key_from_ui, quote, niche, target, keywords):
319
+ # --- Validation directly in the submit handler ---
320
+ if not api_key_from_ui or not api_key_from_ui.strip():
321
+ # Update status first before returning error message
 
 
322
  yield "Error: Gemini API Key is required.", ""
323
+ return # Stop execution if API key is missing
324
+
325
+ if not quote or not niche or not target:
326
+ yield "Error: Quote, Niche/Holiday, and Target Audience are required.", ""
327
+ return # Stop if other core fields are missing
328
 
329
  # Update status to indicate processing
330
  yield "Generating listing... This may take a moment.", "Processing..."
331
 
332
+ # Call the main generation function, passing the API key from the UI
333
+ result = generate_amazon_listing(api_key_from_ui, quote, niche, target, keywords)
334
 
335
  # Update status and output based on the result
336
  if "Error:" in result:
 
338
  else:
339
  yield "Listing generated successfully!", result
340
 
341
+ # Connect the button click to the on_submit function
342
  submit_btn.click(
343
  fn=on_submit,
344
+ # Pass the API key from the UI component as the first input
345
+ inputs=[api_key_ui, quote, niche, target, keywords],
346
  outputs=[status, output],
347
+ show_progress="full"
348
  )
349
 
350
  gr.Markdown("## Example Input")
 
358
 
359
  gr.Markdown("""
360
  ## Notes & Troubleshooting
361
+ * **API Key:** You MUST enter a valid Gemini API key in the field above for the generator to work. Keep your key secure.
362
  * **Character Limits:** The app attempts to generate text close to the limits requested in the prompt, but **strictly enforces maximum lengths** by truncating if necessary (Title: 60, Brand: 50, Bullets: 256). The displayed count reflects the final length.
363
+ * **Errors:** If you see errors related to 'BlockedPromptException' or 'Safety', your input might have triggered content filters. Try rephrasing. 'Permission Denied' or 'Authentication Failed' errors usually indicate an invalid or incorrectly entered API key. Other errors might relate to API connectivity or quota.
 
364
  * **Variations:** The app generates the main listing first, then attempts to generate title/brand variations. Variation generation might fail separately without affecting the main listing.
365
+ * **JSON Request:** The app explicitly requests JSON output from the API (`response_mime_type`).
366
  """)
367
 
368
  return app
 
370
  # --- Create and Launch the Gradio App ---
371
  app = create_interface()
372
 
373
+ # Launch the app (remove debug=True for production deployments)
374
+ # Set share=True to get a temporary public link for testing (use with caution due to API key input)
375
  if __name__ == "__main__":
376
+ app.launch(debug=True) # Add share=True here if needed: app.launch(debug=True, share=True)
377
+ # app.launch() # Use for deployment (e.g., on Hugging Face Spaces - remember security implications)