File size: 21,831 Bytes
7e8ee64
 
 
 
b455c9b
 
 
 
 
 
 
7e8ee64
 
 
 
 
15ee1ac
b455c9b
 
 
 
 
 
 
15ee1ac
 
b455c9b
7e8ee64
 
15ee1ac
 
b455c9b
7e8ee64
15ee1ac
7e8ee64
 
b455c9b
 
 
 
834e130
15ee1ac
 
b455c9b
 
 
834e130
15ee1ac
 
 
 
7e8ee64
15ee1ac
7e8ee64
 
b455c9b
 
 
 
438e750
7e8ee64
b455c9b
 
6171624
15ee1ac
7e8ee64
 
15ee1ac
 
 
 
 
 
 
 
 
 
 
 
b455c9b
15ee1ac
 
 
b455c9b
 
 
15ee1ac
 
b455c9b
 
 
15ee1ac
438e750
b455c9b
15ee1ac
 
 
 
b455c9b
438e750
 
 
 
 
b455c9b
7e8ee64
15ee1ac
b455c9b
 
7e8ee64
b455c9b
438e750
 
b455c9b
7e8ee64
b455c9b
7e8ee64
b455c9b
 
 
7e8ee64
 
b455c9b
438e750
 
b455c9b
438e750
b455c9b
438e750
b455c9b
 
15ee1ac
b455c9b
 
 
 
 
 
 
 
 
 
15ee1ac
b455c9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438e750
b455c9b
813fec2
b455c9b
 
438e750
b455c9b
 
 
 
438e750
b455c9b
 
 
 
 
438e750
b455c9b
 
438e750
 
b455c9b
 
438e750
 
b455c9b
 
438e750
b455c9b
 
438e750
 
b455c9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438e750
b455c9b
 
 
 
 
 
 
438e750
b455c9b
 
 
 
 
 
 
7e8ee64
b455c9b
 
 
7e8ee64
b455c9b
7e8ee64
b455c9b
7e8ee64
b455c9b
 
7e8ee64
b455c9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e8ee64
b455c9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438e750
b455c9b
 
 
 
 
438e750
 
b455c9b
7e8ee64
438e750
15ee1ac
438e750
b455c9b
7e8ee64
b455c9b
7e8ee64
 
b455c9b
 
 
 
 
15ee1ac
b455c9b
438e750
b455c9b
 
 
 
 
 
438e750
b455c9b
7e8ee64
 
b455c9b
7e8ee64
 
b455c9b
7e8ee64
b455c9b
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
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)