ash12321 commited on
Commit
ecf25dd
Β·
verified Β·
1 Parent(s): 33ca129

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +464 -0
app.py ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 🎬 VIRAL HORROR SHORTS GENERATOR - GRADIO APP
3
+ Deploy this on Hugging Face Spaces for a one-click horror video generator!
4
+
5
+ SETUP INSTRUCTIONS FOR HUGGING FACE SPACES:
6
+ 1. Create new Space at https://huggingface.co/new-space
7
+ 2. Choose: Gradio SDK
8
+ 3. Choose: CPU Basic (free tier works!)
9
+ 4. Upload this file as app.py
10
+ 5. Create requirements.txt with dependencies (see bottom of file)
11
+ 6. Your app will be live at: https://huggingface.co/spaces/YOUR_USERNAME/horror-shorts
12
+
13
+ Click "Generate Horror Short" and wait ~2-3 minutes for your video!
14
+ """
15
+
16
+ import gradio as gr
17
+ import torch
18
+ import random
19
+ import numpy as np
20
+ import cv2
21
+ from PIL import Image, ImageDraw, ImageFont
22
+ import os
23
+ import shutil
24
+ from pathlib import Path
25
+
26
+ # Core libraries
27
+ from diffusers import StableDiffusionPipeline
28
+ from gtts import gTTS
29
+ from pydub import AudioSegment
30
+ from pydub.generators import Sine, WhiteNoise
31
+ from pydub.effects import low_pass_filter
32
+
33
+ # ═══════════════════════════════════════════════════════════════════
34
+ # CONFIGURATION
35
+ # ═══════════════════════════════════════════════════════════════════
36
+
37
+ # Horror story templates (optimized for viral engagement)
38
+ HORROR_SCRIPTS = [
39
+ {
40
+ "hook": "I found a door in my apartment that wasn't there yesterday.",
41
+ "build": "Behind it was my living room. But I was already standing in my living room.",
42
+ "twist": "I heard my own voice from inside, asking who's at the door."
43
+ },
44
+ {
45
+ "hook": "The security camera records one extra person leaving than entering.",
46
+ "build": "Every single day. I checked the footage.",
47
+ "twist": "The extra person always leaves at 3:33 AM. It's me."
48
+ },
49
+ {
50
+ "hook": "My phone's camera turned on at 3 AM.",
51
+ "build": "In the photo, someone was standing behind me in the dark.",
52
+ "twist": "I live alone. I checked my apartment. I'm still alone."
53
+ },
54
+ {
55
+ "hook": "I work night shifts at an empty mall.",
56
+ "build": "Last night, all the mannequins were facing the camera room.",
57
+ "twist": "This morning, they're facing the exit. I'm the only one here."
58
+ },
59
+ {
60
+ "hook": "There's a staircase in the woods that leads nowhere.",
61
+ "build": "Every night at midnight, I hear footsteps going up.",
62
+ "twist": "Tonight, I heard them coming back down."
63
+ },
64
+ {
65
+ "hook": "I found VHS tapes in my attic labeled with tomorrow's date.",
66
+ "build": "I played one. It was security footage of my house.",
67
+ "twist": "In the video, I'm not alone."
68
+ },
69
+ {
70
+ "hook": "The elevator has a button for floor 13. We only have 12 floors.",
71
+ "build": "Curiosity got the better of me. I pressed it.",
72
+ "twist": "The doors opened to floor 1. But everyone was gone."
73
+ },
74
+ {
75
+ "hook": "Every mirror here shows the room 30 seconds ago.",
76
+ "build": "I stood still and watched myself in the mirror.",
77
+ "twist": "After 30 seconds, my reflection didn't move with me."
78
+ },
79
+ ]
80
+
81
+ # Visual prompts for liminal/horror aesthetic
82
+ VISUAL_PROMPTS = [
83
+ "empty fluorescent lit hallway, liminal space, eerie, backrooms aesthetic, unsettling, film grain",
84
+ "abandoned mall at night, security camera POV, grainy, dark, ominous shadows",
85
+ "empty parking garage, concrete, flickering lights, analog horror, CCTV footage",
86
+ "dimly lit stairwell, institutional walls, exit sign glowing, uncomfortable perspective",
87
+ "long hotel corridor, identical doors, carpet pattern, yellow lighting, liminal space",
88
+ "empty subway platform at 3am, fluorescent lights, abandoned, eerie",
89
+ "abandoned office space, cubicles in darkness, emergency lighting, liminal",
90
+ "dark basement, old furniture, single hanging light bulb, grainy photo, creepy",
91
+ ]
92
+
93
+ NEGATIVE_PROMPT = "people, faces, bright, colorful, cartoon, cheerful, sunlight, text, watermark"
94
+
95
+ # ═══════════════════════════════════════════════════════════════════
96
+ # UTILITY FUNCTIONS
97
+ # ═══════════════════════════════════════════════════════════════════
98
+
99
+ def setup_directories():
100
+ """Create clean working directories."""
101
+ for folder in ['output', 'temp']:
102
+ if os.path.exists(folder):
103
+ shutil.rmtree(folder)
104
+ os.makedirs(folder)
105
+
106
+ def generate_script():
107
+ """Pick random horror script."""
108
+ story = random.choice(HORROR_SCRIPTS)
109
+ return f"{story['hook']} {story['build']} {story['twist']}"
110
+
111
+ def create_voiceover(script, output_path="temp/voice.mp3"):
112
+ """Generate creepy TTS voiceover."""
113
+ # Generate base TTS
114
+ tts = gTTS(text=script, lang='en', slow=True)
115
+ tts.save("temp/voice_raw.mp3")
116
+
117
+ # Load and process for creepy effect
118
+ audio = AudioSegment.from_mp3("temp/voice_raw.mp3")
119
+ audio = audio - 3 # Lower volume slightly
120
+ audio = low_pass_filter(audio, 2800) # Remove highs = more ominous
121
+ audio = audio.fade_in(100).fade_out(100)
122
+
123
+ # Export
124
+ audio.export(output_path, format='mp3')
125
+
126
+ return output_path, len(audio) / 1000.0 # Return path and duration
127
+
128
+ def create_ambient_sound(duration_sec, output_path="temp/ambient.mp3"):
129
+ """Generate creepy ambient drone."""
130
+ duration_ms = int(duration_sec * 1000)
131
+
132
+ # Deep drone bass (unsettling frequency)
133
+ drone = Sine(60).to_audio_segment(duration=duration_ms)
134
+ drone = drone - 20
135
+
136
+ # High frequency tension
137
+ whine = Sine(8000).to_audio_segment(duration=duration_ms)
138
+ whine = whine - 30
139
+
140
+ # Subtle static
141
+ noise = WhiteNoise().to_audio_segment(duration=duration_ms)
142
+ noise = noise - 35
143
+
144
+ # Mix and fade
145
+ ambient = drone.overlay(whine).overlay(noise)
146
+ ambient = ambient.fade_in(2000).fade_out(2000)
147
+ ambient.export(output_path, format='mp3')
148
+
149
+ return output_path
150
+
151
+ def generate_horror_image(prompt, pipe):
152
+ """Generate single liminal horror image."""
153
+ image = pipe(
154
+ prompt=prompt,
155
+ negative_prompt=NEGATIVE_PROMPT,
156
+ height=768,
157
+ width=512,
158
+ num_inference_steps=20, # Faster generation
159
+ guidance_scale=7.5
160
+ ).images[0]
161
+
162
+ return image
163
+
164
+ def create_zoom_animation(image, duration_sec=3.75, fps=30):
165
+ """Create slow zoom effect on image."""
166
+ # Convert PIL to numpy
167
+ img_array = np.array(image)
168
+ img_array = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)
169
+
170
+ height, width = img_array.shape[:2]
171
+ frames = []
172
+ total_frames = int(duration_sec * fps)
173
+
174
+ for i in range(total_frames):
175
+ progress = i / total_frames
176
+ scale = 1.0 + (progress * 0.15) # Zoom to 115%
177
+
178
+ # Resize
179
+ new_w = int(width * scale)
180
+ new_h = int(height * scale)
181
+ zoomed = cv2.resize(img_array, (new_w, new_h))
182
+
183
+ # Center crop
184
+ start_x = (new_w - width) // 2
185
+ start_y = (new_h - height) // 2
186
+ cropped = zoomed[start_y:start_y+height, start_x:start_x+width]
187
+
188
+ frames.append(cropped)
189
+
190
+ return frames
191
+
192
+ def resize_to_shorts(frame):
193
+ """Resize frame to YouTube Shorts format (1080x1920)."""
194
+ target_h, target_w = 1920, 1080
195
+ current_h, current_w = frame.shape[:2]
196
+
197
+ # Scale to cover target
198
+ scale = max(target_w / current_w, target_h / current_h)
199
+ new_w = int(current_w * scale)
200
+ new_h = int(current_h * scale)
201
+
202
+ # Resize and center crop
203
+ resized = cv2.resize(frame, (new_w, new_h))
204
+ start_x = (new_w - target_w) // 2
205
+ start_y = (new_h - target_h) // 2
206
+ cropped = resized[start_y:start_y+target_h, start_x:start_x+target_w]
207
+
208
+ return cropped
209
+
210
+ def add_subtitle_to_frame(frame, text):
211
+ """Burn subtitle directly onto frame using PIL (no ImageMagick needed)."""
212
+ # Convert to PIL
213
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
214
+ pil_img = Image.fromarray(frame_rgb)
215
+ draw = ImageDraw.Draw(pil_img)
216
+
217
+ # Try to load font, fallback to default
218
+ try:
219
+ font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 50)
220
+ except:
221
+ font = ImageFont.load_default()
222
+
223
+ # Get text size
224
+ bbox = draw.textbbox((0, 0), text, font=font)
225
+ text_width = bbox[2] - bbox[0]
226
+ text_height = bbox[3] - bbox[1]
227
+
228
+ # Position (centered, near bottom)
229
+ x = (1080 - text_width) // 2
230
+ y = 1600
231
+
232
+ # Draw text with stroke (outline)
233
+ stroke_width = 3
234
+ # Draw outline
235
+ for adj_x in range(-stroke_width, stroke_width+1):
236
+ for adj_y in range(-stroke_width, stroke_width+1):
237
+ draw.text((x+adj_x, y+adj_y), text, font=font, fill='black')
238
+ # Draw main text
239
+ draw.text((x, y), text, font=font, fill='white')
240
+
241
+ # Convert back to cv2
242
+ frame_with_text = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
243
+ return frame_with_text
244
+
245
+ def create_video_with_subtitles(frames, script, fps=30, output_path="output/final_short.mp4"):
246
+ """Create video with burned-in subtitles."""
247
+ # Split script into chunks
248
+ words = script.split()
249
+ chunks = [' '.join(words[i:i+3]) for i in range(0, len(words), 3)]
250
+
251
+ # Calculate timing
252
+ total_frames = len(frames)
253
+ frames_per_subtitle = total_frames // len(chunks)
254
+
255
+ # Add subtitles to frames
256
+ frames_with_subs = []
257
+ for i, frame in enumerate(frames):
258
+ subtitle_idx = min(i // frames_per_subtitle, len(chunks) - 1)
259
+ frame_with_sub = add_subtitle_to_frame(frame, chunks[subtitle_idx])
260
+ frames_with_subs.append(frame_with_sub)
261
+
262
+ return frames_with_subs
263
+
264
+ def combine_audio_video(video_frames, voice_path, ambient_path, fps=30, output_path="output/final_short.mp4"):
265
+ """Combine video frames with audio using FFmpeg directly."""
266
+ # Save frames as temporary video
267
+ temp_video = "temp/video_no_audio.mp4"
268
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
269
+ out = cv2.VideoWriter(temp_video, fourcc, fps, (1080, 1920))
270
+
271
+ for frame in video_frames:
272
+ out.write(frame)
273
+ out.release()
274
+
275
+ # Load audio files
276
+ voice = AudioSegment.from_mp3(voice_path)
277
+ ambient = AudioSegment.from_mp3(ambient_path)
278
+
279
+ # Mix audio
280
+ mixed = voice.overlay(ambient - 12) # Ambient quieter
281
+ mixed.export("temp/mixed_audio.mp3", format='mp3')
282
+
283
+ # Combine with FFmpeg
284
+ os.system(f'ffmpeg -y -i {temp_video} -i temp/mixed_audio.mp3 -c:v libx264 -c:a aac -shortest {output_path} -loglevel quiet')
285
+
286
+ return output_path
287
+
288
+ # ═══════════════════════════════════════════════════════════════════
289
+ # MAIN GENERATION FUNCTION
290
+ # ═══════════════════════════════════════════════════════════════════
291
+
292
+ def generate_horror_short(progress=gr.Progress()):
293
+ """Main function: Generate complete horror short."""
294
+
295
+ setup_directories()
296
+
297
+ # Step 1: Generate Script
298
+ progress(0.1, desc="πŸ“ Writing creepy script...")
299
+ script = generate_script()
300
+
301
+ # Step 2: Create Voiceover
302
+ progress(0.2, desc="πŸŽ™οΈ Creating eerie voiceover...")
303
+ voice_path, duration = create_voiceover(script)
304
+
305
+ # Step 3: Create Ambient Sound
306
+ progress(0.25, desc="🎡 Generating ambient horror sound...")
307
+ ambient_path = create_ambient_sound(duration)
308
+
309
+ # Step 4: Load Stable Diffusion
310
+ progress(0.3, desc="πŸ–ΌοΈ Loading image generator...")
311
+ pipe = StableDiffusionPipeline.from_pretrained(
312
+ "runwayml/stable-diffusion-v1-5",
313
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
314
+ safety_checker=None
315
+ )
316
+
317
+ if torch.cuda.is_available():
318
+ pipe = pipe.to("cuda")
319
+
320
+ # Step 5: Generate Horror Images
321
+ num_frames = 4
322
+ all_animated_frames = []
323
+
324
+ for i in range(num_frames):
325
+ progress(0.3 + (i * 0.15), desc=f"πŸ–ΌοΈ Generating horror image {i+1}/{num_frames}...")
326
+
327
+ # Generate image
328
+ prompt = random.choice(VISUAL_PROMPTS)
329
+ image = generate_horror_image(prompt, pipe)
330
+
331
+ # Animate with zoom
332
+ progress(0.3 + (i * 0.15) + 0.05, desc=f"🎞️ Animating image {i+1}/{num_frames}...")
333
+ frames = create_zoom_animation(image, duration_sec=duration/num_frames)
334
+
335
+ # Resize to Shorts format
336
+ frames = [resize_to_shorts(f) for f in frames]
337
+ all_animated_frames.extend(frames)
338
+
339
+ # Step 6: Add Subtitles
340
+ progress(0.85, desc="πŸ“„ Burning in subtitles...")
341
+ frames_with_subs = create_video_with_subtitles(all_animated_frames, script)
342
+
343
+ # Step 7: Combine Everything
344
+ progress(0.95, desc="🎬 Rendering final video...")
345
+ output_video = combine_audio_video(frames_with_subs, voice_path, ambient_path)
346
+
347
+ progress(1.0, desc="βœ… Horror short complete!")
348
+
349
+ return output_video, script
350
+
351
+ # ═══════════════════════════════════════════════════════════════════
352
+ # GRADIO INTERFACE
353
+ # ═══════════════════════════════════════════════════════════════════
354
+
355
+ def create_interface():
356
+ """Create Gradio UI."""
357
+
358
+ with gr.Blocks(theme=gr.themes.Base(primary_hue="red", secondary_hue="gray")) as demo:
359
+ gr.Markdown("""
360
+ # 🎬 Viral Horror Shorts Generator
361
+ ### Create Faceless YouTube Shorts in Creepypasta Style
362
+
363
+ **One-click pipeline:** Script β†’ Voice β†’ Images β†’ Animation β†’ Subtitles β†’ Final Video
364
+
365
+ Click the button below and wait ~2-3 minutes for your horror short!
366
+ """)
367
+
368
+ with gr.Row():
369
+ with gr.Column():
370
+ generate_btn = gr.Button("🎬 Generate Horror Short", variant="primary", size="lg")
371
+
372
+ gr.Markdown("""
373
+ ### πŸ“‹ What This Creates:
374
+ - βœ… Creepy creepypasta-style script
375
+ - βœ… Eerie AI voiceover
376
+ - βœ… Liminal space visuals (Stable Diffusion)
377
+ - βœ… Slow zoom animations
378
+ - βœ… Auto subtitles
379
+ - βœ… Ambient horror soundtrack
380
+ - βœ… 1080x1920 YouTube Shorts format
381
+
382
+ ### ⏱️ Generation Time:
383
+ - **With GPU:** ~2-3 minutes
384
+ - **CPU only:** ~8-10 minutes
385
+
386
+ ### 🎨 Style:
387
+ Liminal spaces, backrooms, analog horror, CCTV aesthetic
388
+ """)
389
+
390
+ with gr.Column():
391
+ video_output = gr.Video(label="Your Horror Short", height=600)
392
+ script_output = gr.Textbox(label="Generated Script", lines=4)
393
+
394
+ generate_btn.click(
395
+ fn=generate_horror_short,
396
+ inputs=[],
397
+ outputs=[video_output, script_output]
398
+ )
399
+
400
+ gr.Markdown("""
401
+ ---
402
+ ### πŸ’‘ Tips:
403
+ - Run multiple times for different stories and visuals
404
+ - Download the video and upload to YouTube Shorts
405
+ - Best results with GPU (use Hugging Face Spaces with GPU)
406
+
407
+ ### πŸš€ Deploy Your Own:
408
+ 1. Fork this Space
409
+ 2. Upgrade to GPU for faster generation
410
+ 3. Customize HORROR_SCRIPTS and VISUAL_PROMPTS in the code
411
+ """)
412
+
413
+ return demo
414
+
415
+ # ═══════════════════════════════════════════════════════════════════
416
+ # LAUNCH APP
417
+ # ═══════════════════════════════════════════════════════════════════
418
+
419
+ if __name__ == "__main__":
420
+ demo = create_interface()
421
+ demo.launch()
422
+
423
+ """
424
+ ═══════════════════════════════════════════════════════════════════
425
+ πŸ“¦ REQUIREMENTS.TXT (Create this file in your Hugging Face Space)
426
+ ═══════════════════════════════════════════════════════════════════
427
+
428
+ gradio
429
+ torch
430
+ torchvision
431
+ diffusers
432
+ transformers
433
+ accelerate
434
+ gtts
435
+ pydub
436
+ opencv-python-headless
437
+ pillow
438
+ numpy
439
+
440
+ ═══════════════════════════════════════════════════════════════════
441
+ πŸš€ DEPLOYMENT INSTRUCTIONS
442
+ ═══════════════════════════════════════════════════════════════════
443
+
444
+ 1. Go to https://huggingface.co/new-space
445
+ 2. Name: "horror-shorts-generator"
446
+ 3. License: MIT
447
+ 4. Select SDK: Gradio
448
+ 5. Select Hardware: CPU Basic (free) or GPU (faster)
449
+ 6. Create Space
450
+
451
+ 7. Click "Files" β†’ "Add file" β†’ "Create new file"
452
+ 8. Name it: app.py
453
+ 9. Paste this entire code
454
+ 10. Save
455
+
456
+ 11. Create another file: requirements.txt
457
+ 12. Paste the dependencies listed above
458
+ 13. Save
459
+
460
+ 14. Your app will build automatically (2-3 minutes)
461
+ 15. Once ready, click "Generate Horror Short"!
462
+
463
+ ═══════════════════════════════════════════════════════════════════
464
+ """