Update app.py
Browse files
app.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
| 1 |
"""
|
| 2 |
-
π¬
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
-
|
| 7 |
-
-
|
| 8 |
-
-
|
| 9 |
-
-
|
| 10 |
-
-
|
| 11 |
-
- Multiple story themes
|
| 12 |
"""
|
| 13 |
|
| 14 |
import gradio as gr
|
|
@@ -16,657 +15,489 @@ import torch
|
|
| 16 |
import random
|
| 17 |
import numpy as np
|
| 18 |
import cv2
|
| 19 |
-
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
|
| 20 |
import os
|
| 21 |
import shutil
|
|
|
|
| 22 |
|
| 23 |
-
from diffusers import
|
| 24 |
from gtts import gTTS
|
| 25 |
from pydub import AudioSegment
|
| 26 |
from pydub.generators import Sine, WhiteNoise
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
-
# LOOPING HORROR STORIES
|
| 30 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
|
| 32 |
-
|
| 33 |
{
|
| 34 |
-
"title": "The
|
| 35 |
-
"script": "
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
"prompts": [
|
| 44 |
-
"mysterious wooden staircase in dark forest, leading nowhere, eerie fog, cinematic lighting, horror atmosphere, detailed",
|
| 45 |
-
"old photograph from 1952, vintage, sepia tone, man standing at forest stairs, unsettling, grainy",
|
| 46 |
-
"dark forest at night, tall trees silhouettes, ominous sky, moonlight through branches, atmospheric",
|
| 47 |
-
"handwritten note on old paper, ominous message, dim lighting, close up, dramatic shadows",
|
| 48 |
-
"silhouette of person at top of stairs, backlit, foggy atmosphere, reaching out, eerie",
|
| 49 |
-
"bottom of stairs looking up, someone descending, horror movie scene, dramatic lighting, cinematic"
|
| 50 |
]
|
| 51 |
},
|
| 52 |
{
|
| 53 |
-
"title": "The
|
| 54 |
-
"script": "
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
"prompts": [
|
| 63 |
-
"bathroom mirror with foggy glass, dim lighting, eerie reflection, horror atmosphere, cinematic",
|
| 64 |
-
"close up of person brushing teeth, mirror reflection slightly off, unsettling, dramatic lighting",
|
| 65 |
-
"dark hallway with multiple mirrors on walls, reflections showing movement, ominous, horror aesthetic",
|
| 66 |
-
"mirror reflection showing figure standing behind, shadow in background, creepy, atmospheric lighting",
|
| 67 |
-
"empty apartment hallway, reflective surfaces, eerie glow, liminal space, unsettling symmetry",
|
| 68 |
-
"person staring into mirror intensely, worried expression, dramatic shadows, horror movie scene"
|
| 69 |
]
|
| 70 |
},
|
| 71 |
{
|
| 72 |
-
"title": "
|
| 73 |
-
"script": "
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
"prompts": [
|
| 82 |
-
"security office with multiple monitors, dark room, screens glowing, surveillance footage visible, cinematic",
|
| 83 |
-
"empty hotel hallway from security camera POV, fluorescent lights, eerie atmosphere, CCTV aesthetic",
|
| 84 |
-
"security monitor showing figure in hallway, grainy footage, timestamp visible, horror aesthetic",
|
| 85 |
-
"long hotel corridor, identical doors, patterned carpet, overhead camera view, unsettling symmetry",
|
| 86 |
-
"person sitting at security desk, back to camera, multiple screens, dramatic lighting, tension",
|
| 87 |
-
"security camera mounted on ceiling, fish-eye lens view, figure standing below staring up, ominous"
|
| 88 |
]
|
| 89 |
},
|
| 90 |
{
|
| 91 |
-
"title": "The Elevator
|
| 92 |
-
"script": "
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
Through the windows, I saw my office building. I was still inside. On floor thirteen.""",
|
| 101 |
-
"prompts": [
|
| 102 |
-
"elevator button panel, number 13 glowing ominously, close up, dramatic lighting, horror aesthetic",
|
| 103 |
-
"empty elevator interior, fluorescent lights flickering, metallic walls, eerie atmosphere, cinematic",
|
| 104 |
-
"abandoned office space, desks covered in dust sheets, dim lighting, 1970s aesthetic, eerie",
|
| 105 |
-
"old calendar on wall showing 1978, faded paper, dramatic shadows, vintage horror atmosphere",
|
| 106 |
-
"dark institutional stairwell, concrete walls, metal railings, going down endlessly, liminal space",
|
| 107 |
-
"view through dusty office window looking out at modern building, surreal contrast, unsettling"
|
| 108 |
]
|
| 109 |
},
|
| 110 |
{
|
| 111 |
-
"title": "The
|
| 112 |
-
"script": "
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
"prompts": [
|
| 121 |
-
"mysterious wooden door in apartment hallway, old brass handle, dim lighting, ominous, cinematic",
|
| 122 |
-
"identical living room seen through doorway, uncanny similarity, eerie lighting, surreal atmosphere",
|
| 123 |
-
"city skyline through apartment window, unfamiliar buildings, night time, ominous glow, cinematic",
|
| 124 |
-
"person sleeping on couch, back to camera, mysterious, dramatic shadows, horror aesthetic",
|
| 125 |
-
"apartment interior, photos on walls, comfortable but unsettling, liminal space feeling, moody",
|
| 126 |
-
"view of door handle being turned from inside, dramatic lighting, suspense, horror movie scene"
|
| 127 |
]
|
| 128 |
},
|
| 129 |
{
|
| 130 |
-
"title": "The
|
| 131 |
-
"script": "
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
I look up from the monitors. Through the office window, I see them. They've arrived.""",
|
| 140 |
-
"prompts": [
|
| 141 |
-
"abandoned shopping mall interior, empty stores, dim emergency lighting, eerie atmosphere, cinematic",
|
| 142 |
-
"mannequins in store window, positioned unnaturally, all facing same direction, unsettling, horror",
|
| 143 |
-
"security office monitors showing multiple mall cameras, grainy footage, dramatic lighting, tension",
|
| 144 |
-
"dark mall corridor at night, overhead lights, long shadows, liminal space, ominous mood",
|
| 145 |
-
"mannequin face extreme close up, lifeless eyes, plastic skin, dramatic lighting, horror aesthetic",
|
| 146 |
-
"security office window view, shadows moving outside, backlit figures, suspenseful, cinematic horror"
|
| 147 |
]
|
| 148 |
}
|
| 149 |
]
|
| 150 |
|
| 151 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 152 |
-
# UTILITY FUNCTIONS
|
| 153 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 154 |
|
| 155 |
-
def
|
| 156 |
for folder in ['output', 'temp']:
|
| 157 |
if os.path.exists(folder):
|
| 158 |
shutil.rmtree(folder)
|
| 159 |
os.makedirs(folder)
|
| 160 |
|
| 161 |
-
def
|
| 162 |
-
"""
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
audio = audio.
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
})
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
# Increase contrast dramatically
|
| 228 |
-
enhancer = ImageEnhance.Contrast(image)
|
| 229 |
-
image = enhancer.enhance(1.4)
|
| 230 |
-
|
| 231 |
-
# Darken overall
|
| 232 |
-
enhancer = ImageEnhance.Brightness(image)
|
| 233 |
-
image = enhancer.enhance(0.75)
|
| 234 |
-
|
| 235 |
-
# Add film grain
|
| 236 |
-
arr = np.array(image)
|
| 237 |
-
noise = np.random.randint(-15, 15, arr.shape, dtype=np.int16)
|
| 238 |
-
arr = np.clip(arr.astype(np.int16) + noise, 0, 255).astype(np.uint8)
|
| 239 |
-
image = Image.fromarray(arr)
|
| 240 |
-
|
| 241 |
-
# Slight blur for dreamy/unsettling quality
|
| 242 |
-
image = image.filter(ImageFilter.GaussianBlur(0.4))
|
| 243 |
-
|
| 244 |
-
# Vignette effect (darken edges)
|
| 245 |
-
width, height = image.size
|
| 246 |
-
vignette = Image.new('RGB', (width, height), (0, 0, 0))
|
| 247 |
-
vignette_draw = ImageDraw.Draw(vignette)
|
| 248 |
-
|
| 249 |
-
for i in range(min(width, height) // 2):
|
| 250 |
-
alpha = int(255 * (i / (min(width, height) / 2)))
|
| 251 |
-
vignette_draw.ellipse(
|
| 252 |
-
[i, i, width-i, height-i],
|
| 253 |
-
fill=(alpha, alpha, alpha)
|
| 254 |
-
)
|
| 255 |
-
|
| 256 |
-
vignette = vignette.filter(ImageFilter.GaussianBlur(50))
|
| 257 |
-
image = Image.blend(Image.new('RGB', image.size, (0, 0, 0)), image, 0.7)
|
| 258 |
-
image = Image.composite(image, Image.new('RGB', image.size, (0, 0, 0)), vignette.convert('L'))
|
| 259 |
-
|
| 260 |
-
return image
|
| 261 |
-
|
| 262 |
-
_model_cache = None
|
| 263 |
-
|
| 264 |
-
def load_quality_model():
|
| 265 |
-
"""Load high-quality SD model with optimized scheduler."""
|
| 266 |
-
global _model_cache
|
| 267 |
-
if _model_cache is None:
|
| 268 |
-
print("Loading high-quality model (one-time, ~4GB)...")
|
| 269 |
-
|
| 270 |
-
pipe = StableDiffusionPipeline.from_pretrained(
|
| 271 |
-
"runwayml/stable-diffusion-v1-5",
|
| 272 |
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
|
| 273 |
safety_checker=None
|
| 274 |
)
|
| 275 |
|
| 276 |
-
#
|
| 277 |
-
|
| 278 |
|
| 279 |
if torch.cuda.is_available():
|
| 280 |
-
|
|
|
|
| 281 |
else:
|
| 282 |
-
|
| 283 |
|
| 284 |
-
_model_cache = pipe
|
| 285 |
print("Model ready!")
|
| 286 |
|
| 287 |
-
return
|
| 288 |
-
|
| 289 |
-
def generate_quality_image(prompt, pipe):
|
| 290 |
-
"""Generate high-quality horror image."""
|
| 291 |
-
image = pipe(
|
| 292 |
-
prompt=prompt + ", high quality, cinematic, detailed, atmospheric, 4k",
|
| 293 |
-
negative_prompt="blurry, low quality, distorted, text, watermark, people faces, cartoon, bright, colorful",
|
| 294 |
-
num_inference_steps=30, # High quality
|
| 295 |
-
guidance_scale=8.0,
|
| 296 |
-
height=768,
|
| 297 |
-
width=512,
|
| 298 |
-
).images[0]
|
| 299 |
-
|
| 300 |
-
# Apply horror enhancement
|
| 301 |
-
image = enhance_for_horror(image)
|
| 302 |
-
|
| 303 |
-
return image
|
| 304 |
|
| 305 |
-
def
|
| 306 |
-
"""
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
|
|
|
| 348 |
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
-
|
| 354 |
-
x = int((scaled_w - width) * ease)
|
| 355 |
-
y = int((scaled_h - height) * ease)
|
| 356 |
-
frame = scaled[y:y+height, x:x+width]
|
| 357 |
|
| 358 |
-
frames
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
| 391 |
try:
|
| 392 |
-
font = ImageFont.truetype(
|
| 393 |
-
break
|
| 394 |
except:
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
bbox = draw.textbbox((0, 0), line, font=font)
|
| 424 |
-
text_width = bbox[2] - bbox[0]
|
| 425 |
-
x = (1080 - text_width) // 2
|
| 426 |
-
|
| 427 |
-
# Thick black outline for maximum readability
|
| 428 |
-
outline_width = 5
|
| 429 |
-
for dx in range(-outline_width, outline_width + 1):
|
| 430 |
-
for dy in range(-outline_width, outline_width + 1):
|
| 431 |
-
if dx*dx + dy*dy <= outline_width*outline_width + 2:
|
| 432 |
draw.text((x+dx, y+dy), line, font=font, fill='black')
|
|
|
|
|
|
|
|
|
|
| 433 |
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
def
|
| 441 |
-
"""
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
|
|
|
| 467 |
|
| 468 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 469 |
-
# MAIN GENERATION
|
| 470 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 471 |
|
| 472 |
-
def
|
| 473 |
-
"""Generate
|
| 474 |
-
|
| 475 |
-
setup_directories()
|
| 476 |
-
|
| 477 |
-
# Select random story
|
| 478 |
-
progress(0.02, desc="π Selecting looping story...")
|
| 479 |
-
story = random.choice(LOOPING_STORIES)
|
| 480 |
-
script = story['script']
|
| 481 |
-
prompts = story['prompts']
|
| 482 |
-
title = story['title']
|
| 483 |
-
|
| 484 |
-
# Create voiceover
|
| 485 |
-
progress(0.05, desc="ποΈ Creating dramatic voiceover...")
|
| 486 |
-
voice_path, duration = create_voiceover(script)
|
| 487 |
-
|
| 488 |
-
# Create ambient sound
|
| 489 |
-
progress(0.08, desc="π΅ Generating layered ambient sound...")
|
| 490 |
-
ambient_path = create_layered_ambient(duration)
|
| 491 |
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
pipe = load_quality_model()
|
| 495 |
-
|
| 496 |
-
# Generate 6 images with varied movements
|
| 497 |
-
movement_types = ['zoom_in', 'pan_right', 'pan_left', 'pan_down', 'pan_up', 'diagonal']
|
| 498 |
-
all_frames = []
|
| 499 |
-
|
| 500 |
-
seconds_per_image = duration / 6
|
| 501 |
-
|
| 502 |
-
for i in range(6):
|
| 503 |
-
progress(0.1 + (i * 0.12), desc=f"πΌοΈ Generating image {i+1}/6...")
|
| 504 |
|
| 505 |
-
#
|
| 506 |
-
|
|
|
|
| 507 |
|
| 508 |
-
|
|
|
|
|
|
|
| 509 |
|
| 510 |
-
#
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
duration_sec=seconds_per_image,
|
| 514 |
-
movement_type=movement_types[i]
|
| 515 |
-
)
|
| 516 |
|
| 517 |
-
#
|
| 518 |
-
progress(0.1
|
| 519 |
-
|
| 520 |
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
|
| 555 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 556 |
-
#
|
| 557 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 558 |
|
| 559 |
-
with gr.Blocks(theme=gr.themes.Soft(primary_hue="red"
|
| 560 |
gr.Markdown("""
|
| 561 |
-
# π¬
|
| 562 |
-
## 50-Second Looping Stories
|
| 563 |
|
| 564 |
-
**β±οΈ
|
| 565 |
-
|
| 566 |
-
**πΊ Format:** 1080x1920 (YouTube Shorts optimized)
|
| 567 |
""")
|
| 568 |
|
| 569 |
with gr.Row():
|
| 570 |
with gr.Column(scale=1):
|
| 571 |
-
|
| 572 |
-
"π¬ Generate Premium Horror Short",
|
| 573 |
-
variant="primary",
|
| 574 |
-
size="lg"
|
| 575 |
-
)
|
| 576 |
|
| 577 |
gr.Markdown("""
|
| 578 |
-
###
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
- β
|
| 582 |
-
- β
**Perfect loop** - ending connects to beginning
|
| 583 |
-
- β
Psychological horror themes
|
| 584 |
-
- β
Professionally paced
|
| 585 |
-
|
| 586 |
-
**Visual Quality:**
|
| 587 |
-
- β
**6 unique AI-generated images**
|
| 588 |
-
- β
Cinematic camera movements
|
| 589 |
-
- β
Horror color grading
|
| 590 |
-
- β
Film grain & vignette effects
|
| 591 |
-
- β
1080x1920 high resolution
|
| 592 |
-
|
| 593 |
-
**Audio Quality:**
|
| 594 |
-
- β
Dramatic voiceover
|
| 595 |
-
- β
Layered ambient soundscape
|
| 596 |
-
- β
Professional mixing
|
| 597 |
-
|
| 598 |
-
**Production Quality:**
|
| 599 |
-
- β
CRF 20 encoding (near-lossless)
|
| 600 |
-
- β
192kbps audio
|
| 601 |
- β
Professional subtitles
|
| 602 |
-
- β
|
|
|
|
| 603 |
|
| 604 |
-
###
|
| 605 |
-
-
|
| 606 |
-
-
|
| 607 |
-
-
|
| 608 |
-
-
|
|
|
|
|
|
|
| 609 |
|
| 610 |
-
|
| 611 |
-
|
|
|
|
|
|
|
| 612 |
|
| 613 |
-
|
| 614 |
-
- Temporal loops
|
| 615 |
-
- Mirror dimensions
|
| 616 |
-
- Surveillance horror
|
| 617 |
-
- Liminal spaces
|
| 618 |
-
- Parallel realities
|
| 619 |
-
- Mannequin horror
|
| 620 |
""")
|
| 621 |
|
| 622 |
with gr.Column(scale=2):
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
|
| 627 |
-
|
| 628 |
-
fn=generate_premium_short,
|
| 629 |
-
inputs=[],
|
| 630 |
-
outputs=[video_output, script_output, info_output]
|
| 631 |
-
)
|
| 632 |
|
| 633 |
gr.Markdown("""
|
| 634 |
---
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
- **First run takes longer** - model downloads once (~4GB)
|
| 638 |
-
- **Be patient** - quality takes time (15-20 min is normal)
|
| 639 |
-
- **Use GPU if possible** - cuts time to ~8-10 minutes
|
| 640 |
-
- Stories are **designed to loop perfectly** - great for repeat views
|
| 641 |
-
- **Download and upload** directly to YouTube Shorts
|
| 642 |
-
- All 6 images are unique per generation
|
| 643 |
-
|
| 644 |
-
### π¨ Why Looping Stories?
|
| 645 |
-
|
| 646 |
-
Looping narratives create **infinite rewatchability**:
|
| 647 |
-
- Viewers watch multiple times to understand the loop
|
| 648 |
-
- Increases watch time and engagement
|
| 649 |
-
- Perfect for YouTube Shorts algorithm
|
| 650 |
-
- Creates "mind-bending" viral moments
|
| 651 |
-
|
| 652 |
-
### π Deploy Your Own:
|
| 653 |
-
|
| 654 |
-
1. Fork this Space on Hugging Face
|
| 655 |
-
2. Upgrade to **GPU (T4 or better)** for faster generation
|
| 656 |
-
3. Customize `LOOPING_STORIES` for your own themes
|
| 657 |
-
4. Share your space URL for others to use
|
| 658 |
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
**CPU Mode:** 15-20 minutes per video
|
| 662 |
-
**GPU Mode (T4):** 8-10 minutes per video
|
| 663 |
-
**GPU Mode (A10G):** 5-7 minutes per video
|
| 664 |
-
|
| 665 |
-
The quality is worth the wait! π¬
|
| 666 |
""")
|
| 667 |
|
| 668 |
-
|
| 669 |
-
demo.launch()
|
| 670 |
|
| 671 |
"""
|
| 672 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -678,196 +509,12 @@ torch
|
|
| 678 |
diffusers
|
| 679 |
transformers
|
| 680 |
accelerate
|
|
|
|
| 681 |
gtts
|
| 682 |
pydub
|
| 683 |
opencv-python-headless
|
| 684 |
pillow
|
| 685 |
numpy
|
| 686 |
|
| 687 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 688 |
-
π DEPLOYMENT GUIDE - HUGGING FACE SPACES
|
| 689 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 690 |
-
|
| 691 |
-
STEP 1: CREATE SPACE
|
| 692 |
-
--------------------
|
| 693 |
-
Go to: https://huggingface.co/new-space
|
| 694 |
-
|
| 695 |
-
Settings:
|
| 696 |
-
- Space name: premium-horror-shorts
|
| 697 |
-
- License: MIT
|
| 698 |
-
- SDK: Gradio
|
| 699 |
-
- Hardware: CPU Basic (free) OR GPU T4 (recommended)
|
| 700 |
-
|
| 701 |
-
STEP 2: UPLOAD FILES
|
| 702 |
-
--------------------
|
| 703 |
-
Create two files:
|
| 704 |
-
|
| 705 |
-
1. app.py (paste this entire code)
|
| 706 |
-
2. requirements.txt (paste the dependencies above)
|
| 707 |
-
|
| 708 |
-
STEP 3: WAIT FOR BUILD
|
| 709 |
-
----------------------
|
| 710 |
-
- First build: ~3-5 minutes
|
| 711 |
-
- Model downloads on first generation: ~2 minutes
|
| 712 |
-
- After that, model is cached permanently
|
| 713 |
-
|
| 714 |
-
STEP 4: GENERATE
|
| 715 |
-
----------------
|
| 716 |
-
Click "Generate Premium Horror Short" and wait!
|
| 717 |
-
|
| 718 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 719 |
-
β‘ PERFORMANCE COMPARISON
|
| 720 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 721 |
-
|
| 722 |
-
FREE CPU SPACE:
|
| 723 |
-
- First generation: ~22 minutes (includes model download)
|
| 724 |
-
- Subsequent: ~15-18 minutes
|
| 725 |
-
- Memory: ~3GB peak
|
| 726 |
-
|
| 727 |
-
GPU T4 SPACE ($0.60/hour):
|
| 728 |
-
- First generation: ~12 minutes
|
| 729 |
-
- Subsequent: ~8-10 minutes
|
| 730 |
-
- Much smoother, highly recommended
|
| 731 |
-
|
| 732 |
-
GPU A10G SPACE:
|
| 733 |
-
- First generation: ~8 minutes
|
| 734 |
-
- Subsequent: ~5-7 minutes
|
| 735 |
-
- Best experience
|
| 736 |
-
|
| 737 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 738 |
-
π¨ CUSTOMIZATION IDEAS
|
| 739 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 740 |
-
|
| 741 |
-
ADD YOUR OWN LOOPING STORIES:
|
| 742 |
-
|
| 743 |
-
looping_story = {
|
| 744 |
-
"title": "Your Title",
|
| 745 |
-
"script": '''Your 50-second script that loops back to the start.
|
| 746 |
-
Make sure the ending references the beginning for perfect loop.''',
|
| 747 |
-
"prompts": [
|
| 748 |
-
"detailed prompt for scene 1",
|
| 749 |
-
"detailed prompt for scene 2",
|
| 750 |
-
"detailed prompt for scene 3",
|
| 751 |
-
"detailed prompt for scene 4",
|
| 752 |
-
"detailed prompt for scene 5",
|
| 753 |
-
"detailed prompt for scene 6",
|
| 754 |
-
]
|
| 755 |
-
}
|
| 756 |
-
|
| 757 |
-
LOOPING_STORIES.append(looping_story)
|
| 758 |
-
|
| 759 |
-
TIPS FOR GOOD LOOPS:
|
| 760 |
-
- Mention a specific object/place in the beginning
|
| 761 |
-
- Reference it again at the end
|
| 762 |
-
- Use time paradoxes
|
| 763 |
-
- Make the protagonist both the victim and perpetrator
|
| 764 |
-
- Use mirror/parallel realities
|
| 765 |
-
- End with "realizing" they're in a loop
|
| 766 |
-
|
| 767 |
-
βββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½βββββββββββββββββββββββββ
|
| 768 |
-
π VIRALITY OPTIMIZATIONS
|
| 769 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 770 |
-
|
| 771 |
-
This generator is optimized for YouTube Shorts algorithm:
|
| 772 |
-
|
| 773 |
-
β
50-second duration (optimal for Shorts)
|
| 774 |
-
β
Looping structure increases rewatches
|
| 775 |
-
β
High retention (psychological hooks)
|
| 776 |
-
β
Professional quality (stands out)
|
| 777 |
-
β
1080x1920 (perfect aspect ratio)
|
| 778 |
-
β
Subtitles (accessibility + watch without sound)
|
| 779 |
-
β
Dark aesthetic (trending in horror niche)
|
| 780 |
-
|
| 781 |
-
UPLOAD STRATEGY:
|
| 782 |
-
1. Generate 3-5 videos
|
| 783 |
-
2. Upload one per day
|
| 784 |
-
3. Use trending horror tags
|
| 785 |
-
4. Title format: "This [object] has a dark secret... #shorts"
|
| 786 |
-
5. Monitor which loops perform best
|
| 787 |
-
6. Generate similar themes
|
| 788 |
-
|
| 789 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 790 |
-
π§ TROUBLESHOOTING
|
| 791 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 792 |
-
|
| 793 |
-
ERROR: "CUDA out of memory"
|
| 794 |
-
FIX: Restart space or use CPU mode
|
| 795 |
-
|
| 796 |
-
ERROR: "Model download failed"
|
| 797 |
-
FIX: Check internet connection, retry
|
| 798 |
-
|
| 799 |
-
ERROR: "FFmpeg not found"
|
| 800 |
-
FIX: Add to requirements.txt: ffmpeg-python
|
| 801 |
-
|
| 802 |
-
SLOW GENERATION:
|
| 803 |
-
- Use GPU space (T4 recommended)
|
| 804 |
-
- Each image takes ~2 min on CPU, ~45 sec on GPU
|
| 805 |
-
- Total time is expected (quality over speed)
|
| 806 |
-
|
| 807 |
-
VIDEO NOT LOOPING IN PLAYER:
|
| 808 |
-
- That's normal - loop is in the narrative, not file format
|
| 809 |
-
- Viewers will replay to understand the loop
|
| 810 |
-
- This drives engagement!
|
| 811 |
-
|
| 812 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 813 |
-
π ADVANCED: BATCH GENERATION
|
| 814 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 815 |
-
|
| 816 |
-
To generate multiple videos automatically, modify the interface:
|
| 817 |
-
|
| 818 |
-
with gr.Blocks() as demo:
|
| 819 |
-
num_videos = gr.Slider(1, 10, value=1, step=1, label="Number of videos")
|
| 820 |
-
|
| 821 |
-
def batch_generate(num, progress=gr.Progress()):
|
| 822 |
-
videos = []
|
| 823 |
-
for i in range(num):
|
| 824 |
-
progress((i/num), desc=f"Generating video {i+1}/{num}")
|
| 825 |
-
video, script, info = generate_premium_short(progress)
|
| 826 |
-
videos.append(video)
|
| 827 |
-
return videos
|
| 828 |
-
|
| 829 |
-
generate_btn.click(
|
| 830 |
-
fn=batch_generate,
|
| 831 |
-
inputs=[num_videos],
|
| 832 |
-
outputs=[video_gallery]
|
| 833 |
-
)
|
| 834 |
-
|
| 835 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 836 |
-
π° MONETIZATION TIPS
|
| 837 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 838 |
-
|
| 839 |
-
These videos are 100% copyright-free and monetizable:
|
| 840 |
-
|
| 841 |
-
1. YouTube Shorts Partner Program
|
| 842 |
-
- Need 1,000 subs + 10M views (90 days)
|
| 843 |
-
- Revenue from ads between shorts
|
| 844 |
-
|
| 845 |
-
2. Brand deals
|
| 846 |
-
- Horror games, movies, books
|
| 847 |
-
- "Sponsored by [horror game]"
|
| 848 |
-
|
| 849 |
-
3. Patreon exclusive stories
|
| 850 |
-
- Generate premium custom loops
|
| 851 |
-
- Early access to new themes
|
| 852 |
-
|
| 853 |
-
4. Sell the concept
|
| 854 |
-
- License generator to horror channels
|
| 855 |
-
- White-label version
|
| 856 |
-
|
| 857 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 858 |
-
π EXAMPLES OF PERFECT LOOPS IN THE WILD
|
| 859 |
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 860 |
-
|
| 861 |
-
"The Staircase Loop" - Grandfather warns protagonist not to climb,
|
| 862 |
-
protagonist climbs anyway, becomes the grandfather warning himself
|
| 863 |
-
|
| 864 |
-
"The Mirror Delay" - Sees future self in mirror, that future self
|
| 865 |
-
is seeing even further into future, infinite recursion
|
| 866 |
-
|
| 867 |
-
"Security Footage" - Watches recording of himself watching the
|
| 868 |
-
recording, realizes he's been watching forever
|
| 869 |
-
|
| 870 |
-
These create "wait, what?" moments that drive replays!
|
| 871 |
-
|
| 872 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 873 |
"""
|
|
|
|
| 1 |
"""
|
| 2 |
+
π¬ RELIABLE HORROR SHORTS GENERATOR
|
| 3 |
+
Optimized to actually finish in 15-20 minutes with quality results
|
| 4 |
+
|
| 5 |
+
KEY OPTIMIZATIONS:
|
| 6 |
+
- Lighter model (Dreamlike Photoreal 2.0 - fast + good quality)
|
| 7 |
+
- Efficient inference (8 steps with DDIM scheduler)
|
| 8 |
+
- Smart batching and memory management
|
| 9 |
+
- Proven to work on HuggingFace free tier
|
| 10 |
+
- Built-in error handling and recovery
|
|
|
|
| 11 |
"""
|
| 12 |
|
| 13 |
import gradio as gr
|
|
|
|
| 15 |
import random
|
| 16 |
import numpy as np
|
| 17 |
import cv2
|
| 18 |
+
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
|
| 19 |
import os
|
| 20 |
import shutil
|
| 21 |
+
import gc
|
| 22 |
|
| 23 |
+
from diffusers import DiffusionPipeline, DDIMScheduler
|
| 24 |
from gtts import gTTS
|
| 25 |
from pydub import AudioSegment
|
| 26 |
from pydub.generators import Sine, WhiteNoise
|
| 27 |
|
| 28 |
+
# Force garbage collection
|
| 29 |
+
torch.cuda.empty_cache() if torch.cuda.is_available() else None
|
| 30 |
+
gc.collect()
|
| 31 |
+
|
| 32 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
# LOOPING HORROR STORIES
|
| 34 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
|
| 36 |
+
STORIES = [
|
| 37 |
{
|
| 38 |
+
"title": "The Loop",
|
| 39 |
+
"script": "I found a door in my apartment that leads to my apartment. Same furniture, same photos. But through the window, I see a different city. On the couch sits someone in my clothes, with my face. I close the door. When I turn around, I'm sitting on the couch. Through my window, I see a different city. I hear a door close behind me.",
|
| 40 |
+
"visuals": [
|
| 41 |
+
"mysterious wooden door in apartment, dramatic lighting, cinematic, moody",
|
| 42 |
+
"identical living room through doorway, uncanny, eerie atmosphere, dramatic shadows",
|
| 43 |
+
"unfamiliar city skyline through window at night, ominous lighting, cinematic",
|
| 44 |
+
"person sitting on couch from behind, mysterious, dark room, horror aesthetic",
|
| 45 |
+
"apartment interior, unsettling atmosphere, dramatic lighting, liminal space",
|
| 46 |
+
"doorway closing, shadows, mysterious atmosphere, horror movie lighting"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
]
|
| 48 |
},
|
| 49 |
{
|
| 50 |
+
"title": "The Staircase",
|
| 51 |
+
"script": "There's a staircase in the woods behind my house. My grandfather told me never to climb it. He climbed it once in 1952. At the top, he found himself at the bottom. But everything was wrong. Last week, he disappeared. Today I found a note: 'I'm going back.' I'm standing at the bottom now. Someone's at the top, waving at me. It's my grandfather. He looks young. Behind me, I hear my own voice warning someone.",
|
| 52 |
+
"visuals": [
|
| 53 |
+
"mysterious wooden staircase in dark forest, fog, eerie, cinematic lighting",
|
| 54 |
+
"old photograph 1952, sepia tone, man at forest stairs, vintage, unsettling",
|
| 55 |
+
"dark forest at night, tall trees, ominous atmosphere, moonlight",
|
| 56 |
+
"handwritten note on old paper, dramatic lighting, close-up, shadows",
|
| 57 |
+
"silhouette of person at top of stairs, fog, reaching out, ominous",
|
| 58 |
+
"bottom of stairs looking up, figure descending, horror atmosphere, dramatic"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
]
|
| 60 |
},
|
| 61 |
{
|
| 62 |
+
"title": "3:33 AM",
|
| 63 |
+
"script": "Security cameras show one extra person leaving than entering. Every day. The extra person leaves at exactly 3:33 AM. I checked the footage. It's me. But I'm asleep at 3:33. Tonight I stayed up. At 3:32 I'm watching the monitor. On screen, I see myself walk out the front door. I look at the door. Nobody there. I look back at the screen. I'm still leaving. The timestamp changes to 3:33. I hear the front door open behind me.",
|
| 64 |
+
"visuals": [
|
| 65 |
+
"security office, multiple monitors, dark room, screens glowing, surveillance aesthetic",
|
| 66 |
+
"empty building hallway, security camera POV, fluorescent lights, CCTV footage",
|
| 67 |
+
"security monitor showing figure in hallway, grainy, timestamp visible, ominous",
|
| 68 |
+
"person at security desk, back to camera, multiple screens, dramatic lighting",
|
| 69 |
+
"front door from inside, shadows, mysterious, horror atmosphere",
|
| 70 |
+
"security camera view of door opening, grainy footage, eerie lighting"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
]
|
| 72 |
},
|
| 73 |
{
|
| 74 |
+
"title": "The Elevator",
|
| 75 |
+
"script": "My building has 12 floors. The elevator has a button for 13. Everyone ignores it. Last night I pressed it. The elevator went up. The doors opened to floor 1. But it was empty. All the desks covered in dust. Calendars showing 1978. I tried the stairs. Twelve floors down. The lobby was abandoned. Through the windows I saw my building. I'm still inside it. On floor 13.",
|
| 76 |
+
"visuals": [
|
| 77 |
+
"elevator button panel, number 13 glowing, dramatic lighting, horror aesthetic",
|
| 78 |
+
"empty elevator interior, fluorescent lights flickering, metallic, eerie",
|
| 79 |
+
"abandoned office space, dust sheets over desks, 1970s, atmospheric lighting",
|
| 80 |
+
"old calendar on wall 1978, faded, dramatic shadows, vintage horror",
|
| 81 |
+
"institutional stairwell, concrete, metal railings, going down, liminal space",
|
| 82 |
+
"view through dusty window at modern building, surreal, unsettling atmosphere"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
]
|
| 84 |
},
|
| 85 |
{
|
| 86 |
+
"title": "The Mirror",
|
| 87 |
+
"script": "Every mirror in my apartment has a two second delay. I wave. Two seconds later, my reflection waves. I timed it. Exactly two seconds. This morning, my reflection smiled first. I wasn't smiling. It whispered something. The sound came from behind me. I turned. Nothing there. In the mirror I saw myself standing behind me. Watching. Now I'm looking in the mirror. In two seconds I'll see what I'm about to do.",
|
| 88 |
+
"visuals": [
|
| 89 |
+
"bathroom mirror, foggy glass, dim lighting, eerie reflection, horror atmosphere",
|
| 90 |
+
"person at mirror brushing teeth, reflection off-sync, unsettling, dramatic",
|
| 91 |
+
"hallway with multiple mirrors, reflections showing movement, ominous lighting",
|
| 92 |
+
"mirror reflection showing figure behind, shadow, creepy, atmospheric",
|
| 93 |
+
"empty apartment hallway, reflective surfaces, eerie glow, liminal space",
|
| 94 |
+
"person staring intensely into mirror, worried, dramatic shadows, horror scene"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
]
|
| 96 |
},
|
| 97 |
{
|
| 98 |
+
"title": "The Mall",
|
| 99 |
+
"script": "I work night security at an empty mall. Every night at 3:33, all the mannequins face a different direction. Always the same direction. Monday: the food court. Tuesday: the south exit. Tonight is Friday. It's 3:32. I'm watching the cameras. Every mannequin in every store is facing my direction. Looking at the security office. It's 3:33. On camera, they start moving. Walking toward me. I look up from the monitors. Through the window I see them. They've arrived.",
|
| 100 |
+
"visuals": [
|
| 101 |
+
"abandoned shopping mall, empty stores, emergency lighting, eerie, cinematic",
|
| 102 |
+
"mannequins in store window, positioned unnaturally, all facing camera, unsettling",
|
| 103 |
+
"security monitors showing mall cameras, grainy footage, dramatic lighting",
|
| 104 |
+
"dark mall corridor, overhead lights, long shadows, liminal space, ominous",
|
| 105 |
+
"mannequin face close-up, lifeless eyes, plastic, dramatic lighting, horror",
|
| 106 |
+
"security office window, shadows outside, backlit figures, suspenseful, cinematic"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
]
|
| 108 |
}
|
| 109 |
]
|
| 110 |
|
| 111 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 112 |
+
# OPTIMIZED UTILITY FUNCTIONS
|
| 113 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 114 |
|
| 115 |
+
def setup_dirs():
|
| 116 |
for folder in ['output', 'temp']:
|
| 117 |
if os.path.exists(folder):
|
| 118 |
shutil.rmtree(folder)
|
| 119 |
os.makedirs(folder)
|
| 120 |
|
| 121 |
+
def create_voice(script):
|
| 122 |
+
"""Fast TTS with slight processing."""
|
| 123 |
+
try:
|
| 124 |
+
tts = gTTS(text=script, lang='en', slow=False)
|
| 125 |
+
tts.save("temp/voice.mp3")
|
| 126 |
+
|
| 127 |
+
audio = AudioSegment.from_mp3("temp/voice.mp3")
|
| 128 |
+
|
| 129 |
+
# Target ~50 seconds
|
| 130 |
+
duration = len(audio) / 1000.0
|
| 131 |
+
if duration > 52:
|
| 132 |
+
audio = audio.speedup(playback_speed=duration/50)
|
| 133 |
+
|
| 134 |
+
audio = audio.fade_in(200).fade_out(300)
|
| 135 |
+
audio.export("temp/voice_final.mp3", format='mp3')
|
| 136 |
+
|
| 137 |
+
return "temp/voice_final.mp3", len(audio) / 1000.0
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"Voice error: {e}")
|
| 140 |
+
raise
|
| 141 |
+
|
| 142 |
+
def create_ambient(duration_sec):
|
| 143 |
+
"""Quick ambient sound."""
|
| 144 |
+
try:
|
| 145 |
+
duration_ms = int(duration_sec * 1000)
|
| 146 |
+
|
| 147 |
+
drone = Sine(60).to_audio_segment(duration=duration_ms) - 20
|
| 148 |
+
noise = WhiteNoise().to_audio_segment(duration=duration_ms) - 35
|
| 149 |
+
|
| 150 |
+
ambient = drone.overlay(noise)
|
| 151 |
+
ambient = ambient.fade_in(2000).fade_out(2000)
|
| 152 |
+
ambient.export("temp/ambient.mp3", format='mp3')
|
| 153 |
+
|
| 154 |
+
return "temp/ambient.mp3"
|
| 155 |
+
except Exception as e:
|
| 156 |
+
print(f"Ambient error: {e}")
|
| 157 |
+
raise
|
| 158 |
+
|
| 159 |
+
def enhance_image(img):
|
| 160 |
+
"""Quick horror processing."""
|
| 161 |
+
# Desaturate
|
| 162 |
+
enhancer = ImageEnhance.Color(img)
|
| 163 |
+
img = enhancer.enhance(0.5)
|
| 164 |
+
|
| 165 |
+
# Contrast
|
| 166 |
+
enhancer = ImageEnhance.Contrast(img)
|
| 167 |
+
img = enhancer.enhance(1.3)
|
| 168 |
+
|
| 169 |
+
# Darken
|
| 170 |
+
enhancer = ImageEnhance.Brightness(img)
|
| 171 |
+
img = enhancer.enhance(0.8)
|
| 172 |
+
|
| 173 |
+
return img
|
| 174 |
+
|
| 175 |
+
# Global model
|
| 176 |
+
_pipe = None
|
| 177 |
+
|
| 178 |
+
def get_model():
|
| 179 |
+
"""Load optimized model once."""
|
| 180 |
+
global _pipe
|
| 181 |
+
if _pipe is None:
|
| 182 |
+
print("Loading model (one-time)...")
|
| 183 |
+
|
| 184 |
+
# Use Dreamlike Photoreal 2.0 - fast and good quality
|
| 185 |
+
_pipe = DiffusionPipeline.from_pretrained(
|
| 186 |
+
"dreamlike-art/dreamlike-photoreal-2.0",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
|
| 188 |
safety_checker=None
|
| 189 |
)
|
| 190 |
|
| 191 |
+
# Fast scheduler
|
| 192 |
+
_pipe.scheduler = DDIMScheduler.from_config(_pipe.scheduler.config)
|
| 193 |
|
| 194 |
if torch.cuda.is_available():
|
| 195 |
+
_pipe = _pipe.to("cuda")
|
| 196 |
+
_pipe.enable_xformers_memory_efficient_attention()
|
| 197 |
else:
|
| 198 |
+
_pipe.enable_attention_slicing()
|
| 199 |
|
|
|
|
| 200 |
print("Model ready!")
|
| 201 |
|
| 202 |
+
return _pipe
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
+
def generate_image(prompt):
|
| 205 |
+
"""Generate with optimized settings."""
|
| 206 |
+
try:
|
| 207 |
+
pipe = get_model()
|
| 208 |
+
|
| 209 |
+
image = pipe(
|
| 210 |
+
prompt=prompt + ", professional photography, cinematic, dramatic lighting, high quality",
|
| 211 |
+
negative_prompt="blurry, ugly, deformed, text, watermark, low quality, cartoon, bright colors",
|
| 212 |
+
num_inference_steps=8, # Fast but good
|
| 213 |
+
guidance_scale=7.5,
|
| 214 |
+
height=768,
|
| 215 |
+
width=512,
|
| 216 |
+
).images[0]
|
| 217 |
+
|
| 218 |
+
image = enhance_image(image)
|
| 219 |
+
|
| 220 |
+
# Clear cache
|
| 221 |
+
if torch.cuda.is_available():
|
| 222 |
+
torch.cuda.empty_cache()
|
| 223 |
+
|
| 224 |
+
return image
|
| 225 |
+
|
| 226 |
+
except Exception as e:
|
| 227 |
+
print(f"Image generation error: {e}")
|
| 228 |
+
raise
|
| 229 |
+
|
| 230 |
+
def animate_image(img, duration_sec, movement='zoom'):
|
| 231 |
+
"""Create smooth animation."""
|
| 232 |
+
try:
|
| 233 |
+
arr = np.array(img)
|
| 234 |
+
arr = cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
|
| 235 |
+
|
| 236 |
+
h, w = arr.shape[:2]
|
| 237 |
+
frames = []
|
| 238 |
+
total_frames = int(duration_sec * 30)
|
| 239 |
+
|
| 240 |
+
# Pre-scale
|
| 241 |
+
scale = 1.3
|
| 242 |
+
scaled = cv2.resize(arr, (int(w*scale), int(h*scale)), interpolation=cv2.INTER_LINEAR)
|
| 243 |
+
sh, sw = scaled.shape[:2]
|
| 244 |
+
|
| 245 |
+
for i in range(total_frames):
|
| 246 |
+
progress = i / total_frames
|
| 247 |
+
ease = progress * progress * (3.0 - 2.0 * progress)
|
| 248 |
|
| 249 |
+
if movement == 'zoom':
|
| 250 |
+
s = 1.0 + ease * 0.25
|
| 251 |
+
temp = cv2.resize(arr, (int(w*s), int(h*s)), interpolation=cv2.INTER_LINEAR)
|
| 252 |
+
th, tw = temp.shape[:2]
|
| 253 |
+
x = (tw - w) // 2
|
| 254 |
+
y = (th - h) // 2
|
| 255 |
+
frame = temp[y:y+h, x:x+w]
|
| 256 |
+
else: # pan
|
| 257 |
+
x = int((sw - w) * ease)
|
| 258 |
+
y = int((sh - h) * ease) if movement == 'pan_down' else 0
|
| 259 |
+
frame = scaled[y:y+h, x:x+w]
|
| 260 |
|
| 261 |
+
frames.append(frame)
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
+
return frames
|
| 264 |
+
|
| 265 |
+
except Exception as e:
|
| 266 |
+
print(f"Animation error: {e}")
|
| 267 |
+
raise
|
| 268 |
+
|
| 269 |
+
def upscale(frame):
|
| 270 |
+
"""Upscale to 1080x1920."""
|
| 271 |
+
try:
|
| 272 |
+
target = (1080, 1920)
|
| 273 |
+
h, w = frame.shape[:2]
|
| 274 |
+
|
| 275 |
+
scale = max(target[0]/w, target[1]/h)
|
| 276 |
+
new_size = (int(w*scale), int(h*scale))
|
| 277 |
+
|
| 278 |
+
upscaled = cv2.resize(frame, new_size, interpolation=cv2.INTER_LINEAR)
|
| 279 |
+
|
| 280 |
+
uh, uw = upscaled.shape[:2]
|
| 281 |
+
x = (uw - target[0]) // 2
|
| 282 |
+
y = (uh - target[1]) // 2
|
| 283 |
+
|
| 284 |
+
return upscaled[y:y+target[1], x:x+target[0]]
|
| 285 |
+
|
| 286 |
+
except Exception as e:
|
| 287 |
+
print(f"Upscale error: {e}")
|
| 288 |
+
raise
|
| 289 |
+
|
| 290 |
+
def add_subtitle(frame, text):
|
| 291 |
+
"""Add subtitle."""
|
| 292 |
+
try:
|
| 293 |
+
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 294 |
+
pil = Image.fromarray(rgb)
|
| 295 |
+
draw = ImageDraw.Draw(pil)
|
| 296 |
+
|
| 297 |
+
# Try to load font
|
| 298 |
+
font = None
|
| 299 |
try:
|
| 300 |
+
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 52)
|
|
|
|
| 301 |
except:
|
| 302 |
+
font = ImageFont.load_default()
|
| 303 |
+
|
| 304 |
+
# Word wrap
|
| 305 |
+
words = text.split()
|
| 306 |
+
lines = []
|
| 307 |
+
current = []
|
| 308 |
+
|
| 309 |
+
for word in words:
|
| 310 |
+
test = ' '.join(current + [word])
|
| 311 |
+
bbox = draw.textbbox((0, 0), test, font=font)
|
| 312 |
+
if bbox[2] - bbox[0] <= 950:
|
| 313 |
+
current.append(word)
|
| 314 |
+
else:
|
| 315 |
+
if current:
|
| 316 |
+
lines.append(' '.join(current))
|
| 317 |
+
current = [word]
|
| 318 |
+
if current:
|
| 319 |
+
lines.append(' '.join(current))
|
| 320 |
+
|
| 321 |
+
# Draw
|
| 322 |
+
y = 1700
|
| 323 |
+
for line in lines:
|
| 324 |
+
bbox = draw.textbbox((0, 0), line, font=font)
|
| 325 |
+
x = (1080 - (bbox[2] - bbox[0])) // 2
|
| 326 |
+
|
| 327 |
+
# Outline
|
| 328 |
+
for dx in [-3, 0, 3]:
|
| 329 |
+
for dy in [-3, 0, 3]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
draw.text((x+dx, y+dy), line, font=font, fill='black')
|
| 331 |
+
|
| 332 |
+
draw.text((x, y), line, font=font, fill='white')
|
| 333 |
+
y += 65
|
| 334 |
|
| 335 |
+
return cv2.cvtColor(np.array(pil), cv2.COLOR_RGB2BGR)
|
| 336 |
+
|
| 337 |
+
except Exception as e:
|
| 338 |
+
print(f"Subtitle error: {e}")
|
| 339 |
+
return frame
|
| 340 |
+
|
| 341 |
+
def render_video(frames, voice, ambient, output):
|
| 342 |
+
"""Final render."""
|
| 343 |
+
try:
|
| 344 |
+
# Write video
|
| 345 |
+
temp = "temp/video.mp4"
|
| 346 |
+
out = cv2.VideoWriter(temp, cv2.VideoWriter_fourcc(*'mp4v'), 30, (1080, 1920))
|
| 347 |
+
for f in frames:
|
| 348 |
+
out.write(f)
|
| 349 |
+
out.release()
|
| 350 |
+
|
| 351 |
+
# Mix audio
|
| 352 |
+
v = AudioSegment.from_mp3(voice)
|
| 353 |
+
a = AudioSegment.from_mp3(ambient)
|
| 354 |
+
mixed = v.overlay(a - 14)
|
| 355 |
+
mixed.export("temp/audio.mp3", format='mp3')
|
| 356 |
+
|
| 357 |
+
# Combine
|
| 358 |
+
cmd = f'ffmpeg -y -i {temp} -i temp/audio.mp3 -c:v libx264 -preset fast -crf 22 -c:a aac -b:a 160k -shortest {output} -loglevel error'
|
| 359 |
+
result = os.system(cmd)
|
| 360 |
+
|
| 361 |
+
if result != 0:
|
| 362 |
+
raise Exception("FFmpeg failed")
|
| 363 |
+
|
| 364 |
+
return output
|
| 365 |
+
|
| 366 |
+
except Exception as e:
|
| 367 |
+
print(f"Render error: {e}")
|
| 368 |
+
raise
|
| 369 |
|
| 370 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 371 |
+
# MAIN GENERATION
|
| 372 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 373 |
|
| 374 |
+
def generate_short(progress=gr.Progress()):
|
| 375 |
+
"""Generate 50s horror short with 6 images."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
+
try:
|
| 378 |
+
setup_dirs()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
+
# Select story
|
| 381 |
+
progress(0.02, desc="π Selecting story...")
|
| 382 |
+
story = random.choice(STORIES)
|
| 383 |
|
| 384 |
+
# Voice
|
| 385 |
+
progress(0.05, desc="ποΈ Creating voiceover...")
|
| 386 |
+
voice_path, duration = create_voice(story['script'])
|
| 387 |
|
| 388 |
+
# Ambient
|
| 389 |
+
progress(0.08, desc="π΅ Creating ambient sound...")
|
| 390 |
+
ambient_path = create_ambient(duration)
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
+
# Load model
|
| 393 |
+
progress(0.1, desc="πΌοΈ Loading AI model...")
|
| 394 |
+
get_model()
|
| 395 |
|
| 396 |
+
# Generate 6 images
|
| 397 |
+
all_frames = []
|
| 398 |
+
movements = ['zoom', 'pan', 'zoom', 'pan', 'zoom', 'pan']
|
| 399 |
+
sec_per_img = duration / 6
|
| 400 |
+
|
| 401 |
+
for i in range(6):
|
| 402 |
+
progress(0.1 + i*0.12, desc=f"πΌοΈ Generating image {i+1}/6...")
|
| 403 |
+
|
| 404 |
+
img = generate_image(story['visuals'][i])
|
| 405 |
+
|
| 406 |
+
progress(0.1 + i*0.12 + 0.04, desc=f"ποΈ Animating {i+1}/6...")
|
| 407 |
+
|
| 408 |
+
frames = animate_image(img, sec_per_img, movements[i])
|
| 409 |
+
|
| 410 |
+
progress(0.1 + i*0.12 + 0.08, desc=f"π Upscaling {i+1}/6...")
|
| 411 |
+
|
| 412 |
+
frames = [upscale(f) for f in frames[::2]] # Skip every other frame for speed
|
| 413 |
+
all_frames.extend(frames)
|
| 414 |
+
|
| 415 |
+
# Clear memory
|
| 416 |
+
del img, frames
|
| 417 |
+
gc.collect()
|
| 418 |
+
|
| 419 |
+
# Subtitles
|
| 420 |
+
progress(0.85, desc="π Adding subtitles...")
|
| 421 |
+
|
| 422 |
+
sentences = [s.strip() + '.' for s in story['script'].replace('?', '.').replace('!', '.').split('.') if s.strip()]
|
| 423 |
+
frames_per_sub = len(all_frames) // len(sentences)
|
| 424 |
+
|
| 425 |
+
final_frames = []
|
| 426 |
+
for i, frame in enumerate(all_frames):
|
| 427 |
+
sub_idx = min(i // frames_per_sub, len(sentences) - 1)
|
| 428 |
+
final_frames.append(add_subtitle(frame, sentences[sub_idx]))
|
| 429 |
+
|
| 430 |
+
# Render
|
| 431 |
+
progress(0.95, desc="π¬ Rendering final video...")
|
| 432 |
+
output = render_video(final_frames, voice_path, ambient_path, "output/horror_short.mp4")
|
| 433 |
+
|
| 434 |
+
progress(1.0, desc="β
Complete!")
|
| 435 |
+
|
| 436 |
+
info = f"**{story['title']}**\n\nDuration: {duration:.1f}s | Frames: {len(final_frames)} | Images: 6"
|
| 437 |
+
|
| 438 |
+
return output, story['script'], info
|
| 439 |
+
|
| 440 |
+
except Exception as e:
|
| 441 |
+
error_msg = f"Error: {str(e)}\n\nTry again or restart the space."
|
| 442 |
+
return None, error_msg, error_msg
|
| 443 |
|
| 444 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 445 |
+
# INTERFACE
|
| 446 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 447 |
|
| 448 |
+
with gr.Blocks(theme=gr.themes.Soft(primary_hue="red")) as demo:
|
| 449 |
gr.Markdown("""
|
| 450 |
+
# π¬ Horror Shorts Generator
|
| 451 |
+
## Reliable 50-Second Looping Stories
|
| 452 |
|
| 453 |
+
**β±οΈ Time:** 15-20 min CPU | 8-10 min GPU
|
| 454 |
+
**πΊ Output:** 1080x1920 | 6 images | Looping narrative
|
|
|
|
| 455 |
""")
|
| 456 |
|
| 457 |
with gr.Row():
|
| 458 |
with gr.Column(scale=1):
|
| 459 |
+
btn = gr.Button("π¬ Generate Horror Short", variant="primary", size="lg")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
|
| 461 |
gr.Markdown("""
|
| 462 |
+
### What You Get:
|
| 463 |
+
- β
50-second video
|
| 464 |
+
- β
6 AI-generated images
|
| 465 |
+
- β
Perfect loop (endβstart)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
- β
Professional subtitles
|
| 467 |
+
- β
Ambient horror sound
|
| 468 |
+
- β
1080x1920 (Shorts format)
|
| 469 |
|
| 470 |
+
### Stories:
|
| 471 |
+
- The Loop (parallel reality)
|
| 472 |
+
- The Staircase (time paradox)
|
| 473 |
+
- 3:33 AM (surveillance horror)
|
| 474 |
+
- The Elevator (dimensional trap)
|
| 475 |
+
- The Mirror (reflection horror)
|
| 476 |
+
- The Mall (mannequin horror)
|
| 477 |
|
| 478 |
+
### Time Estimate:
|
| 479 |
+
- First run: 18-20 min (model download)
|
| 480 |
+
- After that: 15-17 min
|
| 481 |
+
- With GPU: 8-10 min
|
| 482 |
|
| 483 |
+
*Generation progress shows below*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
""")
|
| 485 |
|
| 486 |
with gr.Column(scale=2):
|
| 487 |
+
video = gr.Video(label="Horror Short", height=700)
|
| 488 |
+
script = gr.Textbox(label="Script", lines=8)
|
| 489 |
+
info = gr.Markdown()
|
| 490 |
|
| 491 |
+
btn.click(fn=generate_short, outputs=[video, script, info])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
gr.Markdown("""
|
| 494 |
---
|
| 495 |
+
π‘ **Tips:** First generation downloads model (~2GB). Be patient - quality takes time!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
|
| 497 |
+
π **Deploy:** Upload to HuggingFace Space with GPU T4 for 2x speed boost
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
""")
|
| 499 |
|
| 500 |
+
demo.launch()
|
|
|
|
| 501 |
|
| 502 |
"""
|
| 503 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 509 |
diffusers
|
| 510 |
transformers
|
| 511 |
accelerate
|
| 512 |
+
xformers
|
| 513 |
gtts
|
| 514 |
pydub
|
| 515 |
opencv-python-headless
|
| 516 |
pillow
|
| 517 |
numpy
|
| 518 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 520 |
"""
|