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.
- endoreg_db/management/commands/load_ai_model_data.py +2 -1
- endoreg_db/management/commands/setup_endoreg_db.py +11 -7
- endoreg_db/models/media/pdf/raw_pdf.py +241 -97
- endoreg_db/models/media/video/pipe_1.py +30 -33
- endoreg_db/models/media/video/video_file.py +300 -187
- endoreg_db/models/metadata/model_meta_logic.py +15 -1
- endoreg_db/models/metadata/sensitive_meta_logic.py +391 -70
- endoreg_db/serializers/__init__.py +26 -55
- endoreg_db/serializers/misc/__init__.py +1 -1
- endoreg_db/serializers/misc/file_overview.py +65 -35
- endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
- endoreg_db/serializers/video_examination.py +198 -0
- endoreg_db/services/lookup_service.py +228 -58
- endoreg_db/services/lookup_store.py +174 -30
- endoreg_db/services/pdf_import.py +585 -282
- endoreg_db/services/video_import.py +340 -101
- endoreg_db/urls/__init__.py +36 -23
- endoreg_db/urls/label_video_segments.py +2 -0
- endoreg_db/urls/media.py +3 -2
- endoreg_db/views/__init__.py +6 -3
- endoreg_db/views/media/pdf_media.py +3 -1
- endoreg_db/views/media/video_media.py +1 -1
- endoreg_db/views/media/video_segments.py +187 -259
- endoreg_db/views/pdf/__init__.py +5 -8
- endoreg_db/views/pdf/pdf_stream.py +187 -0
- endoreg_db/views/pdf/reimport.py +110 -94
- endoreg_db/views/requirement/lookup.py +171 -287
- endoreg_db/views/video/__init__.py +0 -2
- endoreg_db/views/video/video_examination_viewset.py +202 -289
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/METADATA +1 -1
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/RECORD +33 -34
- endoreg_db/views/pdf/pdf_media.py +0 -239
- endoreg_db/views/pdf/pdf_stream_views.py +0 -127
- endoreg_db/views/video/video_media.py +0 -158
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
-
|
|
25
|
-
from endoreg_db.
|
|
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
|
|
30
|
-
from endoreg_db.
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
489
|
+
self.logger.info(
|
|
490
|
+
"Frame cleaning completed successfully within timeout"
|
|
491
|
+
)
|
|
432
492
|
except FutureTimeoutError:
|
|
433
|
-
self.logger.warning(
|
|
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(
|
|
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"] =
|
|
450
|
-
|
|
451
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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"] =
|
|
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(
|
|
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
|
|
482
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
668
|
+
self.logger.info(
|
|
669
|
+
"Anonymization completed - marking sensitive meta as processed"
|
|
670
|
+
)
|
|
563
671
|
else:
|
|
564
|
-
self.logger.warning(
|
|
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
|
-
|
|
591
|
-
|
|
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(
|
|
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(
|
|
728
|
+
self.logger.info(
|
|
729
|
+
"Updated processed_file path to: %s", relative_path
|
|
730
|
+
)
|
|
616
731
|
except Exception as exc:
|
|
617
|
-
self.logger.error(
|
|
618
|
-
|
|
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(
|
|
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(
|
|
751
|
+
self.logger.error(
|
|
752
|
+
"Failed to move processed video to anonym_videos: %s", exc
|
|
753
|
+
)
|
|
630
754
|
else:
|
|
631
|
-
self.logger.warning(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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),
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
952
|
+
self.logger.info(
|
|
953
|
+
"Created default SensitiveMeta for video %s", video.uuid
|
|
954
|
+
)
|
|
793
955
|
except Exception as exc:
|
|
794
|
-
self.logger.error(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
859
|
-
|
|
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(
|
|
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(
|
|
1056
|
+
self.logger.info(
|
|
1057
|
+
f"Extracted metadata from frame cleaning: {extracted_metadata}"
|
|
1058
|
+
)
|
|
880
1059
|
|
|
881
|
-
self.logger.info(
|
|
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(
|
|
901
|
-
updated_fields = list(
|
|
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
|
-
|
|
907
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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 [
|
|
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(
|
|
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}")
|