Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,125 +1,85 @@
|
|
| 1 |
# app.py
|
| 2 |
-
# VisPrompt Studio — Image/Text → Detailed Prompts (CPU, uncensored)
|
| 3 |
-
#
|
| 4 |
-
#
|
| 5 |
-
#
|
| 6 |
|
| 7 |
-
import os
|
| 8 |
-
import
|
| 9 |
-
import math
|
| 10 |
-
from typing import Tuple, Optional
|
| 11 |
|
| 12 |
import gradio as gr
|
| 13 |
from PIL import Image, ExifTags
|
| 14 |
import torch
|
| 15 |
-
from transformers import BlipProcessor, BlipForConditionalGeneration
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
return Image.open(inp).convert("RGB")
|
| 40 |
|
| 41 |
-
|
| 42 |
def exif_info(img: Image.Image) -> dict:
|
| 43 |
-
"""Extract a couple of useful EXIF fields if present."""
|
| 44 |
out = {}
|
| 45 |
try:
|
| 46 |
raw = img.getexif()
|
| 47 |
-
if not raw:
|
| 48 |
-
return out
|
| 49 |
tag_map = {ExifTags.TAGS.get(k, k): v for k, v in raw.items()}
|
| 50 |
fl = tag_map.get("FocalLength")
|
| 51 |
-
if isinstance(fl, tuple) and len(fl)
|
| 52 |
-
out["focal_length_mm"] = round(fl[0]
|
| 53 |
elif isinstance(fl, (int, float)):
|
| 54 |
out["focal_length_mm"] = float(fl)
|
| 55 |
-
if "Model" in tag_map:
|
| 56 |
-
|
| 57 |
-
if "Make" in tag_map:
|
| 58 |
-
out["camera_make"] = str(tag_map["Make"]).strip()
|
| 59 |
except Exception:
|
| 60 |
pass
|
| 61 |
return out
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
]
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
]
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
]
|
| 74 |
-
LIGHTING = [
|
| 75 |
-
"soft diffused light", "golden hour rim light", "overcast softbox feel", "hard noon sun with crisp shadows",
|
| 76 |
-
"blue hour ambient", "tungsten practicals", "neon key with colored rim", "studio softbox left + kicker right",
|
| 77 |
-
"volumetric god rays", "moody chiaroscuro"
|
| 78 |
-
]
|
| 79 |
-
COMPOSITION = [
|
| 80 |
-
"rule of thirds", "leading lines", "central composition", "frame within a frame", "negative space",
|
| 81 |
-
"foreground bokeh", "shallow depth of field", "symmetry", "diagonal dynamics", "layered depth"
|
| 82 |
-
]
|
| 83 |
-
POST = [
|
| 84 |
-
"high micro-contrast", "subtle film grain", "cinematic color grade", "Kodak Portra palette",
|
| 85 |
-
"teal–orange split tone", "matte shadows", "HDR tonality restrained", "8k detail", "photorealistic finish"
|
| 86 |
-
]
|
| 87 |
-
STYLES = [
|
| 88 |
-
"documentary realism", "editorial fashion", "street photography", "nature landscape", "architectural minimalism",
|
| 89 |
-
"cyberpunk neon", "sci-fi cinematic", "fantasy epic", "moody noir", "retro 35mm film"
|
| 90 |
-
]
|
| 91 |
-
VIDEO_MOTION = [
|
| 92 |
-
"slow dolly-in", "gentle push-in", "handheld micro-jitters", "locked-off tripod", "gimbal glide", "orbit right-to-left",
|
| 93 |
-
"crane rise", "parallax truck shot", "rack focus mid-shot"
|
| 94 |
-
]
|
| 95 |
-
VIDEO_META = [
|
| 96 |
-
"24 fps", "30 fps", "48 fps", "cinematic motion blur", "shutter 1/48", "global illumination feel"
|
| 97 |
-
]
|
| 98 |
-
VIDEO_TRANSITIONS = ["straight cut", "cinematic fade-in/out", "cross dissolve", "match cut"]
|
| 99 |
-
NEGATIVE = [
|
| 100 |
-
"no distortion", "no chromatic aberration", "no blown highlights", "no excessive sharpening",
|
| 101 |
-
"no artifacts", "no watermark", "no text overlay"
|
| 102 |
-
]
|
| 103 |
-
|
| 104 |
|
| 105 |
def smart_choices(caption: str, w: int, h: int, exif: dict, seed: int):
|
| 106 |
-
"""Pick plausible settings using simple heuristics."""
|
| 107 |
rnd = random.Random(seed)
|
| 108 |
aspect = (w / h) if h > 0 else 1.0
|
| 109 |
-
|
| 110 |
shot = "wide shot" if aspect >= 1.5 else "medium shot"
|
| 111 |
text = caption.lower()
|
| 112 |
-
|
| 113 |
-
if any(k in text for k in ["portrait", "selfie", "face", "headshot"]):
|
| 114 |
shot = "close-up"
|
| 115 |
-
|
| 116 |
-
if any(k in text for k in ["aerial", "drone", "top-down", "overhead", "bird's-eye", "bird’s-eye"]):
|
| 117 |
angle = "aerial/top-down"
|
| 118 |
-
elif any(k in text for k in ["towering",
|
| 119 |
angle = "low-angle"
|
| 120 |
else:
|
| 121 |
-
angle = rnd.choice(CAMERA_ANGLES[:3])
|
| 122 |
-
|
| 123 |
lens = f'{exif["focal_length_mm"]}mm' if "focal_length_mm" in exif else rnd.choice(LENSES)
|
| 124 |
light = rnd.choice(LIGHTING)
|
| 125 |
comp = ", ".join(rnd.sample(COMPOSITION, k=2))
|
|
@@ -128,61 +88,60 @@ def smart_choices(caption: str, w: int, h: int, exif: dict, seed: int):
|
|
| 128 |
neg = ", ".join(NEGATIVE[:3] + rnd.sample(NEGATIVE[3:], k=2))
|
| 129 |
return angle, shot, lens, light, comp, style, post, neg, aspect
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
max_len = int(max(16, min(128, max_len)))
|
| 137 |
-
if beam_search:
|
| 138 |
-
kwargs = dict(max_length=max_len, num_beams=3)
|
| 139 |
-
else:
|
| 140 |
-
kwargs = dict(max_length=max_len)
|
| 141 |
-
out = _model.generate(**inputs, **kwargs)
|
| 142 |
-
return _processor.decode(out[0], skip_special_tokens=True).strip()
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
def build_image_prompt(subject_desc: str, angle: str, shot: str, lens: str,
|
| 146 |
-
light: str, comp: str, style: str, post: str, aspect: float) -> str:
|
| 147 |
-
ar_text = "16:9" if aspect >= 1.6 else ("4:3" if aspect >= 1.3 else "1:1")
|
| 148 |
-
prompt = (
|
| 149 |
-
f"{subject_desc}. "
|
| 150 |
-
f"Camera angle: {angle}. Shot size: {shot}. Lens: {lens}. "
|
| 151 |
-
f"Lighting: {light}. Composition: {comp}. Style: {style}. Post-processing: {post}. "
|
| 152 |
-
f"Aspect ratio hint: {ar_text}. "
|
| 153 |
-
f"Ultra-detailed, physically plausible, consistent scale."
|
| 154 |
-
)
|
| 155 |
-
return prompt
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
mode: str,
|
| 172 |
-
beam_search: bool,
|
| 173 |
-
max_len: int,
|
| 174 |
-
seconds: int,
|
| 175 |
-
seed: int
|
| 176 |
-
):
|
| 177 |
-
"""
|
| 178 |
-
Returns: (caption, image_prompt, video_prompt) per selected mode.
|
| 179 |
-
"""
|
| 180 |
img = safe_open_image(image)
|
| 181 |
caption = ""
|
| 182 |
base_desc = ""
|
| 183 |
|
| 184 |
if img is not None:
|
| 185 |
-
caption = caption_image(img, beam_search
|
| 186 |
w, h = img.size
|
| 187 |
exif = exif_info(img)
|
| 188 |
angle, shot, lens, light, comp, style, post, neg, aspect = smart_choices(caption, w, h, exif, seed)
|
|
@@ -196,8 +155,7 @@ def generate(
|
|
| 196 |
angle, shot, lens, light, comp, style, post, neg, aspect = smart_choices(base_desc, w, h, exif, seed)
|
| 197 |
|
| 198 |
image_prompt = build_image_prompt(base_desc, angle, shot, lens, light, comp, style, post, aspect)
|
| 199 |
-
|
| 200 |
-
|
| 201 |
video_prompt = build_video_prompt(
|
| 202 |
image_prompt,
|
| 203 |
motion=random.choice(VIDEO_MOTION),
|
|
@@ -207,36 +165,36 @@ def generate(
|
|
| 207 |
)
|
| 208 |
|
| 209 |
if mode == "Image prompt":
|
| 210 |
-
return caption, image_prompt +
|
| 211 |
elif mode == "Video prompt":
|
| 212 |
return caption, "", video_prompt
|
| 213 |
else:
|
| 214 |
-
return caption, image_prompt +
|
| 215 |
|
| 216 |
-
|
| 217 |
-
TITLE = "VisPrompt Studio — Image/Text → Prompts (
|
| 218 |
DESC = """
|
| 219 |
Upload an image **or** describe a scene.
|
| 220 |
-
Get a polished **image prompt**, a **video prompt**, or **both** — with camera angle, shot size, lens, lighting, composition, style, post,
|
| 221 |
|
| 222 |
-
|
| 223 |
-
- **
|
|
|
|
| 224 |
"""
|
| 225 |
|
| 226 |
-
|
| 227 |
with gr.Blocks(title=TITLE) as demo:
|
| 228 |
gr.Markdown(f"# {TITLE}\n{DESC}")
|
| 229 |
-
|
| 230 |
with gr.Row():
|
| 231 |
with gr.Column(scale=1):
|
|
|
|
| 232 |
with gr.Tab("Image input"):
|
| 233 |
image = gr.Image(type="pil", label="Upload an image", height=360)
|
| 234 |
with gr.Tab("Text input"):
|
| 235 |
text_scene = gr.Textbox(lines=6, placeholder="Describe the scene you want to reproduce...", label="Scene description")
|
| 236 |
|
| 237 |
-
mode = gr.Radio(choices=["Image prompt",
|
| 238 |
-
beam_search = gr.Checkbox(True, label="
|
| 239 |
-
max_len = gr.Slider(48, 128, value=
|
| 240 |
seconds = gr.Slider(3, 12, value=6, step=1, label="Video duration (seconds)")
|
| 241 |
seed = gr.Slider(0, 9999, value=1234, step=1, label="Style seed")
|
| 242 |
|
|
@@ -244,24 +202,14 @@ with gr.Blocks(title=TITLE) as demo:
|
|
| 244 |
|
| 245 |
with gr.Column(scale=1):
|
| 246 |
caption_box = gr.Textbox(label="Image caption (from model)", interactive=False, lines=3, show_copy_button=True)
|
| 247 |
-
img_prompt_box = gr.Textbox(
|
| 248 |
-
|
| 249 |
-
interactive=False,
|
| 250 |
-
lines=12,
|
| 251 |
-
show_copy_button=True,
|
| 252 |
-
)
|
| 253 |
-
vid_prompt_box = gr.Textbox(
|
| 254 |
-
label="Video Prompt (copy-paste to video generators)",
|
| 255 |
-
interactive=False,
|
| 256 |
-
lines=14,
|
| 257 |
-
show_copy_button=True,
|
| 258 |
-
)
|
| 259 |
|
| 260 |
run.click(
|
| 261 |
fn=generate,
|
| 262 |
-
inputs=[image, text_scene, mode, beam_search, max_len, seconds, seed],
|
| 263 |
outputs=[caption_box, img_prompt_box, vid_prompt_box]
|
| 264 |
)
|
| 265 |
|
| 266 |
if __name__ == "__main__":
|
| 267 |
-
demo.launch()
|
|
|
|
| 1 |
# app.py
|
| 2 |
+
# VisPrompt Studio — Image/Text → Detailed Prompts (CPU, mostly uncensored)
|
| 3 |
+
# Models:
|
| 4 |
+
# - Qwen2-VL-2B-Instruct (default; stronger on people/NSFW, slower on CPU)
|
| 5 |
+
# - Salesforce/blip-image-captioning-base (fallback; faster on CPU)
|
| 6 |
|
| 7 |
+
import os, random, math
|
| 8 |
+
from typing import Optional, Tuple
|
|
|
|
|
|
|
| 9 |
|
| 10 |
import gradio as gr
|
| 11 |
from PIL import Image, ExifTags
|
| 12 |
import torch
|
|
|
|
| 13 |
|
| 14 |
+
from transformers import (
|
| 15 |
+
AutoProcessor,
|
| 16 |
+
AutoModelForVision2Seq,
|
| 17 |
+
BlipProcessor,
|
| 18 |
+
BlipForConditionalGeneration,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# ===== Model IDs =====
|
| 22 |
+
MODEL_QWEN2_VL = "Qwen/Qwen2-VL-2B-Instruct"
|
| 23 |
+
MODEL_BLIP = "Salesforce/blip-image-captioning-base"
|
| 24 |
+
|
| 25 |
+
# Lazy globals for each backend
|
| 26 |
+
_q_processor = None
|
| 27 |
+
_q_model = None
|
| 28 |
+
_b_processor = None
|
| 29 |
+
_b_model = None
|
| 30 |
+
|
| 31 |
+
def device():
|
| 32 |
+
# Free CPU Space: force cpu. (If you later use ZeroGPU, you can set CUDA here.)
|
| 33 |
+
return "cpu"
|
| 34 |
+
|
| 35 |
+
# ---------- Utilities ----------
|
| 36 |
+
def safe_open_image(inp):
|
| 37 |
+
if inp is None: return None
|
| 38 |
+
if isinstance(inp, Image.Image): return inp.convert("RGB")
|
| 39 |
return Image.open(inp).convert("RGB")
|
| 40 |
|
|
|
|
| 41 |
def exif_info(img: Image.Image) -> dict:
|
|
|
|
| 42 |
out = {}
|
| 43 |
try:
|
| 44 |
raw = img.getexif()
|
| 45 |
+
if not raw: return out
|
|
|
|
| 46 |
tag_map = {ExifTags.TAGS.get(k, k): v for k, v in raw.items()}
|
| 47 |
fl = tag_map.get("FocalLength")
|
| 48 |
+
if isinstance(fl, tuple) and len(fl)==2 and fl[1]!=0:
|
| 49 |
+
out["focal_length_mm"] = round(fl[0]/fl[1], 1)
|
| 50 |
elif isinstance(fl, (int, float)):
|
| 51 |
out["focal_length_mm"] = float(fl)
|
| 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 |
+
CAMERA_ANGLES = ["eye-level","low-angle","high-angle","aerial/top-down","worm's-eye","Dutch tilt"]
|
| 59 |
+
SHOT_SIZES = ["extreme close-up","close-up","medium shot","wide shot","extreme wide shot"]
|
| 60 |
+
LENSES = ["14mm ultra-wide","24mm wide-angle","35mm street","50mm standard","85mm portrait","135mm telephoto","200mm telephoto"]
|
| 61 |
+
LIGHTING = ["soft diffused light","golden hour rim light","overcast softbox feel","hard noon sun with crisp shadows","blue hour ambient","tungsten practicals","neon key with colored rim","studio softbox left + kicker right","volumetric god rays","moody chiaroscuro"]
|
| 62 |
+
COMPOSITION = ["rule of thirds","leading lines","central composition","frame within a frame","negative space","foreground bokeh","shallow depth of field","symmetry","diagonal dynamics","layered depth"]
|
| 63 |
+
POST = ["high micro-contrast","subtle film grain","cinematic color grade","Kodak Portra palette","teal–orange split tone","matte shadows","HDR tonality restrained","8k detail","photorealistic finish"]
|
| 64 |
+
STYLES = ["documentary realism","editorial fashion","street photography","nature landscape","architectural minimalism","cyberpunk neon","sci-fi cinematic","fantasy epic","moody noir","retro 35mm film"]
|
| 65 |
+
VIDEO_MOTION = ["slow dolly-in","gentle push-in","handheld micro-jitters","locked-off tripod","gimbal glide","orbit right-to-left","crane rise","parallax truck shot","rack focus mid-shot"]
|
| 66 |
+
VIDEO_META = ["24 fps","30 fps","48 fps","cinematic motion blur","shutter 1/48","global illumination feel"]
|
| 67 |
+
VIDEO_TRANSITIONS = ["straight cut","cinematic fade-in/out","cross dissolve","match cut"]
|
| 68 |
+
NEGATIVE = ["no distortion","no chromatic aberration","no blown highlights","no excessive sharpening","no artifacts","no watermark","no text overlay"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
def smart_choices(caption: str, w: int, h: int, exif: dict, seed: int):
|
|
|
|
| 71 |
rnd = random.Random(seed)
|
| 72 |
aspect = (w / h) if h > 0 else 1.0
|
|
|
|
| 73 |
shot = "wide shot" if aspect >= 1.5 else "medium shot"
|
| 74 |
text = caption.lower()
|
| 75 |
+
if any(k in text for k in ["portrait","selfie","face","headshot"]):
|
|
|
|
| 76 |
shot = "close-up"
|
| 77 |
+
if any(k in text for k in ["aerial","drone","top-down","overhead","bird's-eye","bird’s-eye"]):
|
|
|
|
| 78 |
angle = "aerial/top-down"
|
| 79 |
+
elif any(k in text for k in ["towering","imposing","hero","statue"]):
|
| 80 |
angle = "low-angle"
|
| 81 |
else:
|
| 82 |
+
angle = rnd.choice(CAMERA_ANGLES[:3])
|
|
|
|
| 83 |
lens = f'{exif["focal_length_mm"]}mm' if "focal_length_mm" in exif else rnd.choice(LENSES)
|
| 84 |
light = rnd.choice(LIGHTING)
|
| 85 |
comp = ", ".join(rnd.sample(COMPOSITION, k=2))
|
|
|
|
| 88 |
neg = ", ".join(NEGATIVE[:3] + rnd.sample(NEGATIVE[3:], k=2))
|
| 89 |
return angle, shot, lens, light, comp, style, post, neg, aspect
|
| 90 |
|
| 91 |
+
def build_image_prompt(desc, angle, shot, lens, light, comp, style, post, aspect):
|
| 92 |
+
ar = "16:9" if aspect >= 1.6 else ("4:3" if aspect >= 1.3 else "1:1")
|
| 93 |
+
return (f"{desc}. Camera angle: {angle}. Shot size: {shot}. Lens: {lens}. "
|
| 94 |
+
f"Lighting: {light}. Composition: {comp}. Style: {style}. Post-processing: {post}. "
|
| 95 |
+
f"Aspect ratio hint: {ar}. Ultra-detailed, physically plausible, consistent scale.")
|
| 96 |
|
| 97 |
+
def build_video_prompt(img_prompt: str, motion: str, meta: list, transition: str, seconds: int):
|
| 98 |
+
meta_str = ", ".join(meta)
|
| 99 |
+
return (f"{img_prompt} Now render as a {seconds}s cinematic video; camera motion: {motion}; "
|
| 100 |
+
f"{meta_str}; transitions: {transition}. Maintain temporal consistency, realistic parallax, coherent lighting.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
# ---------- Backends ----------
|
| 103 |
+
@torch.inference_mode()
|
| 104 |
+
def qwen2_vl_caption(img: Image.Image, max_new_tokens: int = 96) -> str:
|
| 105 |
+
global _q_processor, _q_model
|
| 106 |
+
if _q_processor is None or _q_model is None:
|
| 107 |
+
_q_processor = AutoProcessor.from_pretrained(MODEL_QWEN2_VL, trust_remote_code=True)
|
| 108 |
+
_q_model = AutoModelForVision2Seq.from_pretrained(MODEL_QWEN2_VL, torch_dtype=torch.float32, trust_remote_code=True)
|
| 109 |
+
_q_model.to(device()).eval()
|
| 110 |
+
# Simple instruction to push detailed descriptions (uncensored)
|
| 111 |
+
prompt = "Describe this image in exhaustive detail: people, clothing, actions, camera angle, lighting, background."
|
| 112 |
+
inputs = _q_processor(text=prompt, images=img, return_tensors="pt")
|
| 113 |
+
out = _q_model.generate(**inputs, max_new_tokens=max_new_tokens)
|
| 114 |
+
text = _q_processor.batch_decode(out, skip_special_tokens=True)[0].strip()
|
| 115 |
+
return text
|
| 116 |
|
| 117 |
+
@torch.inference_mode()
|
| 118 |
+
def blip_caption(img: Image.Image, beam_search: bool = True, max_len: int = 80) -> str:
|
| 119 |
+
global _b_processor, _b_model
|
| 120 |
+
if _b_processor is None or _b_model is None:
|
| 121 |
+
_b_processor = BlipProcessor.from_pretrained(MODEL_BLIP)
|
| 122 |
+
_b_model = BlipForConditionalGeneration.from_pretrained(MODEL_BLIP)
|
| 123 |
+
_b_model.to(device()).eval()
|
| 124 |
+
inputs = _b_processor(images=img, return_tensors="pt")
|
| 125 |
+
max_len = int(max(16, min(128, max_len)))
|
| 126 |
+
kwargs = dict(max_length=max_len, num_beams=3) if beam_search else dict(max_length=max_len)
|
| 127 |
+
out = _b_model.generate(**inputs, **kwargs)
|
| 128 |
+
return _b_processor.decode(out[0], skip_special_tokens=True).strip()
|
| 129 |
|
| 130 |
+
def caption_image(model_choice: str, img: Image.Image, beam_search: bool, max_len: int) -> str:
|
| 131 |
+
if model_choice == "Qwen2-VL-2B-Instruct":
|
| 132 |
+
return qwen2_vl_caption(img, max_new_tokens=max_len)
|
| 133 |
+
else:
|
| 134 |
+
return blip_caption(img, beam_search=beam_search, max_len=max_len)
|
| 135 |
|
| 136 |
+
# ---------- Main generate ----------
|
| 137 |
+
def generate(image: Optional[Image.Image], text_scene: str, mode: str,
|
| 138 |
+
beam_search: bool, max_len: int, seconds: int, seed: int, model_choice: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
img = safe_open_image(image)
|
| 140 |
caption = ""
|
| 141 |
base_desc = ""
|
| 142 |
|
| 143 |
if img is not None:
|
| 144 |
+
caption = caption_image(model_choice, img, beam_search, max_len)
|
| 145 |
w, h = img.size
|
| 146 |
exif = exif_info(img)
|
| 147 |
angle, shot, lens, light, comp, style, post, neg, aspect = smart_choices(caption, w, h, exif, seed)
|
|
|
|
| 155 |
angle, shot, lens, light, comp, style, post, neg, aspect = smart_choices(base_desc, w, h, exif, seed)
|
| 156 |
|
| 157 |
image_prompt = build_image_prompt(base_desc, angle, shot, lens, light, comp, style, post, aspect)
|
| 158 |
+
neg_line = f"\nNegative prompt hints: {neg}"
|
|
|
|
| 159 |
video_prompt = build_video_prompt(
|
| 160 |
image_prompt,
|
| 161 |
motion=random.choice(VIDEO_MOTION),
|
|
|
|
| 165 |
)
|
| 166 |
|
| 167 |
if mode == "Image prompt":
|
| 168 |
+
return caption, image_prompt + neg_line, ""
|
| 169 |
elif mode == "Video prompt":
|
| 170 |
return caption, "", video_prompt
|
| 171 |
else:
|
| 172 |
+
return caption, image_prompt + neg_line, video_prompt
|
| 173 |
|
| 174 |
+
# ---------- UI ----------
|
| 175 |
+
TITLE = "VisPrompt Studio — Image/Text → Prompts (Qwen2-VL or BLIP)"
|
| 176 |
DESC = """
|
| 177 |
Upload an image **or** describe a scene.
|
| 178 |
+
Get a polished **image prompt**, a **video prompt**, or **both** — with camera angle, shot size, lens, lighting, composition, style, post, AR hints, plus optional negative prompts.
|
| 179 |
|
| 180 |
+
**Models**:
|
| 181 |
+
- **Qwen2-VL-2B-Instruct (default)** — stronger on people/NSFW & fashion; slower on CPU.
|
| 182 |
+
- **BLIP base** — faster on CPU; simpler captions.
|
| 183 |
"""
|
| 184 |
|
|
|
|
| 185 |
with gr.Blocks(title=TITLE) as demo:
|
| 186 |
gr.Markdown(f"# {TITLE}\n{DESC}")
|
|
|
|
| 187 |
with gr.Row():
|
| 188 |
with gr.Column(scale=1):
|
| 189 |
+
model_choice = gr.Dropdown(["Qwen2-VL-2B-Instruct","BLIP base"], value="Qwen2-VL-2B-Instruct", label="Captioning model")
|
| 190 |
with gr.Tab("Image input"):
|
| 191 |
image = gr.Image(type="pil", label="Upload an image", height=360)
|
| 192 |
with gr.Tab("Text input"):
|
| 193 |
text_scene = gr.Textbox(lines=6, placeholder="Describe the scene you want to reproduce...", label="Scene description")
|
| 194 |
|
| 195 |
+
mode = gr.Radio(choices=["Image prompt","Video prompt","Both"], value="Both", label="Output type")
|
| 196 |
+
beam_search = gr.Checkbox(True, label="BLIP: beam search (ignored for Qwen2-VL)")
|
| 197 |
+
max_len = gr.Slider(48, 128, value=96, step=1, label="Caption length / max tokens")
|
| 198 |
seconds = gr.Slider(3, 12, value=6, step=1, label="Video duration (seconds)")
|
| 199 |
seed = gr.Slider(0, 9999, value=1234, step=1, label="Style seed")
|
| 200 |
|
|
|
|
| 202 |
|
| 203 |
with gr.Column(scale=1):
|
| 204 |
caption_box = gr.Textbox(label="Image caption (from model)", interactive=False, lines=3, show_copy_button=True)
|
| 205 |
+
img_prompt_box = gr.Textbox(label="Image Prompt", interactive=False, lines=12, show_copy_button=True)
|
| 206 |
+
vid_prompt_box = gr.Textbox(label="Video Prompt", interactive=False, lines=14, show_copy_button=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
run.click(
|
| 209 |
fn=generate,
|
| 210 |
+
inputs=[image, text_scene, mode, beam_search, max_len, seconds, seed, model_choice],
|
| 211 |
outputs=[caption_box, img_prompt_box, vid_prompt_box]
|
| 212 |
)
|
| 213 |
|
| 214 |
if __name__ == "__main__":
|
| 215 |
+
demo.launch()
|