File size: 10,411 Bytes
7219c67 383026e 7219c67 383026e 7219c67 383026e 7219c67 383026e 7219c67 383026e 7219c67 383026e 7219c67 383026e 7219c67 383026e 7219c67 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 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 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | """
Background Handler for Text Story module.
Handles gameplay video loading from HuggingFace Dataset storage.
"""
import os
import random
import logging
from moviepy.editor import VideoFileClip, vfx
from typing import Optional, List
from huggingface_hub import hf_hub_download, list_repo_files
logger = logging.getLogger(__name__)
# Folder name in HF Dataset for gameplay backgrounds
HF_BACKGROUNDS_FOLDER = "gameplay_backgrounds"
# Local cache path
LOCAL_CACHE_DIR = "cache/gameplay_backgrounds"
class BackgroundHandler:
"""
Handles gameplay background video processing.
Downloads from HuggingFace Dataset (HF_REPO env variable).
Features:
- Download videos from HF Dataset
- Random video selection
- Audio removal
- Slow motion (0.7x)
- Dark overlay
- Seamless looping
"""
def __init__(self):
# Get repo from environment variable (e.g., robiul487/NCAkit)
self.repo_id = os.getenv("HF_REPO", "")
self.folder = HF_BACKGROUNDS_FOLDER
self.cache_dir = LOCAL_CACHE_DIR
if not self.repo_id:
logger.warning("BackgroundHandler: HF_REPO not set! Using green screen background.")
self.available_videos = []
return
# Ensure cache directory exists
os.makedirs(self.cache_dir, exist_ok=True)
# Get list of available videos
self.available_videos = self._list_available_videos()
if self.available_videos:
logger.info(f"BackgroundHandler: Found {len(self.available_videos)} videos in {self.repo_id}/{self.folder}")
else:
logger.warning(f"BackgroundHandler: No videos found in {self.repo_id}/{self.folder}")
# Try to create the folder in HF Dataset
self._ensure_folder_exists()
def _list_available_videos(self) -> List[str]:
"""List available video files in HF Dataset folder."""
if not self.repo_id:
return []
try:
all_files = list_repo_files(
repo_id=self.repo_id,
repo_type="dataset"
)
# Filter for .mp4 videos in gameplay_backgrounds folder only
videos = [
f for f in all_files
if f.startswith(f"{self.folder}/")
and f.lower().endswith('.mp4')
]
return videos
except Exception as e:
logger.error(f"BackgroundHandler: Failed to list files - {e}")
return []
def _ensure_folder_exists(self):
"""Create gameplay_backgrounds folder in HF Dataset with a placeholder file."""
try:
from huggingface_hub import HfApi
import tempfile
hf_token = os.getenv("HF_TOKEN", "")
if not hf_token:
logger.warning("BackgroundHandler: HF_TOKEN not set, cannot create folder")
return
api = HfApi(token=hf_token)
# Create a README placeholder file to create the folder
placeholder_content = """# Gameplay Backgrounds
Place your background videos here (.mp4 only).
These videos will be:
- Randomly selected for text story backgrounds
- Slowed down (0.7x)
- Darkened for better text visibility
- Center-cropped if 16:9
Recommended: 9:16 vertical videos (TikTok/Reels format)
"""
# Upload placeholder to create the folder
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
f.write(placeholder_content)
temp_path = f.name
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=f"{self.folder}/README.md",
repo_id=self.repo_id,
repo_type="dataset"
)
# Cleanup temp file
os.remove(temp_path)
logger.info(f"BackgroundHandler: Created folder {self.folder}/ in HF Dataset")
logger.info(f"BackgroundHandler: Upload .mp4 videos to {self.repo_id}/{self.folder}/")
except Exception as e:
logger.error(f"BackgroundHandler: Failed to create folder - {e}")
def _download_video(self, filename: str) -> Optional[str]:
"""Download a video from HF Dataset to local cache."""
try:
# Check if already cached
local_name = os.path.basename(filename)
cached_path = os.path.join(self.cache_dir, local_name)
if os.path.exists(cached_path):
logger.info(f"BackgroundHandler: Using cached {local_name}")
return cached_path
# Download from HF
logger.info(f"BackgroundHandler: Downloading {filename}...")
downloaded_path = hf_hub_download(
repo_id=self.repo_id,
filename=filename,
repo_type="dataset",
local_dir=self.cache_dir,
local_dir_use_symlinks=False
)
logger.info(f"BackgroundHandler: Downloaded to {downloaded_path}")
return downloaded_path
except Exception as e:
logger.error(f"BackgroundHandler: Download failed - {e}")
return None
def get_random_video(self) -> Optional[str]:
"""Get random video from HF Dataset and download it."""
if not self.available_videos:
# Retry listing
self.available_videos = self._list_available_videos()
if not self.available_videos:
logger.warning("BackgroundHandler: No videos available")
return None
selected = random.choice(self.available_videos)
logger.info(f"BackgroundHandler: Selected {selected}")
return self._download_video(selected)
def load_and_process(self,
target_duration: float,
video_path: str = None) -> Optional[VideoFileClip]:
"""
Load and process a background video.
Args:
target_duration: Required duration in seconds
video_path: Optional specific video path (or random if None)
Returns:
Processed VideoFileClip or None
"""
# Get video path
if video_path is None:
video_path = self.get_random_video()
if not video_path or not os.path.exists(video_path):
logger.warning("BackgroundHandler: No video available, creating solid background")
return self._create_solid_background(target_duration)
try:
# Load video WITHOUT audio
clip = VideoFileClip(video_path).without_audio()
logger.info(f"BackgroundHandler: Loaded {video_path}, duration: {clip.duration:.1f}s")
# Apply slow motion (0.7x speed)
clip = clip.fx(vfx.speedx, 0.7)
# Loop if needed to match target duration
clip = self._loop_to_duration(clip, target_duration)
# Apply visual effects
clip = self._apply_visual_effects(clip)
return clip
except Exception as e:
logger.error(f"BackgroundHandler: Failed to process video - {e}")
return self._create_solid_background(target_duration)
def _loop_to_duration(self, clip: VideoFileClip, target_duration: float) -> VideoFileClip:
"""Loop video to match target duration."""
if clip.duration >= target_duration:
return clip.subclip(0, target_duration)
loops_needed = int(target_duration / clip.duration) + 1
looped = clip.loop(n=loops_needed)
return looped.subclip(0, target_duration)
def _apply_visual_effects(self, clip: VideoFileClip) -> VideoFileClip:
"""
Apply resize, crop (if needed), dark overlay, and saturation reduction.
- 9:16 videos: just resize (no crop needed)
- 16:9 videos: center crop to 9:16
"""
target_w, target_h = 1080, 1920
target_ratio = target_w / target_h # 0.5625 (9:16)
# Calculate source aspect ratio
clip_ratio = clip.w / clip.h
# Check if already 9:16 (or close to it)
# 9:16 ratio is ~0.5625, allow some tolerance
is_vertical = clip_ratio < 0.7 # Less than ~11:16 is considered vertical
if is_vertical:
# Already vertical (9:16)
if clip.w == target_w and clip.h == target_h:
# Perfect match, no resize needed
logger.info(f"BackgroundHandler: Video is already {target_w}x{target_h}, no resize")
else:
# Resize to target resolution
logger.info(f"BackgroundHandler: Video is vertical ({clip.w}x{clip.h}), resizing to {target_w}x{target_h}")
clip = clip.resize(newsize=(target_w, target_h))
else:
# Horizontal (16:9), need to crop
logger.info(f"BackgroundHandler: Video is horizontal ({clip.w}x{clip.h}), center cropping to 9:16")
# Scale to match height, then center crop width
new_h = target_h
new_w = int(clip_ratio * new_h)
clip = clip.resize(height=new_h)
# Center crop
x_center = new_w // 2
clip = clip.crop(x_center=x_center, width=target_w, height=target_h)
# Dark overlay (reduce brightness by 40%)
clip = clip.fx(vfx.colorx, 0.6)
# Saturation reduction
clip = clip.fx(vfx.lum_contrast, lum=-10, contrast=-0.1)
return clip
def _create_solid_background(self, duration: float) -> VideoFileClip:
"""Create GREEN SCREEN background for chroma key overlay."""
from moviepy.editor import ColorClip
# Green screen color (0, 255, 0) for easy chroma key removal
return ColorClip(
size=(1080, 1920),
color=(0, 255, 0), # Pure green for chroma key
duration=duration
)
|