endoreg-db 0.8.4.1__py3-none-any.whl → 0.8.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of endoreg-db might be problematic. Click here for more details.

@@ -8,6 +8,7 @@ Changelog:
8
8
  October 14, 2025: Added file locking mechanism to prevent race conditions
9
9
  during concurrent video imports (matches PDF import pattern)
10
10
  """
11
+
11
12
  from datetime import date
12
13
  import logging
13
14
  import sys
@@ -34,26 +35,25 @@ MAX_LOCK_WAIT_SECONDS = 90 # New: wait up to 90s for a non-stale lock to clear
34
35
  logger = logging.getLogger(__name__)
35
36
 
36
37
 
37
- class VideoImportService():
38
+ class VideoImportService:
38
39
  """
39
40
  Service for importing and anonymizing video files.
40
41
  Uses a central video instance pattern for cleaner state management.
41
-
42
+
42
43
  Features (October 14, 2025):
43
44
  - File locking to prevent concurrent processing of the same video
44
45
  - Stale lock detection and reclamation (600s timeout)
45
46
  - Hash-based duplicate detection
46
47
  - Graceful fallback processing without lx_anonymizer
47
48
  """
48
-
49
+
49
50
  def __init__(self, project_root: Optional[Path] = None):
50
-
51
51
  # Set up project root path
52
52
  if project_root:
53
53
  self.project_root = Path(project_root)
54
54
  else:
55
55
  self.project_root = Path(__file__).parent.parent.parent.parent
56
-
56
+
57
57
  # Track processed files to prevent duplicates
58
58
  try:
59
59
  # Ensure anonym_video directory exists before listing files
@@ -67,13 +67,13 @@ class VideoImportService():
67
67
  except Exception as e:
68
68
  logger.warning(f"Failed to initialize processed files tracking: {e}")
69
69
  self.processed_files = set()
70
-
70
+
71
71
  # Central video instance and processing context
72
72
  self.current_video: Optional[VideoFile] = None
73
73
  self.processing_context: Dict[str, Any] = {}
74
-
74
+
75
75
  self.delete_source = True
76
-
76
+
77
77
  self.logger = logging.getLogger(__name__)
78
78
 
79
79
  def _require_current_video(self) -> VideoFile:
@@ -81,12 +81,12 @@ class VideoImportService():
81
81
  if self.current_video is None:
82
82
  raise RuntimeError("Current video instance is not set")
83
83
  return self.current_video
84
-
84
+
85
85
  @contextmanager
86
86
  def _file_lock(self, path: Path):
87
87
  """
88
88
  Create a file lock to prevent duplicate processing of the same video.
89
-
89
+
90
90
  This context manager creates a .lock file alongside the video file.
91
91
  If the lock file already exists, it checks if it's stale (older than
92
92
  STALE_LOCK_SECONDS) and reclaims it if necessary. If it's not stale,
@@ -110,24 +110,21 @@ class VideoImportService():
110
110
  except FileNotFoundError:
111
111
  # Race: lock removed between exists and stat; retry acquire in next loop
112
112
  age = None
113
-
113
+
114
114
  if age is not None and age > STALE_LOCK_SECONDS:
115
115
  try:
116
- logger.warning(
117
- "Stale lock detected for %s (age %.0fs). Reclaiming lock...",
118
- path, age
119
- )
116
+ logger.warning("Stale lock detected for %s (age %.0fs). Reclaiming lock...", path, age)
120
117
  lock_path.unlink()
121
118
  except Exception as e:
122
119
  logger.warning("Failed to remove stale lock %s: %s", lock_path, e)
123
120
  # Loop continues and retries acquire immediately
124
121
  continue
125
-
122
+
126
123
  # Not stale: wait until deadline, then give up gracefully
127
124
  if time.time() >= deadline:
128
125
  raise ValueError(f"File already being processed: {path}")
129
126
  time.sleep(1.0)
130
-
127
+
131
128
  os.write(fd, b"lock")
132
129
  os.close(fd)
133
130
  fd = None
@@ -140,11 +137,11 @@ class VideoImportService():
140
137
  lock_path.unlink()
141
138
  except OSError:
142
139
  pass
143
-
140
+
144
141
  def processed(self) -> bool:
145
142
  """Indicates if the current file has already been processed."""
146
- return getattr(self, '_processed', False)
147
-
143
+ return getattr(self, "_processed", False)
144
+
148
145
  def import_and_anonymize(
149
146
  self,
150
147
  file_path: Union[Path, str],
@@ -158,13 +155,12 @@ class VideoImportService():
158
155
  Uses the central video instance pattern for improved state management.
159
156
  """
160
157
  # DEFENSIVE: Initialize processing_context immediately to prevent KeyError crashes
161
- self.processing_context = {'file_path': Path(file_path)}
162
-
158
+ self.processing_context = {"file_path": Path(file_path)}
159
+
163
160
  try:
164
161
  # Initialize processing context
165
- self._initialize_processing_context(file_path, center_name, processor_name,
166
- save_video, delete_source)
167
-
162
+ self._initialize_processing_context(file_path, center_name, processor_name, save_video, delete_source)
163
+
168
164
  # Validate and prepare file (may raise ValueError if another worker holds a non-stale lock)
169
165
  try:
170
166
  self._validate_and_prepare_file()
@@ -174,32 +170,32 @@ class VideoImportService():
174
170
  self.logger.info(f"Skipping {file_path}: {ve}")
175
171
  return None
176
172
  raise
177
-
173
+
178
174
  # Create or retrieve video instance
179
175
  self._create_or_retrieve_video_instance()
180
-
176
+
181
177
  # Create sensitive meta file, ensure raw is moved out of processing folder watched by file watcher.
182
178
  self._create_sensitive_file()
183
-
179
+
184
180
  # Setup processing environment
185
181
  self._setup_processing_environment()
186
-
182
+
187
183
  # Process frames and metadata
188
184
  self._process_frames_and_metadata()
189
-
185
+
190
186
  # Finalize processing
191
187
  self._finalize_processing()
192
-
188
+
193
189
  # Move files and cleanup
194
190
  self._cleanup_and_archive()
195
-
191
+
196
192
  return self.current_video
197
-
193
+
198
194
  except Exception as e:
199
195
  # Safe file path access - handles cases where processing_context wasn't initialized
200
- safe_file_path = getattr(self, 'processing_context', {}).get('file_path', file_path)
196
+ safe_file_path = getattr(self, "processing_context", {}).get("file_path", file_path)
201
197
  # Debug: Log context state for troubleshooting
202
- context_keys = list(getattr(self, 'processing_context', {}).keys())
198
+ context_keys = list(getattr(self, "processing_context", {}).keys())
203
199
  self.logger.debug(f"Context keys during error: {context_keys}")
204
200
  self.logger.error(f"Video import and anonymization failed for {safe_file_path}: {e}")
205
201
  self._cleanup_on_error()
@@ -207,87 +203,86 @@ class VideoImportService():
207
203
  finally:
208
204
  self._cleanup_processing_context()
209
205
 
210
- def _initialize_processing_context(self, file_path: Union[Path, str], center_name: str,
211
- processor_name: str, save_video: bool, delete_source: bool):
206
+ def _initialize_processing_context(self, file_path: Union[Path, str], center_name: str, processor_name: str, save_video: bool, delete_source: bool):
212
207
  """Initialize the processing context for the current video import."""
213
208
  self.processing_context = {
214
- 'file_path': Path(file_path),
215
- 'center_name': center_name,
216
- 'processor_name': processor_name,
217
- 'save_video': save_video,
218
- 'delete_source': delete_source,
219
- 'processing_started': False,
220
- 'frames_extracted': False,
221
- 'anonymization_completed': False,
222
- 'error_reason': None
209
+ "file_path": Path(file_path),
210
+ "center_name": center_name,
211
+ "processor_name": processor_name,
212
+ "save_video": save_video,
213
+ "delete_source": delete_source,
214
+ "processing_started": False,
215
+ "frames_extracted": False,
216
+ "anonymization_completed": False,
217
+ "error_reason": None,
223
218
  }
224
-
219
+
225
220
  self.logger.info(f"Initialized processing context for: {file_path}")
226
221
 
227
222
  def _validate_and_prepare_file(self):
228
223
  """
229
224
  Validate the video file and prepare for processing.
230
-
225
+
231
226
  Uses file locking to prevent concurrent processing of the same video file.
232
227
  This prevents race conditions where multiple workers might try to process
233
228
  the same video simultaneously.
234
-
229
+
235
230
  The lock is acquired here and held for the entire import process.
236
231
  See _file_lock() for lock reclamation logic.
237
232
  """
238
- file_path = self.processing_context['file_path']
239
-
233
+ file_path = self.processing_context["file_path"]
234
+
240
235
  # Acquire file lock to prevent concurrent processing
241
236
  # Lock will be held until finally block in import_and_anonymize()
242
237
  try:
243
- self.processing_context['_lock_context'] = self._file_lock(file_path)
244
- self.processing_context['_lock_context'].__enter__()
238
+ self.processing_context["_lock_context"] = self._file_lock(file_path)
239
+ self.processing_context["_lock_context"].__enter__()
245
240
  except Exception:
246
241
  self._cleanup_processing_context()
247
242
  raise
248
-
243
+
249
244
  self.logger.info("Acquired file lock for: %s", file_path)
250
-
245
+
251
246
  # Check if already processed (memory-based check)
252
247
  if str(file_path) in self.processed_files:
253
248
  self.logger.info("File %s already processed, skipping", file_path)
254
249
  self._processed = True
255
250
  raise ValueError(f"File already processed: {file_path}")
256
-
251
+
257
252
  # Check file exists
258
253
  if not file_path.exists():
259
254
  raise FileNotFoundError(f"Video file not found: {file_path}")
260
-
255
+
261
256
  self.logger.info("File validation completed for: %s", file_path)
262
257
 
263
258
  def _create_or_retrieve_video_instance(self):
264
259
  """Create or retrieve the VideoFile instance and move to final storage."""
265
-
260
+
266
261
  self.logger.info("Creating VideoFile instance...")
267
-
262
+
268
263
  self.current_video = VideoFile.create_from_file_initialized(
269
- file_path=self.processing_context['file_path'],
270
- center_name=self.processing_context['center_name'],
271
- processor_name=self.processing_context['processor_name'],
272
- delete_source=self.processing_context['delete_source'],
273
- save_video_file=self.processing_context['save_video'],
264
+ file_path=self.processing_context["file_path"],
265
+ center_name=self.processing_context["center_name"],
266
+ processor_name=self.processing_context["processor_name"],
267
+ delete_source=self.processing_context["delete_source"],
268
+ save_video_file=self.processing_context["save_video"],
274
269
  )
275
-
270
+
276
271
  if not self.current_video:
277
272
  raise RuntimeError("Failed to create VideoFile instance")
278
-
273
+
279
274
  # Immediately move to final storage locations
280
275
  self._move_to_final_storage()
281
-
276
+
282
277
  self.logger.info("Created VideoFile with UUID: %s", self.current_video.uuid)
283
-
278
+
284
279
  # Get and mark processing state
285
280
  state = VideoFile.get_or_create_state(self.current_video)
286
281
  if not state:
287
282
  raise RuntimeError("Failed to create VideoFile state")
288
-
283
+
289
284
  state.mark_processing_started(save=True)
290
- self.processing_context['processing_started'] = True
285
+ self.processing_context["processing_started"] = True
291
286
 
292
287
  def _move_to_final_storage(self):
293
288
  """
@@ -364,7 +359,6 @@ class VideoImportService():
364
359
  self.processing_context["raw_video_path"] = stored_raw_path
365
360
  self.processing_context["video_filename"] = stored_raw_path.name
366
361
 
367
-
368
362
  def _setup_processing_environment(self):
369
363
  """Setup the processing environment without file movement."""
370
364
  video = self._require_current_video()
@@ -374,38 +368,38 @@ class VideoImportService():
374
368
 
375
369
  # Initialize frame objects in database
376
370
  video.initialize_frames()
377
-
371
+
378
372
  # Extract frames BEFORE processing to prevent pipeline 1 conflicts
379
373
  self.logger.info("Pre-extracting frames to avoid pipeline conflicts...")
380
374
  try:
381
375
  frames_extracted = video.extract_frames(overwrite=False)
382
376
  if frames_extracted:
383
- self.processing_context['frames_extracted'] = True
377
+ self.processing_context["frames_extracted"] = True
384
378
  self.logger.info("Frame extraction completed successfully")
385
-
379
+
386
380
  # CRITICAL: Immediately save the frames_extracted state to database
387
381
  # to prevent refresh_from_db() in pipeline 1 from overriding it
388
382
  state = video.get_or_create_state()
389
383
  if not state.frames_extracted:
390
384
  state.frames_extracted = True
391
- state.save(update_fields=['frames_extracted'])
385
+ state.save(update_fields=["frames_extracted"])
392
386
  self.logger.info("Persisted frames_extracted=True to database")
393
387
  else:
394
388
  self.logger.warning("Frame extraction failed, but continuing...")
395
- self.processing_context['frames_extracted'] = False
389
+ self.processing_context["frames_extracted"] = False
396
390
  except Exception as e:
397
391
  self.logger.warning(f"Frame extraction failed during setup: {e}, but continuing...")
398
- self.processing_context['frames_extracted'] = False
399
-
392
+ self.processing_context["frames_extracted"] = False
393
+
400
394
  # Ensure default patient data
401
395
  self._ensure_default_patient_data(video_instance=video)
402
-
396
+
403
397
  self.logger.info("Processing environment setup completed")
404
398
 
405
399
  def _process_frames_and_metadata(self):
406
400
  """Process frames and extract metadata with anonymization."""
407
401
  # Check frame cleaning availability
408
- frame_cleaning_available, frame_cleaner = self._ensure_frame_cleaning_available()
402
+ frame_cleaning_available, frame_cleaner = self._ensure_frame_cleaning_available()
409
403
  video = self._require_current_video()
410
404
 
411
405
  raw_file_field = video.raw_file
@@ -418,25 +412,25 @@ class VideoImportService():
418
412
 
419
413
  try:
420
414
  self.logger.info("Starting frame-level anonymization with processor ROI masking...")
421
-
415
+
422
416
  # Get processor ROI information
423
417
  endoscope_data_roi_nested, endoscope_image_roi = self._get_processor_roi_info()
424
-
418
+
425
419
  # Perform frame cleaning with timeout to prevent blocking
426
420
  from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
427
-
421
+
428
422
  with ThreadPoolExecutor(max_workers=1) as executor:
429
423
  future = executor.submit(self._perform_frame_cleaning, endoscope_data_roi_nested, endoscope_image_roi)
430
424
  try:
431
425
  # Increased timeout to better accommodate ffmpeg + OCR
432
- future.result(timeout=300)
433
- self.processing_context['anonymization_completed'] = True
426
+ future.result(timeout=50000)
427
+ self.processing_context["anonymization_completed"] = True
434
428
  self.logger.info("Frame cleaning completed successfully within timeout")
435
429
  except FutureTimeoutError:
436
430
  self.logger.warning("Frame cleaning timed out; entering grace period check for cleaned output")
437
431
  # Grace period: detect if cleaned file appears shortly after timeout
438
- raw_video_path = self.processing_context.get('raw_video_path')
439
- video_filename = self.processing_context.get('video_filename', Path(raw_video_path).name if raw_video_path else "video.mp4")
432
+ raw_video_path = self.processing_context.get("raw_video_path")
433
+ video_filename = self.processing_context.get("video_filename", Path(raw_video_path).name if raw_video_path else "video.mp4")
440
434
  grace_seconds = 60
441
435
  expected_cleaned_path: Optional[Path] = None
442
436
  processed_field = video.processed_file
@@ -449,8 +443,8 @@ class VideoImportService():
449
443
  if expected_cleaned_path is not None:
450
444
  for _ in range(grace_seconds):
451
445
  if expected_cleaned_path.exists():
452
- self.processing_context['cleaned_video_path'] = expected_cleaned_path
453
- self.processing_context['anonymization_completed'] = True
446
+ self.processing_context["cleaned_video_path"] = expected_cleaned_path
447
+ self.processing_context["anonymization_completed"] = True
454
448
  self.logger.info("Detected cleaned video during grace period: %s", expected_cleaned_path)
455
449
  found = True
456
450
  break
@@ -468,11 +462,10 @@ class VideoImportService():
468
462
  except Exception as fallback_error:
469
463
  self.logger.error("Fallback anonymization also failed: %s", fallback_error)
470
464
  # If even fallback fails, mark as not anonymized but continue import
471
- self.processing_context['anonymization_completed'] = False
472
- self.processing_context['error_reason'] = f"Frame cleaning failed: {e}, Fallback failed: {fallback_error}"
465
+ self.processing_context["anonymization_completed"] = False
466
+ self.processing_context["error_reason"] = f"Frame cleaning failed: {e}, Fallback failed: {fallback_error}"
473
467
 
474
468
  def _save_anonymized_video(self):
475
-
476
469
  original_raw_file_path_to_delete = None
477
470
  original_raw_frame_dir_to_delete = None
478
471
  video = self._require_current_video()
@@ -483,9 +476,7 @@ class VideoImportService():
483
476
 
484
477
  new_processed_hash = get_video_hash(anonymized_video_path)
485
478
  if video.__class__.objects.filter(processed_video_hash=new_processed_hash).exclude(pk=video.pk).exists():
486
- raise ValueError(
487
- f"Processed video hash {new_processed_hash} already exists for another video (Video: {video.uuid})."
488
- )
479
+ raise ValueError(f"Processed video hash {new_processed_hash} already exists for another video (Video: {video.uuid}).")
489
480
 
490
481
  video.processed_video_hash = new_processed_hash
491
482
  video.processed_file.name = anonymized_video_path.relative_to(STORAGE_DIR).as_posix()
@@ -504,11 +495,11 @@ class VideoImportService():
504
495
 
505
496
  update_fields.extend(["raw_file", "video_hash"])
506
497
 
507
- transaction.on_commit(lambda: _cleanup_raw_assets(
508
- video_uuid=video.uuid,
509
- raw_file_path=original_raw_file_path_to_delete,
510
- raw_frame_dir=original_raw_frame_dir_to_delete
511
- ))
498
+ transaction.on_commit(
499
+ lambda: _cleanup_raw_assets(
500
+ video_uuid=video.uuid, raw_file_path=original_raw_file_path_to_delete, raw_frame_dir=original_raw_frame_dir_to_delete
501
+ )
502
+ )
512
503
 
513
504
  video.save(update_fields=update_fields)
514
505
  video.state.mark_anonymized(save=True)
@@ -526,20 +517,20 @@ class VideoImportService():
526
517
  if video is None:
527
518
  self.logger.warning("No VideoFile instance available for fallback anonymization")
528
519
 
529
-
530
520
  # Strategy 2: Simple copy (no processing, just copy raw to processed)
531
521
  self.logger.info("Using simple copy fallback (raw video will be used as 'processed' video)")
532
- self.processing_context['anonymization_completed'] = False
533
- self.processing_context['use_raw_as_processed'] = True
522
+ self.processing_context["anonymization_completed"] = False
523
+ self.processing_context["use_raw_as_processed"] = True
534
524
  self.logger.warning("Fallback: Video will be imported without anonymization (raw copy used)")
535
525
  except Exception as e:
536
526
  self.logger.error(f"Error during fallback anonymization: {e}", exc_info=True)
537
- self.processing_context['anonymization_completed'] = False
538
- self.processing_context['error_reason'] = str(e)
527
+ self.processing_context["anonymization_completed"] = False
528
+ self.processing_context["error_reason"] = str(e)
529
+
539
530
  def _finalize_processing(self):
540
531
  """Finalize processing and update video state."""
541
532
  self.logger.info("Updating video processing state...")
542
-
533
+
543
534
  with transaction.atomic():
544
535
  video = self._require_current_video()
545
536
  try:
@@ -548,36 +539,33 @@ class VideoImportService():
548
539
  self.logger.warning("Could not refresh VideoFile %s from DB: %s", video.uuid, refresh_error)
549
540
 
550
541
  state = video.get_or_create_state()
551
-
542
+
552
543
  # Only mark frames as extracted if they were successfully extracted
553
- if self.processing_context.get('frames_extracted', False):
544
+ if self.processing_context.get("frames_extracted", False):
554
545
  state.frames_extracted = True
555
546
  self.logger.info("Marked frames as extracted in state")
556
547
  else:
557
548
  self.logger.warning("Frames were not extracted, not updating state")
558
-
549
+
559
550
  # Always mark these as true (metadata extraction attempts were made)
560
551
  state.frames_initialized = True
561
552
  state.video_meta_extracted = True
562
553
  state.text_meta_extracted = True
563
-
554
+
564
555
  # ✅ FIX: Only mark as processed if anonymization actually completed
565
- anonymization_completed = self.processing_context.get('anonymization_completed', False)
556
+ anonymization_completed = self.processing_context.get("anonymization_completed", False)
566
557
  if anonymization_completed:
567
558
  state.mark_sensitive_meta_processed(save=False)
568
559
  self.logger.info("Anonymization completed - marking sensitive meta as processed")
569
560
  else:
570
- self.logger.warning(
571
- "Anonymization NOT completed - NOT marking as processed. "
572
- f"Reason: {self.processing_context.get('error_reason', 'Unknown')}"
573
- )
561
+ self.logger.warning(f"Anonymization NOT completed - NOT marking as processed. Reason: {self.processing_context.get('error_reason', 'Unknown')}")
574
562
  # Explicitly mark as NOT processed
575
563
  state.sensitive_meta_processed = False
576
-
564
+
577
565
  # Save all state changes
578
566
  state.save()
579
567
  self.logger.info("Video processing state updated")
580
-
568
+
581
569
  # Signal completion
582
570
  self._signal_completion()
583
571
 
@@ -591,12 +579,12 @@ class VideoImportService():
591
579
  video = self._require_current_video()
592
580
 
593
581
  processed_video_path = None
594
- if 'cleaned_video_path' in self.processing_context:
595
- processed_video_path = self.processing_context['cleaned_video_path']
582
+ if "cleaned_video_path" in self.processing_context:
583
+ processed_video_path = self.processing_context["cleaned_video_path"]
596
584
  else:
597
- raw_video_path = self.processing_context.get('raw_video_path')
585
+ raw_video_path = self.processing_context.get("raw_video_path")
598
586
  if raw_video_path and Path(raw_video_path).exists():
599
- video_filename = self.processing_context.get('video_filename', Path(raw_video_path).name)
587
+ video_filename = self.processing_context.get("video_filename", Path(raw_video_path).name)
600
588
  processed_filename = f"processed_{video_filename}"
601
589
  processed_video_path = Path(raw_video_path).parent / processed_filename
602
590
  try:
@@ -625,13 +613,13 @@ class VideoImportService():
625
613
  except Exception as exc:
626
614
  self.logger.error("Failed to update processed_file path: %s", exc)
627
615
  video.processed_file.name = f"anonym_videos/{anonym_video_filename}"
628
- video.save(update_fields=['processed_file'])
616
+ video.save(update_fields=["processed_file"])
629
617
  self.logger.info(
630
618
  "Updated processed_file path using fallback: %s",
631
619
  f"anonym_videos/{anonym_video_filename}",
632
620
  )
633
621
 
634
- self.processing_context['anonymization_completed'] = True
622
+ self.processing_context["anonymization_completed"] = True
635
623
  else:
636
624
  self.logger.warning("Processed video file not found after move: %s", anonym_target_path)
637
625
  except Exception as exc:
@@ -641,13 +629,14 @@ class VideoImportService():
641
629
 
642
630
  try:
643
631
  from endoreg_db.utils.paths import RAW_FRAME_DIR
632
+
644
633
  shutil.rmtree(RAW_FRAME_DIR, ignore_errors=True)
645
634
  self.logger.debug("Cleaned up temporary frames directory: %s", RAW_FRAME_DIR)
646
635
  except Exception as exc:
647
636
  self.logger.warning("Failed to remove directory %s: %s", RAW_FRAME_DIR, exc)
648
637
 
649
- source_path = self.processing_context['file_path']
650
- if self.processing_context['delete_source'] and Path(source_path).exists():
638
+ source_path = self.processing_context["file_path"]
639
+ if self.processing_context["delete_source"] and Path(source_path).exists():
651
640
  try:
652
641
  os.remove(source_path)
653
642
  self.logger.info("Removed remaining source file: %s", source_path)
@@ -658,25 +647,25 @@ class VideoImportService():
658
647
  self.logger.warning("No processed_file found after cleanup - video will be unprocessed")
659
648
  try:
660
649
  video.anonymize(delete_original_raw=self.delete_source)
661
- video.save(update_fields=['processed_file'])
650
+ video.save(update_fields=["processed_file"])
662
651
  self.logger.info("Late-stage anonymization succeeded")
663
652
  except Exception as e:
664
653
  self.logger.error("Late-stage anonymization failed: %s", e)
665
- self.processing_context['anonymization_completed'] = False
654
+ self.processing_context["anonymization_completed"] = False
666
655
 
667
656
  self.logger.info("Cleanup and archiving completed")
668
657
 
669
- self.processed_files.add(str(self.processing_context['file_path']))
658
+ self.processed_files.add(str(self.processing_context["file_path"]))
670
659
 
671
660
  with transaction.atomic():
672
661
  video.refresh_from_db()
673
- if hasattr(video, 'state') and self.processing_context.get('anonymization_completed'):
662
+ if hasattr(video, "state") and self.processing_context.get("anonymization_completed"):
674
663
  video.state.mark_sensitive_meta_processed(save=True)
675
664
 
676
665
  self.logger.info("Import and anonymization completed for VideoFile UUID: %s", video.uuid)
677
666
  self.logger.info("Raw video stored in: /data/videos")
678
667
  self.logger.info("Processed video stored in: /data/anonym_videos")
679
-
668
+
680
669
  def _create_sensitive_file(
681
670
  self,
682
671
  video_instance: VideoFile | None = None,
@@ -735,11 +724,10 @@ class VideoImportService():
735
724
  "Updated video.raw_file using fallback method: videos/sensitive/%s",
736
725
  target_file_path.name,
737
726
  )
738
-
727
+
739
728
  self.processing_context["raw_video_path"] = target_file_path
740
729
  self.processing_context["video_filename"] = target_file_path.name
741
730
 
742
-
743
731
  self.logger.info("Created sensitive file for %s at %s", video.uuid, target_file_path)
744
732
  return target_file_path
745
733
 
@@ -756,7 +744,7 @@ class VideoImportService():
756
744
  if processor:
757
745
  assert isinstance(processor, EndoscopyProcessor), "Processor is not of type EndoscopyProcessor"
758
746
  endoscope_image_roi = processor.get_roi_endoscope_image()
759
- endoscope_data_roi_nested = processor.get_rois()
747
+ endoscope_data_roi_nested = processor.get_sensitive_rois()
760
748
  self.logger.info("Retrieved processor ROI information: endoscope_image_roi=%s", endoscope_image_roi)
761
749
  else:
762
750
  self.logger.warning(
@@ -826,12 +814,10 @@ class VideoImportService():
826
814
  except Exception as exc:
827
815
  self.logger.error("Failed to update SensitiveMeta for video %s: %s", video.uuid, exc)
828
816
 
829
-
830
-
831
817
  def _ensure_frame_cleaning_available(self):
832
818
  """
833
819
  Ensure frame cleaning modules are available by adding lx-anonymizer to path.
834
-
820
+
835
821
  Returns:
836
822
  Tuple of (availability_flag, FrameCleaner_class, ReportReader_class)
837
823
  """
@@ -841,13 +827,11 @@ class VideoImportService():
841
827
 
842
828
  if FrameCleaner:
843
829
  return True, FrameCleaner
844
-
830
+
845
831
  except Exception as e:
846
832
  self.logger.warning(f"Frame cleaning not available: {e} Please install or update lx_anonymizer.")
847
-
848
- return False, None
849
833
 
850
-
834
+ return False, None
851
835
 
852
836
  def _perform_frame_cleaning(self, endoscope_data_roi_nested, endoscope_image_roi):
853
837
  """Perform frame cleaning and anonymization."""
@@ -858,8 +842,8 @@ class VideoImportService():
858
842
  raise RuntimeError("Frame cleaning not available")
859
843
 
860
844
  # Prepare parameters for frame cleaning
861
- raw_video_path = self.processing_context.get('raw_video_path')
862
-
845
+ raw_video_path = self.processing_context.get("raw_video_path")
846
+
863
847
  if not raw_video_path or not Path(raw_video_path).exists():
864
848
  try:
865
849
  self.current_video = self._require_current_video()
@@ -867,32 +851,28 @@ class VideoImportService():
867
851
  except Exception:
868
852
  raise RuntimeError(f"Raw video path not found: {raw_video_path}")
869
853
 
870
-
871
854
  # Create temporary output path for cleaned video
872
- video_filename = self.processing_context.get('video_filename', Path(raw_video_path).name)
855
+ video_filename = self.processing_context.get("video_filename", Path(raw_video_path).name)
873
856
  cleaned_filename = f"cleaned_{video_filename}"
874
857
  cleaned_video_path = Path(raw_video_path).parent / cleaned_filename
875
-
876
-
877
-
858
+
878
859
  # Clean video with ROI masking (heavy I/O operation)
879
860
  actual_cleaned_path, extracted_metadata = frame_cleaner.clean_video(
880
861
  video_path=Path(raw_video_path),
881
862
  endoscope_image_roi=endoscope_image_roi,
882
863
  endoscope_data_roi_nested=endoscope_data_roi_nested,
883
864
  output_path=cleaned_video_path,
884
- technique="mask_overlay"
865
+ technique="mask_overlay",
885
866
  )
886
-
887
-
867
+
888
868
  # Store cleaned video path for later use in _cleanup_and_archive
889
- self.processing_context['cleaned_video_path'] = actual_cleaned_path
890
- self.processing_context['extracted_metadata'] = extracted_metadata
891
-
869
+ self.processing_context["cleaned_video_path"] = actual_cleaned_path
870
+ self.processing_context["extracted_metadata"] = extracted_metadata
871
+
892
872
  # Update sensitive metadata with extracted information
893
873
  self._update_sensitive_metadata(extracted_metadata)
894
874
  self.logger.info(f"Extracted metadata from frame cleaning: {extracted_metadata}")
895
-
875
+
896
876
  self.logger.info(f"Frame cleaning with ROI masking completed: {actual_cleaned_path}")
897
877
  self.logger.info("Cleaned video will be moved to anonym_videos during cleanup")
898
878
 
@@ -910,13 +890,13 @@ class VideoImportService():
910
890
 
911
891
  sm = sensitive_meta
912
892
  updated_fields = []
913
-
893
+
914
894
  try:
915
895
  sm.update_from_dict(extracted_metadata)
916
896
  updated_fields = list(extracted_metadata.keys())
917
897
  except KeyError as e:
918
898
  self.logger.warning(f"Failed to update SensitiveMeta field {e}")
919
-
899
+
920
900
  if updated_fields:
921
901
  sm.save(update_fields=updated_fields)
922
902
  self.logger.info("Updated SensitiveMeta fields for video %s: %s", video.uuid, updated_fields)
@@ -940,22 +920,18 @@ class VideoImportService():
940
920
  except (ValueError, OSError):
941
921
  raw_exists = False
942
922
 
943
- video_processing_complete = (
944
- video.sensitive_meta is not None and
945
- video.video_meta is not None and
946
- raw_exists
947
- )
923
+ video_processing_complete = video.sensitive_meta is not None and video.video_meta is not None and raw_exists
948
924
 
949
925
  if video_processing_complete:
950
926
  self.logger.info("Video %s processing completed successfully - ready for validation", video.uuid)
951
927
 
952
928
  # Update completion flags if they exist
953
929
  completion_fields = []
954
- for field_name in ['import_completed', 'processing_complete', 'ready_for_validation']:
930
+ for field_name in ["import_completed", "processing_complete", "ready_for_validation"]:
955
931
  if hasattr(video, field_name):
956
932
  setattr(video, field_name, True)
957
933
  completion_fields.append(field_name)
958
-
934
+
959
935
  if completion_fields:
960
936
  video.save(update_fields=completion_fields)
961
937
  self.logger.info("Updated completion flags: %s", completion_fields)
@@ -964,15 +940,15 @@ class VideoImportService():
964
940
  "Video %s processing incomplete - missing required components",
965
941
  video.uuid,
966
942
  )
967
-
943
+
968
944
  except Exception as e:
969
945
  self.logger.warning(f"Failed to signal completion status: {e}")
970
946
 
971
947
  def _cleanup_on_error(self):
972
948
  """Cleanup processing context on error."""
973
- if self.current_video and hasattr(self.current_video, 'state'):
949
+ if self.current_video and hasattr(self.current_video, "state"):
974
950
  try:
975
- if self.processing_context.get('processing_started'):
951
+ if self.processing_context.get("processing_started"):
976
952
  self.current_video.state.frames_extracted = False
977
953
  self.current_video.state.frames_initialized = False
978
954
  self.current_video.state.video_meta_extracted = False
@@ -984,33 +960,32 @@ class VideoImportService():
984
960
  def _cleanup_processing_context(self):
985
961
  """
986
962
  Cleanup processing context and release file lock.
987
-
963
+
988
964
  This method is always called in the finally block of import_and_anonymize()
989
965
  to ensure the file lock is released even if processing fails.
990
966
  """
991
967
  # DEFENSIVE: Ensure processing_context exists before accessing it
992
- if not hasattr(self, 'processing_context'):
968
+ if not hasattr(self, "processing_context"):
993
969
  self.processing_context = {}
994
-
970
+
995
971
  try:
996
972
  # Release file lock if it was acquired
997
- lock_context = self.processing_context.get('_lock_context')
973
+ lock_context = self.processing_context.get("_lock_context")
998
974
  if lock_context is not None:
999
975
  try:
1000
976
  lock_context.__exit__(None, None, None)
1001
977
  self.logger.info("Released file lock")
1002
978
  except Exception as e:
1003
979
  self.logger.warning(f"Error releasing file lock: {e}")
1004
-
980
+
1005
981
  # Remove file from processed set if processing failed
1006
- file_path = self.processing_context.get('file_path')
1007
- if file_path and not self.processing_context.get('anonymization_completed'):
982
+ file_path = self.processing_context.get("file_path")
983
+ if file_path and not self.processing_context.get("anonymization_completed"):
1008
984
  file_path_str = str(file_path)
1009
985
  if file_path_str in self.processed_files:
1010
986
  self.processed_files.remove(file_path_str)
1011
987
  self.logger.info(f"Removed {file_path_str} from processed files (failed processing)")
1012
-
1013
-
988
+
1014
989
  except Exception as e:
1015
990
  self.logger.warning(f"Error during context cleanup: {e}")
1016
991
  finally:
@@ -1018,6 +993,7 @@ class VideoImportService():
1018
993
  self.current_video = None
1019
994
  self.processing_context = {}
1020
995
 
996
+
1021
997
  # Convenience function for callers/tests that expect a module-level import_and_anonymize
1022
998
  def import_and_anonymize(
1023
999
  file_path,
@@ -1036,4 +1012,4 @@ def import_and_anonymize(
1036
1012
  processor_name=processor_name,
1037
1013
  save_video=save_video,
1038
1014
  delete_source=delete_source,
1039
- )
1015
+ )