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'' if bg_data_uri else '' return f""" {bg_img_element}
🚨 Stat Nuke 🚨
{player}
{team}
{stat_val}
{stat_name}

{hook}

""" 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()