christopi commited on
Commit
3108782
Β·
1 Parent(s): 0d5d682
services/compress/scene_detector.py CHANGED
@@ -1,60 +1,25 @@
1
- """
2
- Scene Detection Module
3
-
4
- This module handles scene detection and splitting for video processing.
5
- It supports multiple detection modes including adaptive, time-based, and forced splitting.
6
- """
7
-
8
- import json
9
  import os
10
- from pathlib import Path
11
- from typing import Dict, List, Optional, Tuple, Any
12
-
13
  from utils.fast_scene_detect import adaptive_scene_detection_check
14
  from utils.split_video_into_scenes import split_video_into_scenes
15
  from utils.video_utils import get_keyframes
16
 
17
-
18
- # ============================================================================
19
- # Constants
20
- # ============================================================================
21
-
22
- # Default configuration values
23
- DEFAULT_CONFIG = {
24
- 'directories': {
25
- 'temp_dir': './videos/temp_scenes',
26
- 'output_dir': './output'
27
- },
28
- 'video_processing': {
29
- 'SHORT_VIDEO_THRESHOLD': 20,
30
- 'target_vmaf': 93.0
31
- },
32
- 'scene_detection': {
33
- 'enable_time_based_fallback': True,
34
- 'time_based_scene_duration': 60
35
- }
36
- }
37
-
38
-
39
- # ============================================================================
40
- # Main Function
41
- # ============================================================================
42
-
43
- def scene_detection(video_metadata: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]:
44
  """
45
  Part 2: Scene detection based on video length using metadata from part 1.
46
 
47
  Args:
48
- video_metadata: Metadata from part1_pre_processing containing:
49
- - 'path': video file path
50
- - 'codec': current video codec
51
- - 'original_codec': original codec (if reencoded)
52
- - 'target_codec': target codec for encoding
53
- - 'duration': video duration in seconds
54
- - 'was_reencoded': boolean
55
- - 'target_vmaf': target VMAF score
56
- - 'target_quality': target quality level
57
- - 'processing_info': processing details
58
 
59
  Returns:
60
  list: List of scene metadata dictionaries, each containing:
@@ -66,14 +31,34 @@ def scene_detection(video_metadata: Dict[str, Any]) -> Optional[List[Dict[str, A
66
  - 'original_video_metadata': complete metadata from part 1
67
  """
68
  # Load configuration
69
- config = _load_configuration()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  # Extract metadata
72
  video_path = video_metadata['path']
73
  duration = video_metadata['duration']
74
  codec = video_metadata['codec']
75
  original_codec = video_metadata.get('original_codec', codec)
76
- target_codec = video_metadata.get('target_codec', 'auto')
 
77
  was_reencoded = video_metadata.get('was_reencoded', False)
78
  target_vmaf = video_metadata.get('target_vmaf', 93.0)
79
  target_quality = video_metadata.get('target_quality', 'Medium')
@@ -81,118 +66,110 @@ def scene_detection(video_metadata: Dict[str, Any]) -> Optional[List[Dict[str, A
81
  print(f"🎬 Processing video: {os.path.basename(video_path)}")
82
  print(f" ⏱️ Duration: {duration:.1f}s, Codec: {codec}, Reencoded: {was_reencoded}")
83
  print(f" 🎯 Target: {target_quality} (VMAF: {target_vmaf})")
84
- print(f" πŸŽ₯ Codec flow: {original_codec} β†’ {codec} β†’ {target_codec}")
85
 
86
  # Get processing configuration
87
  video_processing_config = config.get('video_processing', {})
88
  short_video_threshold = video_processing_config.get('SHORT_VIDEO_THRESHOLD', 20)
 
89
  temp_dir = config.get('directories', {}).get('temp_dir', './videos/temp_scenes')
90
 
91
- # Handle short videos
92
  if duration <= short_video_threshold:
93
  print(f"πŸ“ Video is shorter than {short_video_threshold}s. Treating as a single scene.")
94
- return _create_single_scene_metadata(video_metadata, duration)
95
-
96
- # Get scene detection configuration
 
 
 
 
 
97
  scene_config = config.get('scene_detection', {})
98
  detection_mode = scene_config.get('mode', 'adaptive').lower()
99
 
100
  print(f" πŸ” Scene detection mode: {detection_mode}")
101
 
102
- # Execute appropriate detection method
103
  if detection_mode == 'force_time_based':
104
- return _execute_force_time_based_splitting(
105
- video_path, temp_dir, scene_config, duration, video_metadata
106
- )
 
 
107
  elif detection_mode == 'adaptive':
108
- return _execute_adaptive_scene_detection(
109
- video_path, temp_dir, scene_config, duration, video_metadata, config
 
 
110
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  else:
112
  print(f" ❌ Unknown scene detection mode: {detection_mode}. Falling back to adaptive.")
113
- return _execute_adaptive_scene_detection(
114
- video_path, temp_dir, scene_config, duration, video_metadata, config
 
 
115
  )
116
 
117
-
118
- # ============================================================================
119
- # Scene Detection Methods
120
- # ============================================================================
121
-
122
- def _execute_force_time_based_splitting(
123
- video_path: str,
124
- temp_dir: str,
125
- scene_config: Dict[str, Any],
126
- duration: float,
127
- video_metadata: Dict[str, Any]
128
- ) -> List[Dict[str, Any]]:
129
- """Execute forced time-based scene splitting."""
130
- force_duration = scene_config.get('force_time_based_duration', 10)
131
- print(f" ⏰ Forcing time-based splitting with {force_duration}s segments")
132
- return force_time_based_splitting(
133
- video_path, temp_dir, force_duration, duration, video_metadata
134
- )
135
-
136
-
137
- def _execute_adaptive_scene_detection(
138
- video_path: str,
139
- temp_dir: str,
140
- scene_config: Dict[str, Any],
141
- duration: float,
142
- video_metadata: Dict[str, Any],
143
- config: Dict[str, Any]
144
- ) -> List[Dict[str, Any]]:
145
- """Execute adaptive scene detection with fallback options."""
146
- print(f" πŸ” Running adaptive scene detection...")
147
-
148
- has_multiple_scenes, estimated_scenes, _ = adaptive_scene_detection_check(
149
- video_path, duration, logging_enabled=True
150
- )
151
-
152
- if has_multiple_scenes:
153
- print(f" βœ… Multiple scenes detected ({estimated_scenes} estimated). Proceeding with full scene splitting.")
154
- scene_files, _ = split_video_into_scenes(video_path, temp_dir)
155
-
156
- if scene_files and len(scene_files) > 1:
157
- print(f" πŸ“„ Created {len(scene_files)} scene files")
158
- return create_scene_metadata_from_files(scene_files, video_metadata, config)
159
- else:
160
- print(f" ⚠️ Scene splitting failed or found only 1 scene. Falling back to time-based splitting.")
161
- return time_based_splitting(video_path, temp_dir, config, duration, video_metadata)
162
- else:
163
- print(f" πŸ“ Single scene detected.")
164
- enable_fallback = scene_config.get('enable_time_based_fallback', False)
165
-
166
- if enable_fallback:
167
- print(f" πŸ”„ Time-based fallback enabled. Attempting time-based scene splitting...")
168
- return time_based_splitting(video_path, temp_dir, config, duration, video_metadata)
169
  else:
170
- print(f" βœ… Using single scene (time-based fallback disabled)")
171
- return _create_single_scene_metadata(video_metadata, duration)
172
-
173
-
174
- # ============================================================================
175
- # Time-based Splitting Functions
176
- # ============================================================================
177
-
178
- def force_time_based_splitting(
179
- video_path: str,
180
- temp_dir: str,
181
- segment_duration: float,
182
- total_duration: float,
183
- original_metadata: Dict[str, Any],
184
- logging_enabled: bool = True
185
- ) -> List[Dict[str, Any]]:
186
  """
187
  Force time-based splitting with keyframe-aligned boundaries to prevent stuttering.
188
 
189
  Args:
190
- video_path: Path to the video file
191
- temp_dir: Temporary directory for scene files
192
- segment_duration: Target duration for each segment in seconds
193
- total_duration: Total video duration in seconds
194
- original_metadata: Complete metadata from Part 1
195
- logging_enabled: Enable detailed logging
196
 
197
  Returns:
198
  list: List of scene metadata dictionaries
@@ -204,181 +181,434 @@ def force_time_based_splitting(
204
  print(f" βœ‚οΈ Segment duration: {segment_duration}s")
205
 
206
  # Get keyframes for precise cutting
207
- keyframes = _get_video_keyframes(video_path, logging_enabled)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
  # Calculate scene boundaries, preferring keyframe alignment
210
- scene_boundaries = _calculate_scene_boundaries(
211
- total_duration, segment_duration, keyframes, logging_enabled
212
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
- # Create scene metadata
215
- scenes = []
216
- for i, (start_time, end_time) in enumerate(scene_boundaries):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  scene_number = i + 1
218
- scene_duration = end_time - start_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
  if logging_enabled:
221
- print(f" Scene {scene_number}: {start_time:.1f}s - {end_time:.1f}s (duration: {scene_duration:.1f}s)")
222
 
223
- scenes.append({
224
- 'path': video_path, # Original video path for time-based scenes
225
- 'scene_number': scene_number,
226
- 'start_time': start_time,
227
- 'end_time': end_time,
228
- 'duration': scene_duration,
229
- 'original_video_metadata': original_metadata,
230
- 'splitting_method': 'force_time_based',
231
- 'segment_duration': segment_duration
232
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
  if logging_enabled:
235
- print(f" βœ… Created {len(scenes)} time-based scenes")
236
 
237
- return scenes
238
-
239
 
240
- def time_based_splitting(
241
- video_path: str,
242
- temp_dir: str,
243
- config: Dict[str, Any],
244
- duration: float,
245
- video_metadata: Dict[str, Any]
246
- ) -> List[Dict[str, Any]]:
247
  """
248
- Time-based scene splitting using configuration settings.
249
 
250
  Args:
251
- video_path: Path to the video file
252
- temp_dir: Temporary directory for scene files
253
- config: Configuration dictionary
254
- duration: Video duration in seconds
255
- video_metadata: Complete metadata from Part 1
256
-
 
257
  Returns:
258
  list: List of scene metadata dictionaries
259
  """
260
- scene_config = config.get('scene_detection', {})
261
- time_based_duration = scene_config.get('time_based_scene_duration', 90)
262
-
263
- print(f" ⏰ Time-based splitting with {time_based_duration}s segments")
 
264
 
265
- return force_time_based_splitting(
266
- video_path, temp_dir, time_based_duration, duration, video_metadata
267
- )
268
-
269
-
270
- # ============================================================================
271
- # Helper Functions
272
- # ============================================================================
273
-
274
- def _load_configuration() -> Dict[str, Any]:
275
- """Load configuration from config.json or use defaults."""
276
  try:
277
- with open('services/compress/config.json', 'r') as f:
278
- config = json.load(f)
279
- print("βœ… Configuration loaded successfully")
280
- return config
281
- except FileNotFoundError:
282
- print("⚠️ Config file not found, using default configuration")
283
- return DEFAULT_CONFIG
284
-
285
-
286
- def _create_single_scene_metadata(
287
- video_metadata: Dict[str, Any],
288
- duration: float
289
- ) -> List[Dict[str, Any]]:
290
- """Create metadata for a single scene video."""
291
- return [{
292
- 'path': video_metadata['path'],
293
- 'scene_number': 1,
294
- 'start_time': 0.0,
295
- 'end_time': duration,
296
- 'duration': duration,
297
- 'original_video_metadata': video_metadata
298
- }]
299
-
300
-
301
- def _get_video_keyframes(video_path: str, logging_enabled: bool) -> List[float]:
302
- """Get video keyframes for precise scene cutting."""
303
- try:
304
- keyframes = get_keyframes(video_path)
305
  if logging_enabled:
306
- print(f" πŸ”‘ Found {len(keyframes)} keyframes for alignment")
307
- return keyframes
308
- except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  if logging_enabled:
310
- print(f" ⚠️ Could not get keyframes: {e}")
311
- print(f" πŸ“ Using time-based boundaries (may cause stuttering)")
312
- return []
313
-
314
-
315
- def _calculate_scene_boundaries(
316
- total_duration: float,
317
- segment_duration: float,
318
- keyframes: List[float],
319
- logging_enabled: bool
320
- ) -> List[Tuple[float, float]]:
321
- """Calculate scene boundaries with keyframe alignment when possible."""
322
- boundaries = []
323
- current_time = 0.0
324
-
325
- while current_time < total_duration:
326
- end_time = min(current_time + segment_duration, total_duration)
327
 
328
- # Try to align with nearest keyframe
329
- if keyframes:
330
- nearest_keyframe = _find_nearest_keyframe(current_time, keyframes)
331
- if nearest_keyframe and nearest_keyframe > current_time and nearest_keyframe < end_time:
332
- end_time = nearest_keyframe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  if logging_enabled:
334
- print(f" Aligned scene boundary with keyframe at {end_time:.1f}s")
 
 
 
 
 
335
 
336
- boundaries.append((current_time, end_time))
337
- current_time = end_time
338
-
339
- return boundaries
340
-
341
-
342
- def _find_nearest_keyframe(current_time: float, keyframes: List[float]) -> Optional[float]:
343
- """Find the nearest keyframe after the current time."""
344
- for keyframe in keyframes:
345
- if keyframe > current_time:
346
- return keyframe
347
- return None
348
-
349
-
350
- def create_scene_metadata_from_files(
351
- scene_files: List[str],
352
- video_metadata: Dict[str, Any],
353
- config: Dict[str, Any]
354
- ) -> List[Dict[str, Any]]:
 
 
 
 
 
 
 
 
 
355
  """
356
- Create scene metadata from split scene files.
 
357
 
358
  Args:
359
- scene_files: List of scene file paths
360
- video_metadata: Original video metadata
361
- config: Configuration dictionary
362
-
363
  Returns:
364
  list: List of scene metadata dictionaries
365
  """
366
- scenes = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
368
  for i, scene_file in enumerate(scene_files):
369
  scene_number = i + 1
 
 
 
 
 
 
370
 
371
- # Get scene duration (you might want to implement this)
372
- scene_duration = 0.0 # Placeholder
373
 
374
- scenes.append({
375
  'path': scene_file,
376
  'scene_number': scene_number,
377
- 'start_time': 0.0, # Placeholder
378
- 'end_time': scene_duration,
379
- 'duration': scene_duration,
380
- 'original_video_metadata': video_metadata,
381
- 'splitting_method': 'adaptive_detection'
382
- })
383
-
384
- return scenes
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ import json
3
+ import subprocess
 
4
  from utils.fast_scene_detect import adaptive_scene_detection_check
5
  from utils.split_video_into_scenes import split_video_into_scenes
6
  from utils.video_utils import get_keyframes
7
 
8
+ def scene_detection(video_metadata):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  """
10
  Part 2: Scene detection based on video length using metadata from part 1.
11
 
12
  Args:
13
+ video_metadata (dict): Metadata from part1_pre_processing containing:
14
+ - 'path': video file path
15
+ - 'codec': current video codec
16
+ - 'original_codec': original codec (if reencoded)
17
+ - 'target_codec': target codec for encoding (NEW)
18
+ - 'duration': video duration in seconds
19
+ - 'was_reencoded': boolean
20
+ - 'target_vmaf': target VMAF score
21
+ - 'target_quality': target quality level
22
+ - 'processing_info': processing details
23
 
24
  Returns:
25
  list: List of scene metadata dictionaries, each containing:
 
31
  - 'original_video_metadata': complete metadata from part 1
32
  """
33
  # Load configuration
34
+ try:
35
+ with open('services/compress/config.json', 'r') as f:
36
+ config = json.load(f)
37
+ print("βœ… Configuration loaded successfully")
38
+ except FileNotFoundError:
39
+ print("⚠️ Config file not found, using default configuration")
40
+ config = {
41
+ 'directories': {
42
+ 'temp_dir': './videos/temp_scenes',
43
+ 'output_dir': './output'
44
+ },
45
+ 'video_processing': {
46
+ 'SHORT_VIDEO_THRESHOLD': 20,
47
+ 'target_vmaf': 93.0
48
+ },
49
+ 'scene_detection': {
50
+ 'enable_time_based_fallback': True,
51
+ 'time_based_scene_duration': 60
52
+ }
53
+ }
54
 
55
  # Extract metadata
56
  video_path = video_metadata['path']
57
  duration = video_metadata['duration']
58
  codec = video_metadata['codec']
59
  original_codec = video_metadata.get('original_codec', codec)
60
+ target_codec = video_metadata.get('target_codec', 'auto')
61
+ print("***********target_codec", target_codec) # βœ… NEW: Extract target codec
62
  was_reencoded = video_metadata.get('was_reencoded', False)
63
  target_vmaf = video_metadata.get('target_vmaf', 93.0)
64
  target_quality = video_metadata.get('target_quality', 'Medium')
 
66
  print(f"🎬 Processing video: {os.path.basename(video_path)}")
67
  print(f" ⏱️ Duration: {duration:.1f}s, Codec: {codec}, Reencoded: {was_reencoded}")
68
  print(f" 🎯 Target: {target_quality} (VMAF: {target_vmaf})")
69
+ print(f" πŸŽ₯ Codec flow: {original_codec} β†’ {codec} β†’ {target_codec}") # βœ… NEW: Show codec flow
70
 
71
  # Get processing configuration
72
  video_processing_config = config.get('video_processing', {})
73
  short_video_threshold = video_processing_config.get('SHORT_VIDEO_THRESHOLD', 20)
74
+ # Get directories from config only
75
  temp_dir = config.get('directories', {}).get('temp_dir', './videos/temp_scenes')
76
 
 
77
  if duration <= short_video_threshold:
78
  print(f"πŸ“ Video is shorter than {short_video_threshold}s. Treating as a single scene.")
79
+ return [{
80
+ 'path': video_metadata['path'],
81
+ 'scene_number': 1,
82
+ 'start_time': 0.0,
83
+ 'end_time': duration,
84
+ 'duration': duration,
85
+ 'original_video_metadata': video_metadata # βœ… Pass complete metadata including target_codec
86
+ }]
87
  scene_config = config.get('scene_detection', {})
88
  detection_mode = scene_config.get('mode', 'adaptive').lower()
89
 
90
  print(f" πŸ” Scene detection mode: {detection_mode}")
91
 
 
92
  if detection_mode == 'force_time_based':
93
+ # Force time-based splitting regardless of content
94
+ force_duration = scene_config.get('force_time_based_duration', 10)
95
+ print(f" ⏰ Forcing time-based splitting with {force_duration}s segments")
96
+ return force_time_based_splitting(video_path, temp_dir, force_duration, duration, video_metadata)
97
+
98
  elif detection_mode == 'adaptive':
99
+ # Original adaptive scene detection logic
100
+ print(f" πŸ” Running adaptive scene detection...")
101
+ has_multiple_scenes, estimated_scenes, _ = adaptive_scene_detection_check(
102
+ video_path, duration, logging_enabled=True
103
  )
104
+
105
+ if has_multiple_scenes:
106
+ print(f" βœ… Multiple scenes detected ({estimated_scenes} estimated). Proceeding with full scene splitting.")
107
+ scene_files, _ = split_video_into_scenes(video_path, temp_dir)
108
+
109
+ if scene_files and len(scene_files) > 1:
110
+ print(f" πŸ“„ Created {len(scene_files)} scene files")
111
+ return create_scene_metadata_from_files(scene_files, video_metadata, config)
112
+ else:
113
+ print(f" ⚠️ Scene splitting failed or found only 1 scene. Falling back to time-based splitting.")
114
+ return time_based_splitting(video_path, temp_dir, config, duration, video_metadata)
115
+ else:
116
+ print(f" πŸ“ Single scene detected.")
117
+ enable_fallback = scene_config.get('enable_time_based_fallback', False)
118
+
119
+ if enable_fallback:
120
+ print(f" πŸ”„ Time-based fallback enabled. Attempting time-based scene splitting...")
121
+ return time_based_splitting(video_path, temp_dir, config, duration, video_metadata)
122
+ else:
123
+ print(f" βœ… Using single scene (time-based fallback disabled)")
124
+ return [{
125
+ 'path': video_path,
126
+ 'scene_number': 1,
127
+ 'start_time': 0.0,
128
+ 'end_time': duration,
129
+ 'duration': duration,
130
+ 'original_video_metadata': video_metadata # βœ… Pass complete metadata including target_codec
131
+ }]
132
+
133
  else:
134
  print(f" ❌ Unknown scene detection mode: {detection_mode}. Falling back to adaptive.")
135
+ # Fallback to adaptive mode
136
+ print(f" πŸ” Running adaptive scene detection...")
137
+ has_multiple_scenes, estimated_scenes, _ = adaptive_scene_detection_check(
138
+ video_path, duration, logging_enabled=True
139
  )
140
 
141
+ if has_multiple_scenes:
142
+ print(f" βœ… Multiple scenes detected ({estimated_scenes} estimated). Proceeding with full scene splitting.")
143
+ scene_files, _ = split_video_into_scenes(video_path, temp_dir)
144
+
145
+ if scene_files and len(scene_files) > 1:
146
+ print(f" πŸ“„ Created {len(scene_files)} scene files")
147
+ return create_scene_metadata_from_files(scene_files, video_metadata, config)
148
+ else:
149
+ print(f" ⚠️ Scene splitting failed or found only 1 scene. Falling back to time-based splitting.")
150
+ return time_based_splitting(video_path, temp_dir, config, duration, video_metadata)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  else:
152
+ print(f" πŸ“ Single scene detected. Using single scene.")
153
+ return [{
154
+ 'path': video_path,
155
+ 'scene_number': 1,
156
+ 'start_time': 0.0,
157
+ 'end_time': duration,
158
+ 'duration': duration,
159
+ 'original_video_metadata': video_metadata # βœ… Pass complete metadata including target_codec
160
+ }]
161
+
162
+ def force_time_based_splitting(video_path, temp_dir, segment_duration, total_duration, original_metadata, logging_enabled=True):
 
 
 
 
 
163
  """
164
  Force time-based splitting with keyframe-aligned boundaries to prevent stuttering.
165
 
166
  Args:
167
+ video_path (str): Path to the video file
168
+ temp_dir (str): Temporary directory for scene files
169
+ segment_duration (float): Target duration for each segment in seconds
170
+ total_duration (float): Total video duration in seconds
171
+ original_metadata (dict): Complete metadata from Part 1
172
+ logging_enabled (bool): Enable detailed logging
173
 
174
  Returns:
175
  list: List of scene metadata dictionaries
 
181
  print(f" βœ‚οΈ Segment duration: {segment_duration}s")
182
 
183
  # Get keyframes for precise cutting
184
+ try:
185
+ keyframes = get_keyframes(video_path)
186
+ if logging_enabled:
187
+ print(f" πŸ”‘ Found {len(keyframes)} keyframes for alignment")
188
+ except Exception as e:
189
+ # Try alternative import path
190
+ try:
191
+ keyframes = get_keyframes(video_path)
192
+ if logging_enabled:
193
+ print(f" πŸ”‘ Found {len(keyframes)} keyframes for alignment")
194
+ except Exception as e2:
195
+ keyframes = []
196
+ if logging_enabled:
197
+ print(f" ⚠️ Could not get keyframes: {e}")
198
+ print(f" πŸ“ Using time-based boundaries (may cause stuttering)")
199
 
200
  # Calculate scene boundaries, preferring keyframe alignment
201
+ scene_boundaries = []
202
+ current_start = 0
203
+
204
+ while current_start < total_duration:
205
+ target_end = min(current_start + segment_duration, total_duration)
206
+
207
+ # If we have keyframes, find the closest keyframe to target_end
208
+ if keyframes and target_end < total_duration:
209
+ # Find keyframe closest to target_end
210
+ closest_keyframe = min(keyframes, key=lambda kf: abs(kf - target_end))
211
+
212
+ # Only use keyframe if it's reasonably close (within 2 seconds)
213
+ if abs(closest_keyframe - target_end) <= 2.0 and closest_keyframe > current_start:
214
+ scene_end = closest_keyframe
215
+ else:
216
+ scene_end = target_end
217
+ else:
218
+ scene_end = target_end
219
+
220
+ # Handle very short final segments (< 2 seconds) by merging with previous scene
221
+ remaining_duration = total_duration - scene_end
222
+ if remaining_duration > 0 and remaining_duration < 2.0 and scene_boundaries:
223
+ # Extend current scene to end of video instead of creating tiny final scene
224
+ scene_end = total_duration
225
+ if logging_enabled:
226
+ print(f" πŸ”§ Merging final {remaining_duration:.1f}s with current scene to avoid stuttering")
227
+
228
+ scene_boundaries.append((current_start, scene_end))
229
+ current_start = scene_end
230
+
231
+ if logging_enabled:
232
+ print(f" πŸ“Š Will create {len(scene_boundaries)} segments:")
233
+ for i, (start, end) in enumerate(scene_boundaries):
234
+ print(f" Segment {i+1}: {start:.1f}s - {end:.1f}s (duration: {end-start:.1f}s)")
235
 
236
+ # If only one scene, return original video
237
+ if len(scene_boundaries) <= 1:
238
+ if logging_enabled:
239
+ print(f" πŸ“ Video shorter than segment duration, using as single scene")
240
+ return [{
241
+ 'path': video_path,
242
+ 'scene_number': 1,
243
+ 'start_time': 0.0,
244
+ 'end_time': total_duration,
245
+ 'duration': total_duration,
246
+ 'original_video_metadata': original_metadata
247
+ }]
248
+
249
+ # Create output directory
250
+ os.makedirs(temp_dir, exist_ok=True)
251
+
252
+ # Choose scene file extension based on source codec
253
+ source_codec = original_metadata.get('codec', 'h264').lower()
254
+ if source_codec in ['ffv1', 'prores', 'dnxhd']:
255
+ scene_extension = '.mkv' # Use MKV for lossless codecs
256
+ elif source_codec in ['av1', 'vp9']:
257
+ scene_extension = '.webm' # Use WebM for modern codecs
258
+ else:
259
+ scene_extension = '.mp4' # Default to MP4 for compatibility
260
+
261
+ if logging_enabled:
262
+ print(f" πŸ“¦ Using {scene_extension} container for {source_codec} codec")
263
+
264
+ # Split video and create metadata
265
+ scene_metadata_list = []
266
+
267
+ for i, (start_time_sec, end_time_sec) in enumerate(scene_boundaries):
268
  scene_number = i + 1
269
+ scene_filename = f"scene_{scene_number:03d}_time_{start_time_sec:.0f}s{scene_extension}"
270
+ scene_path = os.path.join(temp_dir, scene_filename)
271
+
272
+ duration_scene = end_time_sec - start_time_sec
273
+
274
+ # FFmpeg command with precise cutting to prevent frame alignment issues
275
+ # Use input seeking for efficiency and output seeking for precision
276
+ cmd = [
277
+ 'ffmpeg', '-y',
278
+ '-ss', str(start_time_sec), # Input seeking for efficiency
279
+ '-i', video_path,
280
+ '-t', str(duration_scene), # Precise duration
281
+ '-avoid_negative_ts', 'make_zero',
282
+ '-fflags', '+genpts', # Generate presentation timestamps
283
+ '-map', '0:v:0', # Map video stream explicitly
284
+ '-map', '0:a?', # Map audio if exists (optional)
285
+ ]
286
+
287
+ # Add codec-specific format options before encoding
288
+ format_options = []
289
+ if source_codec == 'ffv1':
290
+ format_options.extend(['-f', 'matroska'])
291
+ elif source_codec in ['av1', 'vp9']:
292
+ format_options.extend(['-f', 'webm'])
293
+
294
+ # Always use fast re-encoding to ensure clean cuts and prevent stuttering
295
+ # Stream copy can cause artifacts at scene boundaries even with keyframe alignment
296
+ cmd.extend([
297
+ '-c:v', 'libx264',
298
+ '-preset', 'ultrafast',
299
+ '-crf', '18',
300
+ '-c:a', 'copy'
301
+ ])
302
+ cmd.extend(format_options) # Add format options
303
+ cmd.append(scene_path) # Add output file path
304
 
305
  if logging_enabled:
306
+ print(f" βœ‚οΈ Creating segment {scene_number}: {scene_filename}")
307
 
308
+ try:
309
+ # Execute FFmpeg command
310
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
311
+
312
+ if result.returncode == 0:
313
+ # Verify file exists and has content
314
+ if os.path.exists(scene_path) and os.path.getsize(scene_path) > 0:
315
+ file_size_mb = os.path.getsize(scene_path) / (1024 * 1024)
316
+
317
+ scene_metadata = {
318
+ 'path': scene_path,
319
+ 'scene_number': scene_number,
320
+ 'start_time': start_time_sec,
321
+ 'end_time': end_time_sec,
322
+ 'duration': duration_scene,
323
+ 'scene_codec': source_codec,
324
+ 'scene_container': scene_extension[1:], # Remove the dot
325
+ 'file_size_mb': file_size_mb,
326
+ 'scene_detection_method': 'force_time_based',
327
+ 'original_video_metadata': original_metadata
328
+ }
329
+ scene_metadata_list.append(scene_metadata)
330
+
331
+ if logging_enabled:
332
+ print(f" βœ… Created: {scene_filename} ({file_size_mb:.1f} MB)")
333
+ else:
334
+ if logging_enabled:
335
+ print(f" ❌ Failed: {scene_filename} (file not created or empty)")
336
+ else:
337
+ if logging_enabled:
338
+ print(f" ❌ FFmpeg failed for {scene_filename}: {result.stderr}")
339
+
340
+ except subprocess.TimeoutExpired:
341
+ if logging_enabled:
342
+ print(f" ❌ Timeout creating {scene_filename}")
343
+ except Exception as e:
344
+ if logging_enabled:
345
+ print(f" ❌ Error creating {scene_filename}: {e}")
346
+
347
+ if not scene_metadata_list:
348
+ if logging_enabled:
349
+ print(f" ❌ All segment creation failed, using original video as single scene")
350
+ return [{
351
+ 'path': video_path,
352
+ 'scene_number': 1,
353
+ 'start_time': 0.0,
354
+ 'end_time': total_duration,
355
+ 'duration': total_duration,
356
+ 'original_video_metadata': original_metadata
357
+ }]
358
 
359
  if logging_enabled:
360
+ print(f" βœ… Successfully created {len(scene_metadata_list)} time-based segments")
361
 
362
+ return scene_metadata_list
 
363
 
364
+ def time_based_splitting(video_path, temp_dir, config, duration, original_metadata, logging_enabled=True):
 
 
 
 
 
 
365
  """
366
+ Split video into scenes and return metadata for each scene.
367
 
368
  Args:
369
+ video_path (str): Path to the video file
370
+ temp_dir (str): Temporary directory for scene files
371
+ config (dict): Configuration dictionary
372
+ duration (float): Video duration in seconds
373
+ original_metadata (dict): Complete metadata from Part 1 including codec info
374
+ logging_enabled (bool): Enable detailed logging
375
+
376
  Returns:
377
  list: List of scene metadata dictionaries
378
  """
379
+ if logging_enabled:
380
+ print(f"\n--- Running time-based scene splitting with metadata ---")
381
+ # βœ… NEW: Log codec information for scene splitting
382
+ print(f" πŸŽ₯ Source codec: {original_metadata.get('codec', 'unknown')}")
383
+ print(f" 🎯 Target codec: {original_metadata.get('target_codec', 'auto')}")
384
 
 
 
 
 
 
 
 
 
 
 
 
385
  try:
386
+ total_duration = duration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  if logging_enabled:
388
+ print(f"Using provided duration: {total_duration:.1f}s")
389
+
390
+ if not total_duration:
391
+ return [{
392
+ 'path': video_path,
393
+ 'scene_number': 1,
394
+ 'start_time': 0.0,
395
+ 'end_time': duration,
396
+ 'duration': duration,
397
+ 'original_video_metadata': original_metadata
398
+ }]
399
+
400
+ # Get scene duration from config
401
+ scene_config = config.get('scene_detection', {})
402
+ scene_duration = scene_config.get('time_based_scene_duration', None)
403
+
404
+ # Auto-calculate scene duration if not specified
405
+ if scene_duration is None:
406
+ if total_duration <= 120:
407
+ scene_duration = 60
408
+ elif total_duration <= 600:
409
+ scene_duration = 120
410
+ elif total_duration <= 1800:
411
+ scene_duration = 300
412
+ else:
413
+ scene_duration = 600
414
+
415
+ if logging_enabled:
416
+ print(f"Auto-calculated scene duration: {scene_duration}s for {total_duration:.1f}s video")
417
+
418
+ # Calculate scene boundaries
419
+ scene_boundaries = []
420
+ current_start = 0
421
+
422
+ while current_start < total_duration:
423
+ scene_end = min(current_start + scene_duration, total_duration)
424
+ scene_boundaries.append((current_start, scene_end))
425
+ current_start = scene_end
426
+
427
  if logging_enabled:
428
+ print(f"Calculated {len(scene_boundaries)} time-based scenes:")
429
+ for i, (start, end) in enumerate(scene_boundaries):
430
+ print(f" Scene {i+1}: {start:.1f}s - {end:.1f}s (duration: {end-start:.1f}s)")
431
+
432
+ # If only one scene, return original video
433
+ if len(scene_boundaries) <= 1:
434
+ return [{
435
+ 'path': video_path,
436
+ 'scene_number': 1,
437
+ 'start_time': 0.0,
438
+ 'end_time': total_duration,
439
+ 'duration': total_duration,
440
+ 'original_video_metadata': original_metadata
441
+ }]
 
 
 
442
 
443
+ # Create output directory
444
+ os.makedirs(temp_dir, exist_ok=True)
445
+
446
+ # βœ… IMPROVED: Choose scene file extension based on source codec
447
+ source_codec = original_metadata.get('codec', 'h264').lower()
448
+ if source_codec in ['ffv1', 'prores', 'dnxhd']:
449
+ scene_extension = '.mkv' # Use MKV for lossless codecs
450
+ elif source_codec in ['av1', 'vp9']:
451
+ scene_extension = '.webm' # Use WebM for modern codecs
452
+ else:
453
+ scene_extension = '.mp4' # Default to MP4 for compatibility
454
+
455
+ if logging_enabled:
456
+ print(f" πŸ“¦ Using {scene_extension} container for {source_codec} codec")
457
+
458
+ # Split video and create metadata
459
+ scene_metadata_list = []
460
+
461
+ for i, (start_time_sec, end_time_sec) in enumerate(scene_boundaries):
462
+ scene_number = i + 1
463
+ scene_filename = f"scene_{scene_number:03d}{scene_extension}"
464
+ scene_path = os.path.join(temp_dir, scene_filename)
465
+
466
+ duration_scene = end_time_sec - start_time_sec
467
+
468
+ # βœ… IMPROVED: FFmpeg command with codec-aware copy settings
469
+ cmd = [
470
+ 'ffmpeg', '-y',
471
+ '-ss', str(start_time_sec),
472
+ '-i', video_path,
473
+ '-t', str(duration_scene),
474
+ '-c', 'copy', # Stream copy for speed
475
+ '-map', '0', # Copy all streams
476
+ '-avoid_negative_ts', 'make_zero',
477
+ scene_path
478
+ ]
479
+
480
+ # βœ… NEW: Add codec-specific options if needed
481
+ if source_codec == 'ffv1':
482
+ # For FFV1, ensure proper MKV muxing
483
+ cmd.extend(['-f', 'matroska'])
484
+ elif source_codec in ['av1', 'vp9']:
485
+ # For modern codecs, ensure proper WebM muxing
486
+ cmd.extend(['-f', 'webm'])
487
+
488
+ if logging_enabled:
489
+ print(f"Extracting scene {scene_number}/{len(scene_boundaries)}: {start_time_sec:.1f}s-{end_time_sec:.1f}s")
490
+
491
+ try:
492
+ result = subprocess.run(
493
+ cmd,
494
+ stdout=subprocess.PIPE,
495
+ stderr=subprocess.PIPE,
496
+ text=True,
497
+ check=True
498
+ )
499
+
500
+ if os.path.exists(scene_path) and os.path.getsize(scene_path) > 0:
501
+ # Create scene metadata
502
+ scene_metadata = {
503
+ 'path': scene_path,
504
+ 'scene_number': scene_number,
505
+ 'start_time': start_time_sec,
506
+ 'end_time': end_time_sec,
507
+ 'duration': duration_scene,
508
+ 'original_video_metadata': original_metadata, # βœ… Complete metadata including codec info
509
+ 'file_size_mb': os.path.getsize(scene_path) / (1024 * 1024),
510
+ 'scene_codec': source_codec, # βœ… NEW: Track scene codec
511
+ 'scene_container': scene_extension[1:] # βœ… NEW: Track container format
512
+ }
513
+ scene_metadata_list.append(scene_metadata)
514
+
515
+ if logging_enabled:
516
+ print(f" βœ… Created: {scene_filename} ({scene_metadata['file_size_mb']:.1f} MB)")
517
+ else:
518
+ if logging_enabled:
519
+ print(f" ❌ Failed: {scene_filename}")
520
+
521
+ except subprocess.CalledProcessError as e:
522
  if logging_enabled:
523
+ print(f" ❌ FFmpeg error for scene {scene_number}: {e.stderr}")
524
+ continue
525
+ except Exception as e:
526
+ if logging_enabled:
527
+ print(f" ❌ Unexpected error for scene {scene_number}: {e}")
528
+ continue
529
 
530
+ if logging_enabled:
531
+ print(f"Time-based splitting completed: {len(scene_metadata_list)} scenes created")
532
+ if scene_metadata_list:
533
+ total_size = sum(scene['file_size_mb'] for scene in scene_metadata_list)
534
+ print(f" πŸ“Š Total scenes size: {total_size:.1f} MB")
535
+
536
+ return scene_metadata_list if scene_metadata_list else [{
537
+ 'path': video_path,
538
+ 'scene_number': 1,
539
+ 'start_time': 0.0,
540
+ 'end_time': total_duration,
541
+ 'duration': total_duration,
542
+ 'original_video_metadata': original_metadata
543
+ }]
544
+
545
+ except Exception as e:
546
+ if logging_enabled:
547
+ print(f"Time-based splitting failed: {e}")
548
+ return [{
549
+ 'path': video_path,
550
+ 'scene_number': 1,
551
+ 'start_time': 0.0,
552
+ 'end_time': duration,
553
+ 'duration': duration,
554
+ 'original_video_metadata': original_metadata
555
+ }]
556
+
557
+ def create_scene_metadata_from_files(scene_files, original_metadata, config):
558
  """
559
+ Create metadata for scenes created by PySceneDetect.
560
+ Note: PySceneDetect doesn't give us exact timing, so we estimate.
561
 
562
  Args:
563
+ scene_files (list): List of scene file paths
564
+ original_metadata (dict): Complete metadata from Part 1 including codec info
565
+ config (dict): Configuration dictionary
566
+
567
  Returns:
568
  list: List of scene metadata dictionaries
569
  """
570
+ scene_metadata_list = []
571
+ total_duration = original_metadata['duration']
572
+ source_codec = original_metadata.get('codec', 'unknown')
573
+
574
+ if not scene_files:
575
+ return [{
576
+ 'path': original_metadata['path'],
577
+ 'scene_number': 1,
578
+ 'start_time': 0.0,
579
+ 'end_time': total_duration,
580
+ 'duration': total_duration,
581
+ 'original_video_metadata': original_metadata
582
+ }]
583
+
584
+ # Estimate timing based on file count (not perfect, but reasonable)
585
+ estimated_scene_duration = total_duration / len(scene_files)
586
+
587
+ print(f" πŸŽ₯ Creating metadata for {len(scene_files)} scenes from {source_codec} source")
588
 
589
  for i, scene_file in enumerate(scene_files):
590
  scene_number = i + 1
591
+ start_time = i * estimated_scene_duration
592
+ end_time = min((i + 1) * estimated_scene_duration, total_duration)
593
+
594
+ # Adjust last scene to cover remaining time
595
+ if i == len(scene_files) - 1:
596
+ end_time = total_duration
597
 
598
+ # βœ… NEW: Extract scene file extension for container tracking
599
+ scene_extension = os.path.splitext(scene_file)[1][1:] if os.path.splitext(scene_file)[1] else 'unknown'
600
 
601
+ scene_metadata = {
602
  'path': scene_file,
603
  'scene_number': scene_number,
604
+ 'start_time': start_time,
605
+ 'end_time': end_time,
606
+ 'duration': end_time - start_time,
607
+ 'original_video_metadata': original_metadata, # βœ… Complete metadata including codec info
608
+ 'file_size_mb': os.path.getsize(scene_file) / (1024 * 1024) if os.path.exists(scene_file) else 0,
609
+ 'scene_codec': source_codec, # βœ… NEW: Track scene codec
610
+ 'scene_container': scene_extension # βœ… NEW: Track container format
611
+ }
612
+ scene_metadata_list.append(scene_metadata)
613
+
614
+ return scene_metadata_list
services/compress/validator_merger.py CHANGED
@@ -1,55 +1,16 @@
1
- """
2
- Validation and Merging Module
3
-
4
- This module handles the final stage of video processing including:
5
- - Scene validation and quality checks
6
- - Video merging from encoded scenes
7
- - Final VMAF calculation
8
- - Comprehensive report generation
9
- """
10
-
11
  import os
12
  import json
13
  import time
14
- import signal
15
  from datetime import datetime
16
- from typing import Dict, List, Any, Optional, Tuple
17
-
18
  from utils.calculate_vmaf_adv import calculate_vmaf_advanced
19
  from utils.merge_videos import merge_videos
 
20
 
21
-
22
- # ============================================================================
23
- # Constants
24
- # ============================================================================
25
-
26
- # Timeout settings
27
- MERGE_TIMEOUT_SECONDS = 300 # 5 minutes
28
- SIGNAL_TIMEOUT_SECONDS = 10
29
-
30
-
31
- # ============================================================================
32
- # Main Function
33
- # ============================================================================
34
-
35
- def validation_and_merging(
36
- original_video_path: str,
37
- encoded_scenes_data: List[Dict[str, Any]],
38
- config: Dict[str, Any],
39
- logging_enabled: bool = True
40
- ) -> Tuple[Optional[str], Optional[float], Optional[Dict[str, Any]]]:
41
  """
42
  Part 4: Validation and merging of encoded scenes with existing VMAF data.
43
-
44
- Args:
45
- original_video_path: Path to the original video file
46
- encoded_scenes_data: List of scene data dictionaries from Part 3
47
- config: Configuration dictionary
48
- logging_enabled: Whether to enable detailed logging
49
-
50
- Returns:
51
- tuple: (final_video_path, final_vmaf, comprehensive_report)
52
  """
 
53
  if logging_enabled:
54
  print(f"\nπŸ”— --- Part 4: Validation and Merging ---")
55
  print(f" 🎬 Processing {len(encoded_scenes_data)} scenes with VMAF data")
@@ -73,81 +34,20 @@ def validation_and_merging(
73
  print(f" πŸ“ Output directory: {output_dir}")
74
  print(f" πŸ“ Temp directory: {temp_dir}")
75
 
76
- # Create output directory with error handling
77
- if not _create_output_directory(output_dir, logging_enabled):
78
- return None, None, None
79
-
80
- # Initialize processing start time
81
- part5_start_time = time.time()
82
-
83
- # Validate encoded scenes
84
- successful_scenes, failed_scenes, temp_files_to_cleanup = _validate_encoded_scenes(
85
- encoded_scenes_data, logging_enabled
86
- )
87
-
88
- if not successful_scenes:
89
- if logging_enabled:
90
- print("❌ No valid scenes found for merging")
91
- return None, None, None
92
-
93
- # Merge video scenes
94
- final_output_path, merge_time, merged_size = _merge_video_scenes(
95
- successful_scenes, output_dir, original_video_name, logging_enabled
96
- )
97
-
98
- if not final_output_path:
99
- return None, None, None
100
-
101
- # Clean up temporary files if enabled
102
- if cleanup_temp_files:
103
- _cleanup_temp_files(temp_files_to_cleanup, successful_scenes, logging_enabled)
104
-
105
- # Calculate final VMAF
106
- final_vmaf, final_vmaf_time = _calculate_final_vmaf(
107
- original_video_path, final_output_path, successful_scenes,
108
- calculate_full_video_vmaf, vmaf_config, config, logging_enabled
109
- )
110
-
111
- # Generate comprehensive report
112
- comprehensive_report = _generate_comprehensive_report(
113
- original_video_path, final_output_path, successful_scenes, failed_scenes,
114
- part5_start_time, merge_time, final_vmaf_time, config, logging_enabled
115
- )
116
-
117
- # Save reports if enabled
118
- if save_comprehensive_report:
119
- _save_reports(comprehensive_report, output_dir, original_video_name, logging_enabled)
120
-
121
- if logging_enabled:
122
- print(f"βœ… Part 4 completed successfully!")
123
- print(f" πŸ“ Final video: {os.path.basename(final_output_path)}")
124
- print(f" 🎯 Final VMAF: {final_vmaf:.2f}" if final_vmaf else " 🎯 Final VMAF: Not calculated")
125
-
126
- return final_output_path, final_vmaf, comprehensive_report
127
-
128
-
129
- # ============================================================================
130
- # Core Processing Functions
131
- # ============================================================================
132
-
133
- def _create_output_directory(output_dir: str, logging_enabled: bool) -> bool:
134
- """Create output directory with error handling."""
135
  try:
136
  os.makedirs(output_dir, exist_ok=True)
137
  if logging_enabled:
138
  print(f" βœ… Output directory ready: {output_dir}")
139
- return True
140
  except Exception as e:
141
  if logging_enabled:
142
  print(f" ❌ Failed to create output directory: {e}")
143
- return False
144
-
145
-
146
- def _validate_encoded_scenes(
147
- encoded_scenes_data: List[Dict[str, Any]],
148
- logging_enabled: bool
149
- ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[str]]:
150
- """Validate encoded scenes and categorize them."""
151
  if logging_enabled:
152
  print(f" πŸ” Validating encoded scenes...")
153
 
@@ -164,250 +64,233 @@ def _validate_encoded_scenes(
164
  if scene_data.get('encoding_success', False) and scene_data.get('encoded_path'):
165
  encoded_path = scene_data.get('encoded_path')
166
 
167
- # Enhanced file validation with timeout protection
168
- if _validate_scene_file(encoded_path, scene_num, logging_enabled):
169
- successful_scenes.append(scene_data)
170
-
171
- # Only add original scene file (temp split files) to cleanup
172
- original_scene_path = scene_data.get('path')
173
- if _should_cleanup_scene_file(original_scene_path, encoded_path):
174
- temp_files_to_cleanup.append(original_scene_path)
175
- else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  failed_scenes.append(scene_data)
177
  else:
178
  if logging_enabled:
179
  print(f" ❌ Scene {scene_num} encoding failed")
180
  failed_scenes.append(scene_data)
181
 
 
 
 
182
  if logging_enabled:
183
- print(f" πŸ“Š Scene validation complete: {len(successful_scenes)} successful, {len(failed_scenes)} failed")
 
 
 
 
 
184
 
185
- return successful_scenes, failed_scenes, temp_files_to_cleanup
186
-
187
-
188
- def _validate_scene_file(encoded_path: str, scene_num: int, logging_enabled: bool) -> bool:
189
- """Validate a single scene file."""
190
- try:
191
- if os.path.exists(encoded_path):
192
- file_size = os.path.getsize(encoded_path)
193
- if file_size > 0:
194
- if logging_enabled:
195
- print(f" βœ… Scene {scene_num} valid ({file_size/1024/1024:.1f} MB)")
196
- return True
197
- else:
198
- if logging_enabled:
199
- print(f" ❌ Scene {scene_num} file is empty")
200
- return False
201
- else:
202
- if logging_enabled:
203
- print(f" ❌ Scene {scene_num} file not found: {encoded_path}")
204
- return False
205
- except Exception as e:
206
  if logging_enabled:
207
- print(f" ❌ Scene {scene_num} validation error: {e}")
208
- return False
209
-
210
-
211
- def _should_cleanup_scene_file(original_scene_path: Optional[str], encoded_path: str) -> bool:
212
- """Determine if a scene file should be cleaned up."""
213
- return (
214
- original_scene_path and
215
- os.path.exists(original_scene_path) and
216
- original_scene_path != encoded_path and # Don't delete if same file
217
- 'temp_scenes' in original_scene_path # Only delete temp scene splits
218
- )
219
-
220
-
221
- def _merge_video_scenes(
222
- successful_scenes: List[Dict[str, Any]],
223
- output_dir: str,
224
- original_video_name: str,
225
- logging_enabled: bool
226
- ) -> Tuple[Optional[str], float, float]:
227
- """Merge video scenes into final output."""
228
  if logging_enabled:
229
- print(f" πŸ”— Merging {len(successful_scenes)} scenes into final video...")
230
 
231
- # Generate output filename
 
 
 
 
 
 
 
232
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
233
- final_output_filename = f"{original_video_name}_compressed_{timestamp}.mp4"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  final_output_path = os.path.join(output_dir, final_output_filename)
235
 
236
  if logging_enabled:
237
- print(f" πŸ“ Output: {final_output_filename}")
238
 
239
- # Set up timeout handler for merging
240
- def timeout_handler(signum, frame):
241
- raise TimeoutError("Video merging timed out")
 
 
 
 
 
242
 
243
- signal.signal(signal.SIGALRM, timeout_handler)
244
- signal.alarm(MERGE_TIMEOUT_SECONDS)
 
245
 
246
  try:
247
- merge_start_time = time.time()
 
 
 
 
 
 
248
 
249
- # Perform video merging
250
- merge_success = merge_videos(
251
- scene_videos=[scene.get('encoded_path') for scene in successful_scenes],
252
- output_video=final_output_path
253
- )
 
254
 
255
  merge_time = time.time() - merge_start_time
256
- signal.alarm(0) # Cancel timeout
257
 
258
- if not merge_success or not os.path.exists(final_output_path):
259
  if logging_enabled:
260
- print(f" ❌ Video merging failed")
261
- return None, merge_time, 0.0
262
 
263
- # Get merged file size
264
- merged_size = os.path.getsize(final_output_path) / (1024 * 1024) # MB
 
 
 
 
 
 
 
 
 
265
 
266
  if logging_enabled:
267
  print(f" βœ… Video merging completed in {merge_time:.1f}s")
268
  print(f" πŸ“ Final video: {final_output_filename} ({merged_size:.1f} MB)")
269
-
270
- return final_output_path, merge_time, merged_size
271
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  except TimeoutError:
273
- signal.alarm(0)
274
  if logging_enabled:
275
- print(f" ❌ Video merging timed out after {MERGE_TIMEOUT_SECONDS} minutes!")
276
- return None, 0.0, 0.0
277
  except Exception as e:
278
- signal.alarm(0)
279
  if logging_enabled:
280
  print(f" ❌ Video merging failed with error: {e}")
281
  import traceback
282
  traceback.print_exc()
283
- return None, 0.0, 0.0
284
-
285
-
286
- def _cleanup_temp_files(
287
- temp_files_to_cleanup: List[str],
288
- successful_scenes: List[Dict[str, Any]],
289
- logging_enabled: bool
290
- ) -> None:
291
- """Clean up temporary files."""
292
- if logging_enabled:
293
- print(f" 🧹 Cleaning up temporary files...")
294
-
295
- # Clean up original scene files
296
- temp_cleanup_success = 0
297
- temp_cleanup_failed = 0
298
-
299
- for temp_file in temp_files_to_cleanup:
300
- if os.path.exists(temp_file):
301
- try:
302
- os.remove(temp_file)
303
- temp_cleanup_success += 1
304
- if logging_enabled:
305
- print(f" πŸ—‘οΈ Deleted: {os.path.basename(temp_file)}")
306
- except Exception as e:
307
- temp_cleanup_failed += 1
308
- if logging_enabled:
309
- print(f" ❌ Failed to delete {os.path.basename(temp_file)}: {e}")
310
-
311
- # Clean up encoded scene files
312
- if logging_enabled:
313
- print(f" 🧹 Cleaning up encoded scene files...")
314
-
315
- encoded_cleanup_success = 0
316
- encoded_cleanup_failed = 0
317
-
318
- for scene_data in successful_scenes:
319
- encoded_path = scene_data.get('encoded_path')
320
- if encoded_path and os.path.exists(encoded_path):
321
- try:
322
- os.remove(encoded_path)
323
- encoded_cleanup_success += 1
324
- if logging_enabled:
325
- print(f" πŸ—‘οΈ Deleted: {os.path.basename(encoded_path)}")
326
- except Exception as e:
327
- encoded_cleanup_failed += 1
328
- if logging_enabled:
329
- print(f" ❌ Failed to delete {os.path.basename(encoded_path)}: {e}")
330
 
331
- if logging_enabled:
332
- print(f" βœ… Cleanup complete: {temp_cleanup_success + encoded_cleanup_success} deleted, {temp_cleanup_failed + encoded_cleanup_failed} failed")
333
-
334
-
335
- def _calculate_final_vmaf(
336
- original_video_path: str,
337
- final_output_path: str,
338
- successful_scenes: List[Dict[str, Any]],
339
- calculate_full_video_vmaf: bool,
340
- vmaf_config: Dict[str, Any],
341
- config: Dict[str, Any],
342
- logging_enabled: bool
343
- ) -> Tuple[Optional[float], float]:
344
- """Calculate final VMAF score."""
345
  final_vmaf = None
346
  final_vmaf_time = 0
347
 
348
- calculate_full_video_vmaf = False
349
-
350
- if calculate_full_video_vmaf:
351
- if logging_enabled:
352
- print(f" πŸ“Š Starting final VMAF calculation...")
353
 
354
- try:
355
- final_vmaf_start_time = time.time()
356
 
357
- final_vmaf = calculate_vmaf_advanced(
358
- input_file=original_video_path,
359
- encoded_file=final_output_path,
360
- use_downscaling=vmaf_config.get('vmaf_use_downscaling', True),
361
- scale_factor=vmaf_config.get('vmaf_downscaling_scale_factor', 0.5),
362
- use_vmafneg=config.get('vmaf_models', {}).get('use_neg_by_default', False),
363
- default_vmaf_model_path_config=config.get('vmaf_models', {}).get('default_model_path'),
364
- vmafneg_model_path_config=config.get('vmaf_models', {}).get('neg_model_path'),
365
- use_frame_rate_scaling=vmaf_config.get('vmaf_use_frame_rate_scaling', False),
366
- target_fps=vmaf_config.get('vmaf_target_fps', 15.0),
367
- frame_rate_scaling_method=vmaf_config.get('vmaf_frame_rate_scaling_method', 'uniform'),
368
- logger=None,
369
- logging_enabled=logging_enabled
370
- )
371
- final_vmaf_time = time.time() - final_vmaf_start_time
372
 
373
- if logging_enabled:
374
- print(f" βœ… Final VMAF calculation completed in {final_vmaf_time:.1f}s")
375
- print(f" 🎯 Final VMAF score: {final_vmaf:.2f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
- except Exception as e:
378
- if logging_enabled:
379
- print(f" ❌ Final VMAF calculation failed: {e}")
380
- final_vmaf = None
381
- else:
382
- if logging_enabled:
383
- print(f" ⏭️ Skipping full video VMAF calculation (disabled in config)")
384
 
385
  # Estimate final VMAF from scene averages if available
386
- if not final_vmaf:
387
- scene_vmafs = [scene.get('actual_vmaf', 0) for scene in successful_scenes if scene.get('actual_vmaf', 0) > 0]
388
- if scene_vmafs:
389
- final_vmaf = sum(scene_vmafs) / len(scene_vmafs)
390
- if logging_enabled:
391
- print(f" πŸ“Š Estimated VMAF from scene average: {final_vmaf:.2f}")
392
- else:
393
- if logging_enabled:
394
- print(f" ⏭️ No scene VMAF data available for estimation")
395
 
396
- return final_vmaf, final_vmaf_time
397
-
398
-
399
- def _generate_comprehensive_report(
400
- original_video_path: str,
401
- final_output_path: str,
402
- successful_scenes: List[Dict[str, Any]],
403
- failed_scenes: List[Dict[str, Any]],
404
- part5_start_time: float,
405
- merge_time: float,
406
- final_vmaf_time: float,
407
- config: Dict[str, Any],
408
- logging_enabled: bool
409
- ) -> Dict[str, Any]:
410
- """Generate comprehensive processing report."""
411
  if logging_enabled:
412
  print(f" πŸ“‹ Generating comprehensive report...")
413
 
@@ -424,207 +307,293 @@ def _generate_comprehensive_report(
424
  target_vmaf = config.get('video_processing', {}).get('target_vmaf', 93.0)
425
  scenes_meeting_target = sum(1 for s in scenes_with_valid_vmaf if s['actual_vmaf'] >= target_vmaf)
426
 
427
- # Extract training data from scenes
428
- training_data = _extract_training_data_from_scenes(successful_scenes)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
 
430
- # Create comprehensive report
431
  comprehensive_report = {
432
  'processing_summary': {
 
433
  'original_video_path': original_video_path,
434
  'final_video_path': final_output_path,
435
- 'total_processing_time_seconds': total_processing_time,
436
- 'merge_time_seconds': merge_time,
437
- 'final_vmaf_calculation_time_seconds': final_vmaf_time,
438
- 'timestamp': datetime.now().isoformat()
439
- },
440
- 'compression_metrics': {
441
- 'input_size_total_mb': input_size_total,
442
- 'output_size_total_mb': output_size_total,
443
- 'overall_compression_ratio_percent': overall_compression_ratio,
444
- 'scenes_processed': len(successful_scenes),
445
- 'scenes_failed': len(failed_scenes)
446
  },
 
447
  'quality_metrics': {
448
- 'final_vmaf': None, # Will be updated later
 
 
449
  'average_scene_vmaf': average_scene_vmaf,
 
 
450
  'scenes_meeting_target': scenes_meeting_target,
451
- 'total_scenes_with_vmaf': len(scenes_with_valid_vmaf),
452
- 'target_vmaf': target_vmaf
 
 
 
 
 
 
 
 
 
453
  },
 
454
  'scene_analysis': {
455
- 'successful_scenes': successful_scenes,
456
- 'failed_scenes': failed_scenes,
457
- 'training_data': training_data
458
- }
459
- }
460
-
461
- return comprehensive_report
462
-
463
-
464
- def _extract_training_data_from_scenes(scenes_data: List[Dict[str, Any]]) -> Dict[str, Any]:
465
- """Extract comprehensive training data directly from scene data."""
466
- training_data = {
467
- 'pipeline_stages_data': {
468
- 'part1_preprocessing': [],
469
- 'part2_scene_detection': [],
470
- 'part3_ai_encoding': [],
471
- 'part4_vmaf_calculation': [],
472
- 'part5_merging': []
473
  },
474
- 'model_training_features': [],
475
- 'prediction_accuracy_analysis': [],
476
- 'scene_classification_results': [],
477
- 'encoding_optimization_data': [],
478
- 'vmaf_validation_data': [],
479
- 'feature_extraction_performance': []
480
- }
481
-
482
- for scene in scenes_data:
483
- scene_num = scene.get('scene_number', 'unknown')
484
 
485
- # Extract Part 1 data (preprocessing info)
486
- if 'original_video_metadata' in scene:
487
- part1_data = scene['original_video_metadata'].copy()
488
- part1_data['scene_number'] = scene_num
489
- training_data['pipeline_stages_data']['part1_preprocessing'].append(part1_data)
490
 
491
- # Extract Part 2 data (scene detection)
492
- part2_data = {
493
- 'scene_number': scene_num,
494
- 'start_time': scene.get('start_time'),
495
- 'end_time': scene.get('end_time'),
496
- 'duration': scene.get('duration'),
497
- 'scene_codec': scene.get('scene_codec'),
498
- 'scene_container': scene.get('scene_container'),
499
- 'file_size_mb': scene.get('file_size_mb'),
500
- 'scene_detection_method': 'content_based' if len(scenes_data) > 1 else 'time_based'
501
- }
502
- training_data['pipeline_stages_data']['part2_scene_detection'].append(part2_data)
503
 
504
- # Extract Part 3 data (AI encoding) - this is the richest data source
505
- if scene.get('encoding_success', False):
506
- part3_data = {
507
- 'scene_number': scene_num,
508
- 'scene_type': scene.get('scene_type'),
509
- 'codec_used': scene.get('codec_used'),
510
- 'final_adjusted_cq': scene.get('final_adjusted_cq'),
511
- 'predicted_vmaf_at_optimal_cq': scene.get('predicted_vmaf_at_optimal_cq'),
512
- 'processing_time_seconds': scene.get('processing_time_seconds'),
513
- 'compression_ratio': scene.get('compression_ratio'),
514
- 'size_ratio': scene.get('size_ratio'),
515
- 'encoding_success': True,
516
- 'input_size_mb': scene.get('input_size_mb'),
517
- 'encoded_file_size_mb': scene.get('encoded_file_size_mb')
518
- }
519
- training_data['pipeline_stages_data']['part3_ai_encoding'].append(part3_data)
520
-
521
- # Extract model training data if available
522
- model_data = scene.get('model_training_data', {})
523
- if model_data:
524
- _extract_model_training_data(training_data, scene_num, model_data, scene)
525
-
526
- return training_data
527
-
528
-
529
- def _extract_model_training_data(
530
- training_data: Dict[str, Any],
531
- scene_num: Any,
532
- model_data: Dict[str, Any],
533
- scene: Dict[str, Any]
534
- ) -> None:
535
- """Extract model training data from scene."""
536
- # Extract model features
537
- model_features = {
538
- 'scene_number': scene_num,
539
- 'raw_video_features': model_data.get('raw_video_features', {}),
540
- 'processed_video_features': model_data.get('processed_video_features', {}),
541
- 'vmaf_model_features': model_data.get('vmaf_model_features', {}),
542
- 'scene_classifier_features': model_data.get('scene_classifier_features', {}),
543
- 'scene_classifier_probabilities': model_data.get('scene_classifier_probabilities', {}),
544
- 'processing_timings': model_data.get('processing_timings', {})
545
  }
546
- training_data['model_training_features'].append(model_features)
547
-
548
- # Extract prediction accuracy
549
- prediction_acc = model_data.get('prediction_accuracy', {})
550
- if prediction_acc:
551
- training_data['prediction_accuracy_analysis'].append({
552
- 'scene_number': scene_num,
553
- 'predicted_vmaf': prediction_acc.get('predicted_vmaf'),
554
- 'prediction_error': prediction_acc.get('prediction_error'),
555
- 'target_vmaf': scene.get('target_vmaf'),
556
- 'actual_vmaf': scene.get('actual_vmaf')
557
- })
558
-
559
- # Extract feature extraction performance
560
- if model_data.get('feature_recovery_attempts'):
561
- training_data['feature_extraction_performance'].append({
562
- 'scene_number': scene_num,
563
- 'recovery_attempts': model_data.get('feature_recovery_attempts', []),
564
- 'feature_extraction_successful': bool(model_data.get('raw_video_features'))
565
- })
566
-
567
-
568
- def _save_reports(
569
- comprehensive_report: Dict[str, Any],
570
- output_dir: str,
571
- original_video_name: str,
572
- logging_enabled: bool
573
- ) -> None:
574
- """Save comprehensive and individual reports."""
575
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
576
-
577
- # Save comprehensive report
578
- comprehensive_report_path = os.path.join(
579
- output_dir,
580
- f"comprehensive_processing_report_{original_video_name}_{timestamp}.json"
581
- )
582
-
583
- try:
584
- with open(comprehensive_report_path, 'w') as f:
585
- json.dump(comprehensive_report, f, indent=2, default=str)
586
 
587
- if logging_enabled:
588
- print(f" πŸ“Š Comprehensive report saved: {os.path.basename(comprehensive_report_path)}")
589
- except Exception as e:
590
- if logging_enabled:
591
- print(f" ❌ Failed to save comprehensive report: {e}")
592
 
593
- # Save individual scene reports if enabled
594
- if comprehensive_report.get('scene_analysis', {}).get('successful_scenes'):
595
- _save_individual_scene_reports(
596
- comprehensive_report['scene_analysis']['successful_scenes'],
597
- output_dir, original_video_name, timestamp, logging_enabled
598
- )
599
-
600
 
601
- def _save_individual_scene_reports(
602
- successful_scenes: List[Dict[str, Any]],
603
- output_dir: str,
604
- original_video_name: str,
605
- timestamp: str,
606
- logging_enabled: bool
607
- ) -> None:
608
- """Save individual scene reports."""
609
- scene_reports_dir = os.path.join(output_dir, 'scene_reports')
610
- os.makedirs(scene_reports_dir, exist_ok=True)
611
-
612
- for scene in successful_scenes:
613
- scene_num = scene.get('scene_number', 'unknown')
614
- scene_report_path = os.path.join(
615
- scene_reports_dir,
616
- f"scene_{scene_num}_report_{original_video_name}_{timestamp}.json"
617
- )
618
-
619
  try:
620
- with open(scene_report_path, 'w') as f:
621
- json.dump(scene, f, indent=2, default=str)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  except Exception as e:
623
  if logging_enabled:
624
- print(f" ❌ Failed to save scene {scene_num} report: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
 
 
626
  if logging_enabled:
627
- print(f" πŸ“„ Individual scene reports saved in: scene_reports/")
 
 
 
 
 
 
 
 
 
628
 
629
 
630
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import json
3
  import time
 
4
  from datetime import datetime
 
 
5
  from utils.calculate_vmaf_adv import calculate_vmaf_advanced
6
  from utils.merge_videos import merge_videos
7
+ import signal
8
 
9
+ def validation_and_merging(original_video_path, encoded_scenes_data, config, logging_enabled=True):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  """
11
  Part 4: Validation and merging of encoded scenes with existing VMAF data.
 
 
 
 
 
 
 
 
 
12
  """
13
+ logging_enabled=True # Ensure logging is enabled for this part
14
  if logging_enabled:
15
  print(f"\nπŸ”— --- Part 4: Validation and Merging ---")
16
  print(f" 🎬 Processing {len(encoded_scenes_data)} scenes with VMAF data")
 
34
  print(f" πŸ“ Output directory: {output_dir}")
35
  print(f" πŸ“ Temp directory: {temp_dir}")
36
 
37
+ # βœ… ENHANCED: Create output directory with error handling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  try:
39
  os.makedirs(output_dir, exist_ok=True)
40
  if logging_enabled:
41
  print(f" βœ… Output directory ready: {output_dir}")
 
42
  except Exception as e:
43
  if logging_enabled:
44
  print(f" ❌ Failed to create output directory: {e}")
45
+ return None, None, None
46
+
47
+ # Initialize processing start time
48
+ part5_start_time = time.time()
49
+
50
+ # ===== SCENE VALIDATION =====
 
 
51
  if logging_enabled:
52
  print(f" πŸ” Validating encoded scenes...")
53
 
 
64
  if scene_data.get('encoding_success', False) and scene_data.get('encoded_path'):
65
  encoded_path = scene_data.get('encoded_path')
66
 
67
+ # βœ… ENHANCED: Better file validation with timeout protection
68
+ try:
69
+ if os.path.exists(encoded_path):
70
+ file_size = os.path.getsize(encoded_path)
71
+ if file_size > 0:
72
+ successful_scenes.append(scene_data)
73
+ # ❌ REMOVED: Don't add encoded files to general cleanup
74
+ # temp_files_to_cleanup.append(encoded_path) # REMOVED THIS LINE
75
+
76
+ # Only add original scene file (temp split files) to cleanup
77
+ original_scene_path = scene_data.get('path')
78
+ if (original_scene_path and
79
+ os.path.exists(original_scene_path) and
80
+ original_scene_path != encoded_path and # Don't delete if same file
81
+ 'temp_scenes' in original_scene_path): # Only delete temp scene splits
82
+ temp_files_to_cleanup.append(original_scene_path)
83
+
84
+ if logging_enabled:
85
+ print(f" βœ… Scene {scene_num} valid ({file_size/1024/1024:.1f} MB)")
86
+ else:
87
+ if logging_enabled:
88
+ print(f" ❌ Scene {scene_num} file is empty")
89
+ failed_scenes.append(scene_data)
90
+ else:
91
+ if logging_enabled:
92
+ print(f" ❌ Scene {scene_num} file not found: {encoded_path}")
93
+ failed_scenes.append(scene_data)
94
+ except Exception as e:
95
+ if logging_enabled:
96
+ print(f" ❌ Scene {scene_num} validation error: {e}")
97
  failed_scenes.append(scene_data)
98
  else:
99
  if logging_enabled:
100
  print(f" ❌ Scene {scene_num} encoding failed")
101
  failed_scenes.append(scene_data)
102
 
103
+ # Count scenes with VMAF data (for reporting only)
104
+ scenes_with_vmaf = sum(1 for scene in successful_scenes if scene.get('actual_vmaf') is not None)
105
+
106
  if logging_enabled:
107
+ print(f" πŸ“Š Scene validation results:")
108
+ print(f" βœ… Successfully encoded scenes: {len(successful_scenes)}")
109
+ print(f" πŸ“Š Scenes with VMAF data: {scenes_with_vmaf}")
110
+ print(f" πŸ” Scenes without VMAF: {len(successful_scenes) - scenes_with_vmaf}")
111
+ if failed_scenes:
112
+ print(f" ❌ Failed scenes: {len(failed_scenes)}")
113
 
114
+ if not successful_scenes:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  if logging_enabled:
116
+ print(f" ❌ No successfully encoded scenes to process!")
117
+ return None, None, None
118
+
119
+ # ===== REMOVED: CALCULATE MISSING VMAF SECTION =====
120
+ # We expect Part 4 to handle all VMAF calculations or skip them entirely
121
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  if logging_enabled:
123
+ print(f" βœ… Using VMAF data from Part 4 (or skipped if disabled)")
124
 
125
+ # ===== VIDEO MERGING =====
126
+ if logging_enabled:
127
+ print(f" πŸ”— Starting video merging process...")
128
+ print(f" Merging {len(successful_scenes)} encoded scenes...")
129
+
130
+ merge_start_time = time.time()
131
+
132
+ # Generate timestamp for all uses (not just fallback)
133
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
134
+ # Use custom filename from config if available
135
+ custom_filename = config.get('output_settings', {}).get('custom_output_filename')
136
+
137
+ if custom_filename:
138
+ # Use the custom filename from Streamlit config
139
+ final_output_filename = custom_filename
140
+ if logging_enabled:
141
+ print(f" πŸ“ Using custom filename: {final_output_filename}")
142
+ else:
143
+ # Fallback to original pattern if no custom filename
144
+ codec_used = successful_scenes[0].get('codec_used', 'unknown') if successful_scenes else 'unknown'
145
+ final_output_filename = f"{original_video_name}_final_{codec_used}_{timestamp}.mp4"
146
+ if logging_enabled:
147
+ print(f" πŸ“ Using default pattern: {final_output_filename}")
148
+
149
  final_output_path = os.path.join(output_dir, final_output_filename)
150
 
151
  if logging_enabled:
152
+ print(f" πŸ“ Output file: {final_output_filename}")
153
 
154
+ # Extract encoded scene paths in correct order
155
+ encoded_scene_paths = []
156
+ for scene_data in sorted(successful_scenes, key=lambda x: x.get('scene_number', 0)):
157
+ scene_path = scene_data['encoded_path']
158
+ encoded_scene_paths.append(scene_path)
159
+ if logging_enabled:
160
+ scene_size = os.path.getsize(scene_path) / 1024 / 1024 if os.path.exists(scene_path) else 0
161
+ print(f" Scene {scene_data.get('scene_number')}: {os.path.basename(scene_path)} ({scene_size:.1f} MB)")
162
 
163
+ # βœ… ENHANCED: Video merging with timeout and better error handling
164
+ if logging_enabled:
165
+ print(f" 🎬 Executing video merge...")
166
 
167
  try:
168
+ def timeout_handler(signum, frame):
169
+ raise TimeoutError("Video merging timeout")
170
+
171
+ # Only set alarm on Unix systems
172
+ if hasattr(signal, 'alarm'):
173
+ signal.signal(signal.SIGALRM, timeout_handler)
174
+ signal.alarm(300) # 5 minute timeout
175
 
176
+ try:
177
+ merge_success = merge_videos(encoded_scene_paths, final_output_path, logging_enabled=logging_enabled)
178
+ finally:
179
+ # Cancel the alarm
180
+ if hasattr(signal, 'alarm'):
181
+ signal.alarm(0)
182
 
183
  merge_time = time.time() - merge_start_time
 
184
 
185
+ if not merge_success:
186
  if logging_enabled:
187
+ print(f" ❌ Video merging failed!")
188
+ return None, None, None
189
 
190
+ # βœ… ENHANCED: Verify merged file
191
+ if not os.path.exists(final_output_path):
192
+ if logging_enabled:
193
+ print(f" ❌ Merged file was not created!")
194
+ return None, None, None
195
+
196
+ merged_size = os.path.getsize(final_output_path) / 1024 / 1024
197
+ if merged_size == 0:
198
+ if logging_enabled:
199
+ print(f" ❌ Merged file is empty!")
200
+ return None, None, None
201
 
202
  if logging_enabled:
203
  print(f" βœ… Video merging completed in {merge_time:.1f}s")
204
  print(f" πŸ“ Final video: {final_output_filename} ({merged_size:.1f} MB)")
205
+ if cleanup_temp_files:
206
+ if logging_enabled:
207
+ print(f" 🧹 Cleaning up encoded scene files...")
208
+
209
+ encoded_cleanup_success = 0
210
+ encoded_cleanup_failed = 0
211
+
212
+ for scene_data in successful_scenes:
213
+ encoded_path = scene_data.get('encoded_path')
214
+ if encoded_path and os.path.exists(encoded_path):
215
+ try:
216
+ os.remove(encoded_path)
217
+ encoded_cleanup_success += 1
218
+ if logging_enabled:
219
+ print(f" πŸ—‘οΈ Deleted: {os.path.basename(encoded_path)}")
220
+ except Exception as e:
221
+ encoded_cleanup_failed += 1
222
+ if logging_enabled:
223
+ print(f" ❌ Failed to delete {os.path.basename(encoded_path)}: {e}")
224
+
225
+ if logging_enabled:
226
+ print(f" βœ… Encoded scenes cleanup: {encoded_cleanup_success} deleted, {encoded_cleanup_failed} failed")
227
+
228
  except TimeoutError:
 
229
  if logging_enabled:
230
+ print(f" ❌ Video merging timed out after 5 minutes!")
231
+ return None, None, None
232
  except Exception as e:
 
233
  if logging_enabled:
234
  print(f" ❌ Video merging failed with error: {e}")
235
  import traceback
236
  traceback.print_exc()
237
+ return None, None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
+ # ===== FINAL VMAF CALCULATION =====
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  final_vmaf = None
241
  final_vmaf_time = 0
242
 
243
+ # calculate_full_video_vmaf = False
244
+
245
+ # if calculate_full_video_vmaf:
246
+ # if logging_enabled:
247
+ # print(f" πŸ“Š Starting final VMAF calculation...")
248
 
249
+ # try:
250
+ # final_vmaf_start_time = time.time()
251
 
252
+ # # Import VMAF calculation function
253
+ # from calculate_vmaf_adv import calculate_vmaf_advanced
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
+ # final_vmaf = calculate_vmaf_advanced(
256
+ # input_file=original_video_path,
257
+ # encoded_file=final_output_path,
258
+ # use_downscaling=vmaf_config.get('vmaf_use_downscaling', True),
259
+ # scale_factor=vmaf_config.get('vmaf_downscaling_scale_factor', 0.5),
260
+ # use_vmafneg=config.get('vmaf_models', {}).get('use_neg_by_default', False),
261
+ # default_vmaf_model_path_config=config.get('vmaf_models', {}).get('default_model_path'),
262
+ # vmafneg_model_path_config=config.get('vmaf_models', {}).get('neg_model_path'),
263
+ # use_frame_rate_scaling=vmaf_config.get('vmaf_use_frame_rate_scaling', False),
264
+ # target_fps=vmaf_config.get('vmaf_target_fps', 15.0),
265
+ # frame_rate_scaling_method=vmaf_config.get('vmaf_frame_rate_scaling_method', 'uniform'),
266
+ # logger=None,
267
+ # logging_enabled=logging_enabled
268
+ # )
269
+ # final_vmaf_time = time.time() - final_vmaf_start_time
270
+
271
+ # if logging_enabled:
272
+ # print(f" βœ… Final VMAF calculation completed in {final_vmaf_time:.1f}s")
273
+ # print(f" 🎯 Final VMAF score: {final_vmaf:.2f}")
274
 
275
+ # except Exception as e:
276
+ # if logging_enabled:
277
+ # print(f" ❌ Final VMAF calculation failed: {e}")
278
+ # final_vmaf = None
279
+ # else:
280
+ if logging_enabled:
281
+ print(f" ⏭️ Skipping full video VMAF calculation (disabled in config)")
282
 
283
  # Estimate final VMAF from scene averages if available
284
+ scene_vmafs = [scene.get('actual_vmaf', 0) for scene in successful_scenes if scene.get('actual_vmaf', 0) > 0]
285
+ if scene_vmafs:
286
+ final_vmaf = sum(scene_vmafs) / len(scene_vmafs)
287
+ if logging_enabled:
288
+ print(f" πŸ“Š Estimated VMAF from scene average: {final_vmaf:.2f}")
289
+ else:
290
+ if logging_enabled:
291
+ print(f" ⏭️ No scene VMAF data available for estimation")
 
292
 
293
+ # ===== COMPREHENSIVE REPORT GENERATION =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  if logging_enabled:
295
  print(f" πŸ“‹ Generating comprehensive report...")
296
 
 
307
  target_vmaf = config.get('video_processing', {}).get('target_vmaf', 93.0)
308
  scenes_meeting_target = sum(1 for s in scenes_with_valid_vmaf if s['actual_vmaf'] >= target_vmaf)
309
 
310
+ # βœ… SIMPLE: Extract training data directly from scenes
311
+ def extract_training_data_from_scenes(scenes_data):
312
+ """Extract comprehensive training data directly from scene data."""
313
+ training_data = {
314
+ 'pipeline_stages_data': {
315
+ 'part1_preprocessing': [],
316
+ 'part2_scene_detection': [],
317
+ 'part3_ai_encoding': [],
318
+ 'part4_vmaf_calculation': [],
319
+ 'part5_merging': []
320
+ },
321
+ 'model_training_features': [],
322
+ 'prediction_accuracy_analysis': [],
323
+ 'scene_classification_results': [],
324
+ 'encoding_optimization_data': [],
325
+ 'vmaf_validation_data': [],
326
+ 'feature_extraction_performance': []
327
+ }
328
+
329
+ for scene in scenes_data:
330
+ scene_num = scene.get('scene_number', 'unknown')
331
+
332
+ # Extract Part 1 data (preprocessing info)
333
+ if 'original_video_metadata' in scene:
334
+ part1_data = scene['original_video_metadata'].copy()
335
+ part1_data['scene_number'] = scene_num
336
+ training_data['pipeline_stages_data']['part1_preprocessing'].append(part1_data)
337
+
338
+ # Extract Part 2 data (scene detection)
339
+ part2_data = {
340
+ 'scene_number': scene_num,
341
+ 'start_time': scene.get('start_time'),
342
+ 'end_time': scene.get('end_time'),
343
+ 'duration': scene.get('duration'),
344
+ 'scene_codec': scene.get('scene_codec'),
345
+ 'scene_container': scene.get('scene_container'),
346
+ 'file_size_mb': scene.get('file_size_mb'),
347
+ 'scene_detection_method': 'content_based' if len(scenes_data) > 1 else 'time_based'
348
+ }
349
+ training_data['pipeline_stages_data']['part2_scene_detection'].append(part2_data)
350
+
351
+ # Extract Part 3 data (AI encoding) - this is the richest data source
352
+ if scene.get('encoding_success', False):
353
+ part3_data = {
354
+ 'scene_number': scene_num,
355
+ 'scene_type': scene.get('scene_type'),
356
+ 'codec_used': scene.get('codec_used'),
357
+ 'final_adjusted_cq': scene.get('final_adjusted_cq'),
358
+ 'predicted_vmaf_at_optimal_cq': scene.get('predicted_vmaf_at_optimal_cq'),
359
+ 'processing_time_seconds': scene.get('processing_time_seconds'),
360
+ 'compression_ratio': scene.get('compression_ratio'),
361
+ 'size_ratio': scene.get('size_ratio'),
362
+ 'encoding_success': True,
363
+ 'input_size_mb': scene.get('input_size_mb'),
364
+ 'encoded_file_size_mb': scene.get('encoded_file_size_mb')
365
+ }
366
+ training_data['pipeline_stages_data']['part3_ai_encoding'].append(part3_data)
367
+
368
+ # Extract model training data if available
369
+ model_data = scene.get('model_training_data', {})
370
+ if model_data:
371
+ model_features = {
372
+ 'scene_number': scene_num,
373
+ 'raw_video_features': model_data.get('raw_video_features', {}),
374
+ 'processed_video_features': model_data.get('processed_video_features', {}),
375
+ 'vmaf_model_features': model_data.get('vmaf_model_features', {}),
376
+ 'scene_classifier_features': model_data.get('scene_classifier_features', {}),
377
+ 'scene_classifier_probabilities': model_data.get('scene_classifier_probabilities', {}),
378
+ 'processing_timings': model_data.get('processing_timings', {})
379
+ }
380
+ training_data['model_training_features'].append(model_features)
381
+
382
+ # Extract prediction accuracy
383
+ prediction_acc = model_data.get('prediction_accuracy', {})
384
+ if prediction_acc:
385
+ training_data['prediction_accuracy_analysis'].append({
386
+ 'scene_number': scene_num,
387
+ 'predicted_vmaf': prediction_acc.get('predicted_vmaf'),
388
+ 'prediction_error': prediction_acc.get('prediction_error'),
389
+ 'target_vmaf': scene.get('target_vmaf'),
390
+ 'actual_vmaf': scene.get('actual_vmaf')
391
+ })
392
+
393
+ # Extract feature extraction performance
394
+ if model_data.get('feature_recovery_attempts'):
395
+ training_data['feature_extraction_performance'].append({
396
+ 'scene_number': scene_num,
397
+ 'recovery_attempts': model_data.get('feature_recovery_attempts', []),
398
+ 'feature_extraction_successful': bool(model_data.get('raw_video_features'))
399
+ })
400
+
401
+ # Scene classification results
402
+ if scene.get('scene_type'):
403
+ training_data['scene_classification_results'].append({
404
+ 'scene_number': scene_num,
405
+ 'classified_type': scene.get('scene_type'),
406
+ 'classification_confidence': scene.get('scene_classification_confidence'),
407
+ 'duration': scene.get('duration'),
408
+ 'file_size_mb': scene.get('file_size_mb')
409
+ })
410
+
411
+ # Encoding optimization data
412
+ training_data['encoding_optimization_data'].append({
413
+ 'scene_number': scene_num,
414
+ 'scene_type': scene.get('scene_type'),
415
+ 'optimal_cq': scene.get('optimal_cq'),
416
+ 'final_adjusted_cq': scene.get('final_adjusted_cq'),
417
+ 'conservative_adjustment': scene.get('conservative_cq_adjustment', 0),
418
+ 'size_protection_used': scene.get('size_protection_used', False),
419
+ 'compression_achieved': scene.get('compression_ratio', 0),
420
+ 'target_vmaf': scene.get('target_vmaf'),
421
+ 'predicted_vmaf': scene.get('predicted_vmaf_at_optimal_cq')
422
+ })
423
+
424
+ # Extract Part 4 data (VMAF calculation)
425
+ part4_data = {
426
+ 'scene_number': scene_num,
427
+ 'actual_vmaf': scene.get('actual_vmaf'),
428
+ 'vmaf_calculation_status': scene.get('vmaf_calculation_status'),
429
+ 'vmaf_calculation_time': scene.get('vmaf_calculation_time', 0),
430
+ 'vmaf_calculation_notes': scene.get('vmaf_calculation_notes'),
431
+ 'target_achieved': scene.get('actual_vmaf', 0) >= scene.get('target_vmaf', 0) if scene.get('actual_vmaf') else None
432
+ }
433
+ training_data['pipeline_stages_data']['part4_vmaf_calculation'].append(part4_data)
434
+
435
+ # VMAF validation data (detailed)
436
+ if scene.get('actual_vmaf') is not None:
437
+ vmaf_data = {
438
+ 'scene_number': scene_num,
439
+ 'actual_vmaf': scene.get('actual_vmaf'),
440
+ 'predicted_vmaf': scene.get('predicted_vmaf_at_optimal_cq'),
441
+ 'target_vmaf': scene.get('target_vmaf'),
442
+ 'vmaf_config_used': scene.get('vmaf_config_used', {}),
443
+ 'calculation_method': scene.get('vmaf_calculation_notes', '').split(' ')[0] if scene.get('vmaf_calculation_notes') else 'unknown'
444
+ }
445
+
446
+ # Add prediction analysis if available
447
+ if scene.get('vmaf_prediction_analysis'):
448
+ vmaf_data.update(scene['vmaf_prediction_analysis'])
449
+
450
+ training_data['vmaf_validation_data'].append(vmaf_data)
451
+
452
+ # Add Part 4 data (merging performance)
453
+ training_data['pipeline_stages_data']['part5_merging'] = {
454
+ 'total_scenes_processed': len(scenes_data),
455
+ 'successful_scenes': len([s for s in scenes_data if s.get('encoding_success')]),
456
+ 'merging_time': merge_time,
457
+ 'final_vmaf_calculation_time': final_vmaf_time,
458
+ 'total_part5_time': total_processing_time,
459
+ 'overall_compression_ratio': overall_compression_ratio
460
+ }
461
+
462
+ return training_data
463
+
464
+ # Extract training data
465
+ comprehensive_training_data = extract_training_data_from_scenes(successful_scenes)
466
 
 
467
  comprehensive_report = {
468
  'processing_summary': {
469
+ 'timestamp': datetime.now().isoformat(),
470
  'original_video_path': original_video_path,
471
  'final_video_path': final_output_path,
472
+ 'total_scenes_processed': len(successful_scenes),
473
+ 'failed_scenes_count': len(failed_scenes),
474
+ 'processing_time_seconds': total_processing_time,
475
+ 'vmaf_calculation_enabled': config.get('vmaf_calculation', {}).get('calculate_scene_vmaf', True)
 
 
 
 
 
 
 
476
  },
477
+
478
  'quality_metrics': {
479
+ 'final_vmaf_score': final_vmaf,
480
+ 'final_vmaf_calculated': calculate_full_video_vmaf,
481
+ 'final_vmaf_source': 'calculated' if calculate_full_video_vmaf else 'estimated_from_scenes' if scenes_with_valid_vmaf else 'unavailable',
482
  'average_scene_vmaf': average_scene_vmaf,
483
+ 'target_vmaf': target_vmaf,
484
+ 'target_achieved': (final_vmaf >= target_vmaf) if final_vmaf else False,
485
  'scenes_meeting_target': scenes_meeting_target,
486
+ 'scenes_with_vmaf_data': len(scenes_with_valid_vmaf),
487
+ 'vmaf_calculation_time': final_vmaf_time,
488
+ 'scene_vmaf_source': 'part4'
489
+ },
490
+
491
+ 'compression_metrics': {
492
+ 'total_input_size_mb': input_size_total,
493
+ 'total_output_size_mb': output_size_total,
494
+ 'size_ratio': output_size_total / input_size_total if input_size_total > 0 else 0,
495
+ 'overall_compression_ratio_percent': overall_compression_ratio,
496
+ 'final_file_size_mb': os.path.getsize(final_output_path) / (1024 * 1024) if os.path.exists(final_output_path) else 0
497
  },
498
+
499
  'scene_analysis': {
500
+ 'scene_types_distribution': {},
501
+ 'codec_usage': {},
502
+ 'prediction_accuracy_stats': {
503
+ 'scenes_meeting_target': scenes_meeting_target,
504
+ 'scenes_with_vmaf': len(scenes_with_valid_vmaf),
505
+ 'average_scene_vmaf': average_scene_vmaf
506
+ }
 
 
 
 
 
 
 
 
 
 
 
507
  },
 
 
 
 
 
 
 
 
 
 
508
 
509
+ 'processing_performance': {
510
+ 'part5_merging_time': merge_time,
511
+ 'part5_final_vmaf_time': final_vmaf_time,
512
+ 'part5_total_time': total_processing_time
513
+ },
514
 
515
+ 'individual_scenes_summary': [],
 
 
 
 
 
 
 
 
 
 
 
516
 
517
+ # βœ… ENHANCED: Add comprehensive training data directly extracted from scenes
518
+ 'comprehensive_training_data': comprehensive_training_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
 
 
 
 
 
 
521
 
522
+ # ===== SAVE REPORTS =====
523
+ if save_comprehensive_report:
524
+ if logging_enabled:
525
+ print(f" πŸ’Ύ Saving comprehensive processing report...")
 
 
 
526
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  try:
528
+ # βœ… SIMPLE: Extract original filename from the original_video_path parameter
529
+ original_name_from_path = os.path.splitext(os.path.basename(original_video_path))[0]
530
+
531
+ # Clean up temp prefixes if present
532
+ if original_name_from_path.startswith('tmp'):
533
+ # Try to get from config if available
534
+ config_original_name = config.get('output_settings', {}).get('original_video_name')
535
+ if config_original_name:
536
+ clean_name = config_original_name
537
+ else:
538
+ # Try to extract from custom filename in config
539
+ custom_filename = config.get('output_settings', {}).get('custom_output_filename')
540
+ if custom_filename and '_encoded_' in custom_filename:
541
+ clean_name = custom_filename.split('_encoded_')[0]
542
+ else:
543
+ clean_name = "video" # Generic fallback
544
+ else:
545
+ clean_name = original_name_from_path
546
+
547
+ report_filename = f"{clean_name}_encoding_report.json"
548
+ report_path = os.path.join(output_dir, report_filename)
549
+
550
+ if logging_enabled:
551
+ print(f" πŸ“ Report filename: {report_filename}")
552
+ print(f" πŸ“Š Extracted from: {os.path.basename(original_video_path)}")
553
+
554
+ with open(report_path, 'w') as f:
555
+ json.dump(comprehensive_report, f, indent=2, default=str)
556
+
557
+ if logging_enabled:
558
+ print(f" πŸ“„ Saved comprehensive report: {report_filename}")
559
  except Exception as e:
560
  if logging_enabled:
561
+ print(f" ❌ Failed to save comprehensive report: {e}")
562
+ # ===== CLEANUP TEMPORARY FILES =====
563
+ if cleanup_temp_files and temp_files_to_cleanup:
564
+ if logging_enabled:
565
+ print(f" 🧹 Cleaning up temporary files...")
566
+
567
+ cleanup_success = 0
568
+ cleanup_failed = 0
569
+
570
+ for temp_file in set(temp_files_to_cleanup):
571
+ try:
572
+ if os.path.exists(temp_file):
573
+ os.remove(temp_file)
574
+ cleanup_success += 1
575
+ if logging_enabled:
576
+ print(f" πŸ—‘οΈ Deleted: {os.path.basename(temp_file)}")
577
+ except Exception as e:
578
+ cleanup_failed += 1
579
+ if logging_enabled:
580
+ print(f" ❌ Failed to delete {os.path.basename(temp_file)}: {e}")
581
+
582
+ if logging_enabled:
583
+ print(f" βœ… Cleanup completed: {cleanup_success} files deleted, {cleanup_failed} failed")
584
 
585
+ # ===== FINAL SUMMARY =====
586
  if logging_enabled:
587
+ print(f"\nπŸŽ‰ --- Part 4 Completed Successfully ---")
588
+ print(f" πŸ“ Final video: {final_output_filename}")
589
+ if final_vmaf:
590
+ print(f" 🎯 Final VMAF: {final_vmaf:.2f} (target: {target_vmaf:.1f}) - {'βœ… ACHIEVED' if final_vmaf >= target_vmaf else '❌ MISSED'}")
591
+ print(f" πŸ—œοΈ Overall compression: {overall_compression_ratio:+.1f}%")
592
+ print(f" ⏱️ Total processing time: {total_processing_time:.1f}s")
593
+ print(f" πŸ“Š Scenes meeting target: {scenes_meeting_target}/{len(scenes_with_valid_vmaf)}")
594
+ print(f" πŸ” VMAF data from Part 4: {len(scenes_with_valid_vmaf)}/{len(successful_scenes)} scenes")
595
+
596
+ return final_output_path, final_vmaf, comprehensive_report
597
 
598
 
599
 
services/compress/video_preprocessor.py CHANGED
@@ -1,53 +1,8 @@
1
- """
2
- Video Pre-processing Module
3
-
4
- This module handles the initial video processing stage including:
5
- - Video validation and duration checks
6
- - Codec detection and handling
7
- - Lossless format conversion when necessary
8
- """
9
-
10
  import os
11
- from datetime import datetime
12
- from pathlib import Path
13
- from typing import Optional, Dict, Any
14
-
15
  from utils.video_utils import get_video_duration, get_video_codec
16
  from utils.encode_video import encode_lossless_video
17
 
18
-
19
- # ============================================================================
20
- # Constants
21
- # ============================================================================
22
-
23
- # Quality to VMAF score mapping
24
- QUALITY_VMAF_MAPPING = {
25
- 'High': 95.0,
26
- 'Medium': 90.0, # Updated from 93.0 to 90.0
27
- 'Low': 85.0 # Updated from 90.0 to 85.0
28
- }
29
-
30
- # Lossless codecs that require re-encoding
31
- LOSSLESS_CODECS = [
32
- 'ffv1', 'h264_lossless', 'utvideo', 'rawvideo',
33
- 'prores_ks', 'dnxhd', 'cineform'
34
- ]
35
-
36
- # Lossless file extensions
37
- LOSSLESS_EXTENSIONS = ['.y4m', '.yuv', '.raw']
38
-
39
-
40
- # ============================================================================
41
- # Main Function
42
- # ============================================================================
43
-
44
- def pre_processing(
45
- video_path: str,
46
- target_quality: str = 'Medium',
47
- codec: str = 'auto',
48
- max_duration: int = 60,
49
- output_dir: str = './output'
50
- ) -> Optional[Dict[str, Any]]:
51
  """
52
  Part 1: Initial video checks and lossless encoding if necessary.
53
 
@@ -59,88 +14,52 @@ def pre_processing(
59
  unmodified, while videos exceeding the maximum duration are rejected.
60
 
61
  Args:
62
- video_path: The full path to the input video file
63
- target_quality: Target quality level - 'High', 'Medium', or 'Low'
64
- Gets converted to VMAF scores: High=95, Medium=93, Low=90
65
- max_duration: Maximum allowed video duration in seconds. Default: 3600 (1 hour)
66
- output_dir: Directory for final output files. Default: './output'
67
- codec: Target encoding codec. Default: 'auto' (auto-detect best available)
68
- Options: 'auto', 'av1_nvenc', 'libx264', 'libx265', 'h264_nvenc', etc.
 
69
 
70
  Returns:
71
- dict or None: Dictionary containing video metadata and processing info if successful,
72
- otherwise None. Keys: 'path', 'codec', 'original_codec', 'duration',
73
- 'was_reencoded', 'encoding_time', 'target_vmaf', 'target_quality',
74
- 'directories'
75
  """
76
- # Resolve codec selection
77
- target_codec = _resolve_target_codec(codec)
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  # Validate and convert target quality
80
- target_quality, target_vmaf = _validate_target_quality(target_quality)
 
 
 
 
81
 
82
  print(f"🎯 Target quality: {target_quality} (VMAF: {target_vmaf})")
83
  print(f"πŸŽ₯ Target codec: {target_codec}")
84
 
85
- # Setup directories
86
- _setup_output_directory(output_dir)
87
- print(f"πŸ“ Output directory: {output_dir}")
88
-
89
- # Validate video duration
90
- duration = _validate_video_duration(video_path, max_duration)
91
- if duration is None:
92
- return None
93
-
94
- # Detect video codec
95
- original_codec = _detect_video_codec(video_path)
96
- if not original_codec:
97
- return None
98
-
99
- # Check if video needs lossless conversion
100
- is_lossless = _is_lossless_format(original_codec, video_path)
101
 
102
- if is_lossless:
103
- return _handle_lossless_conversion(
104
- video_path, original_codec, duration, target_vmaf,
105
- target_quality, target_codec, output_dir
106
- )
107
- else:
108
- return _handle_compressed_video(
109
- video_path, original_codec, duration, target_vmaf,
110
- target_quality, target_codec
111
- )
112
-
113
-
114
- # ============================================================================
115
- # Helper Functions
116
- # ============================================================================
117
-
118
- def _resolve_target_codec(codec: str) -> str:
119
- """Resolve auto codec selection to specific codec."""
120
- if codec.lower() == 'auto':
121
- return 'av1_nvenc' # Default to AV1 for best quality
122
- else:
123
- print(f"🎯 Using specified codec: {codec}")
124
- return codec
125
-
126
-
127
- def _validate_target_quality(target_quality: str) -> tuple[str, float]:
128
- """Validate target quality and convert to VMAF score."""
129
- if target_quality not in QUALITY_VMAF_MAPPING:
130
- print(f"⚠️ Invalid target quality '{target_quality}'. Using 'Medium' as fallback.")
131
- target_quality = 'Medium'
132
 
133
- target_vmaf = QUALITY_VMAF_MAPPING[target_quality]
134
- return target_quality, target_vmaf
135
-
136
-
137
- def _setup_output_directory(output_dir: str) -> None:
138
- """Create output directory if it doesn't exist."""
139
- os.makedirs(output_dir, exist_ok=True)
140
-
141
-
142
- def _validate_video_duration(video_path: str, max_duration: int) -> Optional[float]:
143
- """Validate video duration against maximum allowed duration."""
144
  print(f"⏱️ Checking video duration...")
145
  duration = get_video_duration(video_path)
146
 
@@ -153,11 +72,8 @@ def _validate_video_duration(video_path: str, max_duration: int) -> Optional[flo
153
  return None
154
 
155
  print(f"βœ… Video duration: {duration}s (within limit: {max_duration}s)")
156
- return duration
157
-
158
-
159
- def _detect_video_codec(video_path: str) -> Optional[str]:
160
- """Detect video codec from file."""
161
  print(f"πŸŽ₯ Checking video codec...")
162
  original_codec = get_video_codec(video_path)
163
 
@@ -166,99 +82,81 @@ def _detect_video_codec(video_path: str) -> Optional[str]:
166
  return None
167
 
168
  print(f"πŸŽ₯ Detected codec: {original_codec}")
169
- return original_codec
170
-
171
-
172
- def _is_lossless_format(codec: str, video_path: str) -> bool:
173
- """Check if video is in a lossless format."""
174
- return (
175
- codec.lower() in LOSSLESS_CODECS or
176
- any(video_path.lower().endswith(ext) for ext in LOSSLESS_EXTENSIONS)
177
- )
178
-
179
-
180
- def _handle_lossless_conversion(
181
- video_path: str,
182
- original_codec: str,
183
- duration: float,
184
- target_vmaf: float,
185
- target_quality: str,
186
- target_codec: str,
187
- output_dir: str
188
- ) -> Optional[Dict[str, Any]]:
189
- """Handle conversion of lossless video to standardized format."""
190
- print(f"πŸ”„ Video is in a lossless format ({original_codec}). Re-encoding with standardized lossless compression...")
191
-
192
- # Generate output filename with timestamp
193
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
194
- base_name = os.path.splitext(os.path.basename(video_path))[0]
195
- output_filename = f"{base_name}_lossless_{timestamp}.mkv"
196
- output_path = os.path.join(output_dir, output_filename)
197
 
198
- print(f"πŸ“ Lossless output: {output_filename}")
 
 
199
 
200
- # Perform lossless encoding
201
- encode_log, encode_time = encode_lossless_video(
202
- input_path=video_path,
203
- output_path=output_path,
204
- logging_enabled=True
205
  )
206
 
207
- if encode_log:
208
- print(f"βœ… Lossless encoding successful in {encode_time:.1f}s")
209
- print(f"πŸ“ Output at: {output_path}")
 
 
 
 
 
 
 
 
210
 
211
- # Return comprehensive metadata
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  return {
213
- 'path': output_path,
214
- 'codec': 'ffv1',
215
  'original_codec': original_codec,
216
  'duration': duration,
217
- 'was_reencoded': True,
218
- 'encoding_time': encode_time,
219
  'target_vmaf': target_vmaf,
220
  'target_quality': target_quality,
221
- 'target_codec': target_codec,
222
  'processing_info': {
223
- 'lossless_conversion': True,
224
  'original_format': original_codec,
225
- 'standardized_format': 'ffv1',
226
- 'container': 'mkv'
227
  }
228
  }
229
- else:
230
- print("❌ Lossless encoding failed.")
231
- return None
232
-
233
-
234
- def _handle_compressed_video(
235
- video_path: str,
236
- original_codec: str,
237
- duration: float,
238
- target_vmaf: float,
239
- target_quality: str,
240
- target_codec: str
241
- ) -> Dict[str, Any]:
242
- """Handle compressed video that doesn't need conversion."""
243
- print(f"βœ… Video is already compressed with codec: {original_codec}. Proceeding with original file.")
244
-
245
- # Return metadata for compressed video
246
- return {
247
- 'path': video_path,
248
- 'codec': original_codec,
249
- 'original_codec': original_codec,
250
- 'duration': duration,
251
- 'was_reencoded': False,
252
- 'encoding_time': 0,
253
- 'target_vmaf': target_vmaf,
254
- 'target_quality': target_quality,
255
- 'target_codec': target_codec,
256
- 'processing_info': {
257
- 'lossless_conversion': False,
258
- 'original_format': original_codec,
259
- 'standardized_format': original_codec,
260
- 'container': os.path.splitext(video_path)[1][1:] # Extension without dot
261
- }
262
- }
263
 
264
 
 
 
 
 
 
 
 
 
 
 
1
  import os
 
 
 
 
2
  from utils.video_utils import get_video_duration, get_video_codec
3
  from utils.encode_video import encode_lossless_video
4
 
5
+ def pre_processing(video_path, target_quality='Medium',codec='auto', max_duration=60, output_dir='./output'):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  """
7
  Part 1: Initial video checks and lossless encoding if necessary.
8
 
 
14
  unmodified, while videos exceeding the maximum duration are rejected.
15
 
16
  Args:
17
+ video_path (str): The full path to the input video file.
18
+ target_quality (str): Target quality level - 'High', 'Medium', or 'Low'.
19
+ Gets converted to VMAF scores: High=95, Medium=93, Low=90.
20
+ max_duration (int): Maximum allowed video duration in seconds. Default: 3600 (1 hour).
21
+ output_dir (str): Directory for final output files. Default: './output'.
22
+ codec (str): Target encoding codec. Default: 'auto' (auto-detect best available).
23
+ Options: 'auto', 'av1_nvenc', 'libx264', 'libx265', 'h264_nvenc', etc.
24
+
25
 
26
  Returns:
27
+ dict or None: Dictionary containing video metadata and processing info if successful, otherwise None.
28
+ Keys: 'path', 'codec', 'original_codec', 'duration', 'was_reencoded',
29
+ 'encoding_time', 'target_vmaf', 'target_quality', 'directories'
 
30
  """
31
+
32
+ # βœ… CODEC HANDLING: Resolve auto codec selection
33
+ if codec.lower() == 'auto':
34
+ target_codec = 'av1_nvenc' # Default to AV1 for best quality
35
+
36
+ else:
37
+ target_codec = codec
38
+ print(f"🎯 Using specified codec: {target_codec}")
39
+
40
+ # βœ… QUALITY MAPPING: Convert target quality to VMAF score
41
+ quality_vmaf_mapping = {
42
+ 'High': 95.0,
43
+ 'Medium': 90.0, # Updated from 93.0 to 90.0
44
+ 'Low': 85.0 # Updated from 90.0 to 85.0
45
+ }
46
 
47
  # Validate and convert target quality
48
+ if target_quality not in quality_vmaf_mapping:
49
+ print(f"⚠️ Invalid target quality '{target_quality}'. Using 'Medium' as fallback.")
50
+ target_quality = 'Medium'
51
+
52
+ target_vmaf = quality_vmaf_mapping[target_quality]
53
 
54
  print(f"🎯 Target quality: {target_quality} (VMAF: {target_vmaf})")
55
  print(f"πŸŽ₯ Target codec: {target_codec}")
56
 
57
+ # βœ… DIRECTORY SETUP: Create output and temp directories
58
+ os.makedirs(output_dir, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
+ print(f"πŸ“ Output directory: {output_dir}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
+ # βœ… DURATION CHECK: Validate video duration
 
 
 
 
 
 
 
 
 
 
63
  print(f"⏱️ Checking video duration...")
64
  duration = get_video_duration(video_path)
65
 
 
72
  return None
73
 
74
  print(f"βœ… Video duration: {duration}s (within limit: {max_duration}s)")
75
+
76
+ # CODEC CHECK: Determine video codec
 
 
 
77
  print(f"πŸŽ₯ Checking video codec...")
78
  original_codec = get_video_codec(video_path)
79
 
 
82
  return None
83
 
84
  print(f"πŸŽ₯ Detected codec: {original_codec}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
+ # LOSSLESS CODEC HANDLING: Define lossless codecs and special cases
87
+ lossless_codecs = ['ffv1', 'h264_lossless', 'utvideo', 'rawvideo', 'prores_ks', 'dnxhd', 'cineform']
88
+ lossless_extensions = ['.y4m', '.yuv', '.raw']
89
 
90
+ is_lossless = (
91
+ original_codec.lower() in lossless_codecs or
92
+ any(video_path.lower().endswith(ext) for ext in lossless_extensions)
 
 
93
  )
94
 
95
+ if is_lossless:
96
+ print(f"πŸ”„ Video is in a lossless format ({original_codec}). Re-encoding with standardized lossless compression...")
97
+
98
+ # Generate output filename with timestamp to avoid conflicts
99
+ from datetime import datetime
100
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
101
+ base_name = os.path.splitext(os.path.basename(video_path))[0]
102
+ output_filename = f"{base_name}_lossless_{timestamp}.mkv"
103
+ output_path = os.path.join(output_dir, output_filename)
104
+
105
+ print(f"πŸ“ Lossless output: {output_filename}")
106
 
107
+ # βœ… IMPROVED: Use direct FFmpeg command for lossless encoding
108
+ # This bypasses the main encode_video function which expects lossy codecs
109
+ encode_log, encode_time = encode_lossless_video(
110
+ input_path=video_path,
111
+ output_path=output_path,
112
+ logging_enabled=True
113
+ )
114
+
115
+ if encode_log:
116
+ print(f"βœ… Lossless encoding successful in {encode_time:.1f}s")
117
+ print(f"πŸ“ Output at: {output_path}")
118
+
119
+ # Return comprehensive metadata
120
+ return {
121
+ 'path': output_path,
122
+ 'codec': 'ffv1',
123
+ 'original_codec': original_codec,
124
+ 'duration': duration,
125
+ 'was_reencoded': True,
126
+ 'encoding_time': encode_time,
127
+ 'target_vmaf': target_vmaf,
128
+ 'target_quality': target_quality,
129
+ 'target_codec': target_codec,
130
+ 'processing_info': {
131
+ 'lossless_conversion': True,
132
+ 'original_format': original_codec,
133
+ 'standardized_format': 'ffv1',
134
+ 'container': 'mkv'
135
+ }
136
+ }
137
+ else:
138
+ print("❌ Lossless encoding failed.")
139
+ return None
140
+ else:
141
+ print(f"βœ… Video is already compressed with codec: {original_codec}. Proceeding with original file.")
142
+
143
+ # Return metadata for compressed video
144
  return {
145
+ 'path': video_path,
146
+ 'codec': original_codec,
147
  'original_codec': original_codec,
148
  'duration': duration,
149
+ 'was_reencoded': False,
150
+ 'encoding_time': 0,
151
  'target_vmaf': target_vmaf,
152
  'target_quality': target_quality,
153
+ 'target_codec': target_codec, # βœ… Added target codec
154
  'processing_info': {
155
+ 'lossless_conversion': False,
156
  'original_format': original_codec,
157
+ 'standardized_format': original_codec,
158
+ 'container': os.path.splitext(video_path)[1][1:] # Extension without dot
159
  }
160
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162