endoreg-db 0.8.4.4__py3-none-any.whl → 0.8.6.1__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.

Files changed (36) hide show
  1. endoreg_db/management/commands/load_ai_model_data.py +2 -1
  2. endoreg_db/management/commands/setup_endoreg_db.py +11 -7
  3. endoreg_db/models/media/pdf/raw_pdf.py +241 -97
  4. endoreg_db/models/media/video/pipe_1.py +30 -33
  5. endoreg_db/models/media/video/video_file.py +300 -187
  6. endoreg_db/models/metadata/model_meta_logic.py +15 -1
  7. endoreg_db/models/metadata/sensitive_meta_logic.py +391 -70
  8. endoreg_db/serializers/__init__.py +26 -55
  9. endoreg_db/serializers/misc/__init__.py +1 -1
  10. endoreg_db/serializers/misc/file_overview.py +65 -35
  11. endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
  12. endoreg_db/serializers/video_examination.py +198 -0
  13. endoreg_db/services/lookup_service.py +228 -58
  14. endoreg_db/services/lookup_store.py +174 -30
  15. endoreg_db/services/pdf_import.py +585 -282
  16. endoreg_db/services/video_import.py +340 -101
  17. endoreg_db/urls/__init__.py +36 -23
  18. endoreg_db/urls/label_video_segments.py +2 -0
  19. endoreg_db/urls/media.py +3 -2
  20. endoreg_db/views/__init__.py +6 -3
  21. endoreg_db/views/media/pdf_media.py +3 -1
  22. endoreg_db/views/media/video_media.py +1 -1
  23. endoreg_db/views/media/video_segments.py +187 -259
  24. endoreg_db/views/pdf/__init__.py +5 -8
  25. endoreg_db/views/pdf/pdf_stream.py +187 -0
  26. endoreg_db/views/pdf/reimport.py +110 -94
  27. endoreg_db/views/requirement/lookup.py +171 -287
  28. endoreg_db/views/video/__init__.py +0 -2
  29. endoreg_db/views/video/video_examination_viewset.py +202 -289
  30. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/METADATA +1 -1
  31. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/RECORD +33 -34
  32. endoreg_db/views/pdf/pdf_media.py +0 -239
  33. endoreg_db/views/pdf/pdf_stream_views.py +0 -127
  34. endoreg_db/views/video/video_media.py +0 -158
  35. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/WHEEL +0 -0
  36. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -9,29 +9,32 @@ Changelog:
9
9
  during concurrent video imports (matches PDF import pattern)
10
10
  """
11
11
 
12
- from datetime import date
13
12
  import logging
14
- import sys
15
13
  import os
14
+ import random
16
15
  import shutil
16
+ import sys
17
17
  import time
18
18
  from contextlib import contextmanager
19
+ from datetime import date
19
20
  from pathlib import Path
20
- from typing import Union, Dict, Any, Optional, List, Tuple
21
+ from typing import Any, Dict, List, Optional, Tuple, Union
22
+
21
23
  from django.db import transaction
24
+ from django.db.models.fields.files import FieldFile
22
25
  from lx_anonymizer import FrameCleaner
23
26
  from moviepy import video
24
- from endoreg_db.models import VideoFile, SensitiveMeta
25
- from endoreg_db.utils.paths import STORAGE_DIR, VIDEO_DIR, ANONYM_VIDEO_DIR
26
- import random
27
- from endoreg_db.utils.hashs import get_video_hash
27
+
28
+ from endoreg_db.models import EndoscopyProcessor, SensitiveMeta, VideoFile
28
29
  from endoreg_db.models.media.video.video_file_anonymize import _cleanup_raw_assets
29
- from django.db.models.fields.files import FieldFile
30
- from endoreg_db.models import EndoscopyProcessor
30
+ from endoreg_db.utils.hashs import get_video_hash
31
+ from endoreg_db.utils.paths import ANONYM_VIDEO_DIR, STORAGE_DIR, VIDEO_DIR
31
32
 
32
33
  # File lock configuration (matches PDF import)
33
34
  STALE_LOCK_SECONDS = 6000 # 100 minutes - reclaim locks older than this
34
- MAX_LOCK_WAIT_SECONDS = 90 # New: wait up to 90s for a non-stale lock to clear before skipping
35
+ MAX_LOCK_WAIT_SECONDS = (
36
+ 90 # New: wait up to 90s for a non-stale lock to clear before skipping
37
+ )
35
38
 
36
39
  logger = logging.getLogger(__name__)
37
40
 
@@ -60,7 +63,10 @@ class VideoImportService:
60
63
  # Ensure anonym_video directory exists before listing files
61
64
  anonym_video_dir = Path(ANONYM_VIDEO_DIR)
62
65
  if anonym_video_dir.exists():
63
- self.processed_files = set(str(anonym_video_dir / file) for file in os.listdir(ANONYM_VIDEO_DIR))
66
+ self.processed_files = set(
67
+ str(anonym_video_dir / file)
68
+ for file in os.listdir(ANONYM_VIDEO_DIR)
69
+ )
64
70
  else:
65
71
  logger.info(f"Creating anonym_videos directory: {anonym_video_dir}")
66
72
  anonym_video_dir.mkdir(parents=True, exist_ok=True)
@@ -77,7 +83,9 @@ class VideoImportService:
77
83
 
78
84
  self.logger = logging.getLogger(__name__)
79
85
 
80
- self.cleaner = None # This gets instantiated in the perform_frame_cleaning method
86
+ self.cleaner = (
87
+ None # This gets instantiated in the perform_frame_cleaning method
88
+ )
81
89
 
82
90
  def _require_current_video(self) -> VideoFile:
83
91
  """Return the current VideoFile or raise if it has not been initialized."""
@@ -116,10 +124,16 @@ class VideoImportService:
116
124
 
117
125
  if age is not None and age > STALE_LOCK_SECONDS:
118
126
  try:
119
- logger.warning("Stale lock detected for %s (age %.0fs). Reclaiming lock...", path, age)
127
+ logger.warning(
128
+ "Stale lock detected for %s (age %.0fs). Reclaiming lock...",
129
+ path,
130
+ age,
131
+ )
120
132
  lock_path.unlink()
121
133
  except Exception as e:
122
- logger.warning("Failed to remove stale lock %s: %s", lock_path, e)
134
+ logger.warning(
135
+ "Failed to remove stale lock %s: %s", lock_path, e
136
+ )
123
137
  # Loop continues and retries acquire immediately
124
138
  continue
125
139
 
@@ -162,7 +176,9 @@ class VideoImportService:
162
176
 
163
177
  try:
164
178
  # Initialize processing context
165
- self._initialize_processing_context(file_path, center_name, processor_name, save_video, delete_source)
179
+ self._initialize_processing_context(
180
+ file_path, center_name, processor_name, save_video, delete_source
181
+ )
166
182
 
167
183
  # Validate and prepare file (may raise ValueError if another worker holds a non-stale lock)
168
184
  try:
@@ -196,17 +212,28 @@ class VideoImportService:
196
212
 
197
213
  except Exception as e:
198
214
  # Safe file path access - handles cases where processing_context wasn't initialized
199
- safe_file_path = getattr(self, "processing_context", {}).get("file_path", file_path)
215
+ safe_file_path = getattr(self, "processing_context", {}).get(
216
+ "file_path", file_path
217
+ )
200
218
  # Debug: Log context state for troubleshooting
201
219
  context_keys = list(getattr(self, "processing_context", {}).keys())
202
220
  self.logger.debug(f"Context keys during error: {context_keys}")
203
- self.logger.error(f"Video import and anonymization failed for {safe_file_path}: {e}")
221
+ self.logger.error(
222
+ f"Video import and anonymization failed for {safe_file_path}: {e}"
223
+ )
204
224
  self._cleanup_on_error()
205
225
  raise
206
226
  finally:
207
227
  self._cleanup_processing_context()
208
228
 
209
- def _initialize_processing_context(self, file_path: Union[Path, str], center_name: str, processor_name: str, save_video: bool, delete_source: bool):
229
+ def _initialize_processing_context(
230
+ self,
231
+ file_path: Union[Path, str],
232
+ center_name: str,
233
+ processor_name: str,
234
+ save_video: bool,
235
+ delete_source: bool,
236
+ ):
210
237
  """Initialize the processing context for the current video import."""
211
238
  self.processing_context = {
212
239
  "file_path": Path(file_path),
@@ -319,12 +346,23 @@ class VideoImportService:
319
346
  except Exception:
320
347
  stored_raw_path = None
321
348
 
322
- # Fallback: derive from UUID + suffix
349
+ # Fallback: derive from UUID + suffix - ALWAYS use UUID for consistency
323
350
  if not stored_raw_path:
324
351
  suffix = source_path.suffix or ".mp4"
325
352
  uuid_str = getattr(_current_video, "uuid", None)
326
- filename = f"{uuid_str}{suffix}" if uuid_str else source_path.name
353
+ if uuid_str:
354
+ filename = f"{uuid_str}{suffix}"
355
+ else:
356
+ # Emergency fallback with timestamp to avoid conflicts
357
+ import time
358
+
359
+ timestamp = int(time.time())
360
+ filename = f"video_{timestamp}{suffix}"
361
+ self.logger.warning(
362
+ "No UUID available, using timestamp-based filename: %s", filename
363
+ )
327
364
  stored_raw_path = videos_dir / filename
365
+ self.logger.debug("Using UUID-based raw filename: %s", filename)
328
366
 
329
367
  delete_source = bool(self.processing_context.get("delete_source", True))
330
368
  stored_raw_path.parent.mkdir(parents=True, exist_ok=True)
@@ -339,7 +377,9 @@ class VideoImportService:
339
377
  except Exception:
340
378
  shutil.copy2(source_path, stored_raw_path)
341
379
  os.remove(source_path)
342
- self.logger.info("Copied & removed raw video to: %s", stored_raw_path)
380
+ self.logger.info(
381
+ "Copied & removed raw video to: %s", stored_raw_path
382
+ )
343
383
  else:
344
384
  shutil.copy2(source_path, stored_raw_path)
345
385
  self.logger.info("Copied raw video to: %s", stored_raw_path)
@@ -369,8 +409,7 @@ class VideoImportService:
369
409
  # Initialize video specifications
370
410
  video.initialize_video_specs()
371
411
 
372
- # Initialize frame objects in database
373
- video.initialize_frames()
412
+
374
413
 
375
414
  # Extract frames BEFORE processing to prevent pipeline 1 conflicts
376
415
  self.logger.info("Pre-extracting frames to avoid pipeline conflicts...")
@@ -379,6 +418,8 @@ class VideoImportService:
379
418
  if frames_extracted:
380
419
  self.processing_context["frames_extracted"] = True
381
420
  self.logger.info("Frame extraction completed successfully")
421
+ # Initialize frame objects in database
422
+ video.initialize_frames(video.get_frame_paths())
382
423
 
383
424
  # CRITICAL: Immediately save the frames_extracted state to database
384
425
  # to prevent refresh_from_db() in pipeline 1 from overriding it
@@ -391,7 +432,9 @@ class VideoImportService:
391
432
  self.logger.warning("Frame extraction failed, but continuing...")
392
433
  self.processing_context["frames_extracted"] = False
393
434
  except Exception as e:
394
- self.logger.warning(f"Frame extraction failed during setup: {e}, but continuing...")
435
+ self.logger.warning(
436
+ f"Frame extraction failed during setup: {e}, but continuing..."
437
+ )
395
438
  self.processing_context["frames_extracted"] = False
396
439
 
397
440
  # Ensure default patient data
@@ -402,38 +445,60 @@ class VideoImportService:
402
445
  def _process_frames_and_metadata(self):
403
446
  """Process frames and extract metadata with anonymization."""
404
447
  # Check frame cleaning availability
405
- frame_cleaning_available, frame_cleaner = self._ensure_frame_cleaning_available()
448
+ frame_cleaning_available, frame_cleaner = (
449
+ self._ensure_frame_cleaning_available()
450
+ )
406
451
  video = self._require_current_video()
407
452
 
408
453
  raw_file_field = video.raw_file
409
- has_raw_file = isinstance(raw_file_field, FieldFile) and bool(raw_file_field.name)
454
+ has_raw_file = isinstance(raw_file_field, FieldFile) and bool(
455
+ raw_file_field.name
456
+ )
410
457
 
411
458
  if not (frame_cleaning_available and has_raw_file):
412
- self.logger.warning("Frame cleaning not available or conditions not met, using fallback anonymization.")
459
+ self.logger.warning(
460
+ "Frame cleaning not available or conditions not met, using fallback anonymization."
461
+ )
413
462
  self._fallback_anonymize_video()
414
463
  return
415
464
 
416
465
  try:
417
- self.logger.info("Starting frame-level anonymization with processor ROI masking...")
466
+ self.logger.info(
467
+ "Starting frame-level anonymization with processor ROI masking..."
468
+ )
418
469
 
419
470
  # Get processor ROI information
420
- endoscope_data_roi_nested, endoscope_image_roi = self._get_processor_roi_info()
471
+ endoscope_data_roi_nested, endoscope_image_roi = (
472
+ self._get_processor_roi_info()
473
+ )
421
474
 
422
475
  # Perform frame cleaning with timeout to prevent blocking
423
- from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
476
+ from concurrent.futures import ThreadPoolExecutor
477
+ from concurrent.futures import TimeoutError as FutureTimeoutError
424
478
 
425
479
  with ThreadPoolExecutor(max_workers=1) as executor:
426
- future = executor.submit(self._perform_frame_cleaning, endoscope_data_roi_nested, endoscope_image_roi)
480
+ future = executor.submit(
481
+ self._perform_frame_cleaning,
482
+ endoscope_data_roi_nested,
483
+ endoscope_image_roi,
484
+ )
427
485
  try:
428
486
  # Increased timeout to better accommodate ffmpeg + OCR
429
487
  future.result(timeout=50000)
430
488
  self.processing_context["anonymization_completed"] = True
431
- self.logger.info("Frame cleaning completed successfully within timeout")
489
+ self.logger.info(
490
+ "Frame cleaning completed successfully within timeout"
491
+ )
432
492
  except FutureTimeoutError:
433
- self.logger.warning("Frame cleaning timed out; entering grace period check for cleaned output")
493
+ self.logger.warning(
494
+ "Frame cleaning timed out; entering grace period check for cleaned output"
495
+ )
434
496
  # Grace period: detect if cleaned file appears shortly after timeout
435
497
  raw_video_path = self.processing_context.get("raw_video_path")
436
- video_filename = self.processing_context.get("video_filename", Path(raw_video_path).name if raw_video_path else "video.mp4")
498
+ video_filename = self.processing_context.get(
499
+ "video_filename",
500
+ Path(raw_video_path).name if raw_video_path else "video.mp4",
501
+ )
437
502
  grace_seconds = 60
438
503
  expected_cleaned_path: Optional[Path] = None
439
504
  processed_field = video.processed_file
@@ -446,27 +511,42 @@ class VideoImportService:
446
511
  if expected_cleaned_path is not None:
447
512
  for _ in range(grace_seconds):
448
513
  if expected_cleaned_path.exists():
449
- self.processing_context["cleaned_video_path"] = expected_cleaned_path
450
- self.processing_context["anonymization_completed"] = True
451
- self.logger.info("Detected cleaned video during grace period: %s", expected_cleaned_path)
514
+ self.processing_context["cleaned_video_path"] = (
515
+ expected_cleaned_path
516
+ )
517
+ self.processing_context["anonymization_completed"] = (
518
+ True
519
+ )
520
+ self.logger.info(
521
+ "Detected cleaned video during grace period: %s",
522
+ expected_cleaned_path,
523
+ )
452
524
  found = True
453
525
  break
454
526
  time.sleep(1)
455
527
  else:
456
528
  self._fallback_anonymize_video()
457
529
  if not found:
458
- raise TimeoutError("Frame cleaning operation timed out - likely Ollama connection issue")
530
+ raise TimeoutError(
531
+ "Frame cleaning operation timed out - likely Ollama connection issue"
532
+ )
459
533
 
460
534
  except Exception as e:
461
- self.logger.warning("Frame cleaning failed (reason: %s), falling back to simple copy", e)
535
+ self.logger.warning(
536
+ "Frame cleaning failed (reason: %s), falling back to simple copy", e
537
+ )
462
538
  # Try fallback anonymization when frame cleaning fails
463
539
  try:
464
540
  self._fallback_anonymize_video()
465
541
  except Exception as fallback_error:
466
- self.logger.error("Fallback anonymization also failed: %s", fallback_error)
542
+ self.logger.error(
543
+ "Fallback anonymization also failed: %s", fallback_error
544
+ )
467
545
  # If even fallback fails, mark as not anonymized but continue import
468
546
  self.processing_context["anonymization_completed"] = False
469
- self.processing_context["error_reason"] = f"Frame cleaning failed: {e}, Fallback failed: {fallback_error}"
547
+ self.processing_context["error_reason"] = (
548
+ f"Frame cleaning failed: {e}, Fallback failed: {fallback_error}"
549
+ )
470
550
 
471
551
  def _save_anonymized_video(self):
472
552
  original_raw_file_path_to_delete = None
@@ -475,14 +555,24 @@ class VideoImportService:
475
555
  anonymized_video_path = video.get_target_anonymized_video_path()
476
556
 
477
557
  if not anonymized_video_path.exists():
478
- raise RuntimeError(f"Processed video file not found after assembly for {video.uuid}: {anonymized_video_path}")
558
+ raise RuntimeError(
559
+ f"Processed video file not found after assembly for {video.uuid}: {anonymized_video_path}"
560
+ )
479
561
 
480
562
  new_processed_hash = get_video_hash(anonymized_video_path)
481
- if video.__class__.objects.filter(processed_video_hash=new_processed_hash).exclude(pk=video.pk).exists():
482
- raise ValueError(f"Processed video hash {new_processed_hash} already exists for another video (Video: {video.uuid}).")
563
+ if (
564
+ video.__class__.objects.filter(processed_video_hash=new_processed_hash)
565
+ .exclude(pk=video.pk)
566
+ .exists()
567
+ ):
568
+ raise ValueError(
569
+ f"Processed video hash {new_processed_hash} already exists for another video (Video: {video.uuid})."
570
+ )
483
571
 
484
572
  video.processed_video_hash = new_processed_hash
485
- video.processed_file.name = anonymized_video_path.relative_to(STORAGE_DIR).as_posix()
573
+ video.processed_file.name = anonymized_video_path.relative_to(
574
+ STORAGE_DIR
575
+ ).as_posix()
486
576
 
487
577
  update_fields = [
488
578
  "processed_video_hash",
@@ -500,7 +590,9 @@ class VideoImportService:
500
590
 
501
591
  transaction.on_commit(
502
592
  lambda: _cleanup_raw_assets(
503
- video_uuid=video.uuid, raw_file_path=original_raw_file_path_to_delete, raw_frame_dir=original_raw_frame_dir_to_delete
593
+ video_uuid=video.uuid,
594
+ raw_file_path=original_raw_file_path_to_delete,
595
+ raw_frame_dir=original_raw_frame_dir_to_delete,
504
596
  )
505
597
  )
506
598
 
@@ -518,15 +610,23 @@ class VideoImportService:
518
610
  self.logger.info("Attempting fallback video anonymization...")
519
611
  video = self.current_video
520
612
  if video is None:
521
- self.logger.warning("No VideoFile instance available for fallback anonymization")
613
+ self.logger.warning(
614
+ "No VideoFile instance available for fallback anonymization"
615
+ )
522
616
 
523
617
  # Strategy 2: Simple copy (no processing, just copy raw to processed)
524
- self.logger.info("Using simple copy fallback (raw video will be used as 'processed' video)")
618
+ self.logger.info(
619
+ "Using simple copy fallback (raw video will be used as 'processed' video)"
620
+ )
525
621
  self.processing_context["anonymization_completed"] = False
526
622
  self.processing_context["use_raw_as_processed"] = True
527
- self.logger.warning("Fallback: Video will be imported without anonymization (raw copy used)")
623
+ self.logger.warning(
624
+ "Fallback: Video will be imported without anonymization (raw copy used)"
625
+ )
528
626
  except Exception as e:
529
- self.logger.error(f"Error during fallback anonymization: {e}", exc_info=True)
627
+ self.logger.error(
628
+ f"Error during fallback anonymization: {e}", exc_info=True
629
+ )
530
630
  self.processing_context["anonymization_completed"] = False
531
631
  self.processing_context["error_reason"] = str(e)
532
632
 
@@ -539,7 +639,11 @@ class VideoImportService:
539
639
  try:
540
640
  video.refresh_from_db()
541
641
  except Exception as refresh_error:
542
- self.logger.warning("Could not refresh VideoFile %s from DB: %s", video.uuid, refresh_error)
642
+ self.logger.warning(
643
+ "Could not refresh VideoFile %s from DB: %s",
644
+ video.uuid,
645
+ refresh_error,
646
+ )
543
647
 
544
648
  state = video.get_or_create_state()
545
649
 
@@ -556,12 +660,18 @@ class VideoImportService:
556
660
  state.text_meta_extracted = True
557
661
 
558
662
  # ✅ FIX: Only mark as processed if anonymization actually completed
559
- anonymization_completed = self.processing_context.get("anonymization_completed", False)
663
+ anonymization_completed = self.processing_context.get(
664
+ "anonymization_completed", False
665
+ )
560
666
  if anonymization_completed:
561
667
  state.mark_sensitive_meta_processed(save=False)
562
- self.logger.info("Anonymization completed - marking sensitive meta as processed")
668
+ self.logger.info(
669
+ "Anonymization completed - marking sensitive meta as processed"
670
+ )
563
671
  else:
564
- self.logger.warning(f"Anonymization NOT completed - NOT marking as processed. Reason: {self.processing_context.get('error_reason', 'Unknown')}")
672
+ self.logger.warning(
673
+ f"Anonymization NOT completed - NOT marking as processed. Reason: {self.processing_context.get('error_reason', 'Unknown')}"
674
+ )
565
675
  # Explicitly mark as NOT processed
566
676
  state.sensitive_meta_processed = False
567
677
 
@@ -587,12 +697,15 @@ class VideoImportService:
587
697
  else:
588
698
  raw_video_path = self.processing_context.get("raw_video_path")
589
699
  if raw_video_path and Path(raw_video_path).exists():
590
- video_filename = self.processing_context.get("video_filename", Path(raw_video_path).name)
591
- processed_filename = f"processed_{video_filename}"
700
+ # Use UUID-based naming to avoid conflicts
701
+ suffix = Path(raw_video_path).suffix or ".mp4"
702
+ processed_filename = f"processed_{video.uuid}{suffix}"
592
703
  processed_video_path = Path(raw_video_path).parent / processed_filename
593
704
  try:
594
705
  shutil.copy2(str(raw_video_path), str(processed_video_path))
595
- self.logger.info("Copied raw video for processing: %s", processed_video_path)
706
+ self.logger.info(
707
+ "Copied raw video for processing: %s", processed_video_path
708
+ )
596
709
  except Exception as exc:
597
710
  self.logger.error("Failed to copy raw video: %s", exc)
598
711
  processed_video_path = None
@@ -612,10 +725,16 @@ class VideoImportService:
612
725
  relative_path = anonym_target_path.relative_to(storage_root)
613
726
  video.processed_file.name = str(relative_path)
614
727
  video.save(update_fields=["processed_file"])
615
- self.logger.info("Updated processed_file path to: %s", relative_path)
728
+ self.logger.info(
729
+ "Updated processed_file path to: %s", relative_path
730
+ )
616
731
  except Exception as exc:
617
- self.logger.error("Failed to update processed_file path: %s", exc)
618
- video.processed_file.name = f"anonym_videos/{anonym_video_filename}"
732
+ self.logger.error(
733
+ "Failed to update processed_file path: %s", exc
734
+ )
735
+ video.processed_file.name = (
736
+ f"anonym_videos/{anonym_video_filename}"
737
+ )
619
738
  video.save(update_fields=["processed_file"])
620
739
  self.logger.info(
621
740
  "Updated processed_file path using fallback: %s",
@@ -624,17 +743,26 @@ class VideoImportService:
624
743
 
625
744
  self.processing_context["anonymization_completed"] = True
626
745
  else:
627
- self.logger.warning("Processed video file not found after move: %s", anonym_target_path)
746
+ self.logger.warning(
747
+ "Processed video file not found after move: %s",
748
+ anonym_target_path,
749
+ )
628
750
  except Exception as exc:
629
- self.logger.error("Failed to move processed video to anonym_videos: %s", exc)
751
+ self.logger.error(
752
+ "Failed to move processed video to anonym_videos: %s", exc
753
+ )
630
754
  else:
631
- self.logger.warning("No processed video available - processed_file will remain empty")
755
+ self.logger.warning(
756
+ "No processed video available - processed_file will remain empty"
757
+ )
632
758
 
633
759
  try:
634
760
  from endoreg_db.utils.paths import RAW_FRAME_DIR
635
761
 
636
762
  shutil.rmtree(RAW_FRAME_DIR, ignore_errors=True)
637
- self.logger.debug("Cleaned up temporary frames directory: %s", RAW_FRAME_DIR)
763
+ self.logger.debug(
764
+ "Cleaned up temporary frames directory: %s", RAW_FRAME_DIR
765
+ )
638
766
  except Exception as exc:
639
767
  self.logger.warning("Failed to remove directory %s: %s", RAW_FRAME_DIR, exc)
640
768
 
@@ -644,10 +772,14 @@ class VideoImportService:
644
772
  os.remove(source_path)
645
773
  self.logger.info("Removed remaining source file: %s", source_path)
646
774
  except Exception as exc:
647
- self.logger.warning("Failed to remove source file %s: %s", source_path, exc)
775
+ self.logger.warning(
776
+ "Failed to remove source file %s: %s", source_path, exc
777
+ )
648
778
 
649
779
  if not video.processed_file or not Path(video.processed_file.path).exists():
650
- self.logger.warning("No processed_file found after cleanup - video will be unprocessed")
780
+ self.logger.warning(
781
+ "No processed_file found after cleanup - video will be unprocessed"
782
+ )
651
783
  try:
652
784
  video.anonymize(delete_original_raw=self.delete_source)
653
785
  video.save(update_fields=["processed_file"])
@@ -662,10 +794,14 @@ class VideoImportService:
662
794
 
663
795
  with transaction.atomic():
664
796
  video.refresh_from_db()
665
- if hasattr(video, "state") and self.processing_context.get("anonymization_completed"):
797
+ if hasattr(video, "state") and self.processing_context.get(
798
+ "anonymization_completed"
799
+ ):
666
800
  video.state.mark_sensitive_meta_processed(save=True)
667
801
 
668
- self.logger.info("Import and anonymization completed for VideoFile UUID: %s", video.uuid)
802
+ self.logger.info(
803
+ "Import and anonymization completed for VideoFile UUID: %s", video.uuid
804
+ )
669
805
  self.logger.info("Raw video stored in: /data/videos")
670
806
  self.logger.info("Processed video stored in: /data/anonym_videos")
671
807
 
@@ -692,7 +828,9 @@ class VideoImportService:
692
828
  if source_path is None:
693
829
  raise ValueError("No file path available for creating sensitive file")
694
830
  if not raw_field:
695
- raise ValueError("VideoFile must have a raw_file to create a sensitive file")
831
+ raise ValueError(
832
+ "VideoFile must have a raw_file to create a sensitive file"
833
+ )
696
834
 
697
835
  target_dir = VIDEO_DIR / "sensitive"
698
836
  if not target_dir.exists():
@@ -702,9 +840,13 @@ class VideoImportService:
702
840
  target_file_path = target_dir / source_path.name
703
841
  try:
704
842
  shutil.move(str(source_path), str(target_file_path))
705
- self.logger.info("Moved raw file to sensitive directory: %s", target_file_path)
843
+ self.logger.info(
844
+ "Moved raw file to sensitive directory: %s", target_file_path
845
+ )
706
846
  except Exception as exc:
707
- self.logger.warning("Failed to move raw file to sensitive dir, copying instead: %s", exc)
847
+ self.logger.warning(
848
+ "Failed to move raw file to sensitive dir, copying instead: %s", exc
849
+ )
708
850
  shutil.copy(str(source_path), str(target_file_path))
709
851
  try:
710
852
  os.remove(source_path)
@@ -718,7 +860,10 @@ class VideoImportService:
718
860
  relative_path = target_file_path.relative_to(storage_root)
719
861
  video.raw_file.name = str(relative_path)
720
862
  video.save(update_fields=["raw_file"])
721
- self.logger.info("Updated video.raw_file to point to sensitive location: %s", relative_path)
863
+ self.logger.info(
864
+ "Updated video.raw_file to point to sensitive location: %s",
865
+ relative_path,
866
+ )
722
867
  except Exception as exc:
723
868
  self.logger.warning("Failed to set relative path, using fallback: %s", exc)
724
869
  video.raw_file.name = f"videos/sensitive/{target_file_path.name}"
@@ -731,10 +876,14 @@ class VideoImportService:
731
876
  self.processing_context["raw_video_path"] = target_file_path
732
877
  self.processing_context["video_filename"] = target_file_path.name
733
878
 
734
- self.logger.info("Created sensitive file for %s at %s", video.uuid, target_file_path)
879
+ self.logger.info(
880
+ "Created sensitive file for %s at %s", video.uuid, target_file_path
881
+ )
735
882
  return target_file_path
736
883
 
737
- def _get_processor_roi_info(self) -> Tuple[Optional[List[List[Dict[str, Any]]]], Optional[Dict[str, Any]]]:
884
+ def _get_processor_roi_info(
885
+ self,
886
+ ) -> Tuple[Optional[List[List[Dict[str, Any]]]], Optional[Dict[str, Any]]]:
738
887
  """Get processor ROI information for masking."""
739
888
  endoscope_data_roi_nested = None
740
889
  endoscope_image_roi = None
@@ -745,10 +894,15 @@ class VideoImportService:
745
894
  video_meta = getattr(video, "video_meta", None)
746
895
  processor = getattr(video_meta, "processor", None) if video_meta else None
747
896
  if processor:
748
- assert isinstance(processor, EndoscopyProcessor), "Processor is not of type EndoscopyProcessor"
897
+ assert isinstance(processor, EndoscopyProcessor), (
898
+ "Processor is not of type EndoscopyProcessor"
899
+ )
749
900
  endoscope_image_roi = processor.get_roi_endoscope_image()
750
901
  endoscope_data_roi_nested = processor.get_sensitive_rois()
751
- self.logger.info("Retrieved processor ROI information: endoscope_image_roi=%s", endoscope_image_roi)
902
+ self.logger.info(
903
+ "Retrieved processor ROI information: endoscope_image_roi=%s",
904
+ endoscope_image_roi,
905
+ )
752
906
  else:
753
907
  self.logger.warning(
754
908
  "No processor found for video %s, proceeding without ROI masking",
@@ -770,28 +924,40 @@ class VideoImportService:
770
924
 
771
925
  return endoscope_data_roi_nested, endoscope_image_roi
772
926
 
773
- def _ensure_default_patient_data(self, video_instance: VideoFile | None = None) -> None:
927
+ def _ensure_default_patient_data(
928
+ self, video_instance: VideoFile | None = None
929
+ ) -> None:
774
930
  """Ensure minimum patient data is present on the video's SensitiveMeta."""
775
931
 
776
932
  video = video_instance or self._require_current_video()
777
933
 
778
934
  sensitive_meta = getattr(video, "sensitive_meta", None)
779
935
  if not sensitive_meta:
780
- self.logger.info("No SensitiveMeta found for video %s, creating default", video.uuid)
936
+ self.logger.info(
937
+ "No SensitiveMeta found for video %s, creating default", video.uuid
938
+ )
781
939
  default_data = {
782
940
  "patient_first_name": "Patient",
783
941
  "patient_last_name": "Unknown",
784
942
  "patient_dob": date(1990, 1, 1),
785
943
  "examination_date": date.today(),
786
- "center_name": video.center.name if video.center else "university_hospital_wuerzburg",
944
+ "center_name": video.center.name
945
+ if video.center
946
+ else "university_hospital_wuerzburg",
787
947
  }
788
948
  try:
789
949
  sensitive_meta = SensitiveMeta.create_from_dict(default_data)
790
950
  video.sensitive_meta = sensitive_meta
791
951
  video.save(update_fields=["sensitive_meta"])
792
- self.logger.info("Created default SensitiveMeta for video %s", video.uuid)
952
+ self.logger.info(
953
+ "Created default SensitiveMeta for video %s", video.uuid
954
+ )
793
955
  except Exception as exc:
794
- self.logger.error("Failed to create default SensitiveMeta for video %s: %s", video.uuid, exc)
956
+ self.logger.error(
957
+ "Failed to create default SensitiveMeta for video %s: %s",
958
+ video.uuid,
959
+ exc,
960
+ )
795
961
  return
796
962
  else:
797
963
  update_data: Dict[str, Any] = {}
@@ -815,7 +981,11 @@ class VideoImportService:
815
981
  list(update_data.keys()),
816
982
  )
817
983
  except Exception as exc:
818
- self.logger.error("Failed to update SensitiveMeta for video %s: %s", video.uuid, exc)
984
+ self.logger.error(
985
+ "Failed to update SensitiveMeta for video %s: %s",
986
+ video.uuid,
987
+ exc,
988
+ )
819
989
 
820
990
  def _ensure_frame_cleaning_available(self):
821
991
  """
@@ -832,7 +1002,9 @@ class VideoImportService:
832
1002
  return True, FrameCleaner()
833
1003
 
834
1004
  except Exception as e:
835
- self.logger.warning(f"Frame cleaning not available: {e} Please install or update lx_anonymizer.")
1005
+ self.logger.warning(
1006
+ f"Frame cleaning not available: {e} Please install or update lx_anonymizer."
1007
+ )
836
1008
 
837
1009
  return False, None
838
1010
 
@@ -854,12 +1026,17 @@ class VideoImportService:
854
1026
  except Exception:
855
1027
  raise RuntimeError(f"Raw video path not found: {raw_video_path}")
856
1028
 
857
- # Create temporary output path for cleaned video
858
- video_filename = self.processing_context.get("video_filename", Path(raw_video_path).name)
859
- cleaned_filename = f"cleaned_{video_filename}"
1029
+ # Create temporary output path for cleaned video using UUID to avoid naming conflicts
1030
+ video = self._require_current_video()
1031
+ # Ensure raw_video_path is not None
860
1032
  if not raw_video_path:
861
- raise RuntimeError("raw_video_path is None after fallback, cannot construct cleaned_video_path")
1033
+ raise RuntimeError(
1034
+ "raw_video_path is None, cannot construct cleaned_video_path"
1035
+ )
1036
+ suffix = Path(raw_video_path).suffix or ".mp4"
1037
+ cleaned_filename = f"cleaned_{video.uuid}{suffix}"
862
1038
  cleaned_video_path = Path(raw_video_path).parent / cleaned_filename
1039
+ self.logger.debug("Using UUID-based cleaned filename: %s", cleaned_filename)
863
1040
 
864
1041
  # Clean video with ROI masking (heavy I/O operation)
865
1042
  actual_cleaned_path, extracted_metadata = frame_cleaner.clean_video(
@@ -876,9 +1053,13 @@ class VideoImportService:
876
1053
 
877
1054
  # Update sensitive metadata with extracted information
878
1055
  self._update_sensitive_metadata(extracted_metadata)
879
- self.logger.info(f"Extracted metadata from frame cleaning: {extracted_metadata}")
1056
+ self.logger.info(
1057
+ f"Extracted metadata from frame cleaning: {extracted_metadata}"
1058
+ )
880
1059
 
881
- self.logger.info(f"Frame cleaning with ROI masking completed: {actual_cleaned_path}")
1060
+ self.logger.info(
1061
+ f"Frame cleaning with ROI masking completed: {actual_cleaned_path}"
1062
+ )
882
1063
  self.logger.info("Cleaned video will be moved to anonym_videos during cleanup")
883
1064
 
884
1065
  def _update_sensitive_metadata(self, extracted_metadata: Dict[str, Any]):
@@ -896,21 +1077,66 @@ class VideoImportService:
896
1077
  sm = sensitive_meta
897
1078
  updated_fields = []
898
1079
 
1080
+ # Ensure center is set from video.center if not in extracted_metadata
1081
+ metadata_to_update = extracted_metadata.copy()
1082
+
1083
+ # FIX: Set center object instead of center_name string
1084
+ if not hasattr(sm, "center") or not sm.center:
1085
+ if video.center:
1086
+ metadata_to_update["center"] = video.center
1087
+ self.logger.debug(
1088
+ "Added center object '%s' to metadata for SensitiveMeta update",
1089
+ video.center.name,
1090
+ )
1091
+ else:
1092
+ center_name = metadata_to_update.get("center_name")
1093
+ if center_name:
1094
+ try:
1095
+ from ..models.administration import Center
1096
+
1097
+ center_obj = Center.objects.get(name=center_name)
1098
+ metadata_to_update["center"] = center_obj
1099
+ self.logger.debug(
1100
+ "Loaded center object '%s' from center_name", center_name
1101
+ )
1102
+ metadata_to_update.pop("center_name", None)
1103
+ except Center.DoesNotExist:
1104
+ self.logger.error(
1105
+ "Center '%s' not found in database", center_name
1106
+ )
1107
+ return
1108
+
899
1109
  try:
900
- sm.update_from_dict(extracted_metadata)
901
- updated_fields = list(extracted_metadata.keys())
1110
+ sm.update_from_dict(metadata_to_update)
1111
+ updated_fields = list(
1112
+ extracted_metadata.keys()
1113
+ ) # Only log originally extracted fields
902
1114
  except KeyError as e:
903
1115
  self.logger.warning(f"Failed to update SensitiveMeta field {e}")
1116
+ return
904
1117
 
905
1118
  if updated_fields:
906
- sm.save(update_fields=updated_fields)
907
- self.logger.info("Updated SensitiveMeta fields for video %s: %s", video.uuid, updated_fields)
1119
+ try:
1120
+ sm.save() # Remove update_fields to allow all necessary fields to be saved
1121
+ self.logger.info(
1122
+ "Updated SensitiveMeta fields for video %s: %s",
1123
+ video.uuid,
1124
+ updated_fields,
1125
+ )
908
1126
 
909
- state = video.get_or_create_state()
910
- state.mark_sensitive_meta_processed(save=True)
911
- self.logger.info("Marked sensitive metadata as processed for video %s", video.uuid)
1127
+ state = video.get_or_create_state()
1128
+ state.mark_sensitive_meta_processed(save=True)
1129
+ self.logger.info(
1130
+ "Marked sensitive metadata as processed for video %s", video.uuid
1131
+ )
1132
+ except Exception as e:
1133
+ self.logger.error(f"Failed to save SensitiveMeta: {e}")
1134
+ raise # Re-raise to trigger fallback in calling method
912
1135
  else:
913
- self.logger.info("No SensitiveMeta fields updated for video %s - all existing values preserved", video.uuid)
1136
+ self.logger.info(
1137
+ "No SensitiveMeta fields updated for video %s - all existing values preserved",
1138
+ video.uuid,
1139
+ )
914
1140
 
915
1141
  def _signal_completion(self):
916
1142
  """Signal completion to the tracking system."""
@@ -925,14 +1151,25 @@ class VideoImportService:
925
1151
  except (ValueError, OSError):
926
1152
  raw_exists = False
927
1153
 
928
- video_processing_complete = video.sensitive_meta is not None and video.video_meta is not None and raw_exists
1154
+ video_processing_complete = (
1155
+ video.sensitive_meta is not None
1156
+ and video.video_meta is not None
1157
+ and raw_exists
1158
+ )
929
1159
 
930
1160
  if video_processing_complete:
931
- self.logger.info("Video %s processing completed successfully - ready for validation", video.uuid)
1161
+ self.logger.info(
1162
+ "Video %s processing completed successfully - ready for validation",
1163
+ video.uuid,
1164
+ )
932
1165
 
933
1166
  # Update completion flags if they exist
934
1167
  completion_fields = []
935
- for field_name in ["import_completed", "processing_complete", "ready_for_validation"]:
1168
+ for field_name in [
1169
+ "import_completed",
1170
+ "processing_complete",
1171
+ "ready_for_validation",
1172
+ ]:
936
1173
  if hasattr(video, field_name):
937
1174
  setattr(video, field_name, True)
938
1175
  completion_fields.append(field_name)
@@ -989,7 +1226,9 @@ class VideoImportService:
989
1226
  file_path_str = str(file_path)
990
1227
  if file_path_str in self.processed_files:
991
1228
  self.processed_files.remove(file_path_str)
992
- self.logger.info(f"Removed {file_path_str} from processed files (failed processing)")
1229
+ self.logger.info(
1230
+ f"Removed {file_path_str} from processed files (failed processing)"
1231
+ )
993
1232
 
994
1233
  except Exception as e:
995
1234
  self.logger.warning(f"Error during context cleanup: {e}")