Spaces:
Paused
Paused
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
# Hugging Face Space (Gradio) — CPU-only, uncensored prompt generator
|
| 3 |
+
# - Upload an image or describe a scene
|
| 4 |
+
# - Outputs: Image prompt, Video prompt, or Both
|
| 5 |
+
# Model: Salesforce/blip-image-captioning-base (free, CPU-capable, uncensored)
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import math
|
| 9 |
+
import random
|
| 10 |
+
import gradio as gr
|
| 11 |
+
from PIL import Image, ExifTags
|
| 12 |
+
import torch
|
| 13 |
+
from transformers import BlipProcessor, BlipForConditionalGeneration
|
| 14 |
+
|
| 15 |
+
HF_MODEL_ID = "Salesforce/blip-image-captioning-base"
|
| 16 |
+
|
| 17 |
+
# Lazy globals so startup is fast
|
| 18 |
+
_processor = None
|
| 19 |
+
_model = None
|
| 20 |
+
|
| 21 |
+
def _load_blip():
|
| 22 |
+
global _processor, _model
|
| 23 |
+
if _processor is None or _model is None:
|
| 24 |
+
_processor = BlipProcessor.from_pretrained(HF_MODEL_ID)
|
| 25 |
+
_model = BlipForConditionalGeneration.from_pretrained(HF_MODEL_ID)
|
| 26 |
+
_model.to("cpu")
|
| 27 |
+
_model.eval()
|
| 28 |
+
|
| 29 |
+
def safe_open_image(inp):
|
| 30 |
+
if inp is None:
|
| 31 |
+
return None
|
| 32 |
+
if isinstance(inp, Image.Image):
|
| 33 |
+
return inp.convert("RGB")
|
| 34 |
+
return Image.open(inp).convert("RGB")
|
| 35 |
+
|
| 36 |
+
def exif_info(img: Image.Image):
|
| 37 |
+
"""Pull a few nice-to-have EXIF hints (if present)."""
|
| 38 |
+
out = {}
|
| 39 |
+
try:
|
| 40 |
+
raw = img.getexif()
|
| 41 |
+
if not raw:
|
| 42 |
+
return out
|
| 43 |
+
# Map EXIF keys to names
|
| 44 |
+
tag_map = {ExifTags.TAGS.get(k, k): v for k, v in raw.items()}
|
| 45 |
+
# Focal length can be tuple (num, den)
|
| 46 |
+
fl = tag_map.get("FocalLength")
|
| 47 |
+
if isinstance(fl, tuple) and len(fl) == 2 and fl[1] != 0:
|
| 48 |
+
out["focal_length_mm"] = round(fl[0] / fl[1], 1)
|
| 49 |
+
elif isinstance(fl, (int, float)):
|
| 50 |
+
out["focal_length_mm"] = float(fl)
|
| 51 |
+
# Camera/lens if available
|
| 52 |
+
if "Model" in tag_map: out["camera_model"] = str(tag_map["Model"]).strip()
|
| 53 |
+
if "Make" in tag_map: out["camera_make"] = str(tag_map["Make"]).strip()
|
| 54 |
+
except Exception:
|
| 55 |
+
pass
|
| 56 |
+
return out
|
| 57 |
+
|
| 58 |
+
# Lightweight lexicons for prompt enrichment (kept in-file for mobile simplicity)
|
| 59 |
+
CAMERA_ANGLES = [
|
| 60 |
+
"eye-level", "low-angle", "high-angle", "aerial/top-down", "worm's-eye", "Dutch tilt"
|
| 61 |
+
]
|
| 62 |
+
SHOT_SIZES = [
|
| 63 |
+
"extreme close-up", "close-up", "medium shot", "wide shot", "extreme wide shot"
|
| 64 |
+
]
|
| 65 |
+
LENSES = [
|
| 66 |
+
"14mm ultra-wide", "24mm wide-angle", "35mm street", "50mm standard", "85mm portrait", "135mm telephoto", "200mm telephoto"
|
| 67 |
+
]
|
| 68 |
+
LIGHTING = [
|
| 69 |
+
"soft diffused light", "golden hour rim light", "overcast softbox feel", "hard noon sun with crisp shadows",
|
| 70 |
+
"blue hour ambient", "tungsten practicals", "neon key with colored rim", "studio softbox left + kicker right",
|
| 71 |
+
"volumetric god rays", "moody chiaroscuro"
|
| 72 |
+
]
|
| 73 |
+
COMPOSITION = [
|
| 74 |
+
"rule of thirds", "leading lines", "central composition", "frame within a frame", "negative space",
|
| 75 |
+
"foreground bokeh", "shallow depth of field", "symmetry", "diagonal dynamics", "layered depth"
|
| 76 |
+
]
|
| 77 |
+
POST = [
|
| 78 |
+
"high micro-contrast", "subtle film grain", "cinematic color grade", "Kodak Portra palette",
|
| 79 |
+
"Teal–orange split tone", "matte shadows", "HDR tonality restrained", "8k detail", "photorealistic finish"
|
| 80 |
+
]
|
| 81 |
+
STYLES = [
|
| 82 |
+
"documentary realism", "editorial fashion", "street photography", "nature landscape", "architectural minimalism",
|
| 83 |
+
"cyberpunk neon", "sci-fi cinematic", "fantasy epic", "moody noir", "retro 35mm film"
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
VIDEO_MOTION = [
|
| 87 |
+
"slow dolly-in", "gentle push-in", "handheld micro-jitters", "locked-off tripod", "gimbal glide", "orbit right-to-left",
|
| 88 |
+
"crane rise", "parallax truck shot", "rack focus mid-shot"
|
| 89 |
+
]
|
| 90 |
+
VIDEO_META = [
|
| 91 |
+
"24 fps", "30 fps", "48 fps", "cinematic motion blur", "shutter 1/48", "global illumination feel"
|
| 92 |
+
]
|
| 93 |
+
VIDEO_TRANSITIONS = ["straight cut", "cinematic fade-in/out", "cross dissolve", "match cut"]
|
| 94 |
+
|
| 95 |
+
NEGATIVE = [
|
| 96 |
+
"no distortion", "no chromatic aberration", "no blown highlights", "no excessive sharpening",
|
| 97 |
+
"no artifacts", "no watermark", "no text overlay"
|
| 98 |
+
]
|
| 99 |
+
|
| 100 |
+
def smart_choices(caption: str, w: int, h: int, exif: dict, seed: int):
|
| 101 |
+
"""Pick plausible settings using caption/exif/orientation heuristics."""
|
| 102 |
+
rnd = random.Random(seed)
|
| 103 |
+
# Orientation heuristic
|
| 104 |
+
aspect = w / h if h > 0 else 1.0
|
| 105 |
+
shot = "wide shot" if aspect >= 1.5 else "medium shot"
|
| 106 |
+
if any(k in caption.lower() for k in ["portrait", "selfie", "face", "headshot"]):
|
| 107 |
+
shot = "close-up"
|
| 108 |
+
if any(k in caption.lower() for k in ["aerial", "drone", "top-down", "overhead"]):
|
| 109 |
+
angle = "aerial/top-down"
|
| 110 |
+
elif any(k in caption.lower() for k in ["towering", "imposing", "hero", "statue"]):
|
| 111 |
+
angle = "low-angle"
|
| 112 |
+
elif any(k in caption.lower() for k in ["bird's-eye", "bird’s-eye"]):
|
| 113 |
+
angle = "aerial/top-down"
|
| 114 |
+
else:
|
| 115 |
+
angle = rnd.choice(CAMERA_ANGLES[:3]) # eye/high/low
|
| 116 |
+
|
| 117 |
+
lens = f'{exif["focal_length_mm"]}mm' if "focal_length_mm" in exif else rnd.choice(LENSES)
|
| 118 |
+
light = rnd.choice(LIGHTING)
|
| 119 |
+
comp = ", ".join(rnd.sample(COMPOSITION, k=2))
|
| 120 |
+
style = rnd.choice(STYLES)
|
| 121 |
+
post = ", ".join(rnd.sample(POST, k=2))
|
| 122 |
+
neg = ", ".join(NEGATIVE[:3] + rnd.sample(NEGATIVE[3:], k=2))
|
| 123 |
+
return angle, shot, lens, light, comp, style, post, neg, aspect
|
| 124 |
+
|
| 125 |
+
def caption_image(img: Image.Image, beam_search: bool, max_len: int):
|
| 126 |
+
_load_blip()
|
| 127 |
+
with torch.no_grad():
|
| 128 |
+
inputs = _processor(images=img, return_tensors="pt")
|
| 129 |
+
gen_kwargs = dict(max_length=int(max(16, min(128, max_len))), num_beams=3) if beam_search else dict(max_length=int(max(16, min(128, max_len))))
|
| 130 |
+
out = _model.generate(**inputs, **gen_kwargs)
|
| 131 |
+
text = _processor.decode(out[0], skip_special_tokens=True).strip()
|
| 132 |
+
return text
|
| 133 |
+
|
| 134 |
+
def build_image_prompt(subject_desc: str, angle: str, shot: str, lens: str, light: str, comp: str, style: str, post: str, aspect: float):
|
| 135 |
+
ar_text = "16:9" if aspect >= 1.6 else ("4:3" if aspect >= 1.3 else "1:1")
|
| 136 |
+
prompt = (
|
| 137 |
+
f"{subject_desc}. "
|
| 138 |
+
f"Camera angle: {angle}. Shot size: {shot}. Lens: {lens}. "
|
| 139 |
+
f"Lighting: {light}. Composition: {comp}. Style: {style}. Post-processing: {post}. "
|
| 140 |
+
f"Aspect ratio hint: {ar_text}. "
|
| 141 |
+
f"Ultra-detailed, physically plausible, consistent scale."
|
| 142 |
+
)
|
| 143 |
+
return prompt
|
| 144 |
+
|
| 145 |
+
def build_video_prompt(image_prompt: str, motion: str, meta: list, transition: str, seconds: int):
|
| 146 |
+
meta_str = ", ".join(meta)
|
| 147 |
+
vp = (
|
| 148 |
+
f"{image_prompt} "
|
| 149 |
+
f"Now render as a {seconds}s cinematic video; camera motion: {motion}; "
|
| 150 |
+
f"{meta_str}; transitions: {transition}. "
|
| 151 |
+
f"Maintain temporal consistency, realistic motion parallax, and coherent lighting through the sequence."
|
| 152 |
+
)
|
| 153 |
+
return vp
|
| 154 |
+
|
| 155 |
+
def generate(
|
| 156 |
+
image: Image.Image,
|
| 157 |
+
text_scene: str,
|
| 158 |
+
mode: str,
|
| 159 |
+
beam_search: bool,
|
| 160 |
+
max_len: int,
|
| 161 |
+
seconds: int,
|
| 162 |
+
seed: int
|
| 163 |
+
):
|
| 164 |
+
"""
|
| 165 |
+
Main entry: returns (caption, image_prompt, video_prompt) respecting mode selection.
|
| 166 |
+
"""
|
| 167 |
+
img = safe_open_image(image)
|
| 168 |
+
caption = ""
|
| 169 |
+
base_desc = ""
|
| 170 |
+
|
| 171 |
+
if img is not None:
|
| 172 |
+
caption = caption_image(img, beam_search=beam_search, max_len=max_len)
|
| 173 |
+
w, h = img.size
|
| 174 |
+
exif = exif_info(img)
|
| 175 |
+
angle, shot, lens, light, comp, style, post, neg, aspect = smart_choices(caption, w, h, exif, seed)
|
| 176 |
+
base_desc = caption
|
| 177 |
+
else:
|
| 178 |
+
# Text-only: treat user text as subject description and craft details
|
| 179 |
+
if not text_scene or not text_scene.strip():
|
| 180 |
+
return "", "", ""
|
| 181 |
+
base_desc = text_scene.strip()
|
| 182 |
+
# fabricate a neutral canvas for heuristics
|
| 183 |
+
w, h = (1920, 1080)
|
| 184 |
+
exif = {}
|
| 185 |
+
angle, shot, lens, light, comp, style, post, neg, aspect = smart_choices(base_desc, w, h, exif, seed)
|
| 186 |
+
|
| 187 |
+
# Build prompts
|
| 188 |
+
image_prompt = build_image_prompt(base_desc, angle, shot, lens, light, comp, style, post, aspect)
|
| 189 |
+
# Negative prompt as optional hint line
|
| 190 |
+
neg_line = f"\nNegative prompt hints: {neg}"
|
| 191 |
+
|
| 192 |
+
video_prompt = build_video_prompt(
|
| 193 |
+
image_prompt, motion=random.choice(VIDEO_MOTION),
|
| 194 |
+
meta=random.sample(VIDEO_META, k=2), transition=random.choice(VIDEO_TRANSITIONS),
|
| 195 |
+
seconds=int(max(2, min(20, seconds)))
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
if mode == "Image prompt":
|
| 199 |
+
out_img = image_prompt + neg_line
|
| 200 |
+
return caption, out_img, ""
|
| 201 |
+
elif mode == "Video prompt":
|
| 202 |
+
return caption, "", video_prompt
|
| 203 |
+
else:
|
| 204 |
+
out_img = image_prompt + neg_line
|
| 205 |
+
return caption, out_img, video_prompt
|
| 206 |
+
|
| 207 |
+
TITLE = "Img2Prompt & VideoPrompt (CPU, Uncensored)"
|
| 208 |
+
DESC = """
|
| 209 |
+
**Upload an image or describe a scene.**
|
| 210 |
+
Get a **production-ready image prompt**, a **video prompt**, or **both** — with camera angle, shot size, lens, lighting, composition, style, post, aspect ratio hints, plus optional negative prompt hints.
|
| 211 |
+
|
| 212 |
+
- **Model:** Salesforce/blip-image-captioning-base (CPU; no built-in safety filters)
|
| 213 |
+
- **Hardware:** Works on Hugging Face free CPU Spaces (first run downloads weights)
|
| 214 |
+
"""
|
| 215 |
+
|
| 216 |
+
with gr.Blocks(title=TITLE) as demo:
|
| 217 |
+
gr.Markdown(f"# {TITLE}\n{DESC}")
|
| 218 |
+
|
| 219 |
+
with gr.Row():
|
| 220 |
+
with gr.Column(scale=1):
|
| 221 |
+
with gr.Tab("Image input"):
|
| 222 |
+
image = gr.Image(type="pil", label="Upload an image", height=360)
|
| 223 |
+
with gr.Tab("Text input"):
|
| 224 |
+
text_scene = gr.Textbox(lines=6, placeholder="Describe the scene you want to reproduce...", label="Scene description")
|
| 225 |
+
|
| 226 |
+
mode = gr.Radio(choices=["Image prompt", "Video prompt", "Both"], value="Both", label="Output type")
|
| 227 |
+
beam_search = gr.Checkbox(True, label="Use beam search for caption (better detail, slightly slower)")
|
| 228 |
+
max_len = gr.Slider(48, 128, value=80, step=1, label="Caption max length")
|
| 229 |
+
seconds = gr.Slider(3, 12, value=6, step=1, label="Video duration (seconds)")
|
| 230 |
+
seed = gr.Slider(0, 9999, value=1234, step=1, label="Style seed (change for different angles/lighting)")
|
| 231 |
+
|
| 232 |
+
run = gr.Button("Generate prompts", variant="primary")
|
| 233 |
+
|
| 234 |
+
with gr.Column(scale=1):
|
| 235 |
+
caption_box = gr.Textbox(label="Image caption (from model)", interactive=False, lines=3)
|
| 236 |
+
img_prompt_box = gr.Code(label="Image Prompt (copy-paste to image generators)", language="text")
|
| 237 |
+
vid_prompt_box = gr.Code(label="Video Prompt (copy-paste to video generators)", language="text")
|
| 238 |
+
|
| 239 |
+
run.click(
|
| 240 |
+
fn=generate,
|
| 241 |
+
inputs=[image, text_scene, mode, beam_search, max_len, seconds, seed],
|
| 242 |
+
outputs=[caption_box, img_prompt_box, vid_prompt_box]
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
if __name__ == "__main__":
|
| 246 |
+
demo.launch()
|