Spaces:
Runtime error
Runtime error
| import os | |
| import shutil | |
| import zipfile | |
| import base64 | |
| from collections import defaultdict | |
| import gradio as gr | |
| from supabase import create_client | |
| from html2image import Html2Image | |
| from datetime import datetime | |
| # --- Helper: Convert Image to Base64 (Now supports JPG and PNG) --- | |
| def get_base64_image(image_path): | |
| if image_path and os.path.exists(image_path): | |
| # Detect if it's a jpeg or png to set the correct HTML data type | |
| ext = os.path.splitext(image_path)[1].lower().replace('.', '') | |
| mime_type = 'jpeg' if ext in ['jpg', 'jpeg'] else 'png' | |
| with open(image_path, "rb") as img_file: | |
| b64_str = base64.b64encode(img_file.read()).decode('utf-8') | |
| return f"data:image/{mime_type};base64,{b64_str}" | |
| return "" | |
| # --- The HTML/CSS Template --- | |
| def get_html_template(player, team, stat_val, stat_name, hook, bg_data_uri): | |
| # Inject the exact Base64 Data URI | |
| bg_img_element = f'<img class="bg-image" src="{bg_data_uri}" />' if bg_data_uri else '' | |
| return f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Inter:wght@400;800&display=swap" rel="stylesheet"> | |
| <style> | |
| body {{ | |
| margin: 0; padding: 0; width: 1080px; height: 1350px; | |
| background: radial-gradient(circle at 50% 30%, #1a1a1a 0%, #050505 100%); | |
| font-family: 'Inter', sans-serif; color: white; | |
| position: relative; overflow: hidden; | |
| }} | |
| .bg-image {{ | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 100%; height: 100%; | |
| object-fit: cover; | |
| opacity: 0.20; | |
| mix-blend-mode: lighten; | |
| z-index: 1; | |
| }} | |
| .content-wrapper {{ | |
| position: relative; | |
| z-index: 10; | |
| display: flex; flex-direction: column; justify-content: center; | |
| align-items: center; text-align: center; height: 100%; | |
| }} | |
| .brand-badge {{ position: absolute; top: 80px; text-transform: uppercase; letter-spacing: 4px; font-size: 24px; color: #ff2a2a; font-weight: 800; }} | |
| .player-name {{ font-family: 'Bebas Neue', cursive; font-size: 100px; margin-bottom: -20px; color: #ffffff; text-transform: uppercase; letter-spacing: 2px; text-shadow: 0px 5px 15px rgba(0,0,0,0.8); }} | |
| .team-name {{ font-size: 30px; color: #888888; text-transform: uppercase; letter-spacing: 5px; margin-bottom: 80px; text-shadow: 0px 5px 10px rgba(0,0,0,0.8); }} | |
| .massive-stat {{ font-family: 'Bebas Neue', cursive; font-size: 450px; line-height: 1; color: #ffcc00; text-shadow: 0px 20px 50px rgba(255, 204, 0, 0.4); margin: 0; }} | |
| .stat-label {{ font-size: 45px; font-weight: 800; text-transform: uppercase; color: #dddddd; margin-top: -20px; margin-bottom: 120px; letter-spacing: 3px; text-shadow: 0px 5px 10px rgba(0,0,0,0.8); }} | |
| .hook-box {{ background: #ff2a2a; padding: 30px 60px; border-radius: 15px; max-width: 850px; box-shadow: 0px 15px 30px rgba(0,0,0,0.6); }} | |
| .hook-text {{ font-family: 'Bebas Neue', cursive; font-size: 65px; line-height: 1.1; margin: 0; color: white; }} | |
| </style> | |
| </head> | |
| <body> | |
| {bg_img_element} | |
| <div class="content-wrapper"> | |
| <div class="brand-badge">🚨 Stat Nuke 🚨</div> | |
| <div class="player-name">{player}</div> | |
| <div class="team-name">{team}</div> | |
| <div class="massive-stat">{stat_val}</div> | |
| <div class="stat-label">{stat_name}</div> | |
| <div class="hook-box"><p class="hook-text">{hook}</p></div> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| def generate_and_zip(supabase_url, supabase_key, num_carousels): | |
| if not supabase_url or not supabase_key: | |
| return "❌ Error: Provide Supabase credentials.", None | |
| supabase = create_client(supabase_url, supabase_key) | |
| run_id = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| output_dir = f"posts_{run_id}" | |
| os.makedirs(output_dir, exist_ok=True) | |
| hti = Html2Image(custom_flags=['--no-sandbox', '--disable-gpu', '--hide-scrollbars']) | |
| hti.size = (1080, 1350) | |
| fetch_limit = int(num_carousels) * 5 | |
| response = supabase.table("generated_posts").select("*").eq("is_rendered", False).limit(fetch_limit).execute() | |
| unrendered_posts = response.data | |
| if not unrendered_posts: | |
| return "✅ All posts have already been rendered. Run your Brain script to generate more!", None | |
| logs = f"Found {len(unrendered_posts)} posts to render.\n" | |
| batches = defaultdict(list) | |
| for post in unrendered_posts: | |
| b_id = post.get('batch_id', 'Unbatched') | |
| batches[b_id].append(post) | |
| # Auto-detect if folder is named 'assests' or 'assets' | |
| folder_name = "assests" if os.path.exists("assests") else "assets" | |
| for batch_id, posts in batches.items(): | |
| logs += f"\n📦 Assembling Folder: {batch_id}\n" | |
| batch_dir = os.path.join(output_dir, batch_id) | |
| os.makedirs(batch_dir, exist_ok=True) | |
| hti.output_path = batch_dir | |
| sorted_posts = sorted(posts, key=lambda x: x['created_at']) | |
| for idx, post in enumerate(sorted_posts): | |
| post_id = post['id'] | |
| player = post['player_name'] | |
| context = post['stat_context'] | |
| slide_num = idx + 1 | |
| try: | |
| team = context.split('(')[1].split(')')[0] | |
| remainder = context.split('achieved ')[1] | |
| stat_val = remainder.split(' ')[0] | |
| stat_name = remainder.replace(f"{stat_val} ", "").split(' in ')[0] | |
| except: | |
| team, stat_val, stat_name = "Unknown", "X", "Stat" | |
| hook = post['image_hook'] | |
| caption = post['instagram_caption'] | |
| # Use safe_player for BOTH the search and the output filename | |
| safe_player = player.replace(" ", "_").replace("/", "") | |
| # Check for player image | |
| player_img_path = os.path.join(folder_name, f"{safe_player}.png") | |
| # Check for dummy_bg (supports .jpg, .jpeg, and .png) | |
| dummy_img_path = None | |
| for ext in ['.jpg', '.jpeg', '.png']: | |
| if os.path.exists(os.path.join(folder_name, f"dummy_bg{ext}")): | |
| dummy_img_path = os.path.join(folder_name, f"dummy_bg{ext}") | |
| break | |
| if os.path.exists(player_img_path): | |
| bg_data_uri = get_base64_image(player_img_path) | |
| slide_prefix = "Cover" if slide_num == 1 else "Slide" | |
| else: | |
| bg_data_uri = get_base64_image(dummy_img_path) if dummy_img_path else "" | |
| slide_prefix = "Slide" | |
| filename = f"{slide_num}_{slide_prefix}_{safe_player}.png" | |
| logs += f" 🖼️ Rendering: {filename}...\n" | |
| temp_html = f"{safe_player}_temp.html" | |
| with open(temp_html, "w", encoding="utf-8") as f: | |
| f.write(get_html_template(player, team, stat_val, stat_name, hook, bg_data_uri)) | |
| try: | |
| hti.screenshot(html_file=temp_html, save_as=filename) | |
| caption_file = filename.replace('.png', '_caption.txt') | |
| with open(os.path.join(batch_dir, caption_file), "w", encoding="utf-8") as f: | |
| f.write(f"HOOK: {hook}\n\nCAPTION:\n{caption}") | |
| supabase.table("generated_posts").update({"is_rendered": True}).eq("id", post_id).execute() | |
| except Exception as e: | |
| logs += f" ❌ Failed to render {filename}: {str(e)}\n" | |
| if os.path.exists(temp_html): | |
| os.remove(temp_html) | |
| zip_filename = f"StatNuke_Carousels_{run_id}.zip" | |
| with zipfile.ZipFile(zip_filename, 'w') as zipf: | |
| for root, _, files in os.walk(output_dir): | |
| for file in files: | |
| file_path = os.path.join(root, file) | |
| arcname = os.path.relpath(file_path, output_dir) | |
| zipf.write(file_path, arcname) | |
| logs += "\n✅ Batch complete! Download your ZIP file below." | |
| return logs, zip_filename | |
| # --- GRADIO UI --- | |
| with gr.Blocks(theme=gr.themes.Monochrome()) as app: | |
| gr.Markdown("# ⚽ Stat Nuke: Automated Carousel Compositor") | |
| gr.Markdown("Pulls complete 5-slide carousels from the database, injects background assets, and packages them into a clean ZIP folder.") | |
| with gr.Row(): | |
| url_input = gr.Textbox(label="Supabase URL", type="password") | |
| key_input = gr.Textbox(label="Supabase Anon Key", type="password") | |
| batch_input = gr.Slider(minimum=1, maximum=10, step=1, value=5, label="Number of Carousels to Render (5 posts each)") | |
| generate_btn = gr.Button("Generate Carousels & Download ZIP", variant="primary") | |
| output_log = gr.Textbox(label="Status Logs", lines=12) | |
| output_file = gr.File(label="Download Your Carousels") | |
| generate_btn.click(fn=generate_and_zip, inputs=[url_input, key_input, batch_input], outputs=[output_log, output_file]) | |
| app.launch() |