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
        )