speedn commited on
Commit
46acee4
·
verified ·
1 Parent(s): 8626235

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +246 -0
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()