Skip to content

Commit 992af79

Browse files
committed
windows fixes
1 parent 36cb027 commit 992af79

6 files changed

Lines changed: 305 additions & 42 deletions

File tree

‎sharp_frames/processing/frame_extractor.py‎

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -363,32 +363,81 @@ def _run_ffmpeg_extraction(self, video_path: str, output_dir: str, fps: int,
363363
# Monitor progress by counting extracted files
364364
last_file_count = 0
365365
start_time = time.time()
366+
last_progress_time = start_time # Track when we last saw progress
367+
stall_timeout = 10.0 # Consider stalled if no progress for 10 seconds
366368

367369
while process.poll() is None:
370+
current_time = time.time()
371+
elapsed = current_time - start_time
372+
368373
# Check for timeout
369-
if time.time() - start_time > timeout:
374+
if elapsed > timeout:
370375
process.kill()
371376
raise subprocess.TimeoutExpired(cmd, timeout)
372377

373378
# Count extracted files
374379
if os.path.exists(output_dir):
375-
file_count = len([f for f in os.listdir(output_dir) if f.startswith('frame_')])
376-
377-
if file_count > last_file_count and self.progress_callback:
378-
if estimated_total:
379-
self.progress_callback("extraction", file_count, estimated_total,
380-
f"Extracted {file_count}/{estimated_total} frames")
381-
else:
382-
self.progress_callback("extraction", file_count, 0, f"Extracted {file_count} frames")
383-
last_file_count = file_count
380+
try:
381+
file_count = len([f for f in os.listdir(output_dir) if f.startswith('frame_')])
382+
383+
if file_count > last_file_count:
384+
# We have new frames, update progress time
385+
last_progress_time = current_time
386+
387+
if self.progress_callback:
388+
if estimated_total:
389+
self.progress_callback("extraction", file_count, estimated_total,
390+
f"Extracted {file_count}/{estimated_total} frames")
391+
else:
392+
self.progress_callback("extraction", file_count, 0, f"Extracted {file_count} frames")
393+
last_file_count = file_count
394+
395+
# Check if we've reached the expected total
396+
if estimated_total > 0 and file_count >= estimated_total:
397+
process.terminate()
398+
break
399+
400+
# Check for stalled extraction (prevents hanging on Windows)
401+
if current_time - last_progress_time > stall_timeout:
402+
process.terminate()
403+
break
404+
except Exception:
405+
pass # Continue if file counting fails
384406

385407
time.sleep(0.1) # Small delay to avoid excessive polling
386408

387-
# Get final results
388-
stdout, stderr = process.communicate()
409+
# Ensure process is terminated and wait for it to finish
410+
if process.poll() is None:
411+
process.terminate()
412+
try:
413+
# Wait up to 5 seconds for graceful termination
414+
stdout, stderr = process.communicate(timeout=5)
415+
except subprocess.TimeoutExpired:
416+
process.kill()
417+
stdout, stderr = process.communicate()
418+
else:
419+
# Process already finished, get results
420+
stdout, stderr = process.communicate()
389421

390-
if process.returncode != 0:
391-
print(f"FFmpeg error: {stderr}")
422+
# Check if extraction was successful based on frame count
423+
# Don't rely on return code since we may have terminated the process
424+
if os.path.exists(output_dir):
425+
actual_frame_count = len([f for f in os.listdir(output_dir) if f.startswith('frame_')])
426+
427+
# Consider successful if we have frames and either:
428+
# 1. We reached the expected count, or
429+
# 2. We have a reasonable number of frames (at least 10)
430+
success = actual_frame_count > 0 and (
431+
(estimated_total > 0 and actual_frame_count >= estimated_total * 0.95) or # Allow 5% tolerance
432+
actual_frame_count >= 10
433+
)
434+
435+
if not success:
436+
print(f"FFmpeg extraction incomplete: {actual_frame_count} frames")
437+
if stderr:
438+
print(f"FFmpeg stderr: {stderr}")
439+
return False
440+
else:
392441
return False
393442

394443
# Final progress update
@@ -397,15 +446,18 @@ def _run_ffmpeg_extraction(self, video_path: str, output_dir: str, fps: int,
397446
if self.progress_callback:
398447
self.progress_callback("extraction", final_frame_count, final_frame_count,
399448
f"Extraction complete: {final_frame_count} frames")
400-
449+
401450
return True
402451

403-
except subprocess.TimeoutExpired:
452+
except subprocess.TimeoutExpired as e:
404453
print(f"FFmpeg extraction timeout for {video_path}")
405454
return False
406-
except FileNotFoundError:
455+
except FileNotFoundError as e:
407456
print("FFmpeg not found. Please install FFmpeg.")
408457
return False
458+
except Exception as e:
459+
print(f"Unexpected error during frame extraction: {e}")
460+
return False
409461

410462
def _get_extracted_frame_files(self, temp_dir: str) -> List[str]:
411463
"""Get list of extracted frame files from temp directory."""

‎sharp_frames/processing/sharpness_analyzer.py‎

Lines changed: 119 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
import cv2
66
import concurrent.futures
7+
import os
8+
import sys
9+
import time
10+
import threading
711
from multiprocessing import cpu_count
812
from typing import List, Callable, Optional
913
from tqdm import tqdm
@@ -27,6 +31,20 @@ def __init__(self, max_workers: Optional[int] = None):
2731
max_workers: Maximum number of worker threads. If None, uses cpu_count().
2832
"""
2933
self.max_workers = max_workers or cpu_count()
34+
self._is_windows = os.name == 'nt' or sys.platform.startswith('win')
35+
self._cancellation_event = threading.Event()
36+
37+
# Windows-specific settings
38+
if self._is_windows:
39+
# Reduce thread count on Windows to avoid thread pool issues
40+
self.max_workers = min(self.max_workers, 4)
41+
# Add timeouts for Windows operations
42+
self._operation_timeout = 300 # 5 minutes
43+
self._progress_timeout = 30 # 30 seconds
44+
45+
def cancel_processing(self):
46+
"""Cancel ongoing sharpness analysis."""
47+
self._cancellation_event.set()
3048

3149
def calculate_sharpness(self, extraction_result: ExtractionResult, progress_callback=None) -> ExtractionResult:
3250
"""
@@ -110,7 +128,7 @@ def chunk_progress_callback(phase, chunk_completed, chunk_total, description):
110128

111129
def _process_chunk_parallel(self, frame_paths: List[str],
112130
progress_callback: Optional[Callable] = None) -> List[float]:
113-
"""Process a chunk of frames in parallel."""
131+
"""Process a chunk of frames in parallel with Windows-specific error handling."""
114132
scores = [0.0] * len(frame_paths) # Initialize with default scores
115133
num_workers = min(self.max_workers, len(frame_paths)) if frame_paths else 1
116134

@@ -120,39 +138,115 @@ def _process_chunk_parallel(self, frame_paths: List[str],
120138

121139
futures = {}
122140
try:
123-
with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
141+
# Windows-specific executor configuration
142+
executor_kwargs = {'max_workers': num_workers}
143+
if self._is_windows:
144+
# Use context manager with explicit timeout on Windows
145+
executor_kwargs.update({
146+
'thread_name_prefix': 'SharpFrames_',
147+
})
148+
149+
with concurrent.futures.ThreadPoolExecutor(**executor_kwargs) as executor:
124150
# Submit tasks
125151
for idx, path in enumerate(frame_paths):
152+
# Check for cancellation before submitting
153+
if self._cancellation_event.is_set():
154+
print("Sharpness analysis cancelled")
155+
return scores
156+
126157
future = executor.submit(self._calculate_single_frame_sharpness, path)
127158
futures[future] = {"index": idx, "path": path}
128159

129-
# Process completed futures
130-
for future in concurrent.futures.as_completed(futures):
131-
task_info = futures[future]
132-
idx = task_info["index"]
133-
path = task_info["path"]
134-
135-
try:
136-
score = future.result()
137-
scores[idx] = score
160+
# Process completed futures with timeout on Windows
161+
timeout = self._operation_timeout if self._is_windows else None
162+
163+
try:
164+
for future in concurrent.futures.as_completed(futures, timeout=timeout):
165+
# Check for cancellation
166+
if self._cancellation_event.is_set():
167+
print("Sharpness analysis cancelled")
168+
# Cancel remaining futures
169+
for remaining_future in futures:
170+
remaining_future.cancel()
171+
return scores
172+
173+
task_info = futures[future]
174+
idx = task_info["index"]
175+
path = task_info["path"]
138176

139-
except Exception as e:
140-
print(f"Warning: Failed to process {path}: {e}")
141-
scores[idx] = 0.0 # Use default score for failed frames
142-
143-
# Update progress after each frame
144-
completed_count += 1
145-
if progress_callback:
146-
progress_callback("sharpness", completed_count, total_frames,
147-
f"Analyzed {completed_count}/{total_frames} frames")
177+
try:
178+
# Add timeout for individual result retrieval on Windows
179+
result_timeout = 10 if self._is_windows else None
180+
score = future.result(timeout=result_timeout)
181+
scores[idx] = score
182+
183+
except concurrent.futures.TimeoutError:
184+
print(f"Warning: Timeout processing {path}")
185+
scores[idx] = 0.0
186+
except Exception as e:
187+
print(f"Warning: Failed to process {path}: {e}")
188+
scores[idx] = 0.0 # Use default score for failed frames
189+
190+
# Update progress after each frame with timeout protection
191+
completed_count += 1
192+
if progress_callback:
193+
try:
194+
progress_callback("sharpness", completed_count, total_frames,
195+
f"Analyzed {completed_count}/{total_frames} frames")
196+
except Exception as e:
197+
print(f"Warning: Progress callback failed: {e}")
198+
199+
except concurrent.futures.TimeoutError:
200+
print(f"Warning: Overall processing timeout after {timeout} seconds")
201+
# Cancel remaining futures and return partial results
202+
for future in futures:
203+
future.cancel()
148204

149205
except Exception as e:
150-
print(f"Error in parallel processing: {e}")
206+
error_msg = f"Error in parallel processing: {e}"
207+
print(error_msg)
208+
209+
# On Windows, try fallback to sequential processing for small datasets
210+
if self._is_windows and len(frame_paths) <= 10:
211+
print("Attempting fallback to sequential processing on Windows...")
212+
return self._process_chunk_sequential(frame_paths, progress_callback)
151213
# Return zeros for all frames on complete failure
152214
return [0.0] * len(frame_paths)
153215

154216
return scores
155217

218+
def _process_chunk_sequential(self, frame_paths: List[str],
219+
progress_callback: Optional[Callable] = None) -> List[float]:
220+
"""Fallback sequential processing for Windows when threading fails."""
221+
scores = []
222+
total_frames = len(frame_paths)
223+
224+
for idx, path in enumerate(frame_paths):
225+
if self._cancellation_event.is_set():
226+
print("Sequential processing cancelled")
227+
break
228+
229+
try:
230+
score = self._calculate_single_frame_sharpness(path)
231+
scores.append(score)
232+
except Exception as e:
233+
print(f"Warning: Failed to process {path}: {e}")
234+
scores.append(0.0)
235+
236+
# Update progress
237+
if progress_callback:
238+
try:
239+
progress_callback("sharpness", idx + 1, total_frames,
240+
f"Analyzed {idx + 1}/{total_frames} frames (sequential)")
241+
except Exception as e:
242+
print(f"Warning: Progress callback failed: {e}")
243+
244+
# Pad with zeros if processing was cancelled early
245+
while len(scores) < total_frames:
246+
scores.append(0.0)
247+
248+
return scores
249+
156250
def _calculate_single_frame_sharpness(self, frame_path: str) -> float:
157251
"""
158252
Calculate sharpness score for a single frame using Laplacian variance.
@@ -167,6 +261,10 @@ def _calculate_single_frame_sharpness(self, frame_path: str) -> float:
167261
ImageProcessingError: If frame processing fails
168262
"""
169263
try:
264+
# Check for cancellation
265+
if self._cancellation_event.is_set():
266+
return 0.0
267+
170268
# Read image in grayscale
171269
img_gray = cv2.imread(frame_path, cv2.IMREAD_GRAYSCALE)
172270
if img_gray is None:

‎sharp_frames/processing/tui_processor.py‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import shutil
6+
import os
67
from typing import Dict, Any, Optional
78

89
from ..models.frame_data import ExtractionResult
@@ -22,6 +23,14 @@ def __init__(self):
2223
self.selector = FrameSelector(show_progress=False) # Disable progress bars for thread safety
2324
self.saver = FrameSaver(show_progress=False) # Disable progress bars for thread safety
2425
self.current_result: Optional[ExtractionResult] = None
26+
self._cancelled = False
27+
28+
def cancel_processing(self):
29+
"""Cancel ongoing processing operations."""
30+
self._cancelled = True
31+
# Cancel sharpness analyzer
32+
if hasattr(self.analyzer, 'cancel_processing'):
33+
self.analyzer.cancel_processing()
2534

2635
def extract_and_analyze(self, config: Dict[str, Any], progress_callback=None) -> ExtractionResult:
2736
"""
@@ -40,14 +49,29 @@ def extract_and_analyze(self, config: Dict[str, Any], progress_callback=None) ->
4049
try:
4150
print(f"Phase 1: Extracting and analyzing frames from {config['input_type']}...")
4251

52+
# Check for cancellation
53+
if self._cancelled:
54+
print("Processing cancelled during initialization")
55+
return ExtractionResult(frames=[], metadata={}, temp_dir=None, input_type=config.get('input_type', 'unknown'))
56+
4357
# Report extraction starting
4458
if progress_callback:
45-
progress_callback("extraction", 0, 100, "Starting frame extraction...")
59+
try:
60+
progress_callback("extraction", 0, 100, "Starting frame extraction...")
61+
except Exception as e:
62+
print(f"Warning: Progress callback failed: {e}")
4663

4764
# Extract frames
4865
print("Extracting frames...")
4966
extraction_result = self.extractor.extract_frames(config, progress_callback)
5067

68+
# Check for cancellation after extraction
69+
if self._cancelled:
70+
print("Processing cancelled after frame extraction")
71+
if extraction_result.temp_dir:
72+
self.cleanup_temp_directory()
73+
return ExtractionResult(frames=[], metadata={}, temp_dir=None, input_type=config.get('input_type', 'unknown'))
74+
5175
if not extraction_result.frames:
5276
print("No frames were extracted.")
5377
self.current_result = extraction_result
@@ -57,11 +81,28 @@ def extract_and_analyze(self, config: Dict[str, Any], progress_callback=None) ->
5781

5882
# Report analysis starting
5983
if progress_callback:
60-
progress_callback("analysis", 0, len(extraction_result.frames), "Starting sharpness analysis...")
84+
try:
85+
progress_callback("analysis", 0, len(extraction_result.frames), "Starting sharpness analysis...")
86+
except Exception as e:
87+
print(f"Warning: Progress callback failed: {e}")
88+
89+
# Check for cancellation before sharpness analysis
90+
if self._cancelled:
91+
print("Processing cancelled before sharpness analysis")
92+
if extraction_result.temp_dir:
93+
self.cleanup_temp_directory()
94+
return ExtractionResult(frames=[], metadata={}, temp_dir=None, input_type=config.get('input_type', 'unknown'))
6195

6296
# Calculate sharpness scores
6397
print("Analyzing frame sharpness...")
64-
analyzed_result = self.analyzer.calculate_sharpness(extraction_result, progress_callback)
98+
try:
99+
analyzed_result = self.analyzer.calculate_sharpness(extraction_result, progress_callback)
100+
except Exception as e:
101+
print(f"Error during sharpness analysis: {e}")
102+
# Clean up and re-raise
103+
if extraction_result.temp_dir:
104+
self.cleanup_temp_directory()
105+
raise e
65106

66107
print(f"Analysis complete. Average sharpness: {analyzed_result.average_sharpness:.2f}")
67108
print(f"Sharpness range: {analyzed_result.sharpness_range[0]:.2f} - {analyzed_result.sharpness_range[1]:.2f}")

0 commit comments

Comments
 (0)