ash12321 commited on
Commit
809c6de
Β·
verified Β·
1 Parent(s): 7330610

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +401 -754
app.py CHANGED
@@ -1,14 +1,13 @@
1
  """
2
- 🎬 PREMIUM HORROR SHORTS GENERATOR
3
- High Quality 50-Second Looped Stories with 6 Cinematic Images
4
-
5
- FEATURES:
6
- - 50-second format (optimal for Shorts algorithm)
7
- - 6 unique AI-generated images per video
8
- - Stories that loop perfectly (end connects to beginning)
9
- - Cinematic camera movements
10
- - Professional quality (takes 15-20 min - worth it!)
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, ImageFilter
20
  import os
21
  import shutil
 
22
 
23
- from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
24
  from gtts import gTTS
25
  from pydub import AudioSegment
26
  from pydub.generators import Sine, WhiteNoise
27
 
 
 
 
 
28
  # ═══════════════════════════════════════════════════════════════════
29
- # LOOPING HORROR STORIES - 50 SECONDS
30
  # ═══════════════════════════════════════════════════════════════════
31
 
32
- LOOPING_STORIES = [
33
  {
34
- "title": "The Staircase Loop",
35
- "script": """There's a staircase in the woods behind my house. It just stands there, leading nowhere.
36
- My grandfather warned me never to climb it. He said he climbed it once, in 1952.
37
- When he reached the top, he found himself at the bottom, but everything was slightly wrong.
38
- The trees were taller. The sky was darker. Last week, he disappeared.
39
- I found a note in his handwriting: 'I'm going back up. Don't follow me.'
40
- Today, I'm standing at the bottom of the staircase. I can see someone at the top.
41
- It's my grandfather. He's young. He's waving me up. Behind me, I hear my own voice.
42
- It's warning someone. 'Never climb it,' I hear myself say.""",
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 Mirror Delay",
54
- "script": """Every mirror in my apartment has a two-second delay. I wave, and two seconds later, my reflection waves back.
55
- I thought it was just a quirk of the old building. Then I started testing it. I timed it. Exactly two seconds.
56
- One morning, I was brushing my teeth. My reflection smiled at me first. I wasn't smiling.
57
- It whispered something. The sound came from behind me. I turned around. Nothing.
58
- When I looked back at the mirror, I saw myself standing behind me. Watching me.
59
- I ran out of the bathroom. In the hallway mirror, I saw it again. Still watching.
60
- Every mirror in every room. All showing me something two seconds before it happens.
61
- I'm watching the mirror now. In two seconds, I'll see what I'm about to do.""",
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": "The Security Footage",
73
- "script": """I manage security for a hotel. Twelve cameras. Thirty floors. Nothing ever happens.
74
- Until last Tuesday. Camera Nine went offline at 3:47 AM. I went to check it.
75
- The hallway was empty. The camera was fine. But when I checked the footage, I saw myself.
76
- Walking down the hall. Checking the camera. Exactly as I just did.
77
- But the timestamp said 3:47 AM. I checked my watch. It was 3:52 AM. I watched myself on the screen.
78
- I watched myself walk back to the security office. I watched myself sit down.
79
- I watched myself look at Camera Nine. On my screen, I'm watching Camera Nine.
80
- On Camera Nine, someone is standing in the hallway. Watching the camera. It's me.""",
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 Button",
92
- "script": """My office building has twelve floors. The elevator has a button for thirteen.
93
- Everyone ignores it. I asked the building manager about it. She said the building used to have thirteen floors.
94
- They sealed it off in 1978. 'Why?' I asked. She wouldn't say.
95
- Last Friday, I worked late. I was alone in the elevator. I pressed thirteen.
96
- The elevator went up. And up. And up. The doors opened. Floor one.
97
- But it was wrong. The office was empty. All the desks were covered in dust.
98
- The calendars on the wall said 1978. I pressed the elevator button. Nothing happened.
99
- I took the stairs. All twelve floors. Empty. At the bottom, the lobby was abandoned.
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 Apartment Door",
112
- "script": """I've lived in apartment 4B for three years. Last night, I noticed a new door.
113
- It's between my bedroom and bathroom. Just appeared. Old wood. Brass handle. No lock.
114
- I opened it. It's my apartment. Same furniture. Same photos on the walls. Same stain on the carpet.
115
- But the windows show a different view. A city I don't recognize. On my couch, someone is sleeping.
116
- I stepped closer. They're wearing my clothes. They have my face. I backed out quietly.
117
- I closed the door. When I checked again an hour later, the door was gone.
118
- This morning, I woke up on my couch. I don't remember falling asleep there.
119
- Through my window, I see a city I don't recognize. I hear a door opening. Behind me.""",
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 Night Shift",
131
- "script": """I work night security at an abandoned shopping mall. My shift is midnight to six AM.
132
- Nobody comes here. Nothing ever happens. Except for the mannequins.
133
- Every night at 3:33 AM, they're all facing a different direction. Always the same direction.
134
- I started tracking it. Monday: facing the food court. Tuesday: facing the south exit.
135
- Wednesday: facing the maintenance halls. Thursday: facing the security office.
136
- Tonight is Friday. I'm in the security office. It's 3:32 AM. I'm watching the cameras.
137
- Every mannequin in every store is facing my direction. They're all looking at the security office.
138
- It's 3:33 AM. On the camera, I see them start to move. They're walking. Toward me.
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 setup_directories():
156
  for folder in ['output', 'temp']:
157
  if os.path.exists(folder):
158
  shutil.rmtree(folder)
159
  os.makedirs(folder)
160
 
161
- def create_voiceover(script, output_path="temp/voice.mp3"):
162
- """Create dramatic voiceover with horror processing."""
163
- # Generate TTS with dramatic pacing
164
- tts = gTTS(text=script, lang='en', slow=True, lang_check=False)
165
- tts.save("temp/voice_raw.mp3")
166
-
167
- # Load and process
168
- audio = AudioSegment.from_mp3("temp/voice_raw.mp3")
169
-
170
- # Ensure it's close to 50 seconds by adjusting speed if needed
171
- current_duration = len(audio) / 1000.0
172
- if current_duration > 52:
173
- # Speed up slightly
174
- audio = audio.speedup(playback_speed=current_duration/50.0)
175
- elif current_duration < 48:
176
- # Slow down slightly
177
- audio = audio._spawn(audio.raw_data, overrides={
178
- "frame_rate": int(audio.frame_rate * 0.96)
179
- })
180
- audio = audio.set_frame_rate(44100)
181
-
182
- # Horror audio processing
183
- audio = audio - 1 # Slight volume reduction
184
-
185
- # Add reverb (echo effect)
186
- delayed = audio - 20
187
- audio = audio.overlay(delayed, position=80)
188
-
189
- # Fade in/out
190
- audio = audio.fade_in(300).fade_out(500)
191
-
192
- audio.export(output_path, format='mp3')
193
- return output_path, len(audio) / 1000.0
194
-
195
- def create_layered_ambient(duration_sec, output_path="temp/ambient.mp3"):
196
- """Create rich layered ambient horror sound."""
197
- duration_ms = int(duration_sec * 1000)
198
-
199
- # Multiple drone layers for depth
200
- drone_low = Sine(45).to_audio_segment(duration=duration_ms) - 22
201
- drone_mid = Sine(90).to_audio_segment(duration=duration_ms) - 24
202
- drone_high = Sine(180).to_audio_segment(duration=duration_ms) - 26
203
-
204
- # Tension frequencies
205
- tension1 = Sine(6000).to_audio_segment(duration=duration_ms) - 32
206
- tension2 = Sine(9000).to_audio_segment(duration=duration_ms) - 34
207
-
208
- # Subtle static/noise
209
- noise = WhiteNoise().to_audio_segment(duration=duration_ms) - 38
210
-
211
- # Mix all layers
212
- ambient = drone_low.overlay(drone_mid).overlay(drone_high)
213
- ambient = ambient.overlay(tension1).overlay(tension2).overlay(noise)
214
-
215
- # Long fades for atmosphere
216
- ambient = ambient.fade_in(4000).fade_out(4000)
217
- ambient.export(output_path, format='mp3')
218
-
219
- return output_path
220
-
221
- def enhance_for_horror(image):
222
- """Apply cinematic horror post-processing."""
223
- # Desaturate significantly
224
- enhancer = ImageEnhance.Color(image)
225
- image = enhancer.enhance(0.4)
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
- # Use DPM++ for better quality
277
- pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
278
 
279
  if torch.cuda.is_available():
280
- pipe = pipe.to("cuda")
 
281
  else:
282
- pipe.enable_attention_slicing()
283
 
284
- _model_cache = pipe
285
  print("Model ready!")
286
 
287
- return _model_cache
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 create_cinematic_movement(image, duration_sec, fps=30, movement_type='zoom'):
306
- """Create smooth cinematic camera movements."""
307
- img_array = np.array(image)
308
- img_array = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)
309
-
310
- height, width = img_array.shape[:2]
311
- frames = []
312
- total_frames = int(duration_sec * fps)
313
-
314
- # Pre-scale for movement
315
- scale_factor = 1.3
316
- scaled_w = int(width * scale_factor)
317
- scaled_h = int(height * scale_factor)
318
- scaled = cv2.resize(img_array, (scaled_w, scaled_h), interpolation=cv2.INTER_LANCZOS4)
319
-
320
- for i in range(total_frames):
321
- progress = i / total_frames
322
-
323
- # Ease-in-out curve for smooth movement
324
- ease = progress * progress * (3.0 - 2.0 * progress)
325
-
326
- if movement_type == 'zoom_in':
327
- # Slow zoom in
328
- current_scale = 1.0 + (ease * 0.25)
329
- temp_w = int(width * current_scale)
330
- temp_h = int(height * current_scale)
331
- zoomed = cv2.resize(img_array, (temp_w, temp_h), interpolation=cv2.INTER_LANCZOS4)
332
-
333
- start_x = (temp_w - width) // 2
334
- start_y = (temp_h - height) // 2
335
- frame = zoomed[start_y:start_y+height, start_x:start_x+width]
336
-
337
- elif movement_type == 'pan_right':
338
- x = int((scaled_w - width) * ease)
339
- frame = scaled[0:height, x:x+width]
340
-
341
- elif movement_type == 'pan_left':
342
- x = int((scaled_w - width) * (1 - ease))
343
- frame = scaled[0:height, x:x+width]
344
-
345
- elif movement_type == 'pan_down':
346
- y = int((scaled_h - height) * ease)
347
- frame = scaled[y:y+height, 0:width]
 
348
 
349
- elif movement_type == 'pan_up':
350
- y = int((scaled_h - height) * (1 - ease))
351
- frame = scaled[y:y+height, 0:width]
 
 
 
 
 
 
 
 
352
 
353
- else: # diagonal
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.append(frame)
359
-
360
- return frames
361
-
362
- def upscale_to_shorts(frame):
363
- """Upscale to 1080x1920 with high quality."""
364
- target_w, target_h = 1080, 1920
365
- current_h, current_w = frame.shape[:2]
366
-
367
- scale = max(target_w / current_w, target_h / current_h)
368
- new_w = int(current_w * scale)
369
- new_h = int(current_h * scale)
370
-
371
- upscaled = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
372
-
373
- start_x = (new_w - target_w) // 2
374
- start_y = (new_h - target_h) // 2
375
- cropped = upscaled[start_y:start_y+target_h, start_x:start_x+target_w]
376
-
377
- return cropped
378
-
379
- def add_professional_subtitle(frame, text):
380
- """Add high-quality subtitles with word wrapping."""
381
- frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
382
- pil_img = Image.fromarray(frame_rgb)
383
- draw = ImageDraw.Draw(pil_img)
384
-
385
- # Load best available font
386
- font = None
387
- for font_path in [
388
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
389
- "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
390
- ]:
 
 
 
391
  try:
392
- font = ImageFont.truetype(font_path, 58)
393
- break
394
  except:
395
- continue
396
-
397
- if not font:
398
- font = ImageFont.load_default()
399
-
400
- # Smart word wrapping
401
- max_width = 980
402
- words = text.split()
403
- lines = []
404
- current_line = []
405
-
406
- for word in words:
407
- test_line = ' '.join(current_line + [word])
408
- bbox = draw.textbbox((0, 0), test_line, font=font)
409
- if bbox[2] - bbox[0] <= max_width:
410
- current_line.append(word)
411
- else:
412
- if current_line:
413
- lines.append(' '.join(current_line))
414
- current_line = [word]
415
-
416
- if current_line:
417
- lines.append(' '.join(current_line))
418
-
419
- # Draw with professional outline
420
- y = 1680 - (len(lines) * 35) # Center vertically in lower third
421
-
422
- for line in lines:
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
- # Main white text
435
- draw.text((x, y), line, font=font, fill='white')
436
- y += 70
437
-
438
- return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
439
-
440
- def render_final_video(frames, voice_path, ambient_path, output="output/final_short.mp4"):
441
- """Render with high quality settings."""
442
- temp_video = "temp/video_hq.mp4"
443
- fourcc = cv2.VideoWriter_fourcc(*'mp4v')
444
- out = cv2.VideoWriter(temp_video, fourcc, 30, (1080, 1920))
445
-
446
- for frame in frames:
447
- out.write(frame)
448
- out.release()
449
-
450
- # Mix audio perfectly
451
- voice = AudioSegment.from_mp3(voice_path)
452
- ambient = AudioSegment.from_mp3(ambient_path)
453
-
454
- # Ambient quieter for clarity
455
- mixed = voice.overlay(ambient - 16)
456
- mixed.export("temp/final_audio.mp3", format='mp3')
457
-
458
- # High quality encode
459
- cmd = f'ffmpeg -y -i {temp_video} -i temp/final_audio.mp3 '
460
- cmd += '-c:v libx264 -preset medium -crf 20 ' # High quality
461
- cmd += '-c:a aac -b:a 192k '
462
- cmd += '-pix_fmt yuv420p -movflags +faststart '
463
- cmd += f'-shortest {output} -loglevel quiet'
464
-
465
- os.system(cmd)
466
- return output
 
467
 
468
  # ═══════════════════════════════════════════════════════════════════
469
- # MAIN GENERATION PIPELINE
470
  # ═══════════════════════════════════════════════════════════════════
471
 
472
- def generate_premium_short(progress=gr.Progress()):
473
- """Generate premium 50-second looping horror short with 6 images."""
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
- # Load model
493
- progress(0.1, desc="πŸ–ΌοΈ Loading high-quality AI model...")
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
- # Generate image
506
- image = generate_quality_image(prompts[i], pipe)
 
507
 
508
- progress(0.1 + (i * 0.12) + 0.04, desc=f"🎞️ Animating scene {i+1}/6...")
 
 
509
 
510
- # Animate with varied movement
511
- frames = create_cinematic_movement(
512
- image,
513
- duration_sec=seconds_per_image,
514
- movement_type=movement_types[i]
515
- )
516
 
517
- # Upscale all frames
518
- progress(0.1 + (i * 0.12) + 0.08, desc=f"πŸ“ Upscaling scene {i+1}/6...")
519
- frames = [upscale_to_shorts(f) for f in frames]
520
 
521
- all_frames.extend(frames)
522
-
523
- # Add subtitles
524
- progress(0.85, desc="πŸ“„ Adding professional subtitles...")
525
-
526
- # Split script into segments
527
- import re
528
- sentences = [s.strip() + '.' for s in re.split(r'[.!?]+', script) if s.strip()]
529
-
530
- frames_per_subtitle = len(all_frames) // len(sentences)
531
- final_frames = []
532
-
533
- for i, frame in enumerate(all_frames):
534
- subtitle_idx = min(i // frames_per_subtitle, len(sentences) - 1)
535
- frame_with_sub = add_professional_subtitle(frame, sentences[subtitle_idx])
536
- final_frames.append(frame_with_sub)
537
-
538
- # Final render
539
- progress(0.95, desc="🎬 Rendering final high-quality video...")
540
- output = render_final_video(final_frames, voice_path, ambient_path)
541
-
542
- progress(1.0, desc="βœ… Premium horror short complete!")
543
-
544
- info = f"""
545
- **Title:** {title}
546
- **Duration:** {duration:.1f} seconds
547
- **Frames:** {len(final_frames)}
548
- **Images Generated:** 6
549
- **Quality:** Premium (CRF 20)
550
- **Story Type:** Looping narrative
551
- """
552
-
553
- return output, script, info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
 
555
  # ═══════════════════════════════════════════════════════════════════
556
- # GRADIO INTERFACE
557
  # ═══════════════════════════════════════════════════════════════════
558
 
559
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="red", secondary_hue="slate")) as demo:
560
  gr.Markdown("""
561
- # 🎬 Premium Horror Shorts Generator
562
- ## 50-Second Looping Stories with Cinematic Quality
563
 
564
- **⏱️ Generation Time:** 15-20 minutes (premium quality takes time!)
565
- **🎨 Features:** 6 unique AI images, looping narratives, cinematic movements
566
- **πŸ“Ί Format:** 1080x1920 (YouTube Shorts optimized)
567
  """)
568
 
569
  with gr.Row():
570
  with gr.Column(scale=1):
571
- generate_btn = gr.Button(
572
- "🎬 Generate Premium Horror Short",
573
- variant="primary",
574
- size="lg"
575
- )
576
 
577
  gr.Markdown("""
578
- ### πŸ“‹ What You Get:
579
-
580
- **Story Quality:**
581
- - βœ… 50-second narrative (optimal length)
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
- - βœ… Smooth 30fps
 
603
 
604
- ### ⏱️ Timeline:
605
- - Model load: 1-2 min (first time only)
606
- - 6 images: ~10-12 min
607
- - Animation: ~3 min
608
- - Rendering: ~2 min
 
 
609
 
610
- **Total: 15-20 minutes**
611
- *(Worth it for premium quality!)*
 
 
612
 
613
- ### 🎭 Story Themes:
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
- video_output = gr.Video(label="Premium Horror Short", height=750)
624
- script_output = gr.Textbox(label="Full Script", lines=10)
625
- info_output = gr.Markdown(label="Video Information")
626
 
627
- generate_btn.click(
628
- fn=generate_premium_short,
629
- inputs=[],
630
- outputs=[video_output, script_output, info_output]
631
- )
632
 
633
  gr.Markdown("""
634
  ---
635
- ### πŸ’‘ Pro Tips:
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
- ### πŸ“Š Optimization Notes:
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
- if __name__ == "__main__":
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
  """