Loerstudio/dumpster-dividends
0
1#!/usr/bin/env python32"""3Dumpster Dividends4Images: OpenAI gpt-image-1 (images.edit with reference for consistency)5Audio: Gemini TTS (Charon)6Text: Gemini 2.5 Flash7Video: FFmpeg8"""9 10import os, sys, json, time, wave, shutil, subprocess, uuid, tempfile, base6411from pathlib import Path12from datetime import datetime13 14import gradio as gr15from PIL import Image as PILImage16 17try:18 from google import genai19 from google.genai import types as gtypes20except ImportError:21 print("Installa: pip install google-genai"); sys.exit(1)22 23try:24 from openai import OpenAI25except ImportError:26 print("Installa: pip install openai"); sys.exit(1)27 28# ── Config ────────────────────────────────────────────────────────────────────29FFMPEG = shutil.which("ffmpeg") or "/opt/homebrew/bin/ffmpeg"30FFPROBE = shutil.which("ffprobe") or "/opt/homebrew/bin/ffprobe"31TMP_BASE = Path(tempfile.gettempdir()) / "dumpster_gradio"32SCRIPT_DIR = Path(__file__).parent33ROCKY_REF = SCRIPT_DIR / "rocky_reference.png"34 35VOICE_DIRECTION = (36 "Speak like a raccoon who has seen every financial scam in the book. "37 "Slightly sarcastic when describing how financial products are marketed. "38 "Slow down on real data and numbers. "39 "Sound like you are thinking out loud, never reading a script."40)41 42# ── Image generation (OpenAI) ─────────────────────────────────────────────────43def gen_image(oai_client, prompt, ref_paths, tmp, idx, log):44 fp = tmp / f"frame_{idx+1:03d}.png"45 46 # Pick reference: uploaded images or rocky_reference.png47 ref_file = ref_paths[0] if ref_paths else str(ROCKY_REF)48 49 for attempt in range(3):50 try:51 with open(ref_file, "rb") as img_f:52 result = oai_client.images.edit(53 model="gpt-image-1",54 image=img_f,55 prompt=prompt,56 size="1536x1024",57 n=1,58 )59 img_bytes = base64.b64decode(result.data[0].b64_json)60 fp.write_bytes(img_bytes)61 with PILImage.open(fp) as img:62 if img.size != (1536, 864):63 img.resize((1536, 864), PILImage.LANCZOS).save(fp)64 return fp65 except Exception as e:66 wait = 8 * (attempt + 1)67 log(f" ⚠️ Attempt {attempt+1}/3 failed ({str(e)[:80]}), waiting {wait}s…")68 time.sleep(wait)69 return None70 71 72# ── Gemini helpers ────────────────────────────────────────────────────────────73def split_segments(client, script, log):74 prompt = (75 "Split the following script into segments of ~10-15 words each (~3 seconds of audio).\n"76 "For each segment write one image prompt.\n"77 "ALWAYS start with this exact prefix: "78 "\"Rocky the raccoon, grey fur, black eye mask, striped bushy tail, cartoon style, "79 "thick black outlines, flat muted colors, white background, 16:9, \" "80 "then add the specific [action] and [scene] for that segment.\n"81 "Return ONLY valid JSON, no markdown:\n"82 "{\"segments\": [{\"text\": \"...\", \"image_prompt\": \"...\"}]}\n\n"83 f"SCRIPT:\n{script}"84 )85 for attempt in range(2):86 try:87 r = client.models.generate_content(model="gemini-2.5-flash", contents=prompt)88 raw = r.text.strip().lstrip("```json").lstrip("```").rstrip("```").strip()89 return json.loads(raw)["segments"]90 except Exception as e:91 if attempt == 0:92 log(f" ⚠️ Retry segmentation ({e})…"); time.sleep(5)93 else:94 raise95 96 97def gen_audio(client, script, tmp, log):98 path = tmp / "audio.wav"99 for attempt in range(2):100 try:101 r = client.models.generate_content(102 model="gemini-2.5-flash-preview-tts",103 contents=f"{VOICE_DIRECTION}\n\n{script}",104 config=gtypes.GenerateContentConfig(105 response_modalities=["AUDIO"],106 speech_config=gtypes.SpeechConfig(107 voice_config=gtypes.VoiceConfig(108 prebuilt_voice_config=gtypes.PrebuiltVoiceConfig(voice_name="Charon")109 )110 ),111 ),112 )113 data = r.candidates[0].content.parts[0].inline_data.data114 with wave.open(str(path), "wb") as wf:115 wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(24000)116 wf.writeframes(data)117 dur = audio_duration(path)118 m, s = divmod(int(dur), 60)119 log(f"✅ Audio ready: {m}m {s:02d}s")120 return path121 except Exception as e:122 if attempt == 0:123 log(f" ⚠️ TTS retry ({e})…"); time.sleep(5)124 else:125 raise126 127 128def audio_duration(path):129 r = subprocess.run(130 [FFPROBE, "-v", "error", "-show_entries", "format=duration",131 "-of", "default=noprint_wrappers=1:nokey=1", str(path)],132 capture_output=True, text=True)133 return float(r.stdout.strip())134 135 136def assemble(frames, audio, n, out, log):137 dur = audio_duration(audio)138 spi = dur / n139 concat = audio.parent / "concat.txt"140 with open(concat, "w") as f:141 for p in frames:142 f.write(f"file '{p}'\nduration {spi:.4f}\n")143 f.write(f"file '{frames[-1]}'\n")144 r = subprocess.run([145 FFMPEG, "-y", "-f", "concat", "-safe", "0", "-i", str(concat),146 "-i", str(audio),147 "-vf", "scale=1536:864:force_original_aspect_ratio=decrease,pad=1536:864:(ow-iw)/2:(oh-ih)/2",148 "-c:v", "libx264", "-preset", "fast", "-crf", "23", "-r", "24",149 "-c:a", "aac", "-b:a", "192k", "-shortest", "-movflags", "+faststart",150 str(out),151 ], capture_output=True, text=True)152 if r.returncode != 0:153 raise RuntimeError(r.stderr[-1000:])154 log("✅ Video assembled")155 156 157# ── Main pipeline ─────────────────────────────────────────────────────────────158def run_pipeline(gemini_key, openai_key, script, ref_image_paths, progress=gr.Progress()):159 logs = []160 def log(msg):161 logs.append(msg)162 return "\n".join(logs)163 164 if not gemini_key.strip():165 raise gr.Error("Inserisci la Gemini API key")166 if not openai_key.strip():167 raise gr.Error("Inserisci la OpenAI API key")168 if not script.strip():169 raise gr.Error("Incolla lo script")170 171 gclient = genai.Client(api_key=gemini_key.strip())172 oai_client = OpenAI(api_key=openai_key.strip())173 174 tmp = TMP_BASE / str(uuid.uuid4())175 tmp.mkdir(parents=True, exist_ok=True)176 177 ref_paths = [p for p in (ref_image_paths or []) if p]178 179 # 1. Split180 progress(0.02, desc="Splitting script…")181 log("✂️ Splitting script into segments…")182 yield "\n".join(logs), None183 segments = split_segments(gclient, script, log)184 log(f"✅ {len(script.split())} words → {len(segments)} segments")185 yield "\n".join(logs), None186 187 # 2. Audio188 progress(0.08, desc="Generating audio…")189 log("🎙️ Generating voiceover with Charon…")190 yield "\n".join(logs), None191 audio = gen_audio(gclient, script, tmp, log)192 yield "\n".join(logs), None193 194 # 3. Images195 n = len(segments)196 log(f"🖼️ Generating {n} images with gpt-image-1…")197 yield "\n".join(logs), None198 frames = []199 last_fp = None200 for i, seg in enumerate(segments):201 progress(0.15 + 0.70 * i / n, desc=f"Image {i+1}/{n}…")202 log(f" 🖼️ Image {i+1}/{n}: {seg['image_prompt'][:60]}…")203 yield "\n".join(logs), None204 205 fp = gen_image(oai_client, seg["image_prompt"], ref_paths, tmp, i, log)206 if fp:207 last_fp = fp208 else:209 log(f" ❌ Image {i+1} failed — using last frame")210 fp = tmp / f"frame_{i+1:03d}.png"211 if last_fp:212 shutil.copy(last_fp, fp)213 else:214 PILImage.new("RGB", (1536, 864), (20, 20, 20)).save(fp)215 frames.append(fp)216 if i < n - 1:217 time.sleep(1)218 219 log(f"✅ All {n} images done")220 yield "\n".join(logs), None221 222 # 4. Assemble223 progress(0.90, desc="Assembling video…")224 log("🎬 Assembling video with FFmpeg…")225 yield "\n".join(logs), None226 today = datetime.now().strftime("%Y-%m-%d")227 out_path = Path.home() / "Desktop" / f"dumpster_video_{today}.mp4"228 assemble(frames, audio, n, out_path, log)229 shutil.rmtree(tmp, ignore_errors=True)230 log(f"✅ Video salvato sul Desktop: {out_path.name}")231 progress(1.0, desc="Done!")232 yield "\n".join(logs), str(out_path)233 234 235# ── Gradio UI ─────────────────────────────────────────────────────────────────236with gr.Blocks(title="🦝 Dumpster Dividends") as demo:237 gr.Markdown("# 🦝 Dumpster Dividends\nPaste your script → get a video")238 239 with gr.Row():240 with gr.Column(scale=1):241 gemini_key = gr.Textbox(label="Gemini API Key (TTS + segmentation)", type="password", placeholder="AIza…")242 openai_key = gr.Textbox(label="OpenAI API Key (immagini)", type="password", placeholder="sk-…")243 ref_images = gr.File(244 label="Reference Images (opzionale, max 3 — per character consistency)",245 file_count="multiple", file_types=["image"],246 )247 script = gr.Textbox(label="Script", lines=12, placeholder="Incolla lo script…")248 btn = gr.Button("🎬 Genera Video", variant="primary")249 250 with gr.Column(scale=1):251 terminal = gr.Textbox(label="Log", lines=18, interactive=False)252 video_out = gr.Video(label="Video finale")253 254 btn.click(fn=run_pipeline, inputs=[gemini_key, openai_key, script, ref_images], outputs=[terminal, video_out])255 256if __name__ == "__main__":257 TMP_BASE.mkdir(parents=True, exist_ok=True)258 demo.queue(max_size=5)259 demo.launch(server_name="0.0.0.0", server_port=7860)260 