Spaces:
Sleeping
Sleeping
| import os, io, math, tempfile | |
| import gradio as gr | |
| from PIL import Image, ImageDraw, ImageFont | |
| import torch | |
| from transformers import pipeline | |
| from diffusers import StableDiffusionPipeline | |
| from spaces import GPU # ZeroGPU support | |
| from fpdf import FPDF # make sure requirements.txt includes: fpdf==1.7.2 | |
| # Avoid tokenizers parallelism warning after fork | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| # ------------------ Globals (CPU-safe) ------------------ | |
| _txtgen = None # text generator stays on CPU | |
| _t2i_cpu = None # CPU fallback pipeline | |
| STYLE_PRESETS = { | |
| "Realistic": "realistic photography, finely detailed, natural lighting, 35mm", | |
| "Anime": "anime, vibrant colors, cel shading, clean lineart", | |
| "Comic": "comic book style, halftone, bold lines, dramatic shading", | |
| "Watercolor": "watercolor painting, soft edges, gentle colors, textured paper", | |
| "Sketch": "pencil sketch, cross-hatching, grayscale, paper texture", | |
| } | |
| NEGATIVE = "nsfw, nudity, gore, deformed, extra limbs, low quality, blurry, worst quality, lowres, text artifacts, watermark, logo" | |
| # ------------------ Loaders ------------------ | |
| def get_txtgen_cpu(): | |
| """Load text generator on CPU (ZeroGPU-safe).""" | |
| global _txtgen | |
| if _txtgen is None: | |
| _txtgen = pipeline("text-generation", model="distilgpt2", device=-1) | |
| return _txtgen | |
| def get_t2i_cpu(): | |
| """CPU Stable Diffusion pipeline (fallback).""" | |
| global _t2i_cpu | |
| if _t2i_cpu is None: | |
| _t2i_cpu = StableDiffusionPipeline.from_pretrained( | |
| "stabilityai/sd-turbo", | |
| torch_dtype=torch.float32, | |
| safety_checker=None, | |
| ) | |
| _t2i_cpu.enable_attention_slicing() | |
| return _t2i_cpu | |
| # ------------------ GPU path (ZeroGPU) ------------------ | |
| def t2i_generate_batch_gpu(prompts, width, height, steps, guidance, negative_prompt, seed=None): | |
| """Runs inside a GPU-allocated context (ZeroGPU).""" | |
| pipe = StableDiffusionPipeline.from_pretrained( | |
| "stabilityai/sd-turbo", | |
| torch_dtype=torch.float16, | |
| safety_checker=None, | |
| ).to("cuda") | |
| generator = torch.Generator(device="cuda") | |
| if seed is not None and str(seed).strip().isdigit(): | |
| generator = generator.manual_seed(int(seed)) | |
| images = [] | |
| for p in prompts: | |
| img = pipe( | |
| prompt=p, | |
| negative_prompt=negative_prompt, | |
| num_inference_steps=steps, | |
| guidance_scale=guidance, | |
| width=width, | |
| height=height, | |
| generator=generator, | |
| ).images[0] | |
| images.append(img) | |
| return images | |
| # ------------------ Helpers ------------------ | |
| def build_prompt(user_prompt: str, style: str, panel_idx: int, num_panels: int) -> str: | |
| style_desc = STYLE_PRESETS.get(style, "") | |
| beat = ["opening shot", "rising action", "key moment", "twist", "resolution"] | |
| beat_text = beat[min(panel_idx, len(beat) - 1)] | |
| return f"{user_prompt}, {style_desc}, storyboard panel {panel_idx+1} of {num_panels}, {beat_text}, cinematic composition, wide shot" | |
| def generate_captions(user_prompt: str, n: int = 3): | |
| gen = get_txtgen_cpu() | |
| # Simple, fast prompts; keep it short | |
| outputs = [] | |
| for i in range(n): | |
| text = gen( | |
| f"Write a very short scene caption (<=10 words) about: {user_prompt}", | |
| max_new_tokens=30, | |
| do_sample=True, | |
| temperature=0.9, | |
| top_p=0.95, | |
| num_return_sequences=1, | |
| )[0]["generated_text"].strip() | |
| # Fallback if something weird comes out | |
| if not text or len(text.split()) < 2: | |
| text = f"Scene {i+1}" | |
| outputs.append(text[:80]) | |
| return outputs | |
| def add_caption_strip(img: Image.Image, text: str, width_hint: int) -> Image.Image: | |
| """Add a black strip with white text at the bottom. Uses textbbox (Pillow>=10).""" | |
| out = img.copy() | |
| draw = ImageDraw.Draw(out) | |
| try: | |
| font = ImageFont.truetype("DejaVuSans.ttf", size=max(16, width_hint // 28)) | |
| except Exception: | |
| font = ImageFont.load_default() | |
| bbox = draw.textbbox((0, 0), text, font=font) | |
| text_w = bbox[2] - bbox[0] | |
| text_h = bbox[3] - bbox[1] | |
| strip_h = text_h + 14 | |
| strip = Image.new("RGB", (out.width, strip_h), (0, 0, 0)) | |
| d2 = ImageDraw.Draw(strip) | |
| d2.text(((out.width - text_w) // 2, 7), text, font=font, fill=(255, 255, 255)) | |
| combined = Image.new("RGB", (out.width, out.height + strip_h), (0, 0, 0)) | |
| combined.paste(out, (0, 0)) | |
| combined.paste(strip, (0, out.height)) | |
| return combined | |
| def images_to_pdf_with_fpdf(images): | |
| """Write a simple multipage PDF using FPDF.""" | |
| if not images: | |
| return None | |
| pdf_path = tempfile.mktemp(suffix=".pdf") | |
| pdf = FPDF() | |
| for img in images: | |
| # Save temp PNG to insert in PDF | |
| tmp = tempfile.mktemp(suffix=".png") | |
| img.save(tmp) | |
| pdf.add_page() | |
| # Fit the image nicely within margins | |
| pdf.image(tmp, x=10, y=10, w=190) | |
| pdf.output(pdf_path) | |
| return pdf_path | |
| # ------------------ Core logic ------------------ | |
| def create_storyboard(user_prompt, style, num_panels, width, height, seed): | |
| if not user_prompt or not user_prompt.strip(): | |
| return [], None | |
| # Build prompts + captions | |
| captions = generate_captions(user_prompt, n=num_panels) | |
| prompts = [build_prompt(user_prompt, style, i, num_panels) for i in range(num_panels)] | |
| # Try GPU (ZeroGPU). If it fails (no GPU), fallback to CPU. | |
| images = None | |
| try: | |
| images = t2i_generate_batch_gpu(prompts, width, height, steps=2, guidance=0.0, | |
| negative_prompt=NEGATIVE, seed=seed) | |
| except Exception: | |
| # GPU not available → CPU fallback (slower) | |
| pipe = get_t2i_cpu() | |
| images = [] | |
| # No seed control on CPU path by default; can be added with torch.Generator("cpu") | |
| for p in prompts: | |
| img = pipe( | |
| prompt=p, | |
| negative_prompt=NEGATIVE, | |
| num_inference_steps=4, | |
| guidance_scale=0.0, | |
| width=width, | |
| height=height, | |
| ).images[0] | |
| images.append(img) | |
| # Add caption strips | |
| final_images = [add_caption_strip(img, cap, width_hint=width) for img, cap in zip(images, captions)] | |
| # Build PDF | |
| pdf_path = images_to_pdf_with_fpdf(final_images) | |
| return final_images, pdf_path | |
| # ------------------ UI ------------------ | |
| with gr.Blocks(title="AI Storyboard Creator") as demo: | |
| gr.Markdown( | |
| """ | |
| # 🎬 AI Storyboard Creator | |
| Turn a single prompt into a mini storyboard: 3–6 panels, captions, and a downloadable PDF. | |
| Works on **CPU basic** and supports **ZeroGPU** (GPU on-demand). | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| prompt = gr.Textbox(label="Story prompt", placeholder="A cyberpunk detective in the rain", lines=2) | |
| style = gr.Dropdown(choices=list(STYLE_PRESETS.keys()), value="Comic", label="Style") | |
| num_panels = gr.Slider(3, 6, value=3, step=1, label="Number of panels") | |
| width = gr.Slider(384, 768, value=448, step=64, label="Panel width (px)") | |
| height = gr.Slider(384, 768, value=448, step=64, label="Panel height (px)") | |
| seed = gr.Textbox(label="Seed (optional)", placeholder="e.g., 42") | |
| run_btn = gr.Button("Create Storyboard") | |
| with gr.Column(): | |
| # NOTE: no .style(); use columns=2 instead | |
| gallery = gr.Gallery(label="Preview (grid)", columns=2, height="auto") | |
| pdf_file = gr.File(label="Download PDF") | |
| run_btn.click( | |
| create_storyboard, | |
| inputs=[prompt, style, num_panels, width, height, seed], | |
| outputs=[gallery, pdf_file], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |