Spaces:
Running
Running
import gradio as gr | |
import google.generativeai as genai | |
import re | |
import json | |
import os # Good practice to import os if using env variables later | |
# --- 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 | |
def generate_amazon_listing(api_key, quote, niche, target, keywords): | |
"""Generate Amazon listing using Gemini API, enforcing character limits.""" | |
# Input validation | |
if not 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 the Gemini API with the provided key | |
# Consider using environment variables for production: | |
# api_key = os.getenv("GEMINI_API_KEY") or api_key_input | |
genai.configure(api_key=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.3, # Lower temperature for more predictable output | |
"top_p": 0.8, | |
"max_output_tokens": 1024, # Maximum tokens the API can *return* | |
# Note: 'max_output_tokens' doesn't directly control character count precisely | |
"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) | |
# Since we requested JSON, parse it directly. Add error handling. | |
try: | |
# Access the text part and load as JSON | |
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: | |
# Handle cases where response.text is empty, not valid JSON, or API returned unexpected format | |
print(f"JSON Parsing Error: {json_err}") | |
print(f"Raw response text: {getattr(response, 'text', 'N/A')}") # Log raw response for debugging | |
# Check for safety blocks | |
if 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 --- | |
# Get raw values and immediately truncate if they exceed the MAX limit | |
title = result.get("title", "")[:TITLE_MAX_LEN] | |
brand_name = result.get("brand_name", "")[:BRAND_NAME_MAX_LEN] | |
bullet1 = result.get("bullet_point_1", "")[:BULLET_POINT_MAX_LEN] | |
bullet2 = result.get("bullet_point_2", "")[:BULLET_POINT_MAX_LEN] | |
suggested_keywords = result.get("suggested_keywords", "Error generating suggested keywords") # Keywords don't usually need truncation | |
# --- VALIDATION (using potentially truncated values) --- | |
# Validate that the output actually matches the input criteria (optional but good) | |
# Check if *any* part of the target audience is in the title if it's comma-separated | |
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)): | |
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." | |
# --- Optional: Validate the *lower* bound for bullet points --- | |
# Uncomment these lines if you strictly need the bullets to be AT LEAST min_len characters | |
# Note: This check happens *after* truncation, so if truncation occurred, it might pass this check. | |
# 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 - using the enforced length values | |
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) | |
# Use a separate model instance or reuse if configuration is the same | |
# Using the same model instance here | |
response_var = model.generate_content( | |
variations_prompt, | |
generation_config={ # Can reuse or adjust config for variations | |
"temperature": 0.4, # Slightly higher temp for more variety | |
"top_p": 0.8, | |
"max_output_tokens": 1024, | |
"response_mime_type": "application/json" | |
} | |
) | |
# Parse variations JSON | |
try: | |
response_var_text = response_var.text | |
# Try stripping markdown again | |
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 here too | |
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] # Enforce limit | |
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] # Enforce limit | |
variations_output += f"{i}. {truncated_var} ({count_characters(truncated_var)}/{BRAND_NAME_MAX_LEN} characters)\n" | |
# Combine main output with variations | |
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')}") | |
# Check for safety blocks on variations | |
if 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: | |
# Catch other errors during variation generation | |
print(f"Error generating variations: {var_error}") # Log the error | |
return main_output + f"\n\n(Could not generate variations due to an error)" | |
except genai.types.generation_types.BlockedPromptException as block_error: | |
# Catch blocked prompts specifically for better feedback | |
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: | |
# Catch other potential errors during the main API call | |
print(f"Error during main listing generation: {e}") # Log the error | |
# You might want to check response.candidates[0].finish_reason if available | |
# finish_reason = getattr(response.candidates[0], 'finish_reason', 'UNKNOWN') | |
# safety_ratings = getattr(response.candidates[0].safety_ratings, 'name', 'UNKNOWN') | |
return f"Error generating main listing. Please check logs or try again." | |
except Exception as e: | |
# Catch configuration errors or other unexpected issues | |
print(f"General Error: {e}") # Log the error | |
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): | |
# Recommend using environment variable for API key in real deployments | |
api_key = gr.Textbox( | |
label="Gemini API Key", | |
placeholder="Enter your Gemini API key (or leave blank if set as environment variable)", | |
type="password", | |
# value=os.getenv("GEMINI_API_KEY", "") # Pre-fill if env var exists | |
) | |
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\"") | |
niche = gr.Textbox(label="Niche/Holiday/Event", placeholder="e.g., St Patrick's Day", value="St Patricks Day") | |
target = gr.Textbox(label="Target Audience", placeholder="e.g., Teacher, Mom, Dad", value="Teacher, Teacher Mom") | |
keywords = gr.Textbox( | |
label="Target Keywords (comma-separated)", | |
placeholder="Enter keywords relevant to your design", | |
lines=5, | |
value="lucky, teacher, rainbow, st, patricks, day, t-shirt, patrick's, outfit, design, leopard, cheetah, print, shamrock, clover, perfect, men, women, teachers, celebrate, saint, patrick, special, unique, makes, great, gifts, idea, substitute, love, irish, culture, pattys, holiday, teach, shamrocks, cute, design, awesome, show, students" | |
) | |
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=25, interactive=True) # Make output selectable | |
def on_submit(api_key_input, quote, niche, target, keywords): | |
# Use environment variable if input is blank (optional but good practice) | |
# final_api_key = os.getenv("GEMINI_API_KEY") or api_key_input | |
final_api_key = api_key_input # Keep it simple for now | |
if not final_api_key: | |
# Update status first before returning error message | |
yield "Error: Gemini API Key is required.", "" | |
return # Stop execution | |
# Update status to indicate processing | |
yield "Generating listing... This may take a moment.", "Processing..." | |
# Generate the listing | |
result = generate_amazon_listing(final_api_key, 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 | |
submit_btn.click( | |
fn=on_submit, | |
inputs=[api_key, quote, niche, target, keywords], | |
outputs=[status, output], | |
show_progress="full" # Show more detailed progress | |
) | |
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 | |
* **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. | |
* **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. | |
* **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. | |
* **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 now 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) | |
if __name__ == "__main__": | |
# Set share=True to get a public link (useful for temporary sharing) | |
app.launch(debug=True) | |
# app.launch() # For deployment (e.g., Hugging Face Spaces) |