christopi commited on
Commit ·
9fa2aaa
1
Parent(s): ea3e5ec
feat: impelement randomization chunk timestamps
Browse files- services/compress/encoder.py +8 -7
- services/compress/install_svt.md +0 -0
- services/compress/scene_detector.py +2 -2
- services/compress/utils/encoder_configs.py +15 -11
- services/compress/utils/processing_utils.py +160 -0
- services/compress/video_preprocessor.py +3 -3
- services/video_scheduler/video_utils.py +69 -37
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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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': '
|
| 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': '
|
| 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': '
|
| 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
|
| 132 |
-
MODEL_CQ_REFERENCE_CODEC = "
|
| 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 "
|
| 144 |
-
"libaom-av1": { # Mapping
|
| 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": [ # [
|
| 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
|
| 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": [ # [
|
| 160 |
[20, 18],
|
| 161 |
[30, 23],
|
| 162 |
[40, 28],
|
|
@@ -164,22 +168,22 @@ QUALITY_MAPPING_ANCHORS = {
|
|
| 164 |
]
|
| 165 |
},
|
| 166 |
|
| 167 |
-
"HEVC_NVENC": { # Mapping
|
| 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": [ # [
|
| 172 |
[20, 19], # Might be slightly different scale or perception
|
| 173 |
[30, 25],
|
| 174 |
[40, 31],
|
| 175 |
[50, 37]
|
| 176 |
]
|
| 177 |
},
|
| 178 |
-
"libx265": { # Mapping
|
| 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": [ # [
|
| 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 = '
|
| 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', '
|
| 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='
|
| 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
|
| 358 |
-
This reduces API requests by extracting many overlapping chunks from each downloaded video
|
|
|
|
| 359 |
|
| 360 |
Strategy:
|
| 361 |
-
-
|
| 362 |
-
-
|
| 363 |
-
-
|
| 364 |
"""
|
| 365 |
chunks = []
|
| 366 |
|
| 367 |
-
# Define
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
| 376 |
}
|
| 377 |
|
| 378 |
-
|
|
|
|
|
|
|
| 379 |
|
| 380 |
-
# Calculate
|
| 381 |
-
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
# Set reasonable limits to avoid over-extraction from one video
|
| 384 |
max_chunks_per_video = {
|
| 385 |
-
1:
|
| 386 |
-
2:
|
| 387 |
-
3:
|
| 388 |
-
4:
|
| 389 |
-
5:
|
| 390 |
-
10:
|
| 391 |
-
20:
|
| 392 |
}
|
| 393 |
|
| 394 |
-
max_chunks = max_chunks_per_video.get(clip_duration,
|
| 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 |
-
#
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
chunk_index = 0
|
|
|
|
| 403 |
|
| 404 |
-
while
|
| 405 |
-
|
|
|
|
| 406 |
|
| 407 |
-
|
| 408 |
-
|
|
|
|
| 409 |
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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] >=
|
| 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"
|
| 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 |
|