ayanokoji1331's picture
Update app.py
252adc9 verified
Raw
History Blame Contribute Delete
9.46 kB
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()