christopi commited on
Commit
c6e7086
·
1 Parent(s): 75f767e

update configs

Browse files
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='src/models/preprocessing_pipeline.pkl',
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
- # Import custom preprocessing classes needed for unpickling
 
178
  try:
179
- from utils.data_preprocessing import (
180
- ColumnDropper, VMAFScaler, TargetExtractor, CQScaler,
181
- ResolutionTransformer, FeatureScaler, FrameRateTransformer
182
- )
 
 
183
  except ImportError as e:
184
  if logging_enabled:
185
- print(f"❌ ERROR: Could not import preprocessing classes: {e}")
 
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 src/models/ directory")
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 open(pipeline_path, 'rb') as f:
212
- pipeline_obj = pickle.load(f)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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', "../models/scene_classifier_model.pth")
432
- preprocessing_pipeline_path = model_paths.get('preprocessing_pipeline', "src/models/preprocessing_pipeline.pkl")
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
- output_scene_path = os.path.join(
935
- temp_dir,
936
- f"encoded_scene_{scene_number:03d}_{start_time:.0f}s-{end_time:.0f}s_{codec.lower()}.mp4"
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') # ✅ NEW: Extract target codec
 
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
- # SCENE DETECTION: Check for multiple scenes
87
- print(f" 🔍 Running adaptive scene detection...")
88
- has_multiple_scenes, estimated_scenes, _ = adaptive_scene_detection_check(
89
- video_path, duration, logging_enabled=True
90
- )
91
-
92
- if has_multiple_scenes:
93
- print(f" ✅ Multiple scenes detected ({estimated_scenes} estimated). Proceeding with full scene splitting.")
94
- scene_files, _ = split_video_into_scenes(video_path, temp_dir)
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" ⚠️ Scene splitting failed or found only 1 scene. Falling back to time-based splitting.")
101
- return time_based_splitting(video_path, temp_dir, config, duration, video_metadata)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  else:
103
- print(f" 📏 Single scene detected.")
104
- scene_config = config.get('scene_detection', {})
105
- enable_fallback = scene_config.get('enable_time_based_fallback', False)
106
-
107
- if enable_fallback:
108
- print(f" 🔄 Time-based fallback enabled. Attempting time-based scene splitting...")
109
- return time_based_splitting(video_path, temp_dir, config, duration, video_metadata)
 
 
 
 
 
 
 
 
 
 
110
  else:
111
- print(f" ✅ Using single scene (time-based fallback disabled)")
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': str(Path('./videos/temp_scenes').absolute()),
160
- 'output_dir': str(output_dir_path)
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🤖 === Part 3: AI Encoding ===")
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 individually...")
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
- print(f" 🎯 Target VMAF: {scene_metadata['original_video_metadata']['target_vmaf']}")
 
 
 
288
 
289
  scene_start_time = time.time()
290
 
291
  try:
292
- # ✅ SINGLE SCENE PROCESSING: Call Part 3 for individual scene
293
  encoded_path, scene_data = ai_encoding(
294
  scene_metadata=scene_metadata,
295
  config=config,
296
  resources=resources,
297
- target_vmaf=None, # Let it use the target from original video metadata
 
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" 🎚️ CQ used: {scene_data.get('final_adjusted_cq', 'unknown')}")
 
 
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
- # result = test_video_compression('test1.mp4')
541
- # print(result)
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
- try:
12
- from .encoder_configs import ENCODER_SETTINGS, SCENE_SPECIFIC_PARAMS, MODEL_CQ_REFERENCE_CODEC, QUALITY_MAPPING_ANCHORS
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 == "H264" or "libx264" in 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 == "hevc" or "libx265" in 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
- # --- libaom-av1 ---
90
- elif "libaom-av1" in codec or codec == "av1_fallback":
 
 
91
  if contrast_category == "high":
92
- params['aq-mode'] = 2 # Variance-based adaptive quantization
93
- params['deltaq-mode'] = 3 # Perceptual deltaq mode
 
94
  elif contrast_category == "low":
95
- params['aq-mode'] = 1 # Default adaptive quantization
96
- params['deltaq-mode'] = 0 # Disabled
97
  else:
98
- params['aq-mode'] = 1
99
- params['deltaq-mode'] = 0
100
 
101
- # --- SVT-AV1 ---
102
- elif "libsvtav1" in codec or codec == "av1_optimized":
103
- # params for SVT-AV1
104
  if contrast_category == "high":
105
- params['aq-mode'] = 2
 
 
106
  elif contrast_category == "low":
107
- params['aq-mode'] = 1
 
 
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 "NVENC" in codec:
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 "NVENC" in codec:
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 "NVENC" in codec:
127
  params['rc-lookahead'] = 20 # More lookahead for dramatic lighting
128
  else:
129
- if "NVENC" in codec:
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) # Update base with scene specifics
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 function arguments as highest priority overrides
176
- # Apply 'rate' (which is the model_predicted_av1_cq)
177
  if rate is not None:
178
- model_predicted_ref_cq = int(rate) # This is the CQ from find_optimal_cq, on MODEL_CQ_REFERENCE_CODEC's scale
179
-
180
  if codec == MODEL_CQ_REFERENCE_CODEC:
181
  # Use the predicted CQ directly for the reference codec
182
- if 'cq' in current_settings: # Assuming reference codec uses 'cq'
183
  current_settings['cq'] = model_predicted_ref_cq
184
- if 'crf' in current_settings: del current_settings['crf']
185
- # Ensure rate control is set correctly for CQ (e.g., constqp for NVENC)
186
- if codec.endswith("_NVENC") or codec.endswith("_QSV") or 'videotoolbox' in codec: # Added videotoolbox
187
- current_settings['rc'] = 'constqp' # Or appropriate for videotoolbox if it differs
188
- if logging_enabled: print(f"Applying model CQ directly for {MODEL_CQ_REFERENCE_CODEC}: {current_settings['cq']}")
189
- elif 'crf' in current_settings: # If reference codec uses CRF (less common for AV1 model output)
190
- current_settings['crf'] = model_predicted_ref_cq # Or a 1:1 mapping if scales are identical
191
- if 'cq' in current_settings: del current_settings['cq']
192
- if logging_enabled: print(f"Applying model CQ as CRF for {MODEL_CQ_REFERENCE_CODEC}: {current_settings['crf']}")
 
 
 
 
 
 
193
  else:
194
- if logging_enabled: print(f"Warning: Neither 'cq' nor 'crf' in base settings for {MODEL_CQ_REFERENCE_CODEC}. Applying rate as 'crf'.")
 
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
- # Ensure model_predicted_ref_cq is within the model_ref_cq_range for stable interpolation
205
- clamped_model_cq = np.clip(model_predicted_ref_cq, mapping_config['model_ref_cq_range'][0], mapping_config['model_ref_cq_range'][1])
 
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} for interpolation based on model_ref_cq_range.")
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: del current_settings['crf']
220
- if codec.endswith("_NVENC") or codec.endswith("_QSV") or 'videotoolbox' in codec: # Added videotoolbox
221
- current_settings['rc'] = 'constqp'
222
- if logging_enabled: print(f"Applying mapped CQ for {codec} from model ref CQ {model_predicted_ref_cq}: {current_settings['cq']}")
 
 
 
 
 
223
  elif target_param_type == 'crf':
224
  current_settings['crf'] = mapped_param_int
225
- if 'cq' in current_settings: del current_settings['cq']
226
- # Specific handling for codecs like libaom-av1 if they use CRF and need b:v=0
227
- if base_settings.get("codec") == "libaom-av1" and 'b:v' in current_settings:
228
- current_settings['b:v'] = "0"
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: print(f"Warning: Unknown target_param_type '{target_param_type}' for {codec}. Using direct rate as fallback.")
232
- if 'cq' in current_settings: current_settings['cq'] = model_predicted_ref_cq
233
- elif 'crf' in current_settings: current_settings['crf'] = model_predicted_ref_cq
 
 
 
234
 
235
  else:
 
236
  if logging_enabled:
237
- print(f"Warning: No quality mapping found for {codec} in QUALITY_MAPPING_ANCHORS. Applying model ref CQ {model_predicted_ref_cq} directly (may be inappropriate).")
238
- # Fallback: apply the rate directly, trying 'cq' then 'crf'
239
  if 'cq' in current_settings:
240
- current_settings['cq'] = model_predicted_ref_cq
241
- if 'crf' in current_settings: del current_settings['crf']
242
- if codec.endswith("_NVENC") or codec.endswith("_QSV") or 'videotoolbox' in codec: current_settings['rc'] = 'constqp'
 
 
 
 
 
243
  elif 'crf' in current_settings:
244
- current_settings['crf'] = model_predicted_ref_cq
245
- if 'cq' in current_settings: del current_settings['cq']
246
- if base_settings.get("codec") == "libaom-av1" and 'b:v' in current_settings: current_settings['b:v'] = "0"
247
- else: # Fallback: Assume CRF if neither is defined in base settings
248
- current_settings['crf'] = model_predicted_ref_cq
249
- if logging_enabled: print(f"Applied direct rate {model_predicted_ref_cq} to {codec}.")
250
-
251
- # Apply 'preset' override if provided directly to function
 
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 'max_bit_rate' override (and calculate bufsize)
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']: unit = 'k' # Default to k if unit is missing/invalid
 
265
  bufsize_val = numeric_maxrate * 2
266
  current_settings['bufsize'] = f"{bufsize_val}{unit}"
267
- # Ensure rate control mode supports maxrate (e.g., vbr for NVENC, not constqp)
268
- if (codec.endswith("_NVENC") or codec.endswith("_QSV")) and current_settings.get('rc') == 'constqp':
269
- current_settings['rc'] = 'vbr' # Switch to VBR if maxrate is set
270
- if logging_enabled:
271
- print("Switched rate control to 'vbr' due to max_bit_rate setting.")
 
 
272
  if logging_enabled:
273
  print(f"Applying maxrate: {current_settings['maxrate']}, bufsize: {current_settings['bufsize']}")
274
- except ValueError:
275
- # Keep warning prints
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
- # --- Build output_args dictionary for ffmpeg-python ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  output_args = {}
284
 
285
- # Map internal setting names to actual ffmpeg argument names
286
  key_map = {
287
- 'keyint': 'g',
288
  'bitrate': 'b:v',
289
  'codec': 'vcodec'
290
  }
291
 
292
- # Add all current settings to output_args, applying key mapping
 
 
 
 
 
 
 
 
 
 
293
  for key, value in current_settings.items():
294
- ffmpeg_key = key_map.get(key, key) # Use mapped key or original key if not in map
295
  output_args[ffmpeg_key] = value
296
 
297
- # Ensure vcodec is explicitly set if it wasn't the primary 'codec' key
298
  if 'vcodec' not in output_args and 'codec' in current_settings:
299
- output_args['vcodec'] = current_settings['codec']
300
 
301
- # Remove the internal 'codec' key if it's different from 'vcodec' and still present
302
  if 'codec' in output_args and output_args.get('codec') != output_args.get('vcodec'):
303
- del output_args['codec']
304
 
305
- # **AUDIO HANDLING - Check if audio exists first**
306
  has_audio = check_audio_stream(input_path)
307
 
308
  if has_audio:
309
  output_args['acodec'] = 'copy'
310
- # Map video and audio streams separately - don't use a list
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 (optional, for debugging)
341
  encoding_results_log = None
342
- if logging_enabled: # Only parse log if logging is on
343
  for line in stderr.splitlines():
344
- if "time=" in line: # Find the last line reporting time/stats
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
- print(f"Error encoding with {codec} scene '{scene_type}': {e}")
353
- print(f"FFmpeg stderr: {e.stderr.decode('utf8')}")
354
- return None, None
355
- except Exception as e:
356
- print(f"Unexpected error during encoding with {codec} scene '{scene_type}': {e}")
357
- traceback.print_exc()
358
- return None, None
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
- "AV1_Optimized": {
4
- "codec": "libsvtav1", "preset": "8", "crf": 30, "keyint": 50,
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 encoder settings
10
  },
11
  "av1_nvenc": {
12
- "codec": "av1_nvenc", "preset": "p6", "cq": 30, "keyint": 50, 'pix_fmt': 'yuv420p' # Default AQ settings for NVENC AV1
13
  },
14
- "av1_rust": {
15
- "codec": "librav1e", "preset": "4", "crf": 35, "keyint": 50,
16
- # rav1e has AQ settings too, check its documentation
 
17
  },
18
- "av1_fallback": {
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 # Default AQ for x264
25
  },
26
- "h266_vvc": {
27
- "codec": "libvvenc", "preset": "p4", "crf": 28, "keyint": 50,
28
- # Check libvvenc documentation for AQ parameters
29
  },
30
- "hevc": {
31
  "codec": "libx265", "preset": "medium", "crf": 28, "keyint": 50,
32
- "aq-mode": 2, "aq-strength": 1.0 # Default AQ for x265
33
  },
34
  "hevc_nvenc": {
35
  "codec": "hevc_nvenc", "preset": "p4", "rc": "constqp", "cq": 22, "keyint": 50,
36
- "spatial-aq": 1, "temporal-aq": 0 # Default AQ settings for NVENC HEVC
37
  },
38
  "h264_nvenc": {
39
  "codec": "h264_nvenc", "preset": "p4", "rc": "constqp", "cq": 22, "keyint": 50,
40
- "spatial-aq": 1, "temporal-aq": 0 # Default AQ settings for NVENC H264
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
- 'AV1_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,7 +58,7 @@ SCENE_SPECIFIC_PARAMS = {
74
  'other': {'keyint': 100},
75
  'unclear': {'keyint': 100},
76
  },
77
- 'HEVC_NVENC': {
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
- 'H264_NVENC': {
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
- 'libx265': {
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', 'tune': 'none', 'aq-mode': 2, 'aq-strength': 0.9, 'keyint': 100},
113
  'Gaming Content': {'preset': 'fast', 'tune': 'fastdecode', 'aq-mode': 2, 'aq-strength': 1.0, 'keyint': 75},
114
- 'other': {'preset': 'medium', 'tune': 'none', 'aq-mode': 2, 'aq-strength': 1.0, 'keyint': 100},
115
- 'unclear': {'preset': 'medium', 'tune': 'none', 'aq-mode': 2, 'aq-strength': 1.0, 'keyint': 100},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  },
117
  }
118
 
119
- MODEL_CQ_REFERENCE_CODEC = "libsvtav1"
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  QUALITY_MAPPING_ANCHORS = {
122
- "libaom-av1": {
123
- "model_ref_cq_range": [10, 63],
 
 
124
  "target_param_type": "crf",
125
- "target_param_range": [10, 63],
126
- "anchor_points": [
127
- [20, 22],
128
- [30, 32],
129
- [40, 42],
130
- [50, 52]
131
  ]
132
  },
133
- "H264": {
 
 
 
 
 
 
 
 
 
 
 
 
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
- "HEVC_NVENC": {
146
  "model_ref_cq_range": [10, 63],
147
  "target_param_type": "cq",
148
- "target_param_range": [10, 51],
149
- "anchor_points": [
150
- [20, 19],
151
- [30, 25],
152
- [40, 31],
153
- [50, 37]
 
 
 
 
 
 
 
 
 
 
 
154
  ]
155
  },
156
- "libx265": {
157
  "model_ref_cq_range": [10, 63],
158
  "target_param_type": "crf",
159
- "target_param_range": [0, 51],
160
- "anchor_points": [
161
- [20, 20],
162
- [30, 26],
163
- [40, 32],
164
- [50, 38]
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
- 'libvpx-vp9': {
 
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
- This function checks whether the scene files have audio or not, and builds the
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 the file list
38
  try:
 
39
  cmd = [
40
- 'ffmpeg', '-y', '-f', 'concat', '-safe', '0',
41
- '-i', concat_file, '-c', 'copy', output_video
 
 
 
 
 
 
42
  ]
43
- subprocess.run(cmd, check=True, capture_output=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  if logging_enabled:
45
- print(f"Successfully merged videos into {output_video}")
46
- return True
47
  except subprocess.CalledProcessError as e:
48
  if logging_enabled:
49
- print(f"Error merging videos: {e.stderr}")
 
 
 
 
 
 
50
  return False
51
  finally:
52
  # Clean up temp file
53
- os.unlink(concat_file)
 
 
 
 
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
- from video_analysis import analyze_video_fast
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 = 'libsvtav1' # Default to SVT-AV1 for best quality
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': 93.0,
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
+