christopi commited on
Commit ·
c6e7086
1
Parent(s): 75f767e
update configs
Browse files- services/compress/encoder.py +116 -16
- services/compress/scene_detector.py +267 -24
- services/compress/server.py +78 -20
- services/compress/utils/__init__.py +10 -1
- services/compress/utils/encode_video.py +237 -114
- services/compress/utils/encoder_configs.py +173 -101
- services/compress/utils/merge_videos.py +72 -10
- services/compress/utils/processing_utils.py +2 -1
- services/compress/utils/video_utils.py +37 -0
- services/compress/validator_merger.py +0 -2
- services/compress/video_preprocessor.py +5 -3
services/compress/encoder.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import os
|
|
|
|
| 2 |
import torch
|
| 3 |
import time
|
| 4 |
import subprocess
|
|
@@ -140,7 +141,7 @@ def get_cq_from_lookup_table(scene_type, config, target_vmaf=None, target_qualit
|
|
| 140 |
# "class_mapping": class_mapping
|
| 141 |
# }
|
| 142 |
|
| 143 |
-
def get_scalers_from_pipeline(pipeline_path='
|
| 144 |
verbose=True, logging_enabled=True):
|
| 145 |
"""
|
| 146 |
Load preprocessing pipeline and extract individual components with verbosity control.
|
|
@@ -174,15 +175,19 @@ def get_scalers_from_pipeline(pipeline_path='src/models/preprocessing_pipeline.p
|
|
| 174 |
Returns (None, None, None, None, None) if loading fails
|
| 175 |
"""
|
| 176 |
|
| 177 |
-
#
|
|
|
|
| 178 |
try:
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
| 183 |
except ImportError as e:
|
| 184 |
if logging_enabled:
|
| 185 |
-
print(f"❌ ERROR: Could not
|
|
|
|
| 186 |
return None, None, None, None, None
|
| 187 |
|
| 188 |
# Control verbose output with master logging flag
|
|
@@ -200,7 +205,7 @@ def get_scalers_from_pipeline(pipeline_path='src/models/preprocessing_pipeline.p
|
|
| 200 |
if not os.path.exists(pipeline_path):
|
| 201 |
if actual_verbose:
|
| 202 |
print(f"❌ ERROR: Pipeline file not found at '{pipeline_path}'")
|
| 203 |
-
print(f" 💡 Suggestion: Ensure preprocessing pipeline exists in
|
| 204 |
return None, None, None, None, None
|
| 205 |
|
| 206 |
try:
|
|
@@ -208,8 +213,88 @@ def get_scalers_from_pipeline(pipeline_path='src/models/preprocessing_pipeline.p
|
|
| 208 |
if actual_verbose:
|
| 209 |
print(f"📖 Loading preprocessing pipeline from: {pipeline_path}")
|
| 210 |
|
| 211 |
-
with
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
if actual_verbose:
|
| 215 |
print(f"✅ Pipeline loaded successfully")
|
|
@@ -428,8 +513,8 @@ def load_encoding_resources(config, logging_enabled=True):
|
|
| 428 |
"""
|
| 429 |
# Get model file paths from config
|
| 430 |
model_paths = config.get('model_paths', {})
|
| 431 |
-
scene_model_path = model_paths.get('scene_classifier_model', "
|
| 432 |
-
preprocessing_pipeline_path = model_paths.get('preprocessing_pipeline', "
|
| 433 |
|
| 434 |
# Choose GPU if available, otherwise use CPU
|
| 435 |
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
|
@@ -761,6 +846,11 @@ def ai_encoding(scene_metadata, config, resources, target_vmaf=None, target_qual
|
|
| 761 |
except Exception:
|
| 762 |
scene_duration = 1.0
|
| 763 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
# Get original video information for codec selection
|
| 765 |
original_video_metadata = scene_metadata.get('original_video_metadata', {})
|
| 766 |
|
|
@@ -791,6 +881,9 @@ def ai_encoding(scene_metadata, config, resources, target_vmaf=None, target_qual
|
|
| 791 |
target_vmaf = safe_float(target_vmaf or original_video_metadata.get('target_vmaf') or
|
| 792 |
config.get('video_processing', {}).get('target_vmaf', 0.0), 0.0)
|
| 793 |
|
|
|
|
|
|
|
|
|
|
| 794 |
# Choose encoding codec (prioritize config over auto-detection)
|
| 795 |
original_codec = original_video_metadata.get('original_codec', 'unknown')
|
| 796 |
target_codec_from_part1 = original_video_metadata.get('target_codec', 'auto')
|
|
@@ -931,10 +1024,17 @@ def ai_encoding(scene_metadata, config, resources, target_vmaf=None, target_qual
|
|
| 931 |
}
|
| 932 |
|
| 933 |
# ENCODING
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 938 |
os.makedirs(temp_dir, exist_ok=True)
|
| 939 |
|
| 940 |
size_increase_protection = config.get('video_processing', {}).get('size_increase_protection', True)
|
|
|
|
| 1 |
import os
|
| 2 |
+
import sys
|
| 3 |
import torch
|
| 4 |
import time
|
| 5 |
import subprocess
|
|
|
|
| 141 |
# "class_mapping": class_mapping
|
| 142 |
# }
|
| 143 |
|
| 144 |
+
def get_scalers_from_pipeline(pipeline_path='services/compress/models/preprocessing_pipeline.pkl',
|
| 145 |
verbose=True, logging_enabled=True):
|
| 146 |
"""
|
| 147 |
Load preprocessing pipeline and extract individual components with verbosity control.
|
|
|
|
| 175 |
Returns (None, None, None, None, None) if loading fails
|
| 176 |
"""
|
| 177 |
|
| 178 |
+
# The preprocessing classes are now imported through utils module
|
| 179 |
+
# This ensures they're available when pickle tries to load the pipeline
|
| 180 |
try:
|
| 181 |
+
# Verify that the classes are accessible (this will fail if there's an import issue)
|
| 182 |
+
from utils import ColumnDropper, VMAFScaler, TargetExtractor, CQScaler, ResolutionTransformer, FeatureScaler, FrameRateTransformer
|
| 183 |
+
|
| 184 |
+
if logging_enabled:
|
| 185 |
+
print(f"✅ Preprocessing classes are available through utils module")
|
| 186 |
+
|
| 187 |
except ImportError as e:
|
| 188 |
if logging_enabled:
|
| 189 |
+
print(f"❌ ERROR: Could not access preprocessing classes: {e}")
|
| 190 |
+
print(f" 💡 Check that utils/__init__.py properly imports data_preprocessing classes")
|
| 191 |
return None, None, None, None, None
|
| 192 |
|
| 193 |
# Control verbose output with master logging flag
|
|
|
|
| 205 |
if not os.path.exists(pipeline_path):
|
| 206 |
if actual_verbose:
|
| 207 |
print(f"❌ ERROR: Pipeline file not found at '{pipeline_path}'")
|
| 208 |
+
print(f" 💡 Suggestion: Ensure preprocessing pipeline exists in /models/ directory")
|
| 209 |
return None, None, None, None, None
|
| 210 |
|
| 211 |
try:
|
|
|
|
| 213 |
if actual_verbose:
|
| 214 |
print(f"📖 Loading preprocessing pipeline from: {pipeline_path}")
|
| 215 |
|
| 216 |
+
# Load the pickled preprocessing pipeline with custom unpickler to handle module namespace issues
|
| 217 |
+
class CustomUnpickler(pickle.Unpickler):
|
| 218 |
+
def find_class(self, module, name):
|
| 219 |
+
# if actual_verbose:
|
| 220 |
+
# print(f" 🔍 Unpickler looking for: {module}.{name}")
|
| 221 |
+
|
| 222 |
+
# Handle the case where classes were defined in __main__ context
|
| 223 |
+
if module == '__main__':
|
| 224 |
+
if actual_verbose:
|
| 225 |
+
print(f" 🔧 Handling __main__ context for class: {name}")
|
| 226 |
+
# Try to get the class from utils module
|
| 227 |
+
try:
|
| 228 |
+
from utils import ColumnDropper, VMAFScaler, TargetExtractor, CQScaler, ResolutionTransformer, FeatureScaler, FrameRateTransformer
|
| 229 |
+
class_map = {
|
| 230 |
+
'ColumnDropper': ColumnDropper,
|
| 231 |
+
'VMAFScaler': VMAFScaler,
|
| 232 |
+
'TargetExtractor': TargetExtractor,
|
| 233 |
+
'CQScaler': CQScaler,
|
| 234 |
+
'ResolutionTransformer': ResolutionTransformer,
|
| 235 |
+
'FeatureScaler': FeatureScaler,
|
| 236 |
+
'FrameRateTransformer': FrameRateTransformer
|
| 237 |
+
}
|
| 238 |
+
if name in class_map:
|
| 239 |
+
if actual_verbose:
|
| 240 |
+
print(f" ✅ Found {name} in utils module")
|
| 241 |
+
return class_map[name]
|
| 242 |
+
else:
|
| 243 |
+
if actual_verbose:
|
| 244 |
+
print(f" ❌ Class {name} not found in utils module")
|
| 245 |
+
except ImportError as e:
|
| 246 |
+
if actual_verbose:
|
| 247 |
+
print(f" ❌ Failed to import from utils: {e}")
|
| 248 |
+
|
| 249 |
+
# Handle utils.data_preprocessing module context
|
| 250 |
+
elif module == 'utils.data_preprocessing':
|
| 251 |
+
if actual_verbose:
|
| 252 |
+
print(f" 🔧 Handling utils.data_preprocessing context for class: {name}")
|
| 253 |
+
try:
|
| 254 |
+
from utils import ColumnDropper, VMAFScaler, TargetExtractor, CQScaler, ResolutionTransformer, FeatureScaler, FrameRateTransformer
|
| 255 |
+
class_map = {
|
| 256 |
+
'ColumnDropper': ColumnDropper,
|
| 257 |
+
'VMAFScaler': VMAFScaler,
|
| 258 |
+
'TargetExtractor': TargetExtractor,
|
| 259 |
+
'CQScaler': CQScaler,
|
| 260 |
+
'ResolutionTransformer': ResolutionTransformer,
|
| 261 |
+
'FeatureScaler': FeatureScaler,
|
| 262 |
+
'FrameRateTransformer': FrameRateTransformer
|
| 263 |
+
}
|
| 264 |
+
if name in class_map:
|
| 265 |
+
if actual_verbose:
|
| 266 |
+
print(f" ✅ Found {name} in utils module")
|
| 267 |
+
return class_map[name]
|
| 268 |
+
else:
|
| 269 |
+
if actual_verbose:
|
| 270 |
+
print(f" ❌ Class {name} not found in utils module")
|
| 271 |
+
except ImportError as e:
|
| 272 |
+
if actual_verbose:
|
| 273 |
+
print(f" ❌ Failed to import from utils: {e}")
|
| 274 |
+
|
| 275 |
+
# For other modules, use the default behavior
|
| 276 |
+
if actual_verbose:
|
| 277 |
+
print(f" 🔧 Using default unpickler for {module}.{name}")
|
| 278 |
+
return super().find_class(module, name)
|
| 279 |
+
|
| 280 |
+
# Try to load with custom unpickler
|
| 281 |
+
try:
|
| 282 |
+
with open(pipeline_path, 'rb') as f:
|
| 283 |
+
unpickler = CustomUnpickler(f)
|
| 284 |
+
pipeline_obj = unpickler.load()
|
| 285 |
+
if actual_verbose:
|
| 286 |
+
print(f" ✅ Pipeline loaded with custom unpickler")
|
| 287 |
+
except Exception as e:
|
| 288 |
+
if actual_verbose:
|
| 289 |
+
print(f" ❌ Custom unpickler failed: {e}")
|
| 290 |
+
print(f" 🔧 Attempting to load with standard pickle...")
|
| 291 |
+
|
| 292 |
+
# Fallback to standard pickle loading
|
| 293 |
+
with open(pipeline_path, 'rb') as f:
|
| 294 |
+
pipeline_obj = pickle.load(f)
|
| 295 |
+
|
| 296 |
+
if actual_verbose:
|
| 297 |
+
print(f" ✅ Pipeline loaded with standard pickle")
|
| 298 |
|
| 299 |
if actual_verbose:
|
| 300 |
print(f"✅ Pipeline loaded successfully")
|
|
|
|
| 513 |
"""
|
| 514 |
# Get model file paths from config
|
| 515 |
model_paths = config.get('model_paths', {})
|
| 516 |
+
scene_model_path = model_paths.get('scene_classifier_model', "services/compress/models/scene_classifier_model.pth")
|
| 517 |
+
preprocessing_pipeline_path = model_paths.get('preprocessing_pipeline', "services/compress/models/preprocessing_pipeline.pkl")
|
| 518 |
|
| 519 |
# Choose GPU if available, otherwise use CPU
|
| 520 |
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
|
|
|
| 846 |
except Exception:
|
| 847 |
scene_duration = 1.0
|
| 848 |
|
| 849 |
+
# Check for very short scenes that might cause stuttering
|
| 850 |
+
is_very_short_scene = scene_duration < 1.0
|
| 851 |
+
if is_very_short_scene and logging_enabled:
|
| 852 |
+
print(f" ⚠️ Very short scene detected ({scene_duration:.1f}s) - using special handling")
|
| 853 |
+
|
| 854 |
# Get original video information for codec selection
|
| 855 |
original_video_metadata = scene_metadata.get('original_video_metadata', {})
|
| 856 |
|
|
|
|
| 881 |
target_vmaf = safe_float(target_vmaf or original_video_metadata.get('target_vmaf') or
|
| 882 |
config.get('video_processing', {}).get('target_vmaf', 0.0), 0.0)
|
| 883 |
|
| 884 |
+
|
| 885 |
+
print("original video metadata", original_video_metadata)
|
| 886 |
+
|
| 887 |
# Choose encoding codec (prioritize config over auto-detection)
|
| 888 |
original_codec = original_video_metadata.get('original_codec', 'unknown')
|
| 889 |
target_codec_from_part1 = original_video_metadata.get('target_codec', 'auto')
|
|
|
|
| 1024 |
}
|
| 1025 |
|
| 1026 |
# ENCODING
|
| 1027 |
+
# Use more precise filename for very short scenes
|
| 1028 |
+
if scene_duration < 1.0:
|
| 1029 |
+
output_scene_path = os.path.join(
|
| 1030 |
+
temp_dir,
|
| 1031 |
+
f"encoded_scene_{scene_number:03d}_{start_time:.1f}s-{end_time:.1f}s_{codec.lower()}.mp4"
|
| 1032 |
+
)
|
| 1033 |
+
else:
|
| 1034 |
+
output_scene_path = os.path.join(
|
| 1035 |
+
temp_dir,
|
| 1036 |
+
f"encoded_scene_{scene_number:03d}_{start_time:.0f}s-{end_time:.0f}s_{codec.lower()}.mp4"
|
| 1037 |
+
)
|
| 1038 |
os.makedirs(temp_dir, exist_ok=True)
|
| 1039 |
|
| 1040 |
size_increase_protection = config.get('video_processing', {}).get('size_increase_protection', True)
|
services/compress/scene_detector.py
CHANGED
|
@@ -3,6 +3,7 @@ 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 |
|
| 7 |
def scene_detection(video_metadata):
|
| 8 |
"""
|
|
@@ -56,7 +57,8 @@ def scene_detection(video_metadata):
|
|
| 56 |
duration = video_metadata['duration']
|
| 57 |
codec = video_metadata['codec']
|
| 58 |
original_codec = video_metadata.get('original_codec', codec)
|
| 59 |
-
target_codec = video_metadata.get('target_codec', 'auto')
|
|
|
|
| 60 |
was_reencoded = video_metadata.get('was_reencoded', False)
|
| 61 |
target_vmaf = video_metadata.get('target_vmaf', 93.0)
|
| 62 |
target_quality = video_metadata.get('target_quality', 'Medium')
|
|
@@ -82,33 +84,72 @@ def scene_detection(video_metadata):
|
|
| 82 |
'duration': duration,
|
| 83 |
'original_video_metadata': video_metadata # ✅ Pass complete metadata including target_codec
|
| 84 |
}]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
if scene_files and len(scene_files) > 1:
|
| 97 |
-
print(f" 📄 Created {len(scene_files)} scene files")
|
| 98 |
-
return create_scene_metadata_from_files(scene_files, video_metadata, config)
|
| 99 |
else:
|
| 100 |
-
print(f"
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
else:
|
| 103 |
-
print(f"
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
else:
|
| 111 |
-
print(f"
|
| 112 |
return [{
|
| 113 |
'path': video_path,
|
| 114 |
'scene_number': 1,
|
|
@@ -118,6 +159,208 @@ def scene_detection(video_metadata):
|
|
| 118 |
'original_video_metadata': video_metadata # ✅ Pass complete metadata including target_codec
|
| 119 |
}]
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
def time_based_splitting(video_path, temp_dir, config, duration, original_metadata, logging_enabled=True):
|
| 122 |
"""
|
| 123 |
Split video into scenes and return metadata for each scene.
|
|
|
|
| 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 |
"""
|
|
|
|
| 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')
|
|
|
|
| 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,
|
|
|
|
| 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
|
| 176 |
+
"""
|
| 177 |
+
if logging_enabled:
|
| 178 |
+
print(f"\n--- Force time-based splitting: {segment_duration}s segments ---")
|
| 179 |
+
print(f" 🎥 Source codec: {original_metadata.get('codec', 'unknown')}")
|
| 180 |
+
print(f" ⏱️ Total duration: {total_duration:.1f}s")
|
| 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.
|
services/compress/server.py
CHANGED
|
@@ -156,8 +156,8 @@ def video_compressor(input_file, target_quality='Medium', max_duration=3600, out
|
|
| 156 |
print("⚠️ Config file not found, using default configuration")
|
| 157 |
config = {
|
| 158 |
'directories': {
|
| 159 |
-
'temp_dir':
|
| 160 |
-
'output_dir':
|
| 161 |
},
|
| 162 |
'video_processing': {
|
| 163 |
'SHORT_VIDEO_THRESHOLD': 20,
|
|
@@ -166,7 +166,31 @@ def video_compressor(input_file, target_quality='Medium', max_duration=3600, out
|
|
| 166 |
'size_increase_protection': True,
|
| 167 |
'conservative_cq_adjustment': 2,
|
| 168 |
'max_output_size_ratio': 1.15,
|
| 169 |
-
'max_encoding_retries': 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
},
|
| 171 |
'scene_detection': {
|
| 172 |
'enable_time_based_fallback': True,
|
|
@@ -181,12 +205,17 @@ def video_compressor(input_file, target_quality='Medium', max_duration=3600, out
|
|
| 181 |
'output_settings': {
|
| 182 |
'save_individual_scene_reports': True,
|
| 183 |
'save_comprehensive_report': True
|
|
|
|
|
|
|
|
|
|
| 184 |
}
|
| 185 |
}
|
| 186 |
|
| 187 |
-
# Update output directory from arguments
|
| 188 |
config['directories']['output_dir'] = str(output_dir_path)
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
| 190 |
# Create temp directory if it doesn't exist
|
| 191 |
temp_dir = Path(config['directories']['temp_dir'])
|
| 192 |
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -208,6 +237,8 @@ def video_compressor(input_file, target_quality='Medium', max_duration=3600, out
|
|
| 208 |
max_duration=max_duration,
|
| 209 |
output_dir=output_dir_path
|
| 210 |
)
|
|
|
|
|
|
|
| 211 |
|
| 212 |
part1_time = time.time() - part1_start_time
|
| 213 |
|
|
@@ -231,6 +262,8 @@ def video_compressor(input_file, target_quality='Medium', max_duration=3600, out
|
|
| 231 |
part2_start_time = time.time()
|
| 232 |
|
| 233 |
scenes_metadata = scene_detection(part1_result)
|
|
|
|
|
|
|
| 234 |
|
| 235 |
part2_time = time.time() - part2_start_time
|
| 236 |
|
|
@@ -255,17 +288,34 @@ def video_compressor(input_file, target_quality='Medium', max_duration=3600, out
|
|
| 255 |
if total_scene_size > 0:
|
| 256 |
print(f" 📊 Total scene files: {total_scene_size:.1f} MB")
|
| 257 |
|
| 258 |
-
# ✅ PART 3: AI Encoding (Multiple Scene Processing)
|
| 259 |
-
print(f"\n
|
| 260 |
part3_start_time = time.time()
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
print(f" 🔧 Loading AI models and resources...")
|
| 263 |
|
| 264 |
try:
|
| 265 |
resources = load_encoding_resources(config, logging_enabled=True)
|
| 266 |
-
print(f" ✅ AI resources loaded successfully")
|
|
|
|
| 267 |
except Exception as e:
|
| 268 |
-
print(f" ❌ Failed to load AI resources: {e}")
|
| 269 |
return False
|
| 270 |
|
| 271 |
# Process each scene individually
|
|
@@ -275,7 +325,7 @@ def video_compressor(input_file, target_quality='Medium', max_duration=3600, out
|
|
| 275 |
total_input_size = 0
|
| 276 |
total_output_size = 0
|
| 277 |
|
| 278 |
-
print(f"\n 📊 Processing {len(scenes_metadata)} scenes
|
| 279 |
|
| 280 |
for i, scene_metadata in enumerate(scenes_metadata):
|
| 281 |
scene_number = scene_metadata['scene_number']
|
|
@@ -284,17 +334,21 @@ def video_compressor(input_file, target_quality='Medium', max_duration=3600, out
|
|
| 284 |
|
| 285 |
print(f"\n 🎬 Scene {scene_number}/{len(scenes_metadata)}: {os.path.basename(scene_path)}")
|
| 286 |
print(f" ⏱️ Duration: {scene_duration:.1f}s")
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
scene_start_time = time.time()
|
| 290 |
|
| 291 |
try:
|
| 292 |
-
# ✅
|
| 293 |
encoded_path, scene_data = ai_encoding(
|
| 294 |
scene_metadata=scene_metadata,
|
| 295 |
config=config,
|
| 296 |
resources=resources,
|
| 297 |
-
target_vmaf=None, #
|
|
|
|
| 298 |
logging_enabled=True
|
| 299 |
)
|
| 300 |
|
|
@@ -313,7 +367,9 @@ def video_compressor(input_file, target_quality='Medium', max_duration=3600, out
|
|
| 313 |
print(f" 📁 Output: {os.path.basename(encoded_path)}")
|
| 314 |
print(f" 📊 Size: {input_size_mb:.1f} MB → {size_mb:.1f} MB ({compression:+.1f}% compression)")
|
| 315 |
print(f" 🎭 Scene type: {scene_data.get('scene_type', 'unknown')}")
|
| 316 |
-
print(f"
|
|
|
|
|
|
|
| 317 |
print(f" ⏱️ Processing: {scene_processing_time:.1f}s")
|
| 318 |
|
| 319 |
# Update scene metadata with encoded path for Part 4
|
|
@@ -357,11 +413,13 @@ def video_compressor(input_file, target_quality='Medium', max_duration=3600, out
|
|
| 357 |
part3_time = time.time() - part3_start_time
|
| 358 |
|
| 359 |
# ✅ PART 3 SUMMARY
|
| 360 |
-
print(f"\n 📊 Part 3 Processing Summary:")
|
| 361 |
print(f" ✅ Successful encodings: {successful_encodings}")
|
| 362 |
print(f" ❌ Failed encodings: {failed_encodings}")
|
| 363 |
print(f" 📈 Success rate: {successful_encodings/len(scenes_metadata)*100:.1f}%")
|
| 364 |
print(f" ⏱️ Total processing time: {part3_time:.1f}s")
|
|
|
|
|
|
|
| 365 |
|
| 366 |
if successful_encodings == 0:
|
| 367 |
print("❌ Part 3 failed completely. No scenes were encoded. Pipeline terminated.")
|
|
@@ -532,13 +590,13 @@ def test_video_compression(video_path: str):
|
|
| 532 |
if __name__ == "__main__":
|
| 533 |
import uvicorn
|
| 534 |
|
| 535 |
-
logger.info("Starting video compressor server")
|
| 536 |
-
logger.info(f"Video compressor server running on http://{CONFIG.video_compressor.host}:{CONFIG.video_compressor.port}")
|
| 537 |
|
| 538 |
-
uvicorn.run(app, host=CONFIG.video_compressor.host, port=CONFIG.video_compressor.port)
|
| 539 |
|
| 540 |
-
|
| 541 |
-
|
| 542 |
|
| 543 |
|
| 544 |
#python services/compress/server.py
|
|
|
|
| 156 |
print("⚠️ Config file not found, using default configuration")
|
| 157 |
config = {
|
| 158 |
'directories': {
|
| 159 |
+
'temp_dir': './videos/temp_scenes',
|
| 160 |
+
'output_dir': './output'
|
| 161 |
},
|
| 162 |
'video_processing': {
|
| 163 |
'SHORT_VIDEO_THRESHOLD': 20,
|
|
|
|
| 166 |
'size_increase_protection': True,
|
| 167 |
'conservative_cq_adjustment': 2,
|
| 168 |
'max_output_size_ratio': 1.15,
|
| 169 |
+
'max_encoding_retries': 2,
|
| 170 |
+
# ✅ BASIC MINER: Quality-based CQ lookup tables
|
| 171 |
+
'basic_cq_lookup_by_quality': {
|
| 172 |
+
'High': { # Target VMAF: 95
|
| 173 |
+
'animation': 22,
|
| 174 |
+
'low-action': 20,
|
| 175 |
+
'medium-action': 18,
|
| 176 |
+
'high-action': 16,
|
| 177 |
+
'default': 19
|
| 178 |
+
},
|
| 179 |
+
'Medium': { # Target VMAF: 93
|
| 180 |
+
'animation': 25,
|
| 181 |
+
'low-action': 23,
|
| 182 |
+
'medium-action': 21,
|
| 183 |
+
'high-action': 19,
|
| 184 |
+
'default': 22
|
| 185 |
+
},
|
| 186 |
+
'Low': { # Target VMAF: 90
|
| 187 |
+
'animation': 28,
|
| 188 |
+
'low-action': 26,
|
| 189 |
+
'medium-action': 24,
|
| 190 |
+
'high-action': 22,
|
| 191 |
+
'default': 25
|
| 192 |
+
}
|
| 193 |
+
},
|
| 194 |
},
|
| 195 |
'scene_detection': {
|
| 196 |
'enable_time_based_fallback': True,
|
|
|
|
| 205 |
'output_settings': {
|
| 206 |
'save_individual_scene_reports': True,
|
| 207 |
'save_comprehensive_report': True
|
| 208 |
+
},
|
| 209 |
+
'model_paths': {
|
| 210 |
+
'scene_classifier_model': 'services/compress/models/scene_classifier_model.pth'
|
| 211 |
}
|
| 212 |
}
|
| 213 |
|
|
|
|
| 214 |
config['directories']['output_dir'] = str(output_dir_path)
|
| 215 |
+
if 'video_processing' not in config:
|
| 216 |
+
config['video_processing'] = {}
|
| 217 |
+
config['video_processing']['target_quality'] = target_quality
|
| 218 |
+
|
| 219 |
# Create temp directory if it doesn't exist
|
| 220 |
temp_dir = Path(config['directories']['temp_dir'])
|
| 221 |
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 237 |
max_duration=max_duration,
|
| 238 |
output_dir=output_dir_path
|
| 239 |
)
|
| 240 |
+
|
| 241 |
+
print("part1_result", part1_result)
|
| 242 |
|
| 243 |
part1_time = time.time() - part1_start_time
|
| 244 |
|
|
|
|
| 262 |
part2_start_time = time.time()
|
| 263 |
|
| 264 |
scenes_metadata = scene_detection(part1_result)
|
| 265 |
+
|
| 266 |
+
print("scenes_metadata", scenes_metadata)
|
| 267 |
|
| 268 |
part2_time = time.time() - part2_start_time
|
| 269 |
|
|
|
|
| 288 |
if total_scene_size > 0:
|
| 289 |
print(f" 📊 Total scene files: {total_scene_size:.1f} MB")
|
| 290 |
|
| 291 |
+
# ✅ PART 3: AI Encoding (BASIC MINER - Multiple Scene Processing)
|
| 292 |
+
print(f"\n🧠 === Part 3: AI Encoding (Basic Miner) ===")
|
| 293 |
part3_start_time = time.time()
|
| 294 |
|
| 295 |
+
print(f" 🔧 Loading basic AI models and resources...")
|
| 296 |
+
print(f" 📋 Using quality-based CQ lookup tables for {target_quality} quality")
|
| 297 |
+
print(f" 🎯 Target Quality Level: {target_quality}")
|
| 298 |
+
|
| 299 |
+
# Display CQ ranges for selected quality level
|
| 300 |
+
quality_info = {
|
| 301 |
+
'High': {'vmaf': 95, 'cq_range': '16-22'},
|
| 302 |
+
'Medium': {'vmaf': 93, 'cq_range': '19-25'},
|
| 303 |
+
'Low': {'vmaf': 90, 'cq_range': '22-28'}
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
if target_quality in quality_info:
|
| 307 |
+
info = quality_info[target_quality]
|
| 308 |
+
print(f" 🎚️ CQ Range for {target_quality}: {info['cq_range']} (Target VMAF: {info['vmaf']})")
|
| 309 |
+
|
| 310 |
+
|
| 311 |
print(f" 🔧 Loading AI models and resources...")
|
| 312 |
|
| 313 |
try:
|
| 314 |
resources = load_encoding_resources(config, logging_enabled=True)
|
| 315 |
+
print(f" ✅ Basic AI resources loaded successfully")
|
| 316 |
+
print(f" 🧠 Mode: Scene classification + CQ lookup table")
|
| 317 |
except Exception as e:
|
| 318 |
+
print(f" ❌ Failed to load GGG AI resources: {e}")
|
| 319 |
return False
|
| 320 |
|
| 321 |
# Process each scene individually
|
|
|
|
| 325 |
total_input_size = 0
|
| 326 |
total_output_size = 0
|
| 327 |
|
| 328 |
+
print(f"\n 📊 Processing {len(scenes_metadata)} scenes with basic AI approach...")
|
| 329 |
|
| 330 |
for i, scene_metadata in enumerate(scenes_metadata):
|
| 331 |
scene_number = scene_metadata['scene_number']
|
|
|
|
| 334 |
|
| 335 |
print(f"\n 🎬 Scene {scene_number}/{len(scenes_metadata)}: {os.path.basename(scene_path)}")
|
| 336 |
print(f" ⏱️ Duration: {scene_duration:.1f}s")
|
| 337 |
+
# Show both selected quality level and any indicative target VMAF (if present)
|
| 338 |
+
indicative_vmaf = scene_metadata['original_video_metadata'].get('target_vmaf')
|
| 339 |
+
print(f" 🎯 Target Quality: {target_quality}" + (f" (VMAF≈{indicative_vmaf})" if indicative_vmaf else ""))
|
| 340 |
+
print(f" 🧠 Method: Basic scene classification + CQ lookup")
|
| 341 |
|
| 342 |
scene_start_time = time.time()
|
| 343 |
|
| 344 |
try:
|
| 345 |
+
# ✅ BASIC MINER: Call Part 3 for individual scene using basic approach
|
| 346 |
encoded_path, scene_data = ai_encoding(
|
| 347 |
scene_metadata=scene_metadata,
|
| 348 |
config=config,
|
| 349 |
resources=resources,
|
| 350 |
+
target_vmaf=None, # VMAF not used in basic mode; we pass quality level instead
|
| 351 |
+
target_quality_level=target_quality,
|
| 352 |
logging_enabled=True
|
| 353 |
)
|
| 354 |
|
|
|
|
| 367 |
print(f" 📁 Output: {os.path.basename(encoded_path)}")
|
| 368 |
print(f" 📊 Size: {input_size_mb:.1f} MB → {size_mb:.1f} MB ({compression:+.1f}% compression)")
|
| 369 |
print(f" 🎭 Scene type: {scene_data.get('scene_type', 'unknown')}")
|
| 370 |
+
print(f" � Quality: {scene_data.get('target_quality_level', target_quality)}")
|
| 371 |
+
print(f" �🎚️ CQ used: {scene_data.get('base_cq_for_quality', 'N/A')} → {scene_data.get('final_adjusted_cq', 'unknown')} (after adjustment)")
|
| 372 |
+
print(f" 📋 Method: Quality-based lookup table CQ selection")
|
| 373 |
print(f" ⏱️ Processing: {scene_processing_time:.1f}s")
|
| 374 |
|
| 375 |
# Update scene metadata with encoded path for Part 4
|
|
|
|
| 413 |
part3_time = time.time() - part3_start_time
|
| 414 |
|
| 415 |
# ✅ PART 3 SUMMARY
|
| 416 |
+
print(f"\n 📊 Part 3 Basic Processing Summary:")
|
| 417 |
print(f" ✅ Successful encodings: {successful_encodings}")
|
| 418 |
print(f" ❌ Failed encodings: {failed_encodings}")
|
| 419 |
print(f" 📈 Success rate: {successful_encodings/len(scenes_metadata)*100:.1f}%")
|
| 420 |
print(f" ⏱️ Total processing time: {part3_time:.1f}s")
|
| 421 |
+
print(f" 🎯 Quality Level: {target_quality}")
|
| 422 |
+
print(f" 🧠 AI Method: Scene classification + quality-based CQ lookup")
|
| 423 |
|
| 424 |
if successful_encodings == 0:
|
| 425 |
print("❌ Part 3 failed completely. No scenes were encoded. Pipeline terminated.")
|
|
|
|
| 590 |
if __name__ == "__main__":
|
| 591 |
import uvicorn
|
| 592 |
|
| 593 |
+
# logger.info("Starting video compressor server")
|
| 594 |
+
# logger.info(f"Video compressor server running on http://{CONFIG.video_compressor.host}:{CONFIG.video_compressor.port}")
|
| 595 |
|
| 596 |
+
# uvicorn.run(app, host=CONFIG.video_compressor.host, port=CONFIG.video_compressor.port)
|
| 597 |
|
| 598 |
+
result = test_video_compression('test1.mp4')
|
| 599 |
+
print(result)
|
| 600 |
|
| 601 |
|
| 602 |
#python services/compress/server.py
|
services/compress/utils/__init__.py
CHANGED
|
@@ -9,6 +9,12 @@ from .processing_utils import (
|
|
| 9 |
)
|
| 10 |
from .fast_scene_detect import adaptive_scene_detection_check, fast_scene_detection_check, motion_based_scene_check
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
__all__ = [
|
| 13 |
'monitor_memory_usage', 'ProgressTracker',
|
| 14 |
'calculate_perceptual_contrast', 'get_video_duration',
|
|
@@ -21,5 +27,8 @@ __all__ = [
|
|
| 21 |
'should_skip_encoding',
|
| 22 |
'create_summary_and_save', 'cleanup_temporary_items',
|
| 23 |
'adaptive_scene_detection_check', 'fast_scene_detection_check', 'motion_based_scene_check',
|
| 24 |
-
'VideoProcessingLogger'
|
|
|
|
|
|
|
|
|
|
| 25 |
]
|
|
|
|
| 9 |
)
|
| 10 |
from .fast_scene_detect import adaptive_scene_detection_check, fast_scene_detection_check, motion_based_scene_check
|
| 11 |
|
| 12 |
+
# Import preprocessing classes to make them available for pickle loading
|
| 13 |
+
from .data_preprocessing import (
|
| 14 |
+
ColumnDropper, VMAFScaler, TargetExtractor, CQScaler,
|
| 15 |
+
ResolutionTransformer, FeatureScaler, FrameRateTransformer
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
__all__ = [
|
| 19 |
'monitor_memory_usage', 'ProgressTracker',
|
| 20 |
'calculate_perceptual_contrast', 'get_video_duration',
|
|
|
|
| 27 |
'should_skip_encoding',
|
| 28 |
'create_summary_and_save', 'cleanup_temporary_items',
|
| 29 |
'adaptive_scene_detection_check', 'fast_scene_detection_check', 'motion_based_scene_check',
|
| 30 |
+
'VideoProcessingLogger',
|
| 31 |
+
# Add preprocessing classes to make them available
|
| 32 |
+
'ColumnDropper', 'VMAFScaler', 'TargetExtractor', 'CQScaler',
|
| 33 |
+
'ResolutionTransformer', 'FeatureScaler', 'FrameRateTransformer'
|
| 34 |
]
|
services/compress/utils/encode_video.py
CHANGED
|
@@ -8,14 +8,8 @@ import time
|
|
| 8 |
import traceback
|
| 9 |
import numpy as np
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
except ImportError:
|
| 14 |
-
print("Error: Could not import configurations from 'encoder_configs.py'. Make sure the file exists in the 'src' directory.")
|
| 15 |
-
ENCODER_SETTINGS = {}
|
| 16 |
-
SCENE_SPECIFIC_PARAMS = {}
|
| 17 |
-
MODEL_CQ_REFERENCE_CODEC = ""
|
| 18 |
-
QUALITY_MAPPING_ANCHORS = {}
|
| 19 |
|
| 20 |
|
| 21 |
def get_contrast_optimized_params(scene_type, contrast_value, codec):
|
|
@@ -61,7 +55,7 @@ def get_contrast_optimized_params(scene_type, contrast_value, codec):
|
|
| 61 |
params['temporal-aq'] = 0 # Default
|
| 62 |
|
| 63 |
# --- x264 ---
|
| 64 |
-
elif codec == "
|
| 65 |
# AQ mode and strength adjustments
|
| 66 |
if contrast_category == "high":
|
| 67 |
params['aq-mode'] = 2 # Variance AQ with auto-variance
|
|
@@ -74,7 +68,7 @@ def get_contrast_optimized_params(scene_type, contrast_value, codec):
|
|
| 74 |
params['aq-strength'] = 1.0 # Default
|
| 75 |
|
| 76 |
# --- x265 ---
|
| 77 |
-
elif codec == "
|
| 78 |
# AQ mode and strength adjustments
|
| 79 |
if contrast_category == "high":
|
| 80 |
params['aq-mode'] = 3 # Auto-variance AQ with more aggressive bias
|
|
@@ -86,47 +80,59 @@ def get_contrast_optimized_params(scene_type, contrast_value, codec):
|
|
| 86 |
params['aq-mode'] = 2
|
| 87 |
params['aq-strength'] = 1.0 # Default
|
| 88 |
|
| 89 |
-
# ---
|
| 90 |
-
elif
|
|
|
|
|
|
|
| 91 |
if contrast_category == "high":
|
| 92 |
-
|
| 93 |
-
|
|
|
|
| 94 |
elif contrast_category == "low":
|
| 95 |
-
|
| 96 |
-
|
| 97 |
else:
|
| 98 |
-
|
| 99 |
-
params['deltaq-mode'] = 0
|
| 100 |
|
| 101 |
-
# ---
|
| 102 |
-
elif "
|
| 103 |
-
#
|
| 104 |
if contrast_category == "high":
|
| 105 |
-
params['aq-mode'] =
|
|
|
|
|
|
|
| 106 |
elif contrast_category == "low":
|
| 107 |
-
params['aq-mode'] =
|
|
|
|
|
|
|
| 108 |
else:
|
| 109 |
-
params['aq-mode'] = 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
# --- Scene-specific contrast adjustments ---
|
| 112 |
# For text content, adjust parameters further based on contrast
|
| 113 |
if scene_type == 'Screen Content / Text':
|
| 114 |
if contrast_category == "high":
|
| 115 |
params['sharpness'] = 0 # Preserve sharpness
|
| 116 |
-
if "
|
| 117 |
params['rc-lookahead'] = 20 # More lookahead for complex text
|
| 118 |
elif contrast_category == "low":
|
| 119 |
params['sharpness'] = 1 # Some sharpening for low contrast text
|
| 120 |
-
if "
|
| 121 |
params['rc-lookahead'] = 8 # Less lookahead needed
|
| 122 |
|
| 123 |
# For faces, special handling based on contrast
|
| 124 |
elif scene_type == 'Faces / People':
|
| 125 |
if contrast_category == "high":
|
| 126 |
-
if "
|
| 127 |
params['rc-lookahead'] = 20 # More lookahead for dramatic lighting
|
| 128 |
else:
|
| 129 |
-
if "
|
| 130 |
params['rc-lookahead'] = 15 # Standard lookahead
|
| 131 |
|
| 132 |
return params
|
|
@@ -138,13 +144,27 @@ def encode_video(input_path, output_path, codec, rate=None, max_bit_rate=None, p
|
|
| 138 |
|
| 139 |
Note: encoding decisions should be made BEFORE calling this function.
|
| 140 |
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
if logging_enabled:
|
| 144 |
print(f"Encoding video using codec: {codec}, scene: {scene_type}, model_predicted_rate: {rate}")
|
| 145 |
if contrast_value is not None:
|
| 146 |
print(f"Using contrast value: {contrast_value:.2f}")
|
| 147 |
|
|
|
|
| 148 |
base_settings = ENCODER_SETTINGS.get(codec)
|
| 149 |
if not base_settings:
|
| 150 |
print(f"Error: Codec settings for '{codec}' not found in ENCODER_SETTINGS.")
|
|
@@ -157,12 +177,11 @@ def encode_video(input_path, output_path, codec, rate=None, max_bit_rate=None, p
|
|
| 157 |
# 2. Apply scene-specific overrides if scene_type is provided
|
| 158 |
if scene_type:
|
| 159 |
codec_scene_params = SCENE_SPECIFIC_PARAMS.get(codec, {})
|
| 160 |
-
# Get params for the specific scene, fallback to 'other', then empty dict
|
| 161 |
scene_params = codec_scene_params.get(scene_type, codec_scene_params.get('other', {}))
|
| 162 |
if scene_params:
|
| 163 |
if logging_enabled:
|
| 164 |
print(f"Applying scene-specific params for '{scene_type}': {scene_params}")
|
| 165 |
-
current_settings.update(scene_params)
|
| 166 |
|
| 167 |
# 3. Apply contrast-specific overrides if contrast_value is provided
|
| 168 |
if contrast_value is not None:
|
|
@@ -172,39 +191,47 @@ def encode_video(input_path, output_path, codec, rate=None, max_bit_rate=None, p
|
|
| 172 |
print(f"Applying contrast-specific params: {contrast_params}")
|
| 173 |
current_settings.update(contrast_params)
|
| 174 |
|
| 175 |
-
# 4. Apply
|
| 176 |
-
# Apply 'rate' (which is the model_predicted_av1_cq)
|
| 177 |
if rate is not None:
|
| 178 |
-
model_predicted_ref_cq = int(rate)
|
| 179 |
-
|
| 180 |
if codec == MODEL_CQ_REFERENCE_CODEC:
|
| 181 |
# Use the predicted CQ directly for the reference codec
|
| 182 |
-
if 'cq' in current_settings:
|
| 183 |
current_settings['cq'] = model_predicted_ref_cq
|
| 184 |
-
if 'crf' in current_settings:
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
if
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
else:
|
| 194 |
-
if logging_enabled:
|
|
|
|
| 195 |
current_settings['crf'] = model_predicted_ref_cq
|
| 196 |
|
| 197 |
elif codec in QUALITY_MAPPING_ANCHORS:
|
|
|
|
| 198 |
mapping_config = QUALITY_MAPPING_ANCHORS[codec]
|
| 199 |
|
| 200 |
model_anchor_cqs = [p[0] for p in mapping_config['anchor_points']]
|
| 201 |
target_anchor_params = [p[1] for p in mapping_config['anchor_points']]
|
| 202 |
|
| 203 |
-
# Interpolate
|
| 204 |
-
|
| 205 |
-
|
|
|
|
| 206 |
if logging_enabled and clamped_model_cq != model_predicted_ref_cq:
|
| 207 |
-
print(f"Clamped model_predicted_ref_cq from {model_predicted_ref_cq} to {clamped_model_cq}
|
| 208 |
|
| 209 |
mapped_param_float = np.interp(clamped_model_cq, model_anchor_cqs, target_anchor_params)
|
| 210 |
|
|
@@ -216,101 +243,163 @@ def encode_video(input_path, output_path, codec, rate=None, max_bit_rate=None, p
|
|
| 216 |
target_param_type = mapping_config['target_param_type']
|
| 217 |
if target_param_type == 'cq':
|
| 218 |
current_settings['cq'] = mapped_param_int
|
| 219 |
-
if 'crf' in current_settings:
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
elif target_param_type == 'crf':
|
| 224 |
current_settings['crf'] = mapped_param_int
|
| 225 |
-
if 'cq' in current_settings:
|
| 226 |
-
|
| 227 |
-
if
|
| 228 |
-
current_settings['
|
| 229 |
-
if logging_enabled: print(f"Applying mapped CRF for {codec} from model ref CQ {model_predicted_ref_cq}: {current_settings['crf']}")
|
| 230 |
else:
|
| 231 |
-
if logging_enabled:
|
| 232 |
-
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
else:
|
|
|
|
| 236 |
if logging_enabled:
|
| 237 |
-
print(f"Warning: No quality mapping found for {codec}
|
| 238 |
-
# Fallback: apply the rate directly, trying 'cq' then 'crf'
|
| 239 |
if 'cq' in current_settings:
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
elif 'crf' in current_settings:
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
else:
|
| 248 |
-
|
| 249 |
-
if logging_enabled:
|
| 250 |
-
|
| 251 |
-
|
|
|
|
| 252 |
if preset is not None:
|
| 253 |
current_settings['preset'] = preset
|
| 254 |
if logging_enabled:
|
| 255 |
print(f"Applying preset override: {preset}")
|
| 256 |
|
| 257 |
-
# Apply
|
| 258 |
if max_bit_rate is not None:
|
| 259 |
current_settings['maxrate'] = max_bit_rate
|
| 260 |
try:
|
| 261 |
# Extract numeric part and unit (k or m)
|
| 262 |
numeric_maxrate = int(re.sub(r'\D', '', max_bit_rate))
|
| 263 |
unit = re.sub(r'\d', '', max_bit_rate).lower()
|
| 264 |
-
if unit not in ['k', 'm']:
|
|
|
|
| 265 |
bufsize_val = numeric_maxrate * 2
|
| 266 |
current_settings['bufsize'] = f"{bufsize_val}{unit}"
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
| 272 |
if logging_enabled:
|
| 273 |
print(f"Applying maxrate: {current_settings['maxrate']}, bufsize: {current_settings['bufsize']}")
|
| 274 |
-
except ValueError:
|
| 275 |
-
|
| 276 |
-
print(f"Warning: Could not parse max_bit_rate '{max_bit_rate}'. Ignoring.")
|
| 277 |
-
except Exception as e:
|
| 278 |
-
# Keep warning prints
|
| 279 |
-
print(f"Warning: Error processing max_bit_rate '{max_bit_rate}': {e}. Ignoring.")
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
output_args = {}
|
| 284 |
|
| 285 |
-
# Map internal setting names to
|
| 286 |
key_map = {
|
| 287 |
-
'keyint': 'g',
|
| 288 |
'bitrate': 'b:v',
|
| 289 |
'codec': 'vcodec'
|
| 290 |
}
|
| 291 |
|
| 292 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
for key, value in current_settings.items():
|
| 294 |
-
ffmpeg_key = key_map.get(key, key)
|
| 295 |
output_args[ffmpeg_key] = value
|
| 296 |
|
| 297 |
-
# Ensure vcodec is
|
| 298 |
if 'vcodec' not in output_args and 'codec' in current_settings:
|
| 299 |
-
|
| 300 |
|
| 301 |
-
# Remove
|
| 302 |
if 'codec' in output_args and output_args.get('codec') != output_args.get('vcodec'):
|
| 303 |
-
|
| 304 |
|
| 305 |
-
#
|
| 306 |
has_audio = check_audio_stream(input_path)
|
| 307 |
|
| 308 |
if has_audio:
|
| 309 |
output_args['acodec'] = 'copy'
|
| 310 |
-
|
| 311 |
-
output_args['map'] = '0' # Map all streams, let FFmpeg handle it
|
| 312 |
else:
|
| 313 |
-
# No audio - only map video
|
| 314 |
output_args['map'] = '0:v:0' # Only video stream
|
| 315 |
|
| 316 |
if logging_enabled:
|
|
@@ -320,9 +409,7 @@ def encode_video(input_path, output_path, codec, rate=None, max_bit_rate=None, p
|
|
| 320 |
# --- Execute FFmpeg ---
|
| 321 |
try:
|
| 322 |
start_time = time.time()
|
| 323 |
-
log_level = 'info' if logging_enabled else 'error'
|
| 324 |
|
| 325 |
-
# Create input and output with proper stream mapping
|
| 326 |
input_stream = ffmpeg.input(input_path)
|
| 327 |
output_stream = ffmpeg.output(input_stream, output_path, **output_args)
|
| 328 |
|
|
@@ -337,11 +424,11 @@ def encode_video(input_path, output_path, codec, rate=None, max_bit_rate=None, p
|
|
| 337 |
encoding_time_calculated = round(end_time - start_time, 2)
|
| 338 |
stderr = result[1].decode("utf-8") if result[1] else ""
|
| 339 |
|
| 340 |
-
# Extract final encoding log line
|
| 341 |
encoding_results_log = None
|
| 342 |
-
if logging_enabled:
|
| 343 |
for line in stderr.splitlines():
|
| 344 |
-
if "time=" in line:
|
| 345 |
encoding_results_log = line
|
| 346 |
|
| 347 |
if logging_enabled:
|
|
@@ -349,14 +436,50 @@ def encode_video(input_path, output_path, codec, rate=None, max_bit_rate=None, p
|
|
| 349 |
return encoding_results_log, encoding_time_calculated
|
| 350 |
|
| 351 |
except ffmpeg.Error as e:
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
def encode_lossless_video(input_path, output_path, logging_enabled=True):
|
| 361 |
"""
|
| 362 |
Encode video using FFV1 lossless codec with optimized settings.
|
|
|
|
| 8 |
import traceback
|
| 9 |
import numpy as np
|
| 10 |
|
| 11 |
+
from .encoder_configs import ENCODER_SETTINGS, SCENE_SPECIFIC_PARAMS, MODEL_CQ_REFERENCE_CODEC, QUALITY_MAPPING_ANCHORS
|
| 12 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
def get_contrast_optimized_params(scene_type, contrast_value, codec):
|
|
|
|
| 55 |
params['temporal-aq'] = 0 # Default
|
| 56 |
|
| 57 |
# --- x264 ---
|
| 58 |
+
elif codec == "libx264": # ✅ FIXED: Use standardized codec name
|
| 59 |
# AQ mode and strength adjustments
|
| 60 |
if contrast_category == "high":
|
| 61 |
params['aq-mode'] = 2 # Variance AQ with auto-variance
|
|
|
|
| 68 |
params['aq-strength'] = 1.0 # Default
|
| 69 |
|
| 70 |
# --- x265 ---
|
| 71 |
+
elif codec == "libx265": # ✅ FIXED: Use standardized codec name
|
| 72 |
# AQ mode and strength adjustments
|
| 73 |
if contrast_category == "high":
|
| 74 |
params['aq-mode'] = 3 # Auto-variance AQ with more aggressive bias
|
|
|
|
| 80 |
params['aq-mode'] = 2
|
| 81 |
params['aq-strength'] = 1.0 # Default
|
| 82 |
|
| 83 |
+
# --- SVT-AV1 ---
|
| 84 |
+
elif codec == "libsvtav1": # ✅ FIXED: Use standardized codec name
|
| 85 |
+
# SVT-AV1 doesn't have enable-hdr or aq-mode options
|
| 86 |
+
# Use CRF adjustments and available parameters instead
|
| 87 |
if contrast_category == "high":
|
| 88 |
+
# For high contrast, we can rely on the encoder's default behavior
|
| 89 |
+
# or use dolbyvision if HDR content is detected
|
| 90 |
+
pass # SVT-AV1 handles high contrast well by default
|
| 91 |
elif contrast_category == "low":
|
| 92 |
+
# For low contrast, we might want a slightly lower CRF for better quality
|
| 93 |
+
pass # This would be handled at the CRF level, not here
|
| 94 |
else:
|
| 95 |
+
pass # Default SVT-AV1 behavior is generally good
|
|
|
|
| 96 |
|
| 97 |
+
# --- VP9 ---
|
| 98 |
+
elif codec == "libvpx_vp9": # ✅ FIXED: Use standardized codec name (underscore)
|
| 99 |
+
# VP9 adaptive quantization and quality parameters
|
| 100 |
if contrast_category == "high":
|
| 101 |
+
params['aq-mode'] = 3 # Complexity-based AQ for high contrast
|
| 102 |
+
params['arnr-maxframes'] = 7 # More noise reduction frames
|
| 103 |
+
params['arnr-strength'] = 5 # Higher noise reduction strength
|
| 104 |
elif contrast_category == "low":
|
| 105 |
+
params['aq-mode'] = 2 # Variance-based AQ for low contrast
|
| 106 |
+
params['arnr-maxframes'] = 5 # Standard noise reduction
|
| 107 |
+
params['arnr-strength'] = 3 # Moderate noise reduction
|
| 108 |
else:
|
| 109 |
+
params['aq-mode'] = 1 # Variance-based AQ (default)
|
| 110 |
+
params['arnr-maxframes'] = 7 # Standard noise reduction
|
| 111 |
+
params['arnr-strength'] = 4 # Standard noise reduction strength
|
| 112 |
+
|
| 113 |
+
# VP9-specific quality settings
|
| 114 |
+
params['tune'] = 'psnr' # Optimize for quality
|
| 115 |
+
params['auto-alt-ref'] = 1 # Enable alternate reference frames
|
| 116 |
|
| 117 |
# --- Scene-specific contrast adjustments ---
|
| 118 |
# For text content, adjust parameters further based on contrast
|
| 119 |
if scene_type == 'Screen Content / Text':
|
| 120 |
if contrast_category == "high":
|
| 121 |
params['sharpness'] = 0 # Preserve sharpness
|
| 122 |
+
if "_nvenc" in codec: # ✅ FIXED: lowercase nvenc
|
| 123 |
params['rc-lookahead'] = 20 # More lookahead for complex text
|
| 124 |
elif contrast_category == "low":
|
| 125 |
params['sharpness'] = 1 # Some sharpening for low contrast text
|
| 126 |
+
if "_nvenc" in codec: # ✅ FIXED: lowercase nvenc
|
| 127 |
params['rc-lookahead'] = 8 # Less lookahead needed
|
| 128 |
|
| 129 |
# For faces, special handling based on contrast
|
| 130 |
elif scene_type == 'Faces / People':
|
| 131 |
if contrast_category == "high":
|
| 132 |
+
if "_nvenc" in codec: # ✅ FIXED: lowercase nvenc
|
| 133 |
params['rc-lookahead'] = 20 # More lookahead for dramatic lighting
|
| 134 |
else:
|
| 135 |
+
if "_nvenc" in codec: # ✅ FIXED: lowercase nvenc
|
| 136 |
params['rc-lookahead'] = 15 # Standard lookahead
|
| 137 |
|
| 138 |
return params
|
|
|
|
| 144 |
|
| 145 |
Note: encoding decisions should be made BEFORE calling this function.
|
| 146 |
|
| 147 |
+
Args:
|
| 148 |
+
input_path (str): Path to input video file
|
| 149 |
+
output_path (str): Path for output video file
|
| 150 |
+
codec (str): Video codec to use (e.g., 'av1_nvenc', 'libx264')
|
| 151 |
+
rate (int/float, optional): Quality parameter (CQ value from model)
|
| 152 |
+
max_bit_rate (str, optional): Maximum bitrate (e.g., '5000k', '10m')
|
| 153 |
+
preset (str, optional): Encoder preset override
|
| 154 |
+
scene_type (str, optional): Scene classification for optimization
|
| 155 |
+
contrast_value (float, optional): Perceptual contrast (0.0-1.0)
|
| 156 |
+
logging_enabled (bool): Enable detailed logging
|
| 157 |
|
| 158 |
+
Returns:
|
| 159 |
+
tuple: (encoding_results_log, encoding_time) or (None, None) on failure
|
| 160 |
+
"""
|
| 161 |
+
|
| 162 |
if logging_enabled:
|
| 163 |
print(f"Encoding video using codec: {codec}, scene: {scene_type}, model_predicted_rate: {rate}")
|
| 164 |
if contrast_value is not None:
|
| 165 |
print(f"Using contrast value: {contrast_value:.2f}")
|
| 166 |
|
| 167 |
+
# Get base encoder settings
|
| 168 |
base_settings = ENCODER_SETTINGS.get(codec)
|
| 169 |
if not base_settings:
|
| 170 |
print(f"Error: Codec settings for '{codec}' not found in ENCODER_SETTINGS.")
|
|
|
|
| 177 |
# 2. Apply scene-specific overrides if scene_type is provided
|
| 178 |
if scene_type:
|
| 179 |
codec_scene_params = SCENE_SPECIFIC_PARAMS.get(codec, {})
|
|
|
|
| 180 |
scene_params = codec_scene_params.get(scene_type, codec_scene_params.get('other', {}))
|
| 181 |
if scene_params:
|
| 182 |
if logging_enabled:
|
| 183 |
print(f"Applying scene-specific params for '{scene_type}': {scene_params}")
|
| 184 |
+
current_settings.update(scene_params)
|
| 185 |
|
| 186 |
# 3. Apply contrast-specific overrides if contrast_value is provided
|
| 187 |
if contrast_value is not None:
|
|
|
|
| 191 |
print(f"Applying contrast-specific params: {contrast_params}")
|
| 192 |
current_settings.update(contrast_params)
|
| 193 |
|
| 194 |
+
# 4. Apply rate (CQ) parameter with codec-specific handling
|
|
|
|
| 195 |
if rate is not None:
|
| 196 |
+
model_predicted_ref_cq = int(rate)
|
| 197 |
+
|
| 198 |
if codec == MODEL_CQ_REFERENCE_CODEC:
|
| 199 |
# Use the predicted CQ directly for the reference codec
|
| 200 |
+
if 'cq' in current_settings:
|
| 201 |
current_settings['cq'] = model_predicted_ref_cq
|
| 202 |
+
if 'crf' in current_settings:
|
| 203 |
+
del current_settings['crf']
|
| 204 |
+
# Use VBR for NVENC/QSV with CQ
|
| 205 |
+
if codec.endswith("_nvenc") or codec.endswith("_qsv"):
|
| 206 |
+
current_settings['rc'] = 'vbr'
|
| 207 |
+
if 'qp' in current_settings:
|
| 208 |
+
del current_settings['qp']
|
| 209 |
+
if logging_enabled:
|
| 210 |
+
print(f"Applying model CQ directly for {MODEL_CQ_REFERENCE_CODEC}: {current_settings['cq']}")
|
| 211 |
+
elif 'crf' in current_settings:
|
| 212 |
+
current_settings['crf'] = model_predicted_ref_cq
|
| 213 |
+
if 'cq' in current_settings:
|
| 214 |
+
del current_settings['cq']
|
| 215 |
+
if logging_enabled:
|
| 216 |
+
print(f"Applying model CQ as CRF for {MODEL_CQ_REFERENCE_CODEC}: {current_settings['crf']}")
|
| 217 |
else:
|
| 218 |
+
if logging_enabled:
|
| 219 |
+
print(f"Warning: Neither 'cq' nor 'crf' in base settings for {MODEL_CQ_REFERENCE_CODEC}. Using CRF fallback.")
|
| 220 |
current_settings['crf'] = model_predicted_ref_cq
|
| 221 |
|
| 222 |
elif codec in QUALITY_MAPPING_ANCHORS:
|
| 223 |
+
# Use quality mapping for non-reference codecs
|
| 224 |
mapping_config = QUALITY_MAPPING_ANCHORS[codec]
|
| 225 |
|
| 226 |
model_anchor_cqs = [p[0] for p in mapping_config['anchor_points']]
|
| 227 |
target_anchor_params = [p[1] for p in mapping_config['anchor_points']]
|
| 228 |
|
| 229 |
+
# Interpolate with clamping
|
| 230 |
+
clamped_model_cq = np.clip(model_predicted_ref_cq,
|
| 231 |
+
mapping_config['model_ref_cq_range'][0],
|
| 232 |
+
mapping_config['model_ref_cq_range'][1])
|
| 233 |
if logging_enabled and clamped_model_cq != model_predicted_ref_cq:
|
| 234 |
+
print(f"Clamped model_predicted_ref_cq from {model_predicted_ref_cq} to {clamped_model_cq}")
|
| 235 |
|
| 236 |
mapped_param_float = np.interp(clamped_model_cq, model_anchor_cqs, target_anchor_params)
|
| 237 |
|
|
|
|
| 243 |
target_param_type = mapping_config['target_param_type']
|
| 244 |
if target_param_type == 'cq':
|
| 245 |
current_settings['cq'] = mapped_param_int
|
| 246 |
+
if 'crf' in current_settings:
|
| 247 |
+
del current_settings['crf']
|
| 248 |
+
# Use VBR for NVENC and QSV when using CQ
|
| 249 |
+
if codec.endswith("_nvenc") or codec.endswith("_qsv"):
|
| 250 |
+
current_settings['rc'] = 'vbr'
|
| 251 |
+
if 'qp' in current_settings:
|
| 252 |
+
del current_settings['qp']
|
| 253 |
+
if logging_enabled:
|
| 254 |
+
print(f"Applying mapped CQ (VBR) for {codec} from model ref CQ {model_predicted_ref_cq}: {current_settings['cq']}")
|
| 255 |
elif target_param_type == 'crf':
|
| 256 |
current_settings['crf'] = mapped_param_int
|
| 257 |
+
if 'cq' in current_settings:
|
| 258 |
+
del current_settings['cq']
|
| 259 |
+
if logging_enabled:
|
| 260 |
+
print(f"Applying mapped CRF for {codec} from model ref CQ {model_predicted_ref_cq}: {current_settings['crf']}")
|
|
|
|
| 261 |
else:
|
| 262 |
+
if logging_enabled:
|
| 263 |
+
print(f"Warning: Unknown target_param_type '{target_param_type}' for {codec}. Using fallback.")
|
| 264 |
+
if 'cq' in current_settings:
|
| 265 |
+
current_settings['cq'] = model_predicted_ref_cq
|
| 266 |
+
elif 'crf' in current_settings:
|
| 267 |
+
current_settings['crf'] = model_predicted_ref_cq
|
| 268 |
|
| 269 |
else:
|
| 270 |
+
# Fallback for codecs without mapping
|
| 271 |
if logging_enabled:
|
| 272 |
+
print(f"Warning: No quality mapping found for {codec}. Applying model ref CQ {model_predicted_ref_cq} directly.")
|
|
|
|
| 273 |
if 'cq' in current_settings:
|
| 274 |
+
current_settings['cq'] = model_predicted_ref_cq
|
| 275 |
+
if 'crf' in current_settings:
|
| 276 |
+
del current_settings['crf']
|
| 277 |
+
# Use VBR for NVENC and QSV with CQ
|
| 278 |
+
if codec.endswith("_nvenc") or codec.endswith("_qsv"):
|
| 279 |
+
current_settings['rc'] = 'vbr'
|
| 280 |
+
if 'qp' in current_settings:
|
| 281 |
+
del current_settings['qp']
|
| 282 |
elif 'crf' in current_settings:
|
| 283 |
+
current_settings['crf'] = model_predicted_ref_cq
|
| 284 |
+
if 'cq' in current_settings:
|
| 285 |
+
del current_settings['cq']
|
| 286 |
+
else:
|
| 287 |
+
current_settings['crf'] = model_predicted_ref_cq
|
| 288 |
+
if logging_enabled:
|
| 289 |
+
print(f"Applied direct rate {model_predicted_ref_cq} to {codec}.")
|
| 290 |
+
|
| 291 |
+
# 5. Apply preset override if provided
|
| 292 |
if preset is not None:
|
| 293 |
current_settings['preset'] = preset
|
| 294 |
if logging_enabled:
|
| 295 |
print(f"Applying preset override: {preset}")
|
| 296 |
|
| 297 |
+
# 6. Apply max_bit_rate and enable VBR for NVENC/QSV when maxrate is provided
|
| 298 |
if max_bit_rate is not None:
|
| 299 |
current_settings['maxrate'] = max_bit_rate
|
| 300 |
try:
|
| 301 |
# Extract numeric part and unit (k or m)
|
| 302 |
numeric_maxrate = int(re.sub(r'\D', '', max_bit_rate))
|
| 303 |
unit = re.sub(r'\d', '', max_bit_rate).lower()
|
| 304 |
+
if unit not in ['k', 'm']:
|
| 305 |
+
unit = 'k'
|
| 306 |
bufsize_val = numeric_maxrate * 2
|
| 307 |
current_settings['bufsize'] = f"{bufsize_val}{unit}"
|
| 308 |
+
|
| 309 |
+
# Enable VBR when maxrate is provided for NVENC/QSV
|
| 310 |
+
if codec.endswith("_nvenc") or codec.endswith("_qsv"):
|
| 311 |
+
current_settings['rc'] = 'vbr'
|
| 312 |
+
if logging_enabled:
|
| 313 |
+
print(f"Enabled VBR rate control for {codec} due to maxrate setting")
|
| 314 |
+
|
| 315 |
if logging_enabled:
|
| 316 |
print(f"Applying maxrate: {current_settings['maxrate']}, bufsize: {current_settings['bufsize']}")
|
| 317 |
+
except (ValueError, Exception) as e:
|
| 318 |
+
print(f"Warning: Could not parse max_bit_rate '{max_bit_rate}': {e}. Ignoring.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
+
# 7. Handle CRF usage - disable constqp when CRF is used
|
| 321 |
+
try:
|
| 322 |
+
uses_crf = 'crf' in current_settings
|
| 323 |
+
has_maxrate = 'maxrate' in current_settings
|
| 324 |
+
|
| 325 |
+
if uses_crf:
|
| 326 |
+
# Remove constqp explicitly when CRF is used
|
| 327 |
+
if str(current_settings.get('rc', '')).lower() == 'constqp':
|
| 328 |
+
if logging_enabled:
|
| 329 |
+
print("Disabling 'constqp' because CRF is in use")
|
| 330 |
+
del current_settings['rc']
|
| 331 |
+
|
| 332 |
+
# For NVENC/QSV with CRF + maxrate, use VBR
|
| 333 |
+
if (codec.endswith('_nvenc') or codec.endswith('_qsv')) and has_maxrate:
|
| 334 |
+
current_settings['rc'] = 'vbr'
|
| 335 |
+
if logging_enabled:
|
| 336 |
+
print(f"Using 'vbr' with CRF due to specified maxrate")
|
| 337 |
+
except Exception:
|
| 338 |
+
pass
|
| 339 |
|
| 340 |
+
# 8. CQ policy for NVENC/QSV - use VBR with CQ unless maxrate + CQ requires constqp
|
| 341 |
+
try:
|
| 342 |
+
has_cq = 'cq' in current_settings and isinstance(current_settings.get('cq'), (int, float))
|
| 343 |
+
has_maxrate = 'maxrate' in current_settings
|
| 344 |
+
|
| 345 |
+
if (codec.endswith('_nvenc') or codec.endswith('_qsv')) and has_cq:
|
| 346 |
+
if has_maxrate:
|
| 347 |
+
# Use VBR with both CQ and maxrate for rate-limited quality encoding
|
| 348 |
+
current_settings['rc'] = 'vbr'
|
| 349 |
+
if 'qp' in current_settings:
|
| 350 |
+
del current_settings['qp']
|
| 351 |
+
if logging_enabled:
|
| 352 |
+
print(f"Using VBR with CQ and maxrate for {codec}")
|
| 353 |
+
else:
|
| 354 |
+
# Standard CQ encoding with VBR
|
| 355 |
+
current_settings['rc'] = 'vbr'
|
| 356 |
+
if 'qp' in current_settings:
|
| 357 |
+
del current_settings['qp']
|
| 358 |
+
if logging_enabled:
|
| 359 |
+
print(f"Using VBR with CQ for {codec}")
|
| 360 |
+
except Exception:
|
| 361 |
+
pass
|
| 362 |
+
|
| 363 |
+
# --- Build FFmpeg arguments ---
|
| 364 |
output_args = {}
|
| 365 |
|
| 366 |
+
# Map internal setting names to FFmpeg argument names
|
| 367 |
key_map = {
|
| 368 |
+
'keyint': 'g',
|
| 369 |
'bitrate': 'b:v',
|
| 370 |
'codec': 'vcodec'
|
| 371 |
}
|
| 372 |
|
| 373 |
+
# For NVENC in constqp mode, translate 'cq' to 'qp'
|
| 374 |
+
try:
|
| 375 |
+
if codec.endswith('_nvenc') and str(current_settings.get('rc', '')).lower() == 'constqp':
|
| 376 |
+
if 'cq' in current_settings and 'qp' not in current_settings:
|
| 377 |
+
current_settings['qp'] = int(current_settings.pop('cq'))
|
| 378 |
+
if logging_enabled:
|
| 379 |
+
print("Translating NVENC constqp: using -qp instead of -cq")
|
| 380 |
+
except Exception:
|
| 381 |
+
pass
|
| 382 |
+
|
| 383 |
+
# Add all current settings to output_args
|
| 384 |
for key, value in current_settings.items():
|
| 385 |
+
ffmpeg_key = key_map.get(key, key)
|
| 386 |
output_args[ffmpeg_key] = value
|
| 387 |
|
| 388 |
+
# Ensure vcodec is set
|
| 389 |
if 'vcodec' not in output_args and 'codec' in current_settings:
|
| 390 |
+
output_args['vcodec'] = current_settings['codec']
|
| 391 |
|
| 392 |
+
# Remove internal 'codec' key if different from 'vcodec'
|
| 393 |
if 'codec' in output_args and output_args.get('codec') != output_args.get('vcodec'):
|
| 394 |
+
del output_args['codec']
|
| 395 |
|
| 396 |
+
# --- Audio handling ---
|
| 397 |
has_audio = check_audio_stream(input_path)
|
| 398 |
|
| 399 |
if has_audio:
|
| 400 |
output_args['acodec'] = 'copy'
|
| 401 |
+
output_args['map'] = '0' # Map all streams
|
|
|
|
| 402 |
else:
|
|
|
|
| 403 |
output_args['map'] = '0:v:0' # Only video stream
|
| 404 |
|
| 405 |
if logging_enabled:
|
|
|
|
| 409 |
# --- Execute FFmpeg ---
|
| 410 |
try:
|
| 411 |
start_time = time.time()
|
|
|
|
| 412 |
|
|
|
|
| 413 |
input_stream = ffmpeg.input(input_path)
|
| 414 |
output_stream = ffmpeg.output(input_stream, output_path, **output_args)
|
| 415 |
|
|
|
|
| 424 |
encoding_time_calculated = round(end_time - start_time, 2)
|
| 425 |
stderr = result[1].decode("utf-8") if result[1] else ""
|
| 426 |
|
| 427 |
+
# Extract final encoding log line for debugging
|
| 428 |
encoding_results_log = None
|
| 429 |
+
if logging_enabled:
|
| 430 |
for line in stderr.splitlines():
|
| 431 |
+
if "time=" in line:
|
| 432 |
encoding_results_log = line
|
| 433 |
|
| 434 |
if logging_enabled:
|
|
|
|
| 436 |
return encoding_results_log, encoding_time_calculated
|
| 437 |
|
| 438 |
except ffmpeg.Error as e:
|
| 439 |
+
err_text = e.stderr.decode('utf8') if getattr(e, 'stderr', None) else str(e)
|
| 440 |
+
if logging_enabled:
|
| 441 |
+
print(f"Error encoding with {codec} scene '{scene_type}': {e}")
|
| 442 |
+
print(f"FFmpeg stderr: {err_text}")
|
| 443 |
+
print("Trying with libsvtav1 encoder...")
|
| 444 |
+
|
| 445 |
+
try:
|
| 446 |
+
# switch codec from gpu to cpu
|
| 447 |
+
output_args["vcodec"] = "libsvtav1"
|
| 448 |
+
|
| 449 |
+
# remove nvenc-specific options that might cause fallback errors
|
| 450 |
+
for key in ["spatial-aq", "temporal-aq", "aq-strength", "rc-lookahead", "preset"]:
|
| 451 |
+
output_args.pop(key, None)
|
| 452 |
+
|
| 453 |
+
start_time = time.time()
|
| 454 |
+
input_stream = ffmpeg.input(input_path)
|
| 455 |
+
output_stream = ffmpeg.output(input_stream, output_path, **output_args)
|
| 456 |
+
|
| 457 |
+
result = output_stream.run(
|
| 458 |
+
overwrite_output=True,
|
| 459 |
+
capture_stdout=True,
|
| 460 |
+
capture_stderr=True,
|
| 461 |
+
quiet=(not logging_enabled)
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
end_time = time.time()
|
| 465 |
+
encoding_time_calculated = round(end_time - start_time, 2)
|
| 466 |
+
stderr = result[1].decode("utf-8") if result[1] else ""
|
| 467 |
+
|
| 468 |
+
encoding_results_log = None
|
| 469 |
+
if logging_enabled:
|
| 470 |
+
for line in stderr.splitlines():
|
| 471 |
+
if "time=" in line:
|
| 472 |
+
encoding_results_log = line
|
| 473 |
+
|
| 474 |
+
if logging_enabled:
|
| 475 |
+
print(f"Successfully encoded using libsvtav1 fallback for scene '{scene_type}': {output_path}")
|
| 476 |
+
return encoding_results_log, encoding_time_calculated
|
| 477 |
+
|
| 478 |
+
except Exception as fallback_error:
|
| 479 |
+
if logging_enabled:
|
| 480 |
+
print(f"Fallback encoding with libsvtav1 also failed: {fallback_error}")
|
| 481 |
+
return None, None
|
| 482 |
+
|
| 483 |
def encode_lossless_video(input_path, output_path, logging_enabled=True):
|
| 484 |
"""
|
| 485 |
Encode video using FFV1 lossless codec with optimized settings.
|
services/compress/utils/encoder_configs.py
CHANGED
|
@@ -1,72 +1,56 @@
|
|
| 1 |
|
|
|
|
|
|
|
| 2 |
ENCODER_SETTINGS = {
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
# Add SVT-AV1 specific AQ if needed, e.g., --enable-variance-boost
|
| 6 |
-
},
|
| 7 |
-
"libsvtav1": {
|
| 8 |
"codec": "libsvtav1", "preset": "8", "crf": 30, "keyint": 50,
|
| 9 |
-
# SVT-AV1
|
| 10 |
},
|
| 11 |
"av1_nvenc": {
|
| 12 |
-
"codec": "av1_nvenc", "preset": "p6", "cq": 30, "keyint": 50, 'pix_fmt': 'yuv420p'
|
| 13 |
},
|
| 14 |
-
"
|
| 15 |
-
"codec": "
|
| 16 |
-
|
|
|
|
| 17 |
},
|
| 18 |
-
"
|
| 19 |
-
"codec": "libaom-av1", "preset": "medium", "crf": 30, "b:v": "0", "cpu-used": 4, "keyint": 50,
|
| 20 |
-
"aq-mode": 1 # Default AQ mode for libaom
|
| 21 |
-
},
|
| 22 |
-
"h264": {
|
| 23 |
"codec": "libx264", "preset": "medium", "crf": 23, "keyint": 50,
|
| 24 |
-
"aq-mode": 1, "aq-strength": 1.0
|
| 25 |
},
|
| 26 |
-
"
|
| 27 |
-
"codec": "libvvenc", "preset": "
|
| 28 |
-
#
|
| 29 |
},
|
| 30 |
-
"
|
| 31 |
"codec": "libx265", "preset": "medium", "crf": 28, "keyint": 50,
|
| 32 |
-
"aq-mode": 2, "aq-strength": 1.0
|
| 33 |
},
|
| 34 |
"hevc_nvenc": {
|
| 35 |
"codec": "hevc_nvenc", "preset": "p4", "rc": "constqp", "cq": 22, "keyint": 50,
|
| 36 |
-
"spatial-aq": 1, "temporal-aq": 0
|
| 37 |
},
|
| 38 |
"h264_nvenc": {
|
| 39 |
"codec": "h264_nvenc", "preset": "p4", "rc": "constqp", "cq": 22, "keyint": 50,
|
| 40 |
-
"spatial-aq": 1, "temporal-aq": 0
|
| 41 |
-
},
|
| 42 |
-
"h264_videotoolbox": {
|
| 43 |
-
"codec": "h264_videotoolbox",
|
| 44 |
-
"profile:v": "main",
|
| 45 |
-
"crf": 23,
|
| 46 |
-
"keyint": 50,
|
| 47 |
-
'pix_fmt': 'yuv420p' # Often required for hardware encoders
|
| 48 |
-
},
|
| 49 |
-
"hevc_videotoolbox": {
|
| 50 |
-
"codec": "hevc_videotoolbox",
|
| 51 |
-
"profile:v": "main",
|
| 52 |
-
"crf": 28,
|
| 53 |
-
"keyint": 50,
|
| 54 |
-
'pix_fmt': 'yuv420p'
|
| 55 |
},
|
| 56 |
"ffv1": {
|
| 57 |
"codec": "ffv1",
|
| 58 |
-
"level": 3,
|
| 59 |
-
"coder": 1,
|
| 60 |
-
"context": 1,
|
| 61 |
-
"g": 1,
|
| 62 |
-
"slices": 4,
|
| 63 |
-
"slicecrc": 1,
|
| 64 |
-
"pix_fmt": "yuv420p",
|
| 65 |
},
|
| 66 |
}
|
| 67 |
|
|
|
|
|
|
|
| 68 |
SCENE_SPECIFIC_PARAMS = {
|
| 69 |
-
'
|
| 70 |
'Screen Content / Text': {'preset': 'p7', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 250},
|
| 71 |
'Faces / People': {'preset': 'p6', 'spatial-aq': 1, 'temporal-aq': 1, 'keyint': 100},
|
| 72 |
'Animation / Cartoon / Rendered Graphics': {'preset': 'p5', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 150},
|
|
@@ -74,7 +58,7 @@ SCENE_SPECIFIC_PARAMS = {
|
|
| 74 |
'other': {'keyint': 100},
|
| 75 |
'unclear': {'keyint': 100},
|
| 76 |
},
|
| 77 |
-
'
|
| 78 |
'Screen Content / Text': {'preset': 'p7', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 250},
|
| 79 |
'Faces / People': {'preset': 'p6', 'spatial-aq': 1, 'temporal-aq': 1, 'keyint': 100},
|
| 80 |
'Animation / Cartoon / Rendered Graphics': {'preset': 'p5', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 150},
|
|
@@ -82,7 +66,7 @@ SCENE_SPECIFIC_PARAMS = {
|
|
| 82 |
'other': {'keyint': 100},
|
| 83 |
'unclear': {'keyint': 100},
|
| 84 |
},
|
| 85 |
-
'
|
| 86 |
'Screen Content / Text': {'preset': 'p7', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 250},
|
| 87 |
'Faces / People': {'preset': 'p6', 'spatial-aq': 1, 'temporal-aq': 1, 'keyint': 100},
|
| 88 |
'Animation / Cartoon / Rendered Graphics': {'preset': 'p5', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 150},
|
|
@@ -90,14 +74,6 @@ SCENE_SPECIFIC_PARAMS = {
|
|
| 90 |
'other': {'keyint': 100},
|
| 91 |
'unclear': {'keyint': 100},
|
| 92 |
},
|
| 93 |
-
'libaom-av1': {
|
| 94 |
-
'Screen Content / Text': {'cpu-used': 3, 'tune': 'ssim', 'aq-mode': 2, 'keyint': 250},
|
| 95 |
-
'Animation / Cartoon / Rendered Graphics': {'cpu-used': 5, 'tune': 'animation', 'aq-mode': 1, 'keyint': 150},
|
| 96 |
-
'Faces / People': {'cpu-used': 4, 'tune': 'psnr', 'aq-mode': 1, 'keyint': 100},
|
| 97 |
-
'Gaming Content': {'cpu-used': 6, 'tune': 'fastdecode', 'aq-mode': 1, 'keyint': 75},
|
| 98 |
-
'other': {'cpu-used': 4, 'tune': 'film', 'aq-mode': 1, 'keyint': 100},
|
| 99 |
-
'unclear': {'cpu-used': 4, 'tune': 'film', 'aq-mode': 1, 'keyint': 100},
|
| 100 |
-
},
|
| 101 |
'libx264': {
|
| 102 |
'Screen Content / Text': {'preset': 'slow', 'tune': 'film', 'aq-mode': 1, 'aq-strength': 1.2, 'keyint': 250},
|
| 103 |
'Animation / Cartoon / Rendered Graphics': {'preset': 'medium', 'tune': 'animation', 'aq-mode': 1, 'aq-strength': 1.0, 'keyint': 150},
|
|
@@ -106,86 +82,163 @@ SCENE_SPECIFIC_PARAMS = {
|
|
| 106 |
'other': {'preset': 'medium', 'tune': 'film', 'aq-mode': 1, 'aq-strength': 1.0, 'keyint': 100},
|
| 107 |
'unclear': {'preset': 'medium', 'tune': 'film', 'aq-mode': 1, 'aq-strength': 1.0, 'keyint': 100},
|
| 108 |
},
|
| 109 |
-
|
| 110 |
'Screen Content / Text': {'preset': 'medium', 'tune': 'grain', 'aq-mode': 2, 'aq-strength': 1.1, 'keyint': 250},
|
| 111 |
'Animation / Cartoon / Rendered Graphics': {'preset': 'medium', 'tune': 'animation', 'aq-mode': 2, 'aq-strength': 1.0, 'keyint': 150},
|
| 112 |
-
'Faces / People': {'preset': 'medium', '
|
| 113 |
'Gaming Content': {'preset': 'fast', 'tune': 'fastdecode', 'aq-mode': 2, 'aq-strength': 1.0, 'keyint': 75},
|
| 114 |
-
'other': {'preset': 'medium', '
|
| 115 |
-
'unclear': {'preset': 'medium', '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
},
|
| 117 |
}
|
| 118 |
|
| 119 |
-
MODEL_CQ_REFERENCE_CODEC = "libsvtav1"
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
QUALITY_MAPPING_ANCHORS = {
|
| 122 |
-
"libaom-av1"
|
| 123 |
-
|
|
|
|
|
|
|
| 124 |
"target_param_type": "crf",
|
| 125 |
-
"target_param_range": [
|
| 126 |
-
"anchor_points": [
|
| 127 |
-
[20,
|
| 128 |
-
[30,
|
| 129 |
-
[40,
|
| 130 |
-
[50,
|
| 131 |
]
|
| 132 |
},
|
| 133 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
"model_ref_cq_range": [10, 63],
|
| 135 |
"target_param_type": "crf",
|
| 136 |
-
"target_param_range": [0, 51],
|
| 137 |
-
"anchor_points": [
|
| 138 |
-
[20, 18],
|
| 139 |
-
[30, 23],
|
| 140 |
-
[40, 28],
|
| 141 |
-
[50, 33]
|
| 142 |
]
|
| 143 |
},
|
| 144 |
-
|
| 145 |
-
"
|
| 146 |
"model_ref_cq_range": [10, 63],
|
| 147 |
"target_param_type": "cq",
|
| 148 |
-
"target_param_range": [10, 51],
|
| 149 |
-
"anchor_points": [
|
| 150 |
-
[20,
|
| 151 |
-
[30,
|
| 152 |
-
[40,
|
| 153 |
-
[50,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
]
|
| 155 |
},
|
| 156 |
-
"
|
| 157 |
"model_ref_cq_range": [10, 63],
|
| 158 |
"target_param_type": "crf",
|
| 159 |
-
"target_param_range": [0,
|
| 160 |
-
"anchor_points": [
|
| 161 |
-
[20,
|
| 162 |
-
[30,
|
| 163 |
-
[40,
|
| 164 |
-
[50,
|
| 165 |
]
|
| 166 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
}
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
CODEC_CQ_LIMITS = {
|
|
|
|
| 170 |
'av1_nvenc': {
|
| 171 |
-
'max_cq': 45,
|
| 172 |
-
'recommended_max': 40,
|
| 173 |
-
'quality_range': (20, 40),
|
| 174 |
'description': 'NVIDIA AV1 encoder'
|
| 175 |
},
|
| 176 |
-
'libsvtav1': {
|
| 177 |
'max_cq': 50,
|
| 178 |
'recommended_max': 45,
|
| 179 |
'quality_range': (25, 45),
|
| 180 |
'description': 'SVT-AV1 encoder'
|
| 181 |
},
|
| 182 |
-
'libaom-av1': {
|
| 183 |
-
'max_cq': 50,
|
| 184 |
-
'recommended_max': 45,
|
| 185 |
-
'quality_range': (25, 45),
|
| 186 |
-
'description': 'AOM AV1 encoder'
|
| 187 |
-
},
|
| 188 |
|
|
|
|
|
|
|
| 189 |
'h264_nvenc': {
|
| 190 |
'max_cq': 32,
|
| 191 |
'recommended_max': 28,
|
|
@@ -199,6 +252,7 @@ CODEC_CQ_LIMITS = {
|
|
| 199 |
'description': 'x264 H.264 encoder'
|
| 200 |
},
|
| 201 |
|
|
|
|
| 202 |
'hevc_nvenc': {
|
| 203 |
'max_cq': 35,
|
| 204 |
'recommended_max': 30,
|
|
@@ -212,13 +266,31 @@ CODEC_CQ_LIMITS = {
|
|
| 212 |
'description': 'x265 HEVC encoder'
|
| 213 |
},
|
| 214 |
|
| 215 |
-
|
|
|
|
| 216 |
'max_cq': 45,
|
| 217 |
'recommended_max': 40,
|
| 218 |
'quality_range': (25, 40),
|
| 219 |
'description': 'VP9 encoder'
|
| 220 |
},
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
'default': {
|
| 223 |
'max_cq': 40,
|
| 224 |
'recommended_max': 35,
|
|
|
|
| 1 |
|
| 2 |
+
# Configurable parameters for all encoders
|
| 3 |
+
# Base settings, including default AQ where applicable
|
| 4 |
ENCODER_SETTINGS = {
|
| 5 |
+
|
| 6 |
+
"libsvtav1": { # Changed from "AV1_Optimized"
|
|
|
|
|
|
|
|
|
|
| 7 |
"codec": "libsvtav1", "preset": "8", "crf": 30, "keyint": 50,
|
| 8 |
+
# SVT-AV1 specific settings
|
| 9 |
},
|
| 10 |
"av1_nvenc": {
|
| 11 |
+
"codec": "av1_nvenc", "preset": "p6", "cq": 30, "keyint": 50, 'pix_fmt': 'yuv420p'
|
| 12 |
},
|
| 13 |
+
"libvpx_vp9": { # Changed from "vp9"
|
| 14 |
+
"codec": "libvpx-vp9", "deadline": "good", "cpu-used": 2, "crf": 32, "keyint": 50,
|
| 15 |
+
"aq-mode": 1, "arnr-maxframes": 7, "arnr-strength": 4, "auto-alt-ref": 1,
|
| 16 |
+
"tune": "psnr", "row-mt": 1
|
| 17 |
},
|
| 18 |
+
"libx264": { # Changed from "h264"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
"codec": "libx264", "preset": "medium", "crf": 23, "keyint": 50,
|
| 20 |
+
"aq-mode": 1, "aq-strength": 1.0
|
| 21 |
},
|
| 22 |
+
"libvvenc": { # Changed from "h266_vvc"
|
| 23 |
+
"codec": "libvvenc", "preset": "medium", "crf": 28, "keyint": 50,
|
| 24 |
+
# VVC/H.266 encoder
|
| 25 |
},
|
| 26 |
+
"libx265": { # Changed from "hevc"
|
| 27 |
"codec": "libx265", "preset": "medium", "crf": 28, "keyint": 50,
|
| 28 |
+
"aq-mode": 2, "aq-strength": 1.0
|
| 29 |
},
|
| 30 |
"hevc_nvenc": {
|
| 31 |
"codec": "hevc_nvenc", "preset": "p4", "rc": "constqp", "cq": 22, "keyint": 50,
|
| 32 |
+
"spatial-aq": 1, "temporal-aq": 0
|
| 33 |
},
|
| 34 |
"h264_nvenc": {
|
| 35 |
"codec": "h264_nvenc", "preset": "p4", "rc": "constqp", "cq": 22, "keyint": 50,
|
| 36 |
+
"spatial-aq": 1, "temporal-aq": 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
},
|
| 38 |
"ffv1": {
|
| 39 |
"codec": "ffv1",
|
| 40 |
+
"level": 3,
|
| 41 |
+
"coder": 1,
|
| 42 |
+
"context": 1,
|
| 43 |
+
"g": 1,
|
| 44 |
+
"slices": 4,
|
| 45 |
+
"slicecrc": 1,
|
| 46 |
+
"pix_fmt": "yuv420p",
|
| 47 |
},
|
| 48 |
}
|
| 49 |
|
| 50 |
+
# Scene-Specific Parameter Overrides (including AQ and keyint)
|
| 51 |
+
# These are examples and need tuning based on content and codec specifics.
|
| 52 |
SCENE_SPECIFIC_PARAMS = {
|
| 53 |
+
'av1_nvenc': { # Changed from 'AV1_NVENC'
|
| 54 |
'Screen Content / Text': {'preset': 'p7', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 250},
|
| 55 |
'Faces / People': {'preset': 'p6', 'spatial-aq': 1, 'temporal-aq': 1, 'keyint': 100},
|
| 56 |
'Animation / Cartoon / Rendered Graphics': {'preset': 'p5', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 150},
|
|
|
|
| 58 |
'other': {'keyint': 100},
|
| 59 |
'unclear': {'keyint': 100},
|
| 60 |
},
|
| 61 |
+
'hevc_nvenc': { # Changed from 'HEVC_NVENC'
|
| 62 |
'Screen Content / Text': {'preset': 'p7', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 250},
|
| 63 |
'Faces / People': {'preset': 'p6', 'spatial-aq': 1, 'temporal-aq': 1, 'keyint': 100},
|
| 64 |
'Animation / Cartoon / Rendered Graphics': {'preset': 'p5', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 150},
|
|
|
|
| 66 |
'other': {'keyint': 100},
|
| 67 |
'unclear': {'keyint': 100},
|
| 68 |
},
|
| 69 |
+
'h264_nvenc': { # Changed from 'H264_NVENC'
|
| 70 |
'Screen Content / Text': {'preset': 'p7', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 250},
|
| 71 |
'Faces / People': {'preset': 'p6', 'spatial-aq': 1, 'temporal-aq': 1, 'keyint': 100},
|
| 72 |
'Animation / Cartoon / Rendered Graphics': {'preset': 'p5', 'spatial-aq': 1, 'temporal-aq': 0, 'keyint': 150},
|
|
|
|
| 74 |
'other': {'keyint': 100},
|
| 75 |
'unclear': {'keyint': 100},
|
| 76 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
'libx264': {
|
| 78 |
'Screen Content / Text': {'preset': 'slow', 'tune': 'film', 'aq-mode': 1, 'aq-strength': 1.2, 'keyint': 250},
|
| 79 |
'Animation / Cartoon / Rendered Graphics': {'preset': 'medium', 'tune': 'animation', 'aq-mode': 1, 'aq-strength': 1.0, 'keyint': 150},
|
|
|
|
| 82 |
'other': {'preset': 'medium', 'tune': 'film', 'aq-mode': 1, 'aq-strength': 1.0, 'keyint': 100},
|
| 83 |
'unclear': {'preset': 'medium', 'tune': 'film', 'aq-mode': 1, 'aq-strength': 1.0, 'keyint': 100},
|
| 84 |
},
|
| 85 |
+
'libx265': {
|
| 86 |
'Screen Content / Text': {'preset': 'medium', 'tune': 'grain', 'aq-mode': 2, 'aq-strength': 1.1, 'keyint': 250},
|
| 87 |
'Animation / Cartoon / Rendered Graphics': {'preset': 'medium', 'tune': 'animation', 'aq-mode': 2, 'aq-strength': 1.0, 'keyint': 150},
|
| 88 |
+
'Faces / People': {'preset': 'medium', 'aq-mode': 2, 'aq-strength': 0.9, 'keyint': 100},
|
| 89 |
'Gaming Content': {'preset': 'fast', 'tune': 'fastdecode', 'aq-mode': 2, 'aq-strength': 1.0, 'keyint': 75},
|
| 90 |
+
'other': {'preset': 'medium', 'aq-mode': 2, 'aq-strength': 1.0, 'keyint': 100},
|
| 91 |
+
'unclear': {'preset': 'medium', 'aq-mode': 2, 'aq-strength': 1.0, 'keyint': 100},
|
| 92 |
+
},
|
| 93 |
+
'libvpx_vp9': {
|
| 94 |
+
'Screen Content / Text': {'deadline': 'best', 'cpu-used': 1, 'aq-mode': 2, 'keyint': 250},
|
| 95 |
+
'Faces / People': {'deadline': 'good', 'cpu-used': 2, 'aq-mode': 1, 'keyint': 100},
|
| 96 |
+
'Animation / Cartoon / Rendered Graphics': {'deadline': 'good', 'cpu-used': 3, 'aq-mode': 1, 'keyint': 150},
|
| 97 |
+
'Gaming Content': {'deadline': 'realtime', 'cpu-used': 5, 'aq-mode': 1, 'keyint': 75},
|
| 98 |
+
'other': {'deadline': 'good', 'cpu-used': 2, 'aq-mode': 1, 'keyint': 100},
|
| 99 |
+
'unclear': {'deadline': 'good', 'cpu-used': 2, 'aq-mode': 1, 'keyint': 100},
|
| 100 |
+
},
|
| 101 |
+
'libsvtav1': {
|
| 102 |
+
'Screen Content / Text': {'preset': '6', 'tune': 0, 'keyint': 250}, # tune 0 = subjective quality
|
| 103 |
+
'Faces / People': {'preset': '8', 'tune': 1, 'keyint': 100}, # tune 1 = objective quality
|
| 104 |
+
'Animation / Cartoon / Rendered Graphics': {'preset': '7', 'tune': 0, 'keyint': 150},
|
| 105 |
+
'Gaming Content': {'preset': '9', 'tune': 2, 'keyint': 75}, # tune 2 = fast decode
|
| 106 |
+
'other': {'preset': '8', 'tune': 1, 'keyint': 100},
|
| 107 |
+
'unclear': {'preset': '8', 'tune': 1, 'keyint': 100},
|
| 108 |
+
},
|
| 109 |
+
'libvvenc': {
|
| 110 |
+
'Screen Content / Text': {'preset': 'slow', 'keyint': 250},
|
| 111 |
+
'Faces / People': {'preset': 'medium', 'keyint': 100},
|
| 112 |
+
'Animation / Cartoon / Rendered Graphics': {'preset': 'medium', 'keyint': 150},
|
| 113 |
+
'Gaming Content': {'preset': 'fast', 'keyint': 75},
|
| 114 |
+
'other': {'preset': 'medium', 'keyint': 100},
|
| 115 |
+
'unclear': {'preset': 'medium', 'keyint': 100},
|
| 116 |
},
|
| 117 |
}
|
| 118 |
|
|
|
|
| 119 |
|
| 120 |
+
|
| 121 |
+
# --- START: New Configuration for Quality Parameter Mapping ---
|
| 122 |
+
|
| 123 |
+
# Define which codec's CQ scale your VMAF prediction model was trained on.
|
| 124 |
+
# Example: If your model predicts CQ values suitable for AV1_NVENC.
|
| 125 |
+
MODEL_CQ_REFERENCE_CODEC = "av1_nvenc" # IMPORTANT: User must set this correctly
|
| 126 |
+
|
| 127 |
+
# Define anchor points for mapping the model's reference CQ (from MODEL_CQ_REFERENCE_CODEC)
|
| 128 |
+
# to other codecs' quality parameters.
|
| 129 |
+
# This requires empirical tuning and knowledge of codec quality scales.
|
| 130 |
+
# 'model_ref_cq_range': The typical input CQ range from your VMAF model.
|
| 131 |
+
# 'target_param_type': 'cq' or 'crf' for the target codec.
|
| 132 |
+
# 'target_param_range': The typical/effective output range for the target codec's parameter.
|
| 133 |
+
# 'anchor_points': A list of [model_ref_cq_value, target_codec_param_value] pairs.
|
| 134 |
+
# These points define the mapping relationship.
|
| 135 |
QUALITY_MAPPING_ANCHORS = {
|
| 136 |
+
# Changed "libaom-av1" to match ENCODER_SETTINGS
|
| 137 |
+
# Changed "H264" to "libx264" to match ENCODER_SETTINGS
|
| 138 |
+
"libx264": { # Mapping av1_nvenc CQ to libx264 CRF
|
| 139 |
+
"model_ref_cq_range": [10, 63],
|
| 140 |
"target_param_type": "crf",
|
| 141 |
+
"target_param_range": [0, 51], # Typical CRF range for libx264
|
| 142 |
+
"anchor_points": [ # [model_av1_nvenc_cq, libx264_crf]
|
| 143 |
+
[20, 16], # H.264 is less efficient, needs lower CRF
|
| 144 |
+
[30, 21], # More realistic mapping
|
| 145 |
+
[40, 26], # Conservative mapping for quality
|
| 146 |
+
[50, 31] # Upper range adjustment
|
| 147 |
]
|
| 148 |
},
|
| 149 |
+
# Changed "HEVC_NVENC" to "hevc_nvenc" to match ENCODER_SETTINGS
|
| 150 |
+
"hevc_nvenc": { # Mapping av1_nvenc CQ to hevc_nvenc CQ
|
| 151 |
+
"model_ref_cq_range": [10, 63],
|
| 152 |
+
"target_param_type": "cq",
|
| 153 |
+
"target_param_range": [10, 51], # Typical CQ range for NVENC HEVC
|
| 154 |
+
"anchor_points": [ # [model_av1_nvenc_cq, hevc_nvenc_cq]
|
| 155 |
+
[20, 18], # HEVC NVENC slightly different scale
|
| 156 |
+
[30, 23], # More conservative mapping
|
| 157 |
+
[40, 28], # Quality preservation
|
| 158 |
+
[50, 33] # Upper range adjustment
|
| 159 |
+
]
|
| 160 |
+
},
|
| 161 |
+
"libx265": { # Mapping av1_nvenc CQ to libx265 CRF
|
| 162 |
"model_ref_cq_range": [10, 63],
|
| 163 |
"target_param_type": "crf",
|
| 164 |
+
"target_param_range": [0, 51], # Typical CRF range for libx265
|
| 165 |
+
"anchor_points": [ # [model_av1_nvenc_cq, libx265_crf]
|
| 166 |
+
[20, 18], # x265 is more efficient than H.264
|
| 167 |
+
[30, 23], # Similar to H.264 but slightly better
|
| 168 |
+
[40, 28], # Conservative quality mapping
|
| 169 |
+
[50, 33] # Upper range adjustment
|
| 170 |
]
|
| 171 |
},
|
| 172 |
+
# Missing codec mappings
|
| 173 |
+
"h264_nvenc": { # Mapping av1_nvenc CQ to h264_nvenc CQ
|
| 174 |
"model_ref_cq_range": [10, 63],
|
| 175 |
"target_param_type": "cq",
|
| 176 |
+
"target_param_range": [10, 51], # Typical CQ range for NVENC H.264
|
| 177 |
+
"anchor_points": [ # [model_av1_nvenc_cq, h264_nvenc_cq]
|
| 178 |
+
[20, 17], # NVENC H.264 needs lower CQ for similar quality
|
| 179 |
+
[30, 22],
|
| 180 |
+
[40, 27],
|
| 181 |
+
[50, 32]
|
| 182 |
+
]
|
| 183 |
+
},
|
| 184 |
+
"libvpx_vp9": { # Mapping av1_nvenc CQ to libvpx-vp9 CRF
|
| 185 |
+
"model_ref_cq_range": [10, 63],
|
| 186 |
+
"target_param_type": "crf",
|
| 187 |
+
"target_param_range": [0, 63], # VP9 CRF range
|
| 188 |
+
"anchor_points": [ # [model_av1_nvenc_cq, vp9_crf]
|
| 189 |
+
[20, 22], # VP9 similar efficiency to AV1
|
| 190 |
+
[30, 32],
|
| 191 |
+
[40, 42],
|
| 192 |
+
[50, 52]
|
| 193 |
]
|
| 194 |
},
|
| 195 |
+
"libsvtav1": { # Mapping av1_nvenc CQ to libsvtav1 CRF
|
| 196 |
"model_ref_cq_range": [10, 63],
|
| 197 |
"target_param_type": "crf",
|
| 198 |
+
"target_param_range": [0, 63], # SVT-AV1 CRF range
|
| 199 |
+
"anchor_points": [ # [model_av1_nvenc_cq, svt_av1_crf]
|
| 200 |
+
[20, 21], # SVT-AV1 slightly different from NVENC
|
| 201 |
+
[30, 31],
|
| 202 |
+
[40, 41],
|
| 203 |
+
[50, 51]
|
| 204 |
]
|
| 205 |
},
|
| 206 |
+
"libvvenc": { # Mapping av1_nvenc CQ to libvvenc CRF
|
| 207 |
+
"model_ref_cq_range": [10, 63],
|
| 208 |
+
"target_param_type": "crf",
|
| 209 |
+
"target_param_range": [0, 51], # H.266/VVC CRF range
|
| 210 |
+
"anchor_points": [ # [model_av1_nvenc_cq, vvc_crf]
|
| 211 |
+
[20, 20], # VVC should be more efficient than AV1
|
| 212 |
+
[30, 25], # Better compression than AV1
|
| 213 |
+
[40, 30],
|
| 214 |
+
[50, 35]
|
| 215 |
+
]
|
| 216 |
+
},
|
| 217 |
+
# If a codec is the same as MODEL_CQ_REFERENCE_CODEC, it will use the CQ directly.
|
| 218 |
}
|
| 219 |
|
| 220 |
+
# =============================================================================
|
| 221 |
+
# CODEC-SPECIFIC CQ LIMITS AND QUALITY MAPPINGS
|
| 222 |
+
# =============================================================================
|
| 223 |
+
|
| 224 |
+
# Define maximum recommended CQ values for different codecs to maintain reasonable quality
|
| 225 |
CODEC_CQ_LIMITS = {
|
| 226 |
+
# AV1 Codecs
|
| 227 |
'av1_nvenc': {
|
| 228 |
+
'max_cq': 45, # Maximum CQ for reasonable quality
|
| 229 |
+
'recommended_max': 40, # Conservative maximum for good quality
|
| 230 |
+
'quality_range': (20, 40), # Typical quality range
|
| 231 |
'description': 'NVIDIA AV1 encoder'
|
| 232 |
},
|
| 233 |
+
'libsvtav1': { # Standardized name
|
| 234 |
'max_cq': 50,
|
| 235 |
'recommended_max': 45,
|
| 236 |
'quality_range': (25, 45),
|
| 237 |
'description': 'SVT-AV1 encoder'
|
| 238 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
+
|
| 241 |
+
# H.264 Codecs
|
| 242 |
'h264_nvenc': {
|
| 243 |
'max_cq': 32,
|
| 244 |
'recommended_max': 28,
|
|
|
|
| 252 |
'description': 'x264 H.264 encoder'
|
| 253 |
},
|
| 254 |
|
| 255 |
+
# H.265/HEVC Codecs
|
| 256 |
'hevc_nvenc': {
|
| 257 |
'max_cq': 35,
|
| 258 |
'recommended_max': 30,
|
|
|
|
| 266 |
'description': 'x265 HEVC encoder'
|
| 267 |
},
|
| 268 |
|
| 269 |
+
# VP9 Codec
|
| 270 |
+
'libvpx_vp9': { # Changed from 'libvpx-vp9'
|
| 271 |
'max_cq': 45,
|
| 272 |
'recommended_max': 40,
|
| 273 |
'quality_range': (25, 40),
|
| 274 |
'description': 'VP9 encoder'
|
| 275 |
},
|
| 276 |
|
| 277 |
+
# H.266/VVC Codec
|
| 278 |
+
'libvvenc': {
|
| 279 |
+
'max_cq': 45,
|
| 280 |
+
'recommended_max': 40,
|
| 281 |
+
'quality_range': (20, 40),
|
| 282 |
+
'description': 'VVenC H.266 encoder'
|
| 283 |
+
},
|
| 284 |
+
|
| 285 |
+
# Lossless codec
|
| 286 |
+
'ffv1': {
|
| 287 |
+
'max_cq': 0, # Lossless doesn't use quality parameters
|
| 288 |
+
'recommended_max': 0,
|
| 289 |
+
'quality_range': (0, 0),
|
| 290 |
+
'description': 'FFV1 lossless encoder'
|
| 291 |
+
},
|
| 292 |
+
|
| 293 |
+
# Default fallback for unknown codecs
|
| 294 |
'default': {
|
| 295 |
'max_cq': 40,
|
| 296 |
'recommended_max': 35,
|
services/compress/utils/merge_videos.py
CHANGED
|
@@ -19,35 +19,97 @@ def has_audio(video_path):
|
|
| 19 |
def merge_videos(scene_videos, output_video, logging_enabled=True):
|
| 20 |
"""
|
| 21 |
Merge the list of re-encoded scene files using FFmpeg's concat filter.
|
| 22 |
-
|
| 23 |
-
concat filter accordingly.
|
| 24 |
|
| 25 |
Args:
|
| 26 |
scene_files (list): List of file paths to the scene videos.
|
| 27 |
output_video (str): Path to the final merged video.
|
| 28 |
logging_enabled (bool): Whether to print messages.
|
| 29 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# Create a temporary file with the correct format
|
| 31 |
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
|
| 32 |
for video in scene_videos:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
# Write each file on a separate line with proper syntax
|
| 34 |
f.write(f"file '{os.path.abspath(video)}'\n")
|
| 35 |
concat_file = f.name
|
| 36 |
|
| 37 |
-
# Use the concat demuxer with
|
| 38 |
try:
|
|
|
|
| 39 |
cmd = [
|
| 40 |
-
'ffmpeg', '-y', '-
|
| 41 |
-
'-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
]
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
if logging_enabled:
|
| 45 |
-
print(f"
|
| 46 |
-
return
|
| 47 |
except subprocess.CalledProcessError as e:
|
| 48 |
if logging_enabled:
|
| 49 |
-
print(f"Error merging videos: {e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
return False
|
| 51 |
finally:
|
| 52 |
# Clean up temp file
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
def merge_videos(scene_videos, output_video, logging_enabled=True):
|
| 20 |
"""
|
| 21 |
Merge the list of re-encoded scene files using FFmpeg's concat filter.
|
| 22 |
+
Uses precise frame-level concatenation to prevent stuttering.
|
|
|
|
| 23 |
|
| 24 |
Args:
|
| 25 |
scene_files (list): List of file paths to the scene videos.
|
| 26 |
output_video (str): Path to the final merged video.
|
| 27 |
logging_enabled (bool): Whether to print messages.
|
| 28 |
"""
|
| 29 |
+
if not scene_videos:
|
| 30 |
+
if logging_enabled:
|
| 31 |
+
print("❌ No scene videos to merge")
|
| 32 |
+
return False
|
| 33 |
+
|
| 34 |
+
# For single scene, just copy the file
|
| 35 |
+
if len(scene_videos) == 1:
|
| 36 |
+
try:
|
| 37 |
+
import shutil
|
| 38 |
+
shutil.copy2(scene_videos[0], output_video)
|
| 39 |
+
if logging_enabled:
|
| 40 |
+
print(f"✅ Single scene copied to {output_video}")
|
| 41 |
+
return True
|
| 42 |
+
except Exception as e:
|
| 43 |
+
if logging_enabled:
|
| 44 |
+
print(f"❌ Failed to copy single scene: {e}")
|
| 45 |
+
return False
|
| 46 |
+
|
| 47 |
# Create a temporary file with the correct format
|
| 48 |
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
|
| 49 |
for video in scene_videos:
|
| 50 |
+
# Verify each scene file exists and has content
|
| 51 |
+
if not os.path.exists(video):
|
| 52 |
+
if logging_enabled:
|
| 53 |
+
print(f"⚠️ Scene file missing: {video}")
|
| 54 |
+
return False
|
| 55 |
+
if os.path.getsize(video) == 0:
|
| 56 |
+
if logging_enabled:
|
| 57 |
+
print(f"⚠️ Scene file empty: {video}")
|
| 58 |
+
return False
|
| 59 |
# Write each file on a separate line with proper syntax
|
| 60 |
f.write(f"file '{os.path.abspath(video)}'\n")
|
| 61 |
concat_file = f.name
|
| 62 |
|
| 63 |
+
# Use the concat demuxer with improved frame handling
|
| 64 |
try:
|
| 65 |
+
# Use concat filter instead of demuxer for better frame alignment
|
| 66 |
cmd = [
|
| 67 |
+
'ffmpeg', '-y', '-hide_banner',
|
| 68 |
+
'-f', 'concat', '-safe', '0',
|
| 69 |
+
'-i', concat_file,
|
| 70 |
+
'-c:v', 'copy', # Video stream copy
|
| 71 |
+
'-c:a', 'copy', # Audio stream copy
|
| 72 |
+
'-avoid_negative_ts', 'make_zero', # Handle timestamp alignment
|
| 73 |
+
'-fflags', '+genpts', # Generate proper timestamps
|
| 74 |
+
output_video
|
| 75 |
]
|
| 76 |
+
|
| 77 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
| 78 |
+
|
| 79 |
+
if result.returncode == 0:
|
| 80 |
+
# Verify output file
|
| 81 |
+
if os.path.exists(output_video) and os.path.getsize(output_video) > 0:
|
| 82 |
+
if logging_enabled:
|
| 83 |
+
print(f"✅ Successfully merged {len(scene_videos)} scenes into {output_video}")
|
| 84 |
+
return True
|
| 85 |
+
else:
|
| 86 |
+
if logging_enabled:
|
| 87 |
+
print(f"❌ Merge failed: output file not created or empty")
|
| 88 |
+
return False
|
| 89 |
+
else:
|
| 90 |
+
if logging_enabled:
|
| 91 |
+
print(f"❌ FFmpeg merge failed (code {result.returncode})")
|
| 92 |
+
if result.stderr:
|
| 93 |
+
print(f"Error details: {result.stderr}")
|
| 94 |
+
return False
|
| 95 |
+
|
| 96 |
+
except subprocess.TimeoutExpired:
|
| 97 |
if logging_enabled:
|
| 98 |
+
print(f"❌ Merge process timed out after 5 minutes")
|
| 99 |
+
return False
|
| 100 |
except subprocess.CalledProcessError as e:
|
| 101 |
if logging_enabled:
|
| 102 |
+
print(f"❌ Error merging videos: {e}")
|
| 103 |
+
if hasattr(e, 'stderr') and e.stderr:
|
| 104 |
+
print(f"Error details: {e.stderr}")
|
| 105 |
+
return False
|
| 106 |
+
except Exception as e:
|
| 107 |
+
if logging_enabled:
|
| 108 |
+
print(f"❌ Unexpected error during merge: {e}")
|
| 109 |
return False
|
| 110 |
finally:
|
| 111 |
# Clean up temp file
|
| 112 |
+
try:
|
| 113 |
+
os.unlink(concat_file)
|
| 114 |
+
except:
|
| 115 |
+
pass
|
services/compress/utils/processing_utils.py
CHANGED
|
@@ -5,6 +5,7 @@ import json
|
|
| 5 |
import shutil
|
| 6 |
from .encode_video import encode_video
|
| 7 |
from .classify_scene import classify_scene_with_model, extract_frames_from_scene
|
|
|
|
| 8 |
|
| 9 |
def get_video_bitrate(video_path):
|
| 10 |
"""Get the bitrate of a video file in kbps."""
|
|
@@ -403,7 +404,7 @@ def classify_scene_from_path(scene_path, temp_dir, scene_classifier_model, avail
|
|
| 403 |
print(f"Extracting video features from {scene_path}")
|
| 404 |
|
| 405 |
try:
|
| 406 |
-
|
| 407 |
video_features = analyze_video_fast(scene_path, logging_enabled=False)
|
| 408 |
|
| 409 |
if not video_features or 'error' in video_features:
|
|
|
|
| 5 |
import shutil
|
| 6 |
from .encode_video import encode_video
|
| 7 |
from .classify_scene import classify_scene_with_model, extract_frames_from_scene
|
| 8 |
+
from .analyze_video_fast import analyze_video_fast
|
| 9 |
|
| 10 |
def get_video_bitrate(video_path):
|
| 11 |
"""Get the bitrate of a video file in kbps."""
|
|
|
|
| 404 |
print(f"Extracting video features from {scene_path}")
|
| 405 |
|
| 406 |
try:
|
| 407 |
+
|
| 408 |
video_features = analyze_video_fast(scene_path, logging_enabled=False)
|
| 409 |
|
| 410 |
if not video_features or 'error' in video_features:
|
services/compress/utils/video_utils.py
CHANGED
|
@@ -49,3 +49,40 @@ def get_video_codec(video_path):
|
|
| 49 |
except Exception as e:
|
| 50 |
print(f"An unexpected error occurred while getting video codec: {e}")
|
| 51 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
except Exception as e:
|
| 50 |
print(f"An unexpected error occurred while getting video codec: {e}")
|
| 51 |
return None
|
| 52 |
+
|
| 53 |
+
def get_keyframes(video_path: str, max_keyframes: int = 50):
|
| 54 |
+
"""Get keyframe positions in seconds."""
|
| 55 |
+
try:
|
| 56 |
+
# Use ffprobe to get keyframe timestamps
|
| 57 |
+
cmd = [
|
| 58 |
+
'ffprobe', '-v', 'error',
|
| 59 |
+
'-select_streams', 'v:0',
|
| 60 |
+
'-show_entries', 'packet=pts_time,flags',
|
| 61 |
+
'-of', 'csv=print_section=0',
|
| 62 |
+
video_path
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 66 |
+
|
| 67 |
+
if result.returncode != 0:
|
| 68 |
+
print(f"⚠️ Failed to get keyframes: ffprobe returned {result.returncode}")
|
| 69 |
+
return []
|
| 70 |
+
|
| 71 |
+
# Parse output to find keyframe timestamps
|
| 72 |
+
keyframes = []
|
| 73 |
+
for line in result.stdout.splitlines():
|
| 74 |
+
if "K_" in line or ",K" in line: # Keyframe flag
|
| 75 |
+
parts = line.split(',')
|
| 76 |
+
if len(parts) >= 1 and parts[0].replace('.', '', 1).isdigit():
|
| 77 |
+
timestamp = float(parts[0])
|
| 78 |
+
keyframes.append(timestamp)
|
| 79 |
+
|
| 80 |
+
# Limit to max_keyframes to prevent excessive processing
|
| 81 |
+
if len(keyframes) >= max_keyframes:
|
| 82 |
+
break
|
| 83 |
+
|
| 84 |
+
return keyframes
|
| 85 |
+
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f"❌ Error getting keyframes: {str(e)}")
|
| 88 |
+
return []
|
services/compress/validator_merger.py
CHANGED
|
@@ -165,8 +165,6 @@ def validation_and_merging(original_video_path, encoded_scenes_data, config, log
|
|
| 165 |
print(f" 🎬 Executing video merge...")
|
| 166 |
|
| 167 |
try:
|
| 168 |
-
|
| 169 |
-
|
| 170 |
def timeout_handler(signum, frame):
|
| 171 |
raise TimeoutError("Video merging timeout")
|
| 172 |
|
|
|
|
| 165 |
print(f" 🎬 Executing video merge...")
|
| 166 |
|
| 167 |
try:
|
|
|
|
|
|
|
| 168 |
def timeout_handler(signum, frame):
|
| 169 |
raise TimeoutError("Video merging timeout")
|
| 170 |
|
services/compress/video_preprocessor.py
CHANGED
|
@@ -31,7 +31,7 @@ def pre_processing(video_path, target_quality='Medium',codec='auto', max_duratio
|
|
| 31 |
|
| 32 |
# ✅ CODEC HANDLING: Resolve auto codec selection
|
| 33 |
if codec.lower() == 'auto':
|
| 34 |
-
target_codec = '
|
| 35 |
|
| 36 |
else:
|
| 37 |
target_codec = codec
|
|
@@ -40,8 +40,8 @@ def pre_processing(video_path, target_quality='Medium',codec='auto', max_duratio
|
|
| 40 |
# ✅ QUALITY MAPPING: Convert target quality to VMAF score
|
| 41 |
quality_vmaf_mapping = {
|
| 42 |
'High': 95.0,
|
| 43 |
-
'Medium':
|
| 44 |
-
'Low': 90.0
|
| 45 |
}
|
| 46 |
|
| 47 |
# Validate and convert target quality
|
|
@@ -158,3 +158,5 @@ def pre_processing(video_path, target_quality='Medium',codec='auto', max_duratio
|
|
| 158 |
'container': os.path.splitext(video_path)[1][1:] # Extension without dot
|
| 159 |
}
|
| 160 |
}
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 158 |
'container': os.path.splitext(video_path)[1][1:] # Extension without dot
|
| 159 |
}
|
| 160 |
}
|
| 161 |
+
|
| 162 |
+
|