christopi commited on
Commit
9fa2aaa
·
1 Parent(s): ea3e5ec

feat: impelement randomization chunk timestamps

Browse files
services/compress/encoder.py CHANGED
@@ -10,7 +10,7 @@ from utils.processing_utils import (
10
  should_skip_encoding,
11
  encode_scene_with_size_check,
12
  analyze_input_compression,
13
- # classify_scene_from_path
14
  )
15
  from utils.encode_video import encode_video
16
  from utils.classify_scene import load_scene_classifier_model, CombinedModel
@@ -137,13 +137,15 @@ def ai_encoding(scene_metadata, config, resources, target_vmaf=None, logging_ena
137
  current_codec = original_video_metadata.get('codec', original_codec)
138
  config_codec = config.get('video_processing', {}).get('codec', 'auto')
139
 
 
 
 
140
  if target_codec_from_part1 and target_codec_from_part1 != 'auto':
141
  codec = target_codec_from_part1
142
  elif config_codec != 'auto':
143
  codec = config_codec
144
  else:
145
- codec_upgrade_map = {'h264': 'av1_nvenc', 'hevc': 'av1_nvenc', 'vp9': 'av1_nvenc', 'av1': 'av1_nvenc'}
146
- codec = codec_upgrade_map.get(current_codec.lower(), 'av1_nvenc')
147
 
148
  if logging_enabled:
149
  print(f" 🎥 Selected Codec: {codec}")
@@ -155,7 +157,7 @@ def ai_encoding(scene_metadata, config, resources, target_vmaf=None, logging_ena
155
  scene_type = 'default'
156
  confidence_score = 0.0
157
  try:
158
- classification_result = classify_scene_from_path(
159
  scene_path=scene_path,
160
  temp_dir=temp_dir,
161
  scene_classifier_model=resources['scene_classifier_model'],
@@ -164,8 +166,7 @@ def ai_encoding(scene_metadata, config, resources, target_vmaf=None, logging_ena
164
  class_mapping=resources['class_mapping'],
165
  logging_enabled=logging_enabled
166
  )
167
- scene_type = classification_result.get('scene_type', 'default')
168
- confidence_score = classification_result.get('confidence_score', 0.0)
169
 
170
  if logging_enabled:
171
  print(f" 🎭 Scene classified as: '{scene_type}' (Confidence: {confidence_score:.2f})")
@@ -293,7 +294,7 @@ if __name__ == '__main__':
293
  'original_video_metadata': {
294
  'path': 'original_video.mp4',
295
  'codec': 'h264',
296
- 'target_codec': 'av1_nvenc',
297
  }
298
  }
299
 
 
10
  should_skip_encoding,
11
  encode_scene_with_size_check,
12
  analyze_input_compression,
13
+ classify_scene_from_path
14
  )
15
  from utils.encode_video import encode_video
16
  from utils.classify_scene import load_scene_classifier_model, CombinedModel
 
137
  current_codec = original_video_metadata.get('codec', original_codec)
138
  config_codec = config.get('video_processing', {}).get('codec', 'auto')
139
 
140
+ # Define codec upgrade map outside the conditional blocks
141
+ codec_upgrade_map = {'h264': 'libsvtav1', 'hevc': 'libsvtav1', 'vp9': 'libsvtav1', 'av1': 'libsvtav1'}
142
+
143
  if target_codec_from_part1 and target_codec_from_part1 != 'auto':
144
  codec = target_codec_from_part1
145
  elif config_codec != 'auto':
146
  codec = config_codec
147
  else:
148
+ codec = codec_upgrade_map.get(current_codec.lower(), 'libsvtav1')
 
149
 
150
  if logging_enabled:
151
  print(f" 🎥 Selected Codec: {codec}")
 
157
  scene_type = 'default'
158
  confidence_score = 0.0
159
  try:
160
+ scene_type, detailed_results = classify_scene_from_path(
161
  scene_path=scene_path,
162
  temp_dir=temp_dir,
163
  scene_classifier_model=resources['scene_classifier_model'],
 
166
  class_mapping=resources['class_mapping'],
167
  logging_enabled=logging_enabled
168
  )
169
+ confidence_score = detailed_results.get('confidence_score', 0.0)
 
170
 
171
  if logging_enabled:
172
  print(f" 🎭 Scene classified as: '{scene_type}' (Confidence: {confidence_score:.2f})")
 
294
  'original_video_metadata': {
295
  'path': 'original_video.mp4',
296
  'codec': 'h264',
297
+ 'target_codec': 'libsvtav1',
298
  }
299
  }
300
 
services/compress/install_svt.md ADDED
File without changes
services/compress/scene_detector.py CHANGED
@@ -398,7 +398,7 @@ if __name__ == '__main__':
398
  'path': short_video,
399
  'codec': 'h264',
400
  'original_codec': 'h264',
401
- 'target_codec': 'av1_nvenc', # ✅ NEW: Target codec for encoding
402
  'duration': 15.0,
403
  'was_reencoded': False,
404
  'encoding_time': 0,
@@ -437,7 +437,7 @@ if __name__ == '__main__':
437
  'path': 'test_lossless_converted.mkv',
438
  'codec': 'ffv1',
439
  'original_codec': 'rawvideo',
440
- 'target_codec': 'av1_nvenc', # ✅ Target for final encoding
441
  'duration': 120.0,
442
  'was_reencoded': True,
443
  'encoding_time': 5.2,
 
398
  'path': short_video,
399
  'codec': 'h264',
400
  'original_codec': 'h264',
401
+ 'target_codec': 'libsvtav1', # ✅ NEW: Target codec for encoding
402
  'duration': 15.0,
403
  'was_reencoded': False,
404
  'encoding_time': 0,
 
437
  'path': 'test_lossless_converted.mkv',
438
  'codec': 'ffv1',
439
  'original_codec': 'rawvideo',
440
+ 'target_codec': 'libsvtav1', # ✅ Target for final encoding
441
  'duration': 120.0,
442
  'was_reencoded': True,
443
  'encoding_time': 5.2,
services/compress/utils/encoder_configs.py CHANGED
@@ -6,6 +6,10 @@ ENCODER_SETTINGS = {
6
  "codec": "libsvtav1", "preset": "8", "crf": 30, "keyint": 50,
7
  # Add SVT-AV1 specific AQ if needed, e.g., --enable-variance-boost
8
  },
 
 
 
 
9
  "av1_nvenc": {
10
  "codec": "av1_nvenc", "preset": "p6", "cq": 30, "keyint": 50, 'pix_fmt': 'yuv420p' # Default AQ settings for NVENC AV1
11
  },
@@ -128,8 +132,8 @@ SCENE_SPECIFIC_PARAMS = {
128
  # --- START: New Configuration for Quality Parameter Mapping ---
129
 
130
  # Define which codec's CQ scale your VMAF prediction model was trained on.
131
- # Example: If your model predicts CQ values suitable for AV1_NVENC.
132
- MODEL_CQ_REFERENCE_CODEC = "av1_nvenc" # IMPORTANT: User must set this correctly
133
 
134
  # Define anchor points for mapping the model's reference CQ (from MODEL_CQ_REFERENCE_CODEC)
135
  # to other codecs' quality parameters.
@@ -140,23 +144,23 @@ MODEL_CQ_REFERENCE_CODEC = "av1_nvenc" # IMPORTANT: User must set this correctly
140
  # 'anchor_points': A list of [model_ref_cq_value, target_codec_param_value] pairs.
141
  # These points define the mapping relationship.
142
  QUALITY_MAPPING_ANCHORS = {
143
- # Example: If MODEL_CQ_REFERENCE_CODEC is "AV1_NVENC"
144
- "libaom-av1": { # Mapping AV1_NVENC CQ to libaom-av1 CRF
145
  "model_ref_cq_range": [10, 63], # Expected input range from your AV1 model
146
  "target_param_type": "crf",
147
  "target_param_range": [10, 63], # Typical CRF range for libaom-av1
148
- "anchor_points": [ # [model_av1_nvenc_cq, libaom_av1_crf]
149
  [20, 22], # Lower CQ (better quality) maps to lower CRF
150
  [30, 32], # Medium CQ maps to medium CRF
151
  [40, 42], # Higher CQ (lower quality) maps to higher CRF
152
  [50, 52]
153
  ]
154
  },
155
- "H264": { # Mapping AV1_NVENC CQ to libx264 CRF
156
  "model_ref_cq_range": [10, 63],
157
  "target_param_type": "crf",
158
  "target_param_range": [0, 51], # Typical CRF range for libx264
159
- "anchor_points": [ # [model_av1_nvenc_cq, libx264_crf]
160
  [20, 18],
161
  [30, 23],
162
  [40, 28],
@@ -164,22 +168,22 @@ QUALITY_MAPPING_ANCHORS = {
164
  ]
165
  },
166
 
167
- "HEVC_NVENC": { # Mapping AV1_NVENC CQ to HEVC_NVENC CQ
168
  "model_ref_cq_range": [10, 63],
169
  "target_param_type": "cq",
170
  "target_param_range": [10, 51], # Typical CQ range for NVENC HEVC
171
- "anchor_points": [ # [model_av1_nvenc_cq, hevc_nvenc_cq]
172
  [20, 19], # Might be slightly different scale or perception
173
  [30, 25],
174
  [40, 31],
175
  [50, 37]
176
  ]
177
  },
178
- "libx265": { # Mapping AV1_NVENC CQ to libx265 CRF
179
  "model_ref_cq_range": [10, 63],
180
  "target_param_type": "crf",
181
  "target_param_range": [0, 51], # Typical CRF range for libx265
182
- "anchor_points": [ # [model_av1_nvenc_cq, libx265_crf]
183
  [20, 20],
184
  [30, 26],
185
  [40, 32],
 
6
  "codec": "libsvtav1", "preset": "8", "crf": 30, "keyint": 50,
7
  # Add SVT-AV1 specific AQ if needed, e.g., --enable-variance-boost
8
  },
9
+ "libsvtav1": {
10
+ "codec": "libsvtav1", "preset": "8", "crf": 30, "keyint": 50,
11
+ # SVT-AV1 encoder settings
12
+ },
13
  "av1_nvenc": {
14
  "codec": "av1_nvenc", "preset": "p6", "cq": 30, "keyint": 50, 'pix_fmt': 'yuv420p' # Default AQ settings for NVENC AV1
15
  },
 
132
  # --- START: New Configuration for Quality Parameter Mapping ---
133
 
134
  # Define which codec's CQ scale your VMAF prediction model was trained on.
135
+ # Example: If your model predicts CQ values suitable for libsvtav1.
136
+ MODEL_CQ_REFERENCE_CODEC = "libsvtav1" # IMPORTANT: User must set this correctly
137
 
138
  # Define anchor points for mapping the model's reference CQ (from MODEL_CQ_REFERENCE_CODEC)
139
  # to other codecs' quality parameters.
 
144
  # 'anchor_points': A list of [model_ref_cq_value, target_codec_param_value] pairs.
145
  # These points define the mapping relationship.
146
  QUALITY_MAPPING_ANCHORS = {
147
+ # Example: If MODEL_CQ_REFERENCE_CODEC is "libsvtav1"
148
+ "libaom-av1": { # Mapping libsvtav1 CQ to libaom-av1 CRF
149
  "model_ref_cq_range": [10, 63], # Expected input range from your AV1 model
150
  "target_param_type": "crf",
151
  "target_param_range": [10, 63], # Typical CRF range for libaom-av1
152
+ "anchor_points": [ # [model_libsvtav1_cq, libaom_av1_crf]
153
  [20, 22], # Lower CQ (better quality) maps to lower CRF
154
  [30, 32], # Medium CQ maps to medium CRF
155
  [40, 42], # Higher CQ (lower quality) maps to higher CRF
156
  [50, 52]
157
  ]
158
  },
159
+ "H264": { # Mapping libsvtav1 CQ to libx264 CRF
160
  "model_ref_cq_range": [10, 63],
161
  "target_param_type": "crf",
162
  "target_param_range": [0, 51], # Typical CRF range for libx264
163
+ "anchor_points": [ # [model_libsvtav1_cq, libx264_crf]
164
  [20, 18],
165
  [30, 23],
166
  [40, 28],
 
168
  ]
169
  },
170
 
171
+ "HEVC_NVENC": { # Mapping libsvtav1 CQ to HEVC_NVENC CQ
172
  "model_ref_cq_range": [10, 63],
173
  "target_param_type": "cq",
174
  "target_param_range": [10, 51], # Typical CQ range for NVENC HEVC
175
+ "anchor_points": [ # [model_libsvtav1_cq, hevc_nvenc_cq]
176
  [20, 19], # Might be slightly different scale or perception
177
  [30, 25],
178
  [40, 31],
179
  [50, 37]
180
  ]
181
  },
182
+ "libx265": { # Mapping libsvtav1 CQ to libx265 CRF
183
  "model_ref_cq_range": [10, 63],
184
  "target_param_type": "crf",
185
  "target_param_range": [0, 51], # Typical CRF range for libx265
186
+ "anchor_points": [ # [model_libsvtav1_cq, libx265_crf]
187
  [20, 20],
188
  [30, 26],
189
  [40, 32],
services/compress/utils/processing_utils.py CHANGED
@@ -6,6 +6,7 @@ import gc
6
  import subprocess
7
  import json
8
  import numpy as np
 
9
  import sys
10
  # Adjust sys.path to allow imports from the 'utils' directory
11
  # This assumes check_hardware.py is in src/utilities/
@@ -1127,3 +1128,162 @@ def safe_log_message(message, level="info"):
1127
  print(message)
1128
  else:
1129
  print(message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import subprocess
7
  import json
8
  import numpy as np
9
+ import shutil
10
  import sys
11
  # Adjust sys.path to allow imports from the 'utils' directory
12
  # This assumes check_hardware.py is in src/utilities/
 
1128
  print(message)
1129
  else:
1130
  print(message)
1131
+
1132
+
1133
+ def classify_scene_from_path(scene_path, temp_dir, scene_classifier_model, available_metrics,
1134
+ device='cpu', class_mapping=None, logging_enabled=True,
1135
+ num_frames=3, metrics_scaler=None):
1136
+ """
1137
+ Classify a scene directly from video path - wrapper around classify_scene_with_model
1138
+
1139
+ Args:
1140
+ scene_path: Path to the video file
1141
+ temp_dir: Temporary directory for frame extraction
1142
+ scene_classifier_model: Trained scene classifier model
1143
+ available_metrics: List of available metrics for the model
1144
+ device: Device to run inference on ('cpu' or 'cuda')
1145
+ class_mapping: Class mapping dictionary (optional)
1146
+ logging_enabled: Whether to enable logging
1147
+ num_frames: Number of frames to extract for classification
1148
+ metrics_scaler: Metrics scaler (optional)
1149
+
1150
+ Returns:
1151
+ tuple: (classification_label, detailed_results)
1152
+ Same format as classify_scene_with_model
1153
+ """
1154
+ try:
1155
+ # Step 1: Extract frames from the video
1156
+ if logging_enabled:
1157
+ print(f"Extracting {num_frames} frames from {scene_path}")
1158
+
1159
+ # Create temp directory for this specific classification
1160
+ import tempfile
1161
+ frame_temp_dir = os.path.join(temp_dir, f"frames_{int(time.time())}")
1162
+ os.makedirs(frame_temp_dir, exist_ok=True)
1163
+
1164
+ # For full video, extract frames from beginning to end
1165
+ # We need to get video duration first
1166
+ try:
1167
+ import subprocess
1168
+ # Get video duration using ffprobe
1169
+ cmd = [
1170
+ 'ffprobe', '-v', 'quiet', '-show_entries', 'format=duration',
1171
+ '-of', 'default=noprint_wrappers=1:nokey=1', scene_path
1172
+ ]
1173
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
1174
+ duration = float(result.stdout.strip())
1175
+
1176
+ # Extract frames from start to end
1177
+ frame_paths = extract_frames_from_scene(
1178
+ video_path=scene_path,
1179
+ start_time=0.0,
1180
+ end_time=duration,
1181
+ num_frames=num_frames,
1182
+ output_dir=frame_temp_dir
1183
+ )
1184
+ except Exception as e:
1185
+ if logging_enabled:
1186
+ print(f"Warning: Could not get video duration, using fallback frame extraction: {e}")
1187
+ # Fallback: extract frames without specific timing
1188
+ frame_paths = []
1189
+ for i in range(num_frames):
1190
+ output_frame = os.path.join(frame_temp_dir, f"frame_{i}.jpg")
1191
+ cmd = [
1192
+ 'ffmpeg', '-y', '-i', scene_path,
1193
+ '-vf', f'select=eq(n\\,{i * 10})', # Extract every 10th frame
1194
+ '-frames:v', '1', '-q:v', '2',
1195
+ '-hide_banner', '-loglevel', 'error',
1196
+ output_frame
1197
+ ]
1198
+ try:
1199
+ subprocess.run(cmd, check=True, capture_output=True)
1200
+ if os.path.exists(output_frame) and os.path.getsize(output_frame) > 0:
1201
+ frame_paths.append(output_frame)
1202
+ except:
1203
+ continue
1204
+
1205
+ if not frame_paths:
1206
+ if logging_enabled:
1207
+ print("Warning: No frames could be extracted from video")
1208
+ return "unclear", {
1209
+ 'confidence_score': 0.0,
1210
+ 'prob_screen_content': 0.0,
1211
+ 'prob_animation': 0.0,
1212
+ 'prob_faces': 0.0,
1213
+ 'prob_gaming': 0.0,
1214
+ 'prob_other': 0.0,
1215
+ 'prob_unclear': 1.0,
1216
+ 'frame_predictions': [],
1217
+ 'error': 'Frame extraction failed'
1218
+ }
1219
+
1220
+ # Step 2: Extract video features
1221
+ if logging_enabled:
1222
+ print(f"Extracting video features from {scene_path}")
1223
+
1224
+ try:
1225
+ # Try to import and use analyze_video_fast
1226
+ from analyze_video_fast import analyze_video_fast
1227
+ video_features = analyze_video_fast(scene_path, logging_enabled=False)
1228
+
1229
+ if not video_features or 'error' in video_features:
1230
+ # Create default video features if extraction fails
1231
+ if logging_enabled:
1232
+ print("Warning: Video feature extraction failed, using defaults")
1233
+ video_features = {}
1234
+ for metric in available_metrics:
1235
+ video_features[metric] = 0.5 # Default neutral values
1236
+ except ImportError:
1237
+ if logging_enabled:
1238
+ print("Warning: analyze_video_fast not available, using default features")
1239
+ # Create default video features
1240
+ video_features = {}
1241
+ for metric in available_metrics:
1242
+ video_features[metric] = 0.5 # Default neutral values
1243
+
1244
+ # Step 3: Classify using existing function
1245
+ classification_label, detailed_results = classify_scene_with_model(
1246
+ frame_paths=frame_paths,
1247
+ video_features=video_features,
1248
+ scene_classifier=scene_classifier_model,
1249
+ metrics_scaler=metrics_scaler,
1250
+ available_metrics=available_metrics,
1251
+ device=device,
1252
+ logging_enabled=logging_enabled
1253
+ )
1254
+
1255
+ # Cleanup temporary frames
1256
+ try:
1257
+ # You need to import shutil at the top of your file:
1258
+ #
1259
+ shutil.rmtree(frame_temp_dir)
1260
+ except Exception as e:
1261
+ if logging_enabled:
1262
+ print(f"Warning: Could not cleanup temp frames: {e}")
1263
+ print("--------------------------------")
1264
+ print(f"Classification label: {classification_label}")
1265
+ print(f"Detailed results: {detailed_results}")
1266
+ return classification_label, detailed_results
1267
+
1268
+ except Exception as e:
1269
+ if logging_enabled:
1270
+ print(f"Error in classify_scene_from_path: {e}")
1271
+
1272
+ # Cleanup on error
1273
+ try:
1274
+ if 'frame_temp_dir' in locals():
1275
+ shutil.rmtree(frame_temp_dir)
1276
+ except:
1277
+ pass
1278
+
1279
+ return "unclear", {
1280
+ 'confidence_score': 0.0,
1281
+ 'prob_screen_content': 0.0,
1282
+ 'prob_animation': 0.0,
1283
+ 'prob_faces': 0.0,
1284
+ 'prob_gaming': 0.0,
1285
+ 'prob_other': 0.0,
1286
+ 'prob_unclear': 1.0,
1287
+ 'frame_predictions': [],
1288
+ 'error': str(e)
1289
+ }
services/compress/video_preprocessor.py CHANGED
@@ -40,7 +40,7 @@ def pre_processing(video_path, target_quality='Medium',codec='auto', max_duratio
40
 
41
  # ✅ CODEC HANDLING: Resolve auto codec selection
42
  if codec.lower() == 'auto':
43
- target_codec = 'av1_nvenc' # Default to AV1 for best quality
44
 
45
  else:
46
  target_codec = codec
@@ -185,7 +185,7 @@ if __name__ == '__main__':
185
  # Test with different quality levels and codec options
186
  test_cases = [
187
  ('High', 'auto'),
188
- ('Medium', 'av1_nvenc'),
189
  ('Low', 'libx264'),
190
  ('Medium', 'auto') # Test auto-detection
191
  ]
@@ -233,7 +233,7 @@ if __name__ == '__main__':
233
  video_path=lossless_video,
234
  target_quality='High',
235
  max_duration=7200,
236
- codec='av1_nvenc' # Test with specific codec
237
  )
238
 
239
  if result:
 
40
 
41
  # ✅ CODEC HANDLING: Resolve auto codec selection
42
  if codec.lower() == 'auto':
43
+ target_codec = 'libsvtav1' # Default to SVT-AV1 for best quality
44
 
45
  else:
46
  target_codec = codec
 
185
  # Test with different quality levels and codec options
186
  test_cases = [
187
  ('High', 'auto'),
188
+ ('Medium', 'libsvtav1'),
189
  ('Low', 'libx264'),
190
  ('Medium', 'auto') # Test auto-detection
191
  ]
 
233
  video_path=lossless_video,
234
  target_quality='High',
235
  max_duration=7200,
236
+ codec='libsvtav1' # Test with specific codec
237
  )
238
 
239
  if result:
services/video_scheduler/video_utils.py CHANGED
@@ -354,74 +354,106 @@ def download_trim_downscale_video(
354
 
355
  def generate_chunk_timestamps(total_duration, clip_duration):
356
  """
357
- Generate chunk timestamps using sliding window approach to maximize chunks per video.
358
- This reduces API requests by extracting many overlapping chunks from each downloaded video.
 
359
 
360
  Strategy:
361
- - 5s clips: Slide by 1s intervals (1-6s, 2-7s, 3-8s, etc.)
362
- - 10s clips: Slide by 2s intervals (0-10s, 2-12s, 4-14s, etc.)
363
- - 20s clips: Slide by 3s intervals (0-20s, 3-23s, 6-26s, etc.)
364
  """
365
  chunks = []
366
 
367
- # Define slide intervals for each clip duration
368
- slide_intervals = {
369
- 1: 0.5, # For 1s clips, slide by 0.5s
370
- 2: 0.5, # For 2s clips, slide by 0.5s
371
- 3: 1, # For 3s clips, slide by 1s
372
- 4: 1, # For 4s clips, slide by 1s
373
- 5: 1, # For 5s clips, slide by 1s
374
- 10: 2, # For 10s clips, slide by 2s
375
- 20: 3 # For 20s clips, slide by 3s
 
376
  }
377
 
378
- slide_interval = slide_intervals.get(clip_duration, 1)
 
 
379
 
380
- # Calculate maximum number of chunks we can extract
381
- max_chunks_possible = int((total_duration - clip_duration) / slide_interval) + 1
 
 
 
382
 
383
  # Set reasonable limits to avoid over-extraction from one video
384
  max_chunks_per_video = {
385
- 1: min(20, max_chunks_possible), # Max 20 chunks for 1s clips
386
- 2: min(15, max_chunks_possible), # Max 15 chunks for 2s clips
387
- 3: min(12, max_chunks_possible), # Max 12 chunks for 3s clips
388
- 4: min(10, max_chunks_possible), # Max 10 chunks for 4s clips
389
- 5: min(8, max_chunks_possible), # Max 8 chunks for 5s clips
390
- 10: min(6, max_chunks_possible), # Max 6 chunks for 10s clips
391
- 20: min(4, max_chunks_possible) # Max 4 chunks for 20s clips
392
  }
393
 
394
- max_chunks = max_chunks_per_video.get(clip_duration, max_chunks_possible)
395
 
396
  # Ensure we have enough video duration
397
  if total_duration < clip_duration:
398
  return [(0, 0, min(total_duration, clip_duration))]
399
 
400
- # Generate sliding window chunks
401
- current_start = 0
 
 
 
 
 
402
  chunk_index = 0
 
403
 
404
- while (current_start + clip_duration <= total_duration and
405
- chunk_index < max_chunks):
 
406
 
407
- end_time = current_start + clip_duration
408
- chunks.append((chunk_index, current_start, end_time))
 
409
 
410
- current_start += slide_interval
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  chunk_index += 1
412
 
413
- # Add one final chunk from the end if we have remaining duration
414
  if (chunk_index < max_chunks and
415
  total_duration > clip_duration and
416
- current_start < total_duration - 2): # At least 2s remaining
417
 
418
  final_start = total_duration - clip_duration
419
  # Only add if it doesn't overlap too much with the last chunk
420
- if not chunks or final_start - chunks[-1][1] >= slide_interval:
421
  chunks.append((chunk_index, final_start, total_duration))
422
 
423
- safe_print(f"Generated {len(chunks)} chunks for {clip_duration}s duration from {total_duration:.1f}s video")
424
- safe_print(f"Chunk extraction details: slide_interval={slide_interval}s, max_chunks={max_chunks}")
425
 
426
  return chunks
427
 
 
354
 
355
  def generate_chunk_timestamps(total_duration, clip_duration):
356
  """
357
+ Generate chunk timestamps using randomized approach to maximize chunks per video.
358
+ This reduces API requests by extracting many overlapping chunks from each downloaded video
359
+ while using randomized start points to avoid predictable patterns.
360
 
361
  Strategy:
362
+ - Uses random start points within safe zones to avoid predictable patterns
363
+ - Maintains reasonable overlap between chunks for continuity
364
+ - Adapts to different clip durations with appropriate randomization ranges
365
  """
366
  chunks = []
367
 
368
+ # Define safe zones and overlap ranges for each clip duration
369
+ # Safe zones avoid very beginning/end of video where quality might be poor
370
+ safe_zone_configs = {
371
+ 1: {"safe_start": 0.2, "safe_end_ratio": 0.8, "min_overlap": 0.3, "max_overlap": 0.7},
372
+ 2: {"safe_start": 0.3, "safe_end_ratio": 0.85, "min_overlap": 0.5, "max_overlap": 1.0},
373
+ 3: {"safe_start": 0.4, "safe_end_ratio": 0.9, "min_overlap": 0.8, "max_overlap": 1.5},
374
+ 4: {"safe_start": 0.5, "safe_end_ratio": 0.9, "min_overlap": 1.0, "max_overlap": 2.0},
375
+ 5: {"safe_start": 0.5, "safe_end_ratio": 0.9, "min_overlap": 1.0, "max_overlap": 2.5},
376
+ 10: {"safe_start": 0.5, "safe_end_ratio": 0.9, "min_overlap": 2.0, "max_overlap": 4.0},
377
+ 20: {"safe_start": 0.5, "safe_end_ratio": 0.9, "min_overlap": 3.0, "max_overlap": 6.0}
378
  }
379
 
380
+ config = safe_zone_configs.get(clip_duration, {
381
+ "safe_start": 0.5, "safe_end_ratio": 0.9, "min_overlap": 1.0, "max_overlap": 2.0
382
+ })
383
 
384
+ # Calculate safe zone boundaries
385
+ safe_start = config["safe_start"]
386
+ safe_end = total_duration * config["safe_end_ratio"]
387
+ min_overlap = config["min_overlap"]
388
+ max_overlap = config["max_overlap"]
389
 
390
  # Set reasonable limits to avoid over-extraction from one video
391
  max_chunks_per_video = {
392
+ 1: 20, # Max 20 chunks for 1s clips
393
+ 2: 15, # Max 15 chunks for 2s clips
394
+ 3: 12, # Max 12 chunks for 3s clips
395
+ 4: 10, # Max 10 chunks for 4s clips
396
+ 5: 8, # Max 8 chunks for 5s clips
397
+ 10: 6, # Max 6 chunks for 10s clips
398
+ 20: 4 # Max 4 chunks for 20s clips
399
  }
400
 
401
+ max_chunks = max_chunks_per_video.get(clip_duration, 8)
402
 
403
  # Ensure we have enough video duration
404
  if total_duration < clip_duration:
405
  return [(0, 0, min(total_duration, clip_duration))]
406
 
407
+ # Ensure safe zone is valid
408
+ if safe_end - safe_start < clip_duration:
409
+ # If safe zone is too small, use the entire video
410
+ safe_start = 0
411
+ safe_end = total_duration
412
+
413
+ # Generate randomized chunks
414
  chunk_index = 0
415
+ last_end = safe_start
416
 
417
+ while chunk_index < max_chunks:
418
+ # Calculate the maximum start time for this chunk
419
+ max_start = min(safe_end - clip_duration, last_end + max_overlap)
420
 
421
+ # If we can't fit another chunk, break
422
+ if max_start < last_end - min_overlap:
423
+ break
424
 
425
+ # Generate random start time within valid range
426
+ min_start = max(safe_start, last_end - min_overlap)
427
+ if min_start >= max_start:
428
+ break
429
+
430
+ start_time = random.uniform(min_start, max_start)
431
+ end_time = start_time + clip_duration
432
+
433
+ # Ensure we don't exceed video duration
434
+ if end_time > total_duration:
435
+ end_time = total_duration
436
+ start_time = end_time - clip_duration
437
+ if start_time < 0:
438
+ start_time = 0
439
+
440
+ chunks.append((chunk_index, start_time, end_time))
441
+
442
+ last_end = end_time
443
  chunk_index += 1
444
 
445
+ # Add one final chunk from the end if we have remaining duration and space
446
  if (chunk_index < max_chunks and
447
  total_duration > clip_duration and
448
+ total_duration - clip_duration > last_end - min_overlap):
449
 
450
  final_start = total_duration - clip_duration
451
  # Only add if it doesn't overlap too much with the last chunk
452
+ if not chunks or final_start - chunks[-1][1] >= min_overlap:
453
  chunks.append((chunk_index, final_start, total_duration))
454
 
455
+ safe_print(f"Generated {len(chunks)} randomized chunks for {clip_duration}s duration from {total_duration:.1f}s video")
456
+ safe_print(f"Randomization details: safe_zone={safe_start:.1f}s-{safe_end:.1f}s, overlap_range={min_overlap:.1f}s-{max_overlap:.1f}s, max_chunks={max_chunks}")
457
 
458
  return chunks
459