AMZ-Listing-Pro / app.py
mroccuper's picture
Update app.py
da60cb5 verified
# -*- coding: utf-8 -*-
################################################################################
# WARNING: API Key Security
# This application requires you to enter your Gemini API key manually in the UI.
# DO NOT paste your API key directly into this code if you plan to share it.
# Ensure your API key is kept confidential. Sharing your key can lead to
# unauthorized use and potential charges to your account.
# Consider using secure methods like environment variables or secrets management
# for deployed applications.
################################################################################
import gradio as gr
import google.generativeai as genai
import re
import json
import os # Still useful for potential future path operations, etc.
# --- Constants for Limits ---
TITLE_MAX_LEN = 60
BRAND_NAME_MAX_LEN = 50
BULLET_POINT_MAX_LEN = 256
BULLET_POINT_MIN_LEN = 240 # Keep this if you want to check minimum length later
def count_characters(text):
"""Count characters in text."""
return len(text) if text else 0
def format_output(title, brand_name, bullet1, bullet2, suggested_keywords=None):
"""Format the output with character counts (reflecting potentially truncated lengths)."""
# Display the actual length after potential truncation
output = f"Title ({count_characters(title)}/{TITLE_MAX_LEN} characters):\n{title}\n\n"
output += f"Brand Name ({count_characters(brand_name)}/{BRAND_NAME_MAX_LEN} characters):\n{brand_name}\n\n"
output += f"Bullet Point 1 ({count_characters(bullet1)}/{BULLET_POINT_MAX_LEN} characters):\n{bullet1}\n\n"
output += f"Bullet Point 2 ({count_characters(bullet2)}/{BULLET_POINT_MAX_LEN} characters):\n{bullet2}"
if suggested_keywords:
output += f"\n\nSuggested Additional Keywords:\n{suggested_keywords}"
return output
def generate_prompt(quote, niche, target, keywords):
"""Generate the prompt for Gemini API."""
# Keep the detailed prompt instructions as they help guide the AI
combined_prompt = f"""You are an Amazon Merch on Demand SEO expert specializing in creating optimized t-shirt and apparel listings.
MY INPUT IS ABOUT: A {niche} t-shirt with the design/quote: "{quote}" for {target}.
YOU MUST ONLY create an Amazon apparel listing about that EXACT input - no substitutions or different themes allowed.
Generate a listing that includes:
1. Title (try for exactly {TITLE_MAX_LEN} characters): Must include "{niche}" and reference the design/quote "{quote}" and target audience "{target}"
2. Brand Name (try for 34-{BRAND_NAME_MAX_LEN} characters): Create a fitting brand name for this specific {niche} apparel for {target}
3. Bullet Point 1 (try for {BULLET_POINT_MIN_LEN}-{BULLET_POINT_MAX_LEN} characters): Highlight key features using ALL CAPS for the first 2-3 words. Focus ONLY on the design, quote, and niche theme.
4. Bullet Point 2 (try for {BULLET_POINT_MIN_LEN}-{BULLET_POINT_MAX_LEN} characters): Highlight additional features using ALL CAPS for the first 2-3 words. Focus ONLY on the design, quote, and niche theme.
IMPORTANT RULES — STRICT ENFORCEMENT:
- DO NOT include generic phrases like "PREMIUM QUALITY" or references to material quality
- DO NOT include phrases like "This comfortable and stylish tee is made with high-quality materials for a soft feel and long-lasting wear"
- Bullet point 1 must be between {BULLET_POINT_MIN_LEN} and {BULLET_POINT_MAX_LEN} characters. Aim for the higher end.
- Bullet point 2 must be between {BULLET_POINT_MIN_LEN} and {BULLET_POINT_MAX_LEN} characters. Aim for the higher end.
- DO NOT exceed the character limits ({TITLE_MAX_LEN} for title, {BRAND_NAME_MAX_LEN} for brand, {BULLET_POINT_MAX_LEN} for bullets).
- Count characters carefully. Ensure compliance before outputting.
- Focus ONLY on the specific design, niche, and quote provided
- Every sentence must directly relate to the quote, niche theme, and target audience
- Do not include any content that strays from the specific theme provided
The listing should be specifically for t-shirts, hoodies, or sweaters for the Amazon Merch on Demand program.
The listing MUST be about: {niche} + {quote} + for {target}. Do not generate content about other holidays, quotes, or audiences.
Use these specific keywords in your listing: {keywords}
Respond ONLY with a JSON object in this format:
{{
"title": "The title aiming for {TITLE_MAX_LEN} characters",
"brand_name": "Brand name aiming for 34-{BRAND_NAME_MAX_LEN} characters",
"bullet_point_1": "First bullet point aiming for {BULLET_POINT_MIN_LEN}-{BULLET_POINT_MAX_LEN} characters",
"bullet_point_2": "Second bullet point aiming for {BULLET_POINT_MIN_LEN}-{BULLET_POINT_MAX_LEN} characters",
"suggested_keywords": "5 additional keywords separated by commas"
}}
REMINDER: Make sure to count the characters carefully. Aim for Title exactly {TITLE_MAX_LEN} characters. Aim for Bullet points 1 and 2 between {BULLET_POINT_MIN_LEN}-{BULLET_POINT_MAX_LEN} characters.
STRICT ENFORCEMENT: Bullet point 1 and 2 MUST be between {BULLET_POINT_MIN_LEN} and {BULLET_POINT_MAX_LEN} characters. If it's {BULLET_POINT_MAX_LEN+1}+, the result is invalid. Carefully count and ensure compliance.
"""
return combined_prompt
def generate_multiple_variations_prompt(quote, niche, target, keywords):
"""Generate the prompt for multiple variations of title and brand name."""
combined_prompt = f"""You are an Amazon Merch on Demand SEO expert specializing in creating optimized t-shirt and apparel listings.
MY INPUT IS ABOUT: A {niche} t-shirt with the design/quote: "{quote}" for {target}.
YOU MUST ONLY create variations about that EXACT input - no substitutions or different themes allowed.
Generate 3 different variations for the Title and Brand Name based on the provided information.
All variations MUST be about {niche} + "{quote}" + for {target} audience.
The titles MUST include:
- The specific holiday/event: {niche}
- Reference to the quote/design: "{quote}"
- The target audience: {target}
Focus only on t-shirts, sweaters, and hoodies for the Amazon Merch on Demand program.
All titles must aim for exactly {TITLE_MAX_LEN} characters and brand names between 34-{BRAND_NAME_MAX_LEN} characters.
Respond ONLY with a JSON object in this format:
{{
"title_variations": [
"Title variation 1 - aim for {TITLE_MAX_LEN} characters, count carefully",
"Title variation 2 - aim for {TITLE_MAX_LEN} characters, count carefully",
"Title variation 3 - aim for {TITLE_MAX_LEN} characters, count carefully"
],
"brand_name_variations": [
"Brand name variation 1 (aim for 34-{BRAND_NAME_MAX_LEN} characters)",
"Brand name variation 2 (aim for 34-{BRAND_NAME_MAX_LEN} characters)",
"Brand name variation 3 (aim for 34-{BRAND_NAME_MAX_LEN} characters)"
]
}}
REMINDER: Make sure each title aims for EXACTLY {TITLE_MAX_LEN} characters. Count carefully!"""
return combined_prompt
# Takes the API Key directly as an argument
def generate_amazon_listing(api_key_input, quote, niche, target, keywords):
"""Generate Amazon listing using Gemini API, enforcing character limits."""
# --- Input Validation ---
# Clean the API key first to handle potential whitespace issues
cleaned_api_key = api_key_input.strip() if api_key_input else ""
if not cleaned_api_key:
return "Error: Please enter a valid Gemini API key."
if not quote or not niche or not target:
return "Error: Please fill in all required fields (Quote, Holiday/Event, and Target Audience)."
try:
# --- Configure API ---
# Use the cleaned API key from the input
print(f"DEBUG: Configuring with API Key (length {len(cleaned_api_key)}): '{cleaned_api_key[:5]}...{cleaned_api_key[-5:]}'") # Log sanitized key
if '\n' in cleaned_api_key or '\r' in cleaned_api_key:
print("WARNING: API Key may contain newline characters!")
genai.configure(api_key=cleaned_api_key)
# Create model with optimized settings
model = genai.GenerativeModel(
'gemini-1.5-pro', # Or 'gemini-1.5-flash' for potentially faster/cheaper generation
generation_config={
"temperature": 0.2, # Lower temperature for more predictable output
"top_p": 0.7,
"max_output_tokens": 512, # Maximum tokens the API can *return*
"response_mime_type": "application/json" # Request JSON directly
}
)
# Generate the main listing
prompt = generate_prompt(quote, niche, target, keywords)
try:
# --- Generate Main Listing Content ---
response = model.generate_content(prompt)
# Parse JSON response
try:
response_text = response.text
# Sometimes the API might still wrap it in markdown, try to strip it
json_match = re.search(r'```json\s*({.*?})\s*```', response_text, re.DOTALL | re.IGNORECASE)
if json_match:
json_str = json_match.group(1)
else:
# Fallback if no markdown backticks are found
json_str = response_text.strip()
result = json.loads(json_str)
except (json.JSONDecodeError, AttributeError, IndexError) as json_err:
print(f"JSON Parsing Error: {json_err}")
print(f"Raw response text: {getattr(response, 'text', 'N/A')}")
if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
return f"Error: Generation blocked due to: {response.prompt_feedback.block_reason}. Content filters may have been triggered."
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."
# --- ENFORCE CHARACTER LIMITS ---
title = result.get("title", "")[:TITLE_MAX_LEN]
brand_name = result.get("brand_name", "")[:BRAND_NAME_MAX_LEN]
bullet1 = result.get("bullet_point_1", "")
bullet2 = result.get("bullet_point_2", "")
suggested_keywords = result.get("suggested_keywords", "Error generating suggested keywords")
# --- VALIDATION (using potentially truncated values) ---
target_parts = [t.strip().lower() for t in target.split(',')]
title_lower = title.lower()
if not (quote.lower() in title_lower or
niche.lower() in title_lower or
any(t_part in title_lower for t_part in target_parts)):
# This is a soft warning, not necessarily a hard error
print(f"Warning: Generated title ('{title}') might not strongly match the requested theme: '{quote}', '{niche}', or '{target}'.")
# --- Optional: Validate the *lower* bound for bullet points ---
# if len(bullet1) < BULLET_POINT_MIN_LEN:
# 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."
# if len(bullet2) < BULLET_POINT_MIN_LEN:
# return f"Error: Bullet point 2 length ({len(bullet2)}) is less than the required minimum {BULLET_POINT_MIN_LEN} characters after generation/truncation. Please try again."
# Check for generic content in bullet points
generic_phrases = ["premium quality", "high-quality materials", "soft feel", "long-lasting wear",
"comfortable and stylish"]
bullet1_lower = bullet1.lower()
bullet2_lower = bullet2.lower()
for phrase in generic_phrases:
if phrase in bullet1_lower or phrase in bullet2_lower:
return f"Error: Generated bullet points contain disallowed generic phrase '{phrase}'. Please try again."
# Format main output first
main_output = format_output(
title,
brand_name,
bullet1,
bullet2,
suggested_keywords
)
# --- Generate Variations (Optional Second Call) ---
try:
variations_prompt = generate_multiple_variations_prompt(quote, niche, target, keywords)
response_var = model.generate_content(
variations_prompt,
generation_config={
"temperature": 0.2,
"top_p": 0.7,
"max_output_tokens": 512,
"response_mime_type": "application/json"
}
)
# Parse variations JSON
try:
response_var_text = response_var.text
json_match_var = re.search(r'```json\s*({.*?})\s*```', response_var_text, re.DOTALL | re.IGNORECASE)
if json_match_var:
json_str_var = json_match_var.group(1)
else:
json_str_var = response_var_text.strip()
variations = json.loads(json_str_var)
# Format variations output, enforcing limits
variations_output = "\n\nADDITIONAL VARIATIONS:\n\n"
variations_output += "Title Variations:\n"
for i, var in enumerate(variations.get("title_variations", []), 1):
truncated_var = var[:TITLE_MAX_LEN]
variations_output += f"{i}. {truncated_var} ({count_characters(truncated_var)}/{TITLE_MAX_LEN} characters)\n"
variations_output += "\nBrand Name Variations:\n"
for i, var in enumerate(variations.get("brand_name_variations", []), 1):
truncated_var = var[:BRAND_NAME_MAX_LEN]
variations_output += f"{i}. {truncated_var} ({count_characters(truncated_var)}/{BRAND_NAME_MAX_LEN} characters)\n"
return main_output + variations_output
except (json.JSONDecodeError, AttributeError, IndexError) as json_var_err:
print(f"JSON Parsing Error (Variations): {json_var_err}")
print(f"Raw variations response text: {getattr(response_var, 'text', 'N/A')}")
if hasattr(response_var, 'prompt_feedback') and response_var.prompt_feedback.block_reason:
return main_output + f"\n\n(Could not generate variations: Blocked - {response_var.prompt_feedback.block_reason})"
return main_output + "\n\n(Could not parse variations response)"
except genai.types.generation_types.BlockedPromptException as var_block_error:
return main_output + f"\n\n(Variations prompt blocked: {var_block_error})"
except Exception as var_error:
print(f"Error generating variations: {var_error}")
return main_output + f"\n\n(Could not generate variations due to an error)"
except genai.types.generation_types.BlockedPromptException as block_error:
return f"Error: The main prompt was blocked by Gemini API safety filters: {block_error}. Please modify your input and try again."
except Exception as e:
print(f"Error during main listing generation: {e}")
# Attempt to get more specific feedback if available
finish_reason = "Unknown"
safety_ratings = "Unknown"
if hasattr(response, 'candidates') and response.candidates:
finish_reason = getattr(response.candidates[0], 'finish_reason', 'UNKNOWN')
safety_ratings = getattr(response.candidates[0], 'safety_ratings', 'UNKNOWN')
print(f"Finish Reason: {finish_reason}, Safety Ratings: {safety_ratings}")
return f"Error generating main listing (Reason: {finish_reason}). Please check logs or try again."
# Catch configuration errors or other unexpected issues outside the API call block
except google.api_core.exceptions.PermissionDenied as perm_denied:
print(f"Permission Denied Error: {perm_denied}")
return "Error: Permission Denied. Please check if your API key is valid, correctly entered, and has the necessary permissions enabled."
except Exception as e:
print(f"General Error: {e}")
# Check if it's an authentication error which often manifests differently
if "authentication" in str(e).lower() or "credentials" in str(e).lower():
return "Error: Authentication failed. Please double-check your API key."
return f"An unexpected error occurred: {str(e)}"
# --- Create the Gradio Interface ---
def create_interface():
with gr.Blocks(title="Amazon Merch on Demand Listing Generator", theme=gr.themes.Soft()) as app:
gr.Markdown("# Amazon Merch on Demand Listing Generator")
gr.Markdown("Generate SEO-optimized t-shirt and apparel listings for Amazon Merch on Demand using Gemini AI. Character limits are enforced.")
with gr.Row():
with gr.Column(scale=1):
# API Key input is now mandatory here
api_key_ui = gr.Textbox(
label="Gemini API Key",
placeholder="Enter your Gemini API key here",
type="password", lines=1,
# Make sure this input component is mandatory in the logic below
)
quote = gr.Textbox(label="Quote/Design/Idea" , lines=1, placeholder="e.g., Lucky To Be A Teacher")
niche = gr.Textbox(label="Niche/Holiday/Event" , lines=1, placeholder="e.g., St Patrick's Day")
target = gr.Textbox(label="Target Audience", lines=1, placeholder="e.g., Teacher, Mom, Dad")
keywords = gr.Textbox(
label="Target Keywords (comma-separated)",
placeholder="Enter keywords relevant to your design",
lines=5
)
submit_btn = gr.Button("Generate Amazon Listing", variant="primary")
with gr.Column(scale=2):
status = gr.Textbox(label="Status", value="Ready", interactive=False, lines=1)
output = gr.Textbox(label="Generated Amazon Listing", lines=24, interactive=True) # Make output selectable
# This function is called when the button is clicked
def on_submit(api_key_from_ui, quote, niche, target, keywords):
# --- Validation directly in the submit handler ---
if not api_key_from_ui or not api_key_from_ui.strip():
# Update status first before returning error message
yield "Error: Gemini API Key is required.", ""
return # Stop execution if API key is missing
if not quote or not niche or not target:
yield "Error: Quote, Niche/Holiday, and Target Audience are required.", ""
return # Stop if other core fields are missing
# Update status to indicate processing
yield "Generating listing... This may take a moment.", "Processing..."
# Call the main generation function, passing the API key from the UI
result = generate_amazon_listing(api_key_from_ui, quote, niche, target, keywords)
# Update status and output based on the result
if "Error:" in result:
yield f"Finished with Error.", result
else:
yield "Listing generated successfully!", result
# Connect the button click to the on_submit function
submit_btn.click(
fn=on_submit,
# Pass the API key from the UI component as the first input
inputs=[api_key_ui, quote, niche, target, keywords],
outputs=[status, output],
show_progress="full"
)
gr.Markdown("## Example Input")
gr.Markdown('''
Use the pre-filled example above or enter your own details:
* **Quote/Design:** The core text or visual idea on the shirt.
* **Niche/Holiday:** The specific event, theme, or category (e.g., Halloween, Fishing, Dog Lover).
* **Target Audience:** Who is this shirt for? (e.g., Nurse, Engineer, Grandpa).
* **Keywords:** Relevant terms customers might search for.
''')
gr.Markdown("""
## Notes & Troubleshooting
* **API Key:** You MUST enter a valid Gemini API key in the field above for the generator to work. Keep your key secure.
* **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.
* **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.
* **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.
* **JSON Request:** The app explicitly requests JSON output from the API (`response_mime_type`).
""")
return app
# --- Create and Launch the Gradio App ---
app = create_interface()
# Launch the app (remove debug=True for production deployments)
# Set share=True to get a temporary public link for testing (use with caution due to API key input)
if __name__ == "__main__":
app.launch(debug=True) # Add share=True here if needed: app.launch(debug=True, share=True)
# app.launch() # Use for deployment (e.g., on Hugging Face Spaces - remember security implications)