christopi commited on
Commit Β·
3108782
1
Parent(s): 0d5d682
update
Browse files- services/compress/scene_detector.py +508 -278
- services/compress/validator_merger.py +453 -484
- services/compress/video_preprocessor.py +100 -202
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 |
-
|
| 11 |
-
|
| 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 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 105 |
-
|
| 106 |
-
)
|
|
|
|
|
|
|
| 107 |
elif detection_mode == 'adaptive':
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
else:
|
| 112 |
print(f" β Unknown scene detection mode: {detection_mode}. Falling back to adaptive.")
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
| 115 |
)
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 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"
|
| 171 |
-
return
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
# Calculate scene boundaries, preferring keyframe alignment
|
| 210 |
-
scene_boundaries =
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
-
#
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
scene_number = i + 1
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
if logging_enabled:
|
| 221 |
-
print(f"
|
| 222 |
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
if logging_enabled:
|
| 235 |
-
print(f" β
|
| 236 |
|
| 237 |
-
return
|
| 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 |
-
|
| 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 |
-
|
| 256 |
-
|
|
|
|
| 257 |
Returns:
|
| 258 |
list: List of scene metadata dictionaries
|
| 259 |
"""
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
| 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 |
-
|
| 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"
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
if logging_enabled:
|
| 310 |
-
print(f"
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
while current_time < total_duration:
|
| 326 |
-
end_time = min(current_time + segment_duration, total_duration)
|
| 327 |
|
| 328 |
-
#
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
if logging_enabled:
|
| 334 |
-
print(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
"""
|
| 356 |
-
Create
|
|
|
|
| 357 |
|
| 358 |
Args:
|
| 359 |
-
scene_files: List of scene file paths
|
| 360 |
-
|
| 361 |
-
config: Configuration dictionary
|
| 362 |
-
|
| 363 |
Returns:
|
| 364 |
list: List of scene metadata dictionaries
|
| 365 |
"""
|
| 366 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
for i, scene_file in enumerate(scene_files):
|
| 369 |
scene_number = i + 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
-
#
|
| 372 |
-
|
| 373 |
|
| 374 |
-
|
| 375 |
'path': scene_file,
|
| 376 |
'scene_number': scene_number,
|
| 377 |
-
'start_time':
|
| 378 |
-
'end_time':
|
| 379 |
-
'duration':
|
| 380 |
-
'original_video_metadata':
|
| 381 |
-
'
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 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 |
-
#
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
-
|
| 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"
|
| 208 |
-
return
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 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"
|
| 230 |
|
| 231 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
final_output_path = os.path.join(output_dir, final_output_filename)
|
| 235 |
|
| 236 |
if logging_enabled:
|
| 237 |
-
print(f"
|
| 238 |
|
| 239 |
-
#
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
|
|
|
| 245 |
|
| 246 |
try:
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
|
|
|
| 254 |
|
| 255 |
merge_time = time.time() - merge_start_time
|
| 256 |
-
signal.alarm(0) # Cancel timeout
|
| 257 |
|
| 258 |
-
if not merge_success
|
| 259 |
if logging_enabled:
|
| 260 |
-
print(f" β Video merging failed")
|
| 261 |
-
return None,
|
| 262 |
|
| 263 |
-
#
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
except TimeoutError:
|
| 273 |
-
signal.alarm(0)
|
| 274 |
if logging_enabled:
|
| 275 |
-
print(f" β Video merging timed out after
|
| 276 |
-
return None,
|
| 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,
|
| 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 |
-
|
| 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 |
-
|
| 352 |
-
|
| 353 |
|
| 354 |
-
|
| 355 |
-
|
| 356 |
|
| 357 |
-
|
| 358 |
-
|
| 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 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
else:
|
| 382 |
-
|
| 383 |
-
|
| 384 |
|
| 385 |
# Estimate final VMAF from scene averages if available
|
| 386 |
-
if
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
print(f" βοΈ No scene VMAF data available for estimation")
|
| 395 |
|
| 396 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
'
|
| 436 |
-
'
|
| 437 |
-
'
|
| 438 |
-
'
|
| 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 |
-
'
|
|
|
|
|
|
|
| 449 |
'average_scene_vmaf': average_scene_vmaf,
|
|
|
|
|
|
|
| 450 |
'scenes_meeting_target': scenes_meeting_target,
|
| 451 |
-
'
|
| 452 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
},
|
|
|
|
| 454 |
'scene_analysis': {
|
| 455 |
-
'
|
| 456 |
-
'
|
| 457 |
-
'
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 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 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
|
| 491 |
-
|
| 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 |
-
#
|
| 505 |
-
|
| 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 |
-
#
|
| 594 |
-
if
|
| 595 |
-
|
| 596 |
-
|
| 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 |
-
|
| 621 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
except Exception as e:
|
| 623 |
if logging_enabled:
|
| 624 |
-
print(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
|
|
|
|
| 626 |
if logging_enabled:
|
| 627 |
-
print(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
|
|
|
| 69 |
|
| 70 |
Returns:
|
| 71 |
-
dict or None: Dictionary containing video metadata and processing info if successful,
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
'directories'
|
| 75 |
"""
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
# Validate and convert target quality
|
| 80 |
-
target_quality
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
print(f"π― Target quality: {target_quality} (VMAF: {target_vmaf})")
|
| 83 |
print(f"π₯ Target codec: {target_codec}")
|
| 84 |
|
| 85 |
-
#
|
| 86 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 199 |
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
output_path=output_path,
|
| 204 |
-
logging_enabled=True
|
| 205 |
)
|
| 206 |
|
| 207 |
-
if
|
| 208 |
-
print(f"
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
return {
|
| 213 |
-
'path':
|
| 214 |
-
'codec':
|
| 215 |
'original_codec': original_codec,
|
| 216 |
'duration': duration,
|
| 217 |
-
'was_reencoded':
|
| 218 |
-
'encoding_time':
|
| 219 |
'target_vmaf': target_vmaf,
|
| 220 |
'target_quality': target_quality,
|
| 221 |
-
'target_codec': target_codec,
|
| 222 |
'processing_info': {
|
| 223 |
-
'lossless_conversion':
|
| 224 |
'original_format': original_codec,
|
| 225 |
-
'standardized_format':
|
| 226 |
-
'container':
|
| 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 |
|