File size: 12,237 Bytes
1fff877
 
 
9fa2aaa
15b449b
8787204
 
c6e7086
1fff877
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15b449b
1fff877
 
15b449b
1fff877
 
 
 
 
 
 
 
15b449b
1fff877
 
 
 
15b449b
1fff877
4e4aa87
1fff877
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4e4aa87
 
1fff877
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9fa2aaa
 
 
 
75f767e
 
 
9fa2aaa
 
 
 
 
 
 
 
 
 
 
 
 
75f767e
 
 
 
9fa2aaa
 
75f767e
 
 
9fa2aaa
 
 
 
 
 
 
 
 
 
 
 
 
75f767e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9fa2aaa
 
 
 
 
 
 
 
 
 
 
 
 
 
75f767e
9fa2aaa
75f767e
9fa2aaa
 
 
 
c6e7086
9fa2aaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75f767e
9fa2aaa
 
 
 
 
 
 
 
 
 
75f767e
9fa2aaa
 
 
 
 
75f767e
 
9fa2aaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75f767e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
import os
import time
import json
import shutil
import subprocess
from .encode_video import encode_video
from .classify_scene import classify_scene_with_model, extract_frames_from_scene
from .analyze_video_fast import analyze_video_fast

def get_video_bitrate(video_path):
    """Get the bitrate of a video file in kbps."""
    try:
        cmd = [
            'ffprobe', '-v', 'quiet', '-print_format', 'json', 
            '-show_format', video_path
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        data = json.loads(result.stdout)
        
        bitrate = data.get('format', {}).get('bit_rate')
        if bitrate:
            return float(bitrate) / 1000  # Convert to kbps
        return None
    except Exception as e:
        print(f"Error getting bitrate for {video_path}: {e}")
        return None

def analyze_input_compression(video_path):
    """Analyze if input video is already well compressed."""
    try:
        cmd = [
            'ffprobe', '-v', 'quiet', '-print_format', 'json', 
            '-show_streams', '-show_format', video_path
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        data = json.loads(result.stdout)
        
        video_stream = next((s for s in data.get('streams', []) if s.get('codec_type') == 'video'), None)
        if not video_stream:
            return None
            
        codec = video_stream.get('codec_name', '')
        bitrate = data.get('format', {}).get('bit_rate')
        duration = float(data.get('format', {}).get('duration', 0))
        
        # Calculate pixels per second
        width = int(video_stream.get('width', 0))
        height = int(video_stream.get('height', 0))
        fps_str = video_stream.get('r_frame_rate', '0/1')
        fps = eval(fps_str) if '/' in fps_str else float(fps_str)
        
        pixels_per_second = width * height * fps
        
        return {
            'codec': codec,
            'bitrate_kbps': float(bitrate) / 1000 if bitrate else None,
            'duration': duration,
            'resolution': (width, height),
            'fps': fps,
            'pixels_per_second': pixels_per_second,
            'bits_per_pixel': (float(bitrate) / pixels_per_second) if bitrate and pixels_per_second > 0 else None
        }
    except Exception as e:
        print(f"Error analyzing input compression: {e}")
        return None

def should_skip_encoding(input_path, estimated_cq, target_vmaf, logging_enabled=True):
    """Determine if encoding should be skipped to prevent size increase."""
    
    input_analysis = analyze_input_compression(input_path)
    if not input_analysis:
        return False, estimated_cq + 1
    
    input_bitrate = input_analysis.get('bitrate_kbps', 0)
    bits_per_pixel = input_analysis.get('bits_per_pixel', 0)
    codec = input_analysis.get('codec', '').lower()
    
    if bits_per_pixel and bits_per_pixel < 0.02 and target_vmaf < 90:  
        if logging_enabled:
            print(f"⚠️ Input very well compressed (bits/pixel: {bits_per_pixel:.4f}). Encoding anyway with conservative CQ.")
        return False, min(estimated_cq + 8, 51)
    
    if target_vmaf >= 90:
        if logging_enabled:
            print(f"🎯 High target VMAF ({target_vmaf}) - encoding anyway with adjusted CQ")
        return False, estimated_cq
    
    adjusted_cq = calculate_bitrate_aware_cq(estimated_cq, input_analysis, target_vmaf, "unknown")
    
    if input_bitrate > 10000:  
        adjusted_cq = min(adjusted_cq + 4, 51)
        if logging_enabled:
            print(f"Very high bitrate input ({input_bitrate:.0f} kbps) - adding safety margin. CQ: {adjusted_cq}")
    
    return False, adjusted_cq 

def encode_scene_with_size_check(scene_path, output_path, codec, adjusted_cq, content_type, contrast_value, codec_mode=None, target_bitrate=None, max_retries=2, logging_enabled=True):
    
    original_size = os.path.getsize(scene_path)
    input_analysis = analyze_input_compression(scene_path)
    
    # Pre-check if encoding should be skipped
    skip_encoding, final_cq = should_skip_encoding(scene_path, adjusted_cq, 93, logging_enabled)
    
    if skip_encoding:
        if logging_enabled:
            print("Skipping encoding - input already optimally compressed")
        return None, 0.0
    
    if final_cq != adjusted_cq:
        if logging_enabled:
            print(f"Adjusted CQ from {adjusted_cq} to {final_cq} based on input analysis")
        adjusted_cq = final_cq
    
    input_bitrate = input_analysis.get('bitrate_kbps', 0) if input_analysis else 0
    if input_bitrate > 4000:
        max_size_ratio = 1.15  # Allow 15% increase for high-bitrate content
    else:
        max_size_ratio = 1.05  # Only allow 5% for normal content
    
    for attempt in range(max_retries + 1):
        if logging_enabled and attempt > 0:
            print(f"Retry attempt {attempt} - increasing CQ to prevent size increase")
        
        # FIXED: Actually increase CQ on retries
        retry_cq = adjusted_cq + (attempt * 4)  # Start with base CQ, then +4, +8
        retry_cq = min(retry_cq, 51)  # Cap at 51
        
        if logging_enabled and attempt > 0:
            print(f"Using CQ {retry_cq} for attempt {attempt}")
        
        start_time = time.time()
        _, encoding_time = encode_video(
            input_path=scene_path,
            output_path=output_path,
            codec=codec,
            rate=retry_cq,  # FIXED: Use retry_cq instead of adjusted_cq
            scene_type=content_type if attempt == 0 else None,
            contrast_value=contrast_value if attempt == 0 else None,
            codec_mode=codec_mode,
            target_bitrate=target_bitrate,
            logging_enabled=logging_enabled
        )
        
        if not os.path.exists(output_path):
            continue
            
        output_size = os.path.getsize(output_path)
        size_ratio = output_size / original_size
        
        if logging_enabled:
            print(f"Size ratio: {size_ratio:.2f} (output/input) with CQ {retry_cq}")
        
        # Accept the result if within threshold OR it's the final attempt
        if size_ratio <= max_size_ratio or attempt == max_retries:
            if size_ratio > max_size_ratio and logging_enabled:
                print(f"⚠️  Accepting oversized output ({size_ratio:.2f}x) - final attempt")
            return output_path, encoding_time
        else:
            if logging_enabled:
                print(f"Output too large ({size_ratio:.2f}x), retrying with higher CQ")
            os.remove(output_path)  # Remove oversized file
    
    return None, 0.0

def classify_scene_from_path(scene_path, temp_dir, scene_classifier_model, available_metrics, 
                           device='cpu', class_mapping=None, logging_enabled=True, 
                           num_frames=3, metrics_scaler=None):
    """
    Classify a scene directly from video path - wrapper that uses enhanced functions
    
    This function is a simplified wrapper that leverages the robust implementations
    
    Args:
        scene_path: Path to the video file
        temp_dir: Temporary directory for frame extraction
        scene_classifier_model: Trained scene classifier model
        available_metrics: List of available metrics for the model
        device: Device to run inference on ('cpu' or 'cuda')
        class_mapping: Class mapping dictionary (optional)
        logging_enabled: Whether to enable logging
        num_frames: Number of frames to extract for classification
        metrics_scaler: Metrics scaler (optional)
        
    Returns:
        tuple: (classification_label, detailed_results, video_features)
        classification_label: Scene classification result
        detailed_results: Classification confidence and probabilities
        video_features: Video analysis metrics from analyze_video_fast
    """
    try:
        
        
        # Step 1: Get video duration using ffprobe
        if logging_enabled:
            print(f"Extracting {num_frames} frames from {scene_path}")
        
        try:
            import subprocess
            cmd = [
                'ffprobe', '-v', 'quiet', '-show_entries', 'format=duration',
                '-of', 'default=noprint_wrappers=1:nokey=1', scene_path
            ]
            result = subprocess.run(cmd, capture_output=True, text=True, check=True)
            duration = float(result.stdout.strip())
        except Exception as e:
            if logging_enabled:
                print(f"Warning: Could not get video duration: {e}, using default 30s")
            duration = 30.0  # Fallback duration
        
        # Step 2: Create temp directory for frame extraction
        import tempfile
        frame_temp_dir = os.path.join(temp_dir, f"frames_{int(time.time())}")
        os.makedirs(frame_temp_dir, exist_ok=True)
        
        # Step 3: Extract frames using the robust enhanced function
        frame_paths = extract_frames_from_scene(
            video_path=scene_path,
            start_time=0.0,
            end_time=duration,
            num_frames=num_frames,
            output_dir=frame_temp_dir
        )
        
        if not frame_paths:
            if logging_enabled:
                print("Warning: No frames could be extracted from video")
            return "unclear", {
                'confidence_score': 0.0,
                'prob_screen_content': 0.0,
                'prob_animation': 0.0,
                'prob_faces': 0.0,
                'prob_gaming': 0.0,
                'prob_other': 0.0,
                'prob_unclear': 1.0,
                'frame_predictions': [],
                'error': 'Frame extraction failed'
            }, {}
        
        # Step 4: Extract video features
        if logging_enabled:
            print(f"Extracting video features from {scene_path}")
        
        try:
            
            video_features = analyze_video_fast(scene_path, logging_enabled=False)
            
            if not video_features or 'error' in video_features:
                # Create default video features if extraction fails
                if logging_enabled:
                    print("Warning: Video feature extraction failed, using defaults")
                video_features = {}
                for metric in available_metrics:
                    video_features[metric] = 0.5  # Default neutral values
        except ImportError:
            if logging_enabled:
                print("Warning: analyze_video_fast not available, using default features")
            # Create default video features
            video_features = {}
            for metric in available_metrics:
                video_features[metric] = 0.5  # Default neutral values
        
        # Step 5: Classify using the enhanced classification function
        classification_label, detailed_results = classify_scene_with_model(
            frame_paths=frame_paths,
            video_features=video_features,
            scene_classifier=scene_classifier_model,
            metrics_scaler=metrics_scaler,
            available_metrics=available_metrics,
            device=device,
            logging_enabled=logging_enabled
        )
        
        # Cleanup temporary frames
        try:
            shutil.rmtree(frame_temp_dir)
        except Exception as e:
            if logging_enabled:
                print(f"Warning: Could not cleanup temp frames: {e}")
        
        return classification_label, detailed_results, video_features
        
    except Exception as e:
        if logging_enabled:
            print(f"Error in classify_scene_from_path: {e}")
        
        # Cleanup on error
        try:
            if 'frame_temp_dir' in locals():
                shutil.rmtree(frame_temp_dir)
        except:
            pass
        
        return "unclear", {
            'confidence_score': 0.0,
            'prob_screen_content': 0.0,
            'prob_animation': 0.0,
            'prob_faces': 0.0,
            'prob_gaming': 0.0,
            'prob_other': 0.0,
            'prob_unclear': 1.0,
            'frame_predictions': [],
            'error': str(e)
        }, {}