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