CoolFace
Apppublic

Loerstudio/dumpster-dividends

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
app.py260 linesDownload Raw Back to root
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