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

@@ -1,7 +1,7 @@
1
1
  from endoreg_db.models.media.processing_history.processing_history import ProcessingHistory
2
2
  from endoreg_db.utils.paths import IMPORT_REPORT_DIR, IMPORT_VIDEO_DIR, ANONYM_REPORT_DIR, ANONYM_VIDEO_DIR
3
3
 
4
-
4
+ import os
5
5
  import logging
6
6
  import shutil
7
7
  from pathlib import Path
@@ -187,6 +187,8 @@ def finalize_video_success(
187
187
  - Mark VideoState as anonymized + sensitive_meta_processed
188
188
  - Mark ProcessingHistory.success = True
189
189
  """
190
+ nuke = nuke_transcoding_dir()
191
+ assert(nuke is True)
190
192
  instance = ctx.current_video
191
193
  if not isinstance(instance, VideoFile):
192
194
  logger.warning("finalize_video_success called with non-VideoFile instance")
@@ -265,6 +267,8 @@ def finalize_video_success(
265
267
 
266
268
  # --- Update VideoState flags (mirrors report) ---
267
269
  state = _ensure_instance_state(instance)
270
+
271
+
268
272
 
269
273
  with transaction.atomic():
270
274
  if state is not None:
@@ -355,46 +359,138 @@ def finalize_failure(
355
359
  ctx.file_path,
356
360
  )
357
361
 
358
- def delete_associated_files(ctx:ImportContext):
359
- try:
360
- assert isinstance(ctx.original_path, Path)
361
- except AssertionError as e:
362
- logger.warning(f"Original file restored from sensitive copy. This is because the original file is gone. {e}")
363
- if ctx.file_type =="video":
364
- if isinstance(ctx.sensitive_path, Path):
365
- try:
366
- ctx.original_path = Path(shutil.copy2(ctx.sensitive_path, IMPORT_VIDEO_DIR))
367
- except Exception as e:
368
- logger.error(f"Error during safety copy: {e} Original file could not be restored!")
369
- raise
370
- elif ctx.file_type == "report":
371
- if isinstance(ctx.sensitive_path, Path):
372
- try:
373
- ctx.original_path = Path(shutil.copy2(ctx.sensitive_path, IMPORT_REPORT_DIR))
374
- except Exception as e:
375
- logger.error(f"Error during safety copy: {e} Original file could not be restored!")
376
- raise
377
-
378
- if isinstance(ctx.anonymized_path, Path):
362
+ def delete_associated_files(ctx: ImportContext) -> None:
363
+ """
364
+ Best-effort cleanup of anonymized, sensitive and transcoding artefacts.
365
+
366
+ - Ensure ctx.original_path points to an existing import file; if not, try to restore
367
+ from ctx.sensitive_path into the appropriate IMPORT_*_DIR.
368
+ - Delete anonymized file (if any).
369
+ - Nuke transcoding directory.
370
+ - Delete sensitive file (if any).
371
+
372
+ This function should *not* raise on non-critical cleanup errors; it logs instead.
373
+ Only restoration of the original import file is treated as critical.
374
+ """
375
+
376
+ # --- 1. Restore original import file if needed (critical) ---
377
+ original_missing = not isinstance(ctx.original_path, Path) or not ctx.original_path.exists()
378
+ if original_missing:
379
+ logger.warning(
380
+ "Original file missing in ctx (file_type=%s); "
381
+ "trying to restore from sensitive copy.",
382
+ ctx.file_type,
383
+ )
384
+
385
+ if not isinstance(ctx.sensitive_path, Path) or not ctx.sensitive_path.exists():
386
+ # This is serious: we lost both original and sensitive copy
387
+ msg = (
388
+ f"Cannot restore original file for {ctx.file_type}: "
389
+ "sensitive copy missing as well."
390
+ )
391
+ logger.error(msg)
392
+ raise RuntimeError(msg)
393
+
379
394
  try:
380
- Path.unlink(ctx.anonymized_path)
381
- assert(ctx.anonymized_path is not Path)
395
+ if ctx.file_type == "video":
396
+ target_dir = IMPORT_VIDEO_DIR
397
+ elif ctx.file_type == "report":
398
+ target_dir = IMPORT_REPORT_DIR
399
+ else:
400
+ raise ValueError(f"Unknown file_type in context: {ctx.file_type}")
382
401
 
402
+ target_dir.mkdir(parents=True, exist_ok=True)
403
+ restored_path = shutil.copy2(ctx.sensitive_path, target_dir)
404
+ ctx.original_path = Path(restored_path)
405
+ logger.info("Restored original file for %s to %s", ctx.file_type, ctx.original_path)
383
406
  except Exception as e:
384
- logger.error(f"Error when unlinking anonymized path. {e}")
407
+ logger.error("Error during safety copy / restore of original file: %s", e, exc_info=True)
385
408
  raise
386
-
387
- ctx.anonymized_path = None
388
-
409
+
410
+ # --- 2. Delete anonymized file (best-effort) ---
411
+ if isinstance(ctx.anonymized_path, Path):
412
+ try:
413
+ if ctx.anonymized_path.exists() and isinstance(ctx.anonymized_path, Path):
414
+ ctx.anonymized_path.unlink()
415
+ logger.info("Deleted anonymized file %s", ctx.anonymized_path)
416
+ except Exception as e:
417
+ logger.error("Error when unlinking anonymized path %s: %s", ctx.anonymized_path, e, exc_info=True)
418
+ if ctx.anonymized_path.exists() and isinstance( ctx.anonymized_path, str):
419
+ if isinstance(ctx.current_video, VideoFile):
420
+ p = Path(path_utils.data_paths["anonym_video" / ctx.anonymized_path])
421
+ p.unlink()
422
+ elif isinstance(ctx.current_report, RawPdfFile):
423
+ p = Path(path_utils.data_paths["anonym_report" / ctx.anonymized_path])
424
+ p.unlink()
425
+ if ctx.anonymized_path.exists():
426
+ ctx.anonymized_path.rmdir()
427
+ finally:
428
+ if ctx.anonymized_path.exists():
429
+ raise AssertionError("Anonym file remains after all deletion attempts.")
430
+ ctx.anonymized_path = None
431
+
432
+ # --- 3. Nuke transcoding directory (best-effort) ---
433
+ if not nuke_transcoding_dir():
434
+ logger.warning("Transcoding directory cleanup returned False; there may be leftover files.")
435
+
436
+ # --- 4. Delete sensitive file (best-effort) ---
389
437
  if isinstance(ctx.sensitive_path, Path):
390
438
  try:
391
- Path.unlink(ctx.sensitive_path)
392
- assert(ctx.sensitive_path is not Path)
393
-
439
+ if ctx.sensitive_path.exists():
440
+ ctx.sensitive_path.unlink()
441
+ logger.info("Deleted sensitive file %s", ctx.sensitive_path)
394
442
  except Exception as e:
395
- logger.error(f"Error when unlinking anonymized path. {e}")
443
+ logger.error("Error when unlinking sensitive path %s: %s", ctx.sensitive_path, e, exc_info=True)
444
+ if ctx.sensitive_path.exists() and isinstance( ctx.sensitive_path, str):
445
+ if isinstance(ctx.current_video, VideoFile):
446
+ p = Path(path_utils.data_paths["sensitive_video" / ctx.sensitive_path])
447
+ p.unlink()
448
+ elif isinstance(ctx.current_report, RawPdfFile):
449
+ p = Path(path_utils.data_paths["sensitive_report" / ctx.sensitive_path])
450
+ p.unlink()
451
+ if ctx.sensitive_path.exists():
452
+ ctx.sensitive_path.rmdir()
453
+ finally:
454
+ if ctx.sensitive_path.exists():
455
+ raise AssertionError("Sensitive file remains after all deletion attempts.")
456
+ ctx.sensitive_path = None
396
457
 
397
- raise
398
-
399
- ctx.sensitive_path = None
400
-
458
+
459
+ def nuke_transcoding_dir(
460
+ transcoding_dir: Union[str, Path, None] = None
461
+ ) -> bool:
462
+ """
463
+ Delete all files and subdirectories inside the transcoding directory.
464
+
465
+ Returns:
466
+ True if the directory was either empty / successfully cleaned,
467
+ False if something went wrong (error is logged).
468
+ """
469
+ try:
470
+ if transcoding_dir is None:
471
+ transcoding_dir = path_utils.data_paths["transcoding"]
472
+
473
+ transcoding_dir = Path(transcoding_dir)
474
+
475
+ if not transcoding_dir.exists():
476
+ logger.info("Transcoding dir %s does not exist; nothing to clean.", transcoding_dir)
477
+ return True
478
+
479
+ if not transcoding_dir.is_dir():
480
+ logger.error("Configured transcoding path %s is not a directory.", transcoding_dir)
481
+ return False
482
+
483
+ for entry in transcoding_dir.iterdir():
484
+ try:
485
+ if entry.is_file() or entry.is_symlink():
486
+ entry.unlink()
487
+ elif entry.is_dir():
488
+ shutil.rmtree(entry)
489
+ except Exception as e:
490
+ logger.warning("Failed to remove entry %s in transcoding dir: %s", entry, e)
491
+ # Continue trying to delete other entries
492
+ return True
493
+
494
+ except Exception as e:
495
+ logger.error("Unexpected error while nuking transcoding dir: %s", e, exc_info=True)
496
+ return False
File without changes
@@ -117,6 +117,7 @@ class VideoState(models.Model):
117
117
  return AnonymizationState.STARTED
118
118
  if self.anonymized:
119
119
  return AnonymizationState.ANONYMIZED
120
+
120
121
  return AnonymizationState.NOT_STARTED
121
122
 
122
123
  def mark_processing_not_started(self) -> None:
@@ -31,7 +31,11 @@ def _resolve_ffmpeg_executable() -> Optional[str]:
31
31
  try:
32
32
  from django.conf import settings
33
33
 
34
- env_candidates.extend(getattr(settings, attr) for attr in ("FFMPEG_EXECUTABLE", "FFMPEG_BINARY", "FFMPEG_PATH") if hasattr(settings, attr))
34
+ env_candidates.extend(
35
+ getattr(settings, attr)
36
+ for attr in ("FFMPEG_EXECUTABLE", "FFMPEG_BINARY", "FFMPEG_PATH")
37
+ if hasattr(settings, attr)
38
+ )
35
39
  except Exception:
36
40
  # Django might not be configured for every consumer
37
41
  pass
@@ -83,9 +87,24 @@ def _detect_nvenc_support() -> bool:
83
87
  """
84
88
  try:
85
89
  # Test NVENC availability with a minimal command (minimum size for NVENC)
86
- cmd = ["ffmpeg", "-f", "lavfi", "-i", "testsrc=duration=1:size=256x256:rate=1", "-c:v", "h264_nvenc", "-preset", "p1", "-f", "null", "-"]
90
+ cmd = [
91
+ "ffmpeg",
92
+ "-f",
93
+ "lavfi",
94
+ "-i",
95
+ "testsrc=duration=1:size=256x256:rate=1",
96
+ "-c:v",
97
+ "h264_nvenc",
98
+ "-preset",
99
+ "p1",
100
+ "-f",
101
+ "null",
102
+ "-",
103
+ ]
87
104
 
88
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=15, check=False)
105
+ result = subprocess.run(
106
+ cmd, capture_output=True, text=True, timeout=15, check=False
107
+ )
89
108
 
90
109
  if result.returncode == 0:
91
110
  logger.debug("NVENC h264 encoding test successful")
@@ -141,7 +160,11 @@ def _get_preferred_encoder() -> Dict[str, str]:
141
160
  return _preferred_encoder
142
161
 
143
162
 
144
- def _build_encoder_args(quality_mode: str = "balanced", fallback: bool = False, custom_crf: Optional[int] = None) -> Tuple[List[str], str]:
163
+ def _build_encoder_args(
164
+ quality_mode: str = "balanced",
165
+ fallback: bool = False,
166
+ custom_crf: Optional[int] = None,
167
+ ) -> Tuple[List[str], str]:
145
168
  """
146
169
  Build encoder command arguments based on available hardware and quality requirements.
147
170
 
@@ -207,7 +230,16 @@ def _build_encoder_args(quality_mode: str = "balanced", fallback: bool = False,
207
230
  if custom_crf is not None:
208
231
  quality = str(custom_crf)
209
232
 
210
- return ["-c:v", encoder["name"], encoder["preset_param"], preset, encoder["quality_param"], quality, "-profile:v", "high"], encoder["type"]
233
+ return [
234
+ "-c:v",
235
+ encoder["name"],
236
+ encoder["preset_param"],
237
+ preset,
238
+ encoder["quality_param"],
239
+ quality,
240
+ "-profile:v",
241
+ "high",
242
+ ], encoder["type"]
211
243
 
212
244
 
213
245
  def is_ffmpeg_available() -> bool:
@@ -231,7 +263,9 @@ def check_ffmpeg_availability():
231
263
  True if FFmpeg is available.
232
264
  """
233
265
  if not is_ffmpeg_available():
234
- error_msg = "FFmpeg is not available. Please install it and ensure it's in your PATH."
266
+ error_msg = (
267
+ "FFmpeg is not available. Please install it and ensure it's in your PATH."
268
+ )
235
269
  logger.error(error_msg)
236
270
  raise FileNotFoundError(error_msg)
237
271
  # logger.info("FFmpeg is available.") # Caller can log if needed
@@ -292,9 +326,15 @@ def assemble_video_from_frames( # Renamed from assemble_video
292
326
  if first_frame is None:
293
327
  raise IOError(f"Could not read first frame: {frame_paths[0]}")
294
328
  height, width, _ = first_frame.shape
295
- logger.info("Determined video dimensions from first frame: %dx%d", width, height)
329
+ logger.info(
330
+ "Determined video dimensions from first frame: %dx%d", width, height
331
+ )
296
332
  except Exception as e:
297
- logger.error("Error reading first frame to determine dimensions: %s", e, exc_info=True)
333
+ logger.error(
334
+ "Error reading first frame to determine dimensions: %s",
335
+ e,
336
+ exc_info=True,
337
+ )
298
338
  return None
299
339
 
300
340
  fourcc = cv2.VideoWriter_fourcc(*"mp4v")
@@ -305,7 +345,9 @@ def assemble_video_from_frames( # Renamed from assemble_video
305
345
  logger.error("Could not open video writer for path: %s", output_path)
306
346
  return None
307
347
 
308
- logger.info("Assembling video %s from %d frames...", output_path.name, len(frame_paths))
348
+ logger.info(
349
+ "Assembling video %s from %d frames...", output_path.name, len(frame_paths)
350
+ )
309
351
  try:
310
352
  for frame_path in tqdm(frame_paths, desc=f"Assembling {output_path.name}"):
311
353
  frame = cv2.imread(str(frame_path))
@@ -314,7 +356,9 @@ def assemble_video_from_frames( # Renamed from assemble_video
314
356
  continue
315
357
  # Ensure frame dimensions match - resize if necessary (or log error)
316
358
  if frame.shape[1] != width or frame.shape[0] != height:
317
- logger.warning(f"Frame {frame_path} has dimensions {frame.shape[1]}x{frame.shape[0]}, expected {width}x{height}. Resizing.")
359
+ logger.warning(
360
+ f"Frame {frame_path} has dimensions {frame.shape[1]}x{frame.shape[0]}, expected {width}x{height}. Resizing."
361
+ )
318
362
  frame = cv2.resize(frame, (width, height))
319
363
  video_writer.write(frame)
320
364
  finally:
@@ -364,7 +408,9 @@ def transcode_video(
364
408
  if codec == "auto" or preset == "auto":
365
409
  if force_cpu:
366
410
  # Force CPU encoding
367
- encoder_args, encoder_type = _build_encoder_args(quality_mode, fallback=False, custom_crf=crf)
411
+ encoder_args, encoder_type = _build_encoder_args(
412
+ quality_mode, fallback=False, custom_crf=crf
413
+ )
368
414
  # Override to use CPU encoder
369
415
  encoder_args[1] = "libx264" # Replace encoder name
370
416
  encoder_args[3] = "medium" if preset == "auto" else preset # Replace preset
@@ -372,7 +418,9 @@ def transcode_video(
372
418
  encoder_args[5] = str(crf) # Replace quality value
373
419
  else:
374
420
  # Use automatic hardware detection
375
- encoder_args, encoder_type = _build_encoder_args(quality_mode, fallback=False, custom_crf=crf)
421
+ encoder_args, encoder_type = _build_encoder_args(
422
+ quality_mode, fallback=False, custom_crf=crf
423
+ )
376
424
  else:
377
425
  # Manual codec/preset specification (backward compatibility)
378
426
  encoder_args = [
@@ -402,11 +450,18 @@ def transcode_video(
402
450
  command.extend(extra_args)
403
451
  command.append(str(output_path))
404
452
 
405
- logger.info("Starting transcoding: %s -> %s (using %s)", input_path.name, output_path.name, encoder_type)
453
+ logger.info(
454
+ "Starting transcoding: %s -> %s (using %s)",
455
+ input_path.name,
456
+ output_path.name,
457
+ encoder_type,
458
+ )
406
459
  logger.debug("FFmpeg command: %s", " ".join(command))
407
460
 
408
461
  try:
409
- process = subprocess.Popen(command, stderr=subprocess.PIPE, text=True, universal_newlines=True)
462
+ process = subprocess.Popen(
463
+ command, stderr=subprocess.PIPE, text=True, universal_newlines=True
464
+ )
410
465
 
411
466
  # Progress reporting and error handling
412
467
  stderr_output = ""
@@ -420,32 +475,56 @@ def transcode_video(
420
475
  logger.info("Transcoding finished successfully: %s", output_path)
421
476
  return output_path
422
477
  else:
423
- logger.error("FFmpeg transcoding failed for %s with return code %d.", input_path.name, process.returncode)
478
+ logger.error(
479
+ "FFmpeg transcoding failed for %s with return code %d.",
480
+ input_path.name,
481
+ process.returncode,
482
+ )
424
483
  logger.error("FFmpeg stderr:\n%s", stderr_output)
425
484
 
426
485
  # Try fallback to CPU if NVENC failed
427
486
  if encoder_type == "nvenc" and not force_cpu:
428
487
  logger.warning("NVENC transcoding failed, trying CPU fallback...")
429
- return _transcode_video_fallback(input_path, output_path, audio_codec, audio_bitrate, extra_args, quality_mode, crf)
488
+ return _transcode_video_fallback(
489
+ input_path,
490
+ output_path,
491
+ audio_codec,
492
+ audio_bitrate,
493
+ extra_args,
494
+ quality_mode,
495
+ crf,
496
+ )
430
497
 
431
498
  # Clean up potentially corrupted output file
432
499
  if output_path.exists():
433
500
  try:
434
501
  output_path.unlink()
435
502
  except OSError as e:
436
- logger.error("Failed to delete incomplete output file %s: %s", output_path, e)
503
+ logger.error(
504
+ "Failed to delete incomplete output file %s: %s", output_path, e
505
+ )
437
506
  return None
438
507
 
439
508
  except FileNotFoundError:
440
- logger.error("ffmpeg command not found. Ensure FFmpeg is installed and in the system's PATH.")
509
+ logger.error(
510
+ "ffmpeg command not found. Ensure FFmpeg is installed and in the system's PATH."
511
+ )
441
512
  return None
442
513
  except Exception as e:
443
- logger.error("Error during transcoding of %s: %s", input_path.name, e, exc_info=True)
514
+ logger.error(
515
+ "Error during transcoding of %s: %s", input_path.name, e, exc_info=True
516
+ )
444
517
  return None
445
518
 
446
519
 
447
520
  def _transcode_video_fallback(
448
- input_path: Path, output_path: Path, audio_codec: str, audio_bitrate: str, extra_args: Optional[List[str]], quality_mode: str, custom_crf: Optional[int]
521
+ input_path: Path,
522
+ output_path: Path,
523
+ audio_codec: str,
524
+ audio_bitrate: str,
525
+ extra_args: Optional[List[str]],
526
+ quality_mode: str,
527
+ custom_crf: Optional[int],
449
528
  ) -> Optional[Path]:
450
529
  """
451
530
  Fallback transcoding using CPU encoding.
@@ -464,7 +543,9 @@ def _transcode_video_fallback(
464
543
  """
465
544
  try:
466
545
  # Build CPU encoder arguments
467
- encoder_args, _ = _build_encoder_args(quality_mode, fallback=True, custom_crf=custom_crf)
546
+ encoder_args, _ = _build_encoder_args(
547
+ quality_mode, fallback=True, custom_crf=custom_crf
548
+ )
468
549
  # Force CPU encoder
469
550
  encoder_args[1] = "libx264"
470
551
 
@@ -484,10 +565,14 @@ def _transcode_video_fallback(
484
565
  command.extend(extra_args)
485
566
  command.append(str(output_path))
486
567
 
487
- logger.info("CPU fallback transcoding: %s -> %s", input_path.name, output_path.name)
568
+ logger.info(
569
+ "CPU fallback transcoding: %s -> %s", input_path.name, output_path.name
570
+ )
488
571
  logger.debug("Fallback FFmpeg command: %s", " ".join(command))
489
572
 
490
- process = subprocess.Popen(command, stderr=subprocess.PIPE, text=True, universal_newlines=True)
573
+ process = subprocess.Popen(
574
+ command, stderr=subprocess.PIPE, text=True, universal_newlines=True
575
+ )
491
576
  stderr_output = ""
492
577
  if process.stderr:
493
578
  for line in process.stderr:
@@ -511,7 +596,9 @@ def _transcode_video_fallback(
511
596
  logger.debug("FFmpeg command: %s", " ".join(command))
512
597
 
513
598
  try:
514
- process = subprocess.Popen(command, stderr=subprocess.PIPE, text=True, universal_newlines=True)
599
+ process = subprocess.Popen(
600
+ command, stderr=subprocess.PIPE, text=True, universal_newlines=True
601
+ )
515
602
 
516
603
  # Optional: Progress reporting (can be complex to parse ffmpeg output reliably)
517
604
  # For simplicity, just wait and check the return code
@@ -528,21 +615,31 @@ def _transcode_video_fallback(
528
615
  logger.info("Transcoding finished successfully: %s", output_path)
529
616
  return output_path
530
617
  else:
531
- logger.error("FFmpeg transcoding failed for %s with return code %d.", input_path.name, process.returncode)
618
+ logger.error(
619
+ "FFmpeg transcoding failed for %s with return code %d.",
620
+ input_path.name,
621
+ process.returncode,
622
+ )
532
623
  logger.error("FFmpeg stderr:\n%s", stderr_output)
533
624
  # Clean up potentially corrupted output file
534
625
  if output_path.exists():
535
626
  try:
536
627
  output_path.unlink()
537
628
  except OSError as e:
538
- logger.error("Failed to delete incomplete output file %s: %s", output_path, e)
629
+ logger.error(
630
+ "Failed to delete incomplete output file %s: %s", output_path, e
631
+ )
539
632
  return None
540
633
 
541
634
  except FileNotFoundError:
542
- logger.error("ffmpeg command not found. Ensure FFmpeg is installed and in the system's PATH.")
635
+ logger.error(
636
+ "ffmpeg command not found. Ensure FFmpeg is installed and in the system's PATH."
637
+ )
543
638
  return None
544
639
  except Exception as e:
545
- logger.error("Error during transcoding of %s: %s", input_path.name, e, exc_info=True)
640
+ logger.error(
641
+ "Error during transcoding of %s: %s", input_path.name, e, exc_info=True
642
+ )
546
643
  return None
547
644
 
548
645
 
@@ -561,10 +658,15 @@ def transcode_videofile_if_required(
561
658
  """
562
659
  stream_info = get_stream_info(input_path)
563
660
  if not stream_info or "streams" not in stream_info:
564
- logger.error("Could not get stream info for %s to check if transcoding is required.", input_path)
661
+ logger.error(
662
+ "Could not get stream info for %s to check if transcoding is required.",
663
+ input_path,
664
+ )
565
665
  return None
566
666
 
567
- video_stream = next((s for s in stream_info["streams"] if s.get("codec_type") == "video"), None)
667
+ video_stream = next(
668
+ (s for s in stream_info["streams"] if s.get("codec_type") == "video"), None
669
+ )
568
670
 
569
671
  if not video_stream:
570
672
  logger.error("No video stream found in %s.", input_path)
@@ -573,7 +675,9 @@ def transcode_videofile_if_required(
573
675
  codec_name = video_stream.get("codec_name")
574
676
  pixel_format = video_stream.get("pix_fmt")
575
677
  # Check color range as well, default is usually 'tv' (limited)
576
- color_range = video_stream.get("color_range", "tv") # Default to tv if not specified
678
+ color_range = video_stream.get(
679
+ "color_range", "tv"
680
+ ) # Default to tv if not specified
577
681
 
578
682
  needs_transcoding = False
579
683
  transcode_reason = []
@@ -583,16 +687,25 @@ def transcode_videofile_if_required(
583
687
  transcode_reason.append(reason)
584
688
  needs_transcoding = True
585
689
  # Check both pixel format and color range for yuv420p
586
- if pixel_format != required_pixel_format or (pixel_format == "yuv420p" and color_range != "pc"):
690
+ if pixel_format != required_pixel_format or (
691
+ pixel_format == "yuv420p" and color_range != "pc"
692
+ ):
587
693
  reason = f"Pixel format/color range mismatch (pix_fmt: {pixel_format}, color_range: {color_range} != {required_pixel_format} with color_range=pc)"
588
694
  logger.info("%s for %s. Transcoding required.", reason, input_path.name)
589
695
  transcode_reason.append(reason)
590
696
  needs_transcoding = True
591
697
 
592
698
  if needs_transcoding:
593
- logger.info("Transcoding %s to %s due to: %s", input_path.name, output_path.name, "; ".join(transcode_reason))
699
+ logger.info(
700
+ "Transcoding %s to %s due to: %s",
701
+ input_path.name,
702
+ output_path.name,
703
+ "; ".join(transcode_reason),
704
+ )
594
705
  # Ensure codec and pixel format are set in options if not already present
595
- transcode_options.setdefault("codec", "libx264" if required_codec == "h264" else required_codec)
706
+ transcode_options.setdefault(
707
+ "codec", "libx264" if required_codec == "h264" else required_codec
708
+ )
596
709
  transcode_options.setdefault("extra_args", [])
597
710
 
598
711
  # Ensure pixel format and color range are correctly set in extra_args
@@ -604,11 +717,17 @@ def transcode_videofile_if_required(
604
717
  try:
605
718
  pix_fmt_index = extra_args.index("-pix_fmt")
606
719
  if extra_args[pix_fmt_index + 1] != required_pixel_format:
607
- logger.warning("Overriding existing -pix_fmt '%s' with '%s'", extra_args[pix_fmt_index + 1], required_pixel_format)
720
+ logger.warning(
721
+ "Overriding existing -pix_fmt '%s' with '%s'",
722
+ extra_args[pix_fmt_index + 1],
723
+ required_pixel_format,
724
+ )
608
725
  extra_args[pix_fmt_index + 1] = required_pixel_format
609
726
  except (ValueError, IndexError):
610
727
  # Should not happen if '-pix_fmt' is in extra_args, but handle defensively
611
- logger.error("Error processing existing -pix_fmt argument. Appending required format.")
728
+ logger.error(
729
+ "Error processing existing -pix_fmt argument. Appending required format."
730
+ )
612
731
  extra_args.extend(["-pix_fmt", required_pixel_format])
613
732
 
614
733
  if "-color_range" not in extra_args:
@@ -619,16 +738,24 @@ def transcode_videofile_if_required(
619
738
  try:
620
739
  color_range_index = extra_args.index("-color_range")
621
740
  if extra_args[color_range_index + 1] != "pc":
622
- logger.warning("Overriding existing -color_range '%s' with 'pc'", extra_args[color_range_index + 1])
741
+ logger.warning(
742
+ "Overriding existing -color_range '%s' with 'pc'",
743
+ extra_args[color_range_index + 1],
744
+ )
623
745
  extra_args[color_range_index + 1] = "pc"
624
746
  except (ValueError, IndexError):
625
- logger.error("Error processing existing -color_range argument. Appending 'pc'.")
747
+ logger.error(
748
+ "Error processing existing -color_range argument. Appending 'pc'."
749
+ )
626
750
  extra_args.extend(["-color_range", "pc"])
627
751
 
628
752
  return transcode_video(input_path, output_path, **transcode_options)
629
753
  else:
630
754
  logger.info(
631
- "Video %s already meets requirements (%s, %s, color_range=pc). No transcoding needed.", input_path.name, required_codec, required_pixel_format
755
+ "Video %s already meets requirements (%s, %s, color_range=pc). No transcoding needed.",
756
+ input_path.name,
757
+ required_codec,
758
+ required_pixel_format,
632
759
  )
633
760
  # If no transcoding is needed, should we copy/link or just return the original path?
634
761
  # For simplicity, let's assume the caller handles the file location.
@@ -638,15 +765,27 @@ def transcode_videofile_if_required(
638
765
  try:
639
766
  output_path.parent.mkdir(parents=True, exist_ok=True)
640
767
  shutil.copy2(input_path, output_path)
641
- logger.info("Copied %s to %s as it met requirements.", input_path.name, output_path.name)
768
+ logger.info(
769
+ "Copied %s to %s as it met requirements.",
770
+ input_path.name,
771
+ output_path.name,
772
+ )
642
773
  return output_path
643
774
  except Exception as e:
644
- logger.error("Failed to copy %s to %s: %s", input_path.name, output_path.name, e)
775
+ logger.error(
776
+ "Failed to copy %s to %s: %s", input_path.name, output_path.name, e
777
+ )
645
778
  return None
646
779
  return input_path # Return original path if no copy needed
647
780
 
648
781
 
649
- def extract_frames(video_path: Path, output_dir: Path, quality: int, ext: str = "jpg", fps: Optional[float] = None) -> List[Path]:
782
+ def extract_frames(
783
+ video_path: Path,
784
+ output_dir: Path,
785
+ quality: int,
786
+ ext: str = "jpg",
787
+ fps: Optional[float] = None,
788
+ ) -> List[Path]:
650
789
  """
651
790
  Extracts frames from a video file using FFmpeg.
652
791
 
@@ -702,7 +841,9 @@ def extract_frames(video_path: Path, output_dir: Path, quality: int, ext: str =
702
841
  # Return empty list on error as frames were likely not created correctly
703
842
  return []
704
843
  except Exception as e:
705
- logger.error("An unexpected error occurred during FFmpeg execution: %s", e, exc_info=True)
844
+ logger.error(
845
+ "An unexpected error occurred during FFmpeg execution: %s", e, exc_info=True
846
+ )
706
847
  return []
707
848
 
708
849
  # Collect paths of extracted frames
@@ -743,7 +884,11 @@ def extract_frame_range(
743
884
  RuntimeError: If FFmpeg fails to extract the requested frames.
744
885
  """
745
886
  if start_frame >= end_frame:
746
- logger.warning("extract_frame_range called with start_frame (%d) >= end_frame (%d). No frames to extract.", start_frame, end_frame)
887
+ logger.warning(
888
+ "extract_frame_range called with start_frame (%d) >= end_frame (%d). No frames to extract.",
889
+ start_frame,
890
+ end_frame,
891
+ )
747
892
  return []
748
893
 
749
894
  ffmpeg_executable = _resolve_ffmpeg_executable()
@@ -790,18 +935,30 @@ def extract_frame_range(
790
935
  logger.error("FFmpeg stderr:\n%s", e.stderr)
791
936
  logger.error("FFmpeg stdout:\n%s", e.stdout)
792
937
  # Clean up potentially partially created files in the target directory within the expected range
793
- logger.warning("Attempting cleanup of potentially incomplete frames in %s", output_dir)
938
+ logger.warning(
939
+ "Attempting cleanup of potentially incomplete frames in %s", output_dir
940
+ )
794
941
  for i in range(start_frame, end_frame):
795
942
  potential_file = output_dir / f"frame_{i:07d}.{ext}"
796
943
  if potential_file.exists():
797
944
  try:
798
945
  potential_file.unlink()
799
946
  except OSError as unlink_err:
800
- logger.error("Failed to delete potential frame %s during cleanup: %s", potential_file, unlink_err)
801
- raise RuntimeError(f"FFmpeg frame range extraction failed for {video_path}") from e
947
+ logger.error(
948
+ "Failed to delete potential frame %s during cleanup: %s",
949
+ potential_file,
950
+ unlink_err,
951
+ )
952
+ raise RuntimeError(
953
+ f"FFmpeg frame range extraction failed for {video_path}"
954
+ ) from e
802
955
  except Exception as e:
803
- logger.error("An unexpected error occurred during FFmpeg execution: %s", e, exc_info=True)
804
- raise RuntimeError(f"Unexpected error during FFmpeg frame range extraction for {video_path}") from e
956
+ logger.error(
957
+ "An unexpected error occurred during FFmpeg execution: %s", e, exc_info=True
958
+ )
959
+ raise RuntimeError(
960
+ f"Unexpected error during FFmpeg frame range extraction for {video_path}"
961
+ ) from e
805
962
 
806
963
  # Collect paths of extracted frames matching the pattern and expected range
807
964
  # FFmpeg might create files outside the exact range depending on version/flags,
@@ -813,9 +970,17 @@ def extract_frame_range(
813
970
  extracted_files.append(frame_file)
814
971
  else:
815
972
  # This might happen if ffmpeg fails silently for some frames or if the video ends early.
816
- logger.warning("Expected frame file %s not found after extraction.", frame_file)
817
-
818
- logger.info("Found %d extracted frame files in range [%d, %d) for video %s.", len(extracted_files), start_frame, end_frame, video_path.name)
973
+ logger.warning(
974
+ "Expected frame file %s not found after extraction.", frame_file
975
+ )
976
+
977
+ logger.info(
978
+ "Found %d extracted frame files in range [%d, %d) for video %s.",
979
+ len(extracted_files),
980
+ start_frame,
981
+ end_frame,
982
+ video_path.name,
983
+ )
819
984
  return extracted_files
820
985
 
821
986
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: endoreg-db
3
- Version: 0.8.8.9
3
+ Version: 0.8.9.2
4
4
  Summary: EndoReg Db Django App
5
5
  Project-URL: Homepage, https://info.coloreg.de
6
6
  Project-URL: Repository, https://github.com/wg-lux/endoreg-db
@@ -255,13 +255,12 @@ endoreg_db/import_files/file_storage/__init__.py,sha256=3YkOalkE6R5Y84dPOzhjJuGq
255
255
  endoreg_db/import_files/file_storage/create_report_file.py,sha256=u4nq-FrnhEnrnIyHuPk1ShTwSDxcq8vlikqFJCMdYkQ,2666
256
256
  endoreg_db/import_files/file_storage/create_video_file.py,sha256=OkSJGGdwlUvuM57EXQb4pNo8CuC8UanK_Y9E9jmJtB0,2562
257
257
  endoreg_db/import_files/file_storage/sensitive_meta_storage.py,sha256=1fHENzURgBgqEbAa_xMupS93CbIQj4AC4WUBurzQxjA,1452
258
- endoreg_db/import_files/file_storage/state_management.py,sha256=kl8j27UuI8v0VL9ywE6EMcAjsdyfnhUiKx5LTfvRJDI,13939
258
+ endoreg_db/import_files/file_storage/state_management.py,sha256=1tgVIytpl4Oix8D8slIhDhl7KcfzL03Q3ZTed5OnjrU,18360
259
259
  endoreg_db/import_files/file_storage/storage.py,sha256=ITntL7AyXAovTgE1mtURgZRRBwUTIj2obrho3R-PBxg,1084
260
260
  endoreg_db/import_files/processing/__init__.py,sha256=p4R0j6aC28TNFeboyum2R9pF9P98nvMk-CQsy_0yWtA,220
261
261
  endoreg_db/import_files/processing/sensitive_meta_adapter.py,sha256=BXoiIxfSqKzze_ss6opEaW_C9TnKzwZkeyS-jJl8e_8,1768
262
262
  endoreg_db/import_files/processing/report_processing/report_anonymization.py,sha256=CBJbJ0AEVLuRg27CddN2yhaX53OTZkEBJO2E9lqyukY,3469
263
263
  endoreg_db/import_files/processing/video_processing/video_anonymization.py,sha256=POOkFSXD5rDc8eiYXnxSneXrksRVZmvQemPS4PJLF-0,4116
264
- endoreg_db/import_files/processing/video_processing/video_cleanup_on_error.py,sha256=tUGjGokoM-__gY-qXpTyRgP5ObSmAmdnsyQz9by2KME,4567
265
264
  endoreg_db/import_files/pseudonymization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
266
265
  endoreg_db/import_files/pseudonymization/fake.py,sha256=UAObfuNT4YrUHZX8E53RLp7D3X3bpDs6f0UdoKh-8gE,1347
267
266
  endoreg_db/import_files/pseudonymization/k_anonymity.py,sha256=59d82ljv7cUmTVSA0jK0i_yov2l2yaqkpLPhZQnjGXk,5595
@@ -331,6 +330,7 @@ endoreg_db/mermaid/morphology.md,sha256=9_--hWVwXW1UB1E9od9yddyUFJonm3eCGr986nwp
331
330
  endoreg_db/mermaid/patient_creation.md,sha256=P0U50Pejxn_AATzHTJ3U9iydoEVSnpRjGEUOTRNJrGs,384
332
331
  endoreg_db/mermaid/video_segmentation_annotation.md,sha256=oouo5htDabP8m-W86C6aWXyIxi1A7zAoPqa3o5xr354,536
333
332
  endoreg_db/migrations/0001_initial.py,sha256=nhv_mB0U8cXlGgTF16sMtF4nr6NMmnq9Qn0eplvPOfc,123647
333
+ endoreg_db/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
334
334
  endoreg_db/models/__init__.py,sha256=sVRsrTj4AHjUinFOGjIUtliXzMd5u_IfnS7JWO4FN74,6567
335
335
  endoreg_db/models/upload_job.py,sha256=jWG-FuKp9yllUKrw86C-jPQyhaaLxib-V-i5TTWvKvw,3089
336
336
  endoreg_db/models/utils.py,sha256=mA-lBrwOrhQOZ1EojIquDDLUH2FoH3ZUg9B7i9U9nLw,4420
@@ -527,7 +527,7 @@ endoreg_db/models/state/audit_ledger.py,sha256=Pt8RIGg-6wb6md1Rlr5VhvIJFgc0uYNk7
527
527
  endoreg_db/models/state/label_video_segment.py,sha256=Hv8cE27yn0GOm0t4PlcamzN47GCbE2LYLLDADr7Vm3A,854
528
528
  endoreg_db/models/state/raw_pdf.py,sha256=6U4c6kyLqTh6tfKyrGQSPEQYPgu75-L9LRA55Rm0-QA,8283
529
529
  endoreg_db/models/state/sensitive_meta.py,sha256=fzzBehjJ9mcBJHrRM7y1A868qX76UADeh1mc8tLkzL0,1406
530
- endoreg_db/models/state/video.py,sha256=KziXPTnRrGGpgXtNyDjbzBXldTB3P666t2lSfs9RzeI,9175
530
+ endoreg_db/models/state/video.py,sha256=x1r2QTe9PgRFI3gQj5K2IQW08cLunpjj7OI7QPODSCQ,9184
531
531
  endoreg_db/queries/__init__.py,sha256=7Qp0uKn8VLlerdYABw1p-2xphGyd-hT80O-wNUv043o,117
532
532
  endoreg_db/queries/annotations/__init__.py,sha256=76O3dAIzuSye09VNPGSNPnqPEtgXZcBAGXKdh89y0ts,95
533
533
  endoreg_db/queries/annotations/legacy.py,sha256=KOHWLDf3CLvIT9GpQi3ps4bUi3JDJUhJXH4gvw9T47E,6418
@@ -683,7 +683,7 @@ endoreg_db/utils/requirement_operator_logic/_old/lab_value_operators.py,sha256=t
683
683
  endoreg_db/utils/requirement_operator_logic/_old/model_evaluators.py,sha256=TxLcfYP71nCwFpF5XhVXVAspdl5ydXYJdVSYCoLbLh4,30228
684
684
  endoreg_db/utils/video/__init__.py,sha256=EOAcatQ8bI1f3LhkE2E3YOzmm0FHqulk0O-jjZBgZFg,823
685
685
  endoreg_db/utils/video/extract_frames.py,sha256=Pj9_pyfiwy-CFWiT4qysXn6VLCC-dQ1HpXOyyGqq0zE,3180
686
- endoreg_db/utils/video/ffmpeg_wrapper.py,sha256=FDA-XIYE3CcfKHptE-rs-3ajIVL4YOTZmNJmNgO_AYA,33191
686
+ endoreg_db/utils/video/ffmpeg_wrapper.py,sha256=jwGMhw1C9u16w60N2df6lsYydx7-z0ZjyZUfVJkEjBM,35310
687
687
  endoreg_db/utils/video/names.py,sha256=m268j2Ynt94OYH6dYxeL8gzU5ODtFJD4OmzS7l0nBPU,1449
688
688
  endoreg_db/utils/video/streaming_processor.py,sha256=C-39DtxhSnL7B2cObFE5k829VLXl_Fl0KQFrFP368JA,13747
689
689
  endoreg_db/utils/video/video_splitter.py,sha256=EZEnhNjaUva_9VxjcjScgRSrxsEuifhBjlwIMLX1qaA,3698
@@ -765,7 +765,7 @@ endoreg_db/views/video/video_meta_stats.py,sha256=h8dasBKwTl3havbEz6YciEt3jkt5Wz
765
765
  endoreg_db/views/video/video_processing_history.py,sha256=mhFuS8RG5GV8E-lTtuD0qrq-bIpnUFp8vy9aERfC-J8,770
766
766
  endoreg_db/views/video/video_remove_frames.py,sha256=2FmvNrSPM0fUXiBxINN6vBUUDCqDlBkNcGR3WsLDgKo,1696
767
767
  endoreg_db/views/video/video_stream.py,sha256=_V1Gc11i6CHtc-PNjGMRPzFml4L8rDVcIHEMSNy5rD4,12162
768
- endoreg_db-0.8.8.9.dist-info/METADATA,sha256=DaHWSQys6xh9sOtwC20FVK3oNvz9NWTVZY_EcwHEHII,14852
769
- endoreg_db-0.8.8.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
770
- endoreg_db-0.8.8.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
771
- endoreg_db-0.8.8.9.dist-info/RECORD,,
768
+ endoreg_db-0.8.9.2.dist-info/METADATA,sha256=zvZUsV6EIXoi6KPWdvQFcJjybDIRZgpfU45bMyMh3VM,14852
769
+ endoreg_db-0.8.9.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
770
+ endoreg_db-0.8.9.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
771
+ endoreg_db-0.8.9.2.dist-info/RECORD,,
@@ -1,119 +0,0 @@
1
- # endoreg_db/services/video_processing/video_cleanup_on_error.py
2
- import logging
3
- import shutil
4
- from pathlib import Path
5
- from typing import Any, Dict, MutableSet, Optional
6
-
7
- logger = logging.getLogger(__name__)
8
-
9
-
10
- def cleanup_video_on_error(
11
- *,
12
- current_video: Any,
13
- original_file_path: Optional[str | Path],
14
- processing_context: Dict[str, Any],
15
- ) -> None:
16
- """
17
- Cleanup processing context on error for video imports.
18
-
19
- This is extracted from VideoImportService._cleanup_on_error and kept as
20
- close as possible to the original behavior.
21
- """
22
- try:
23
- if not current_video or not hasattr(current_video, "state"):
24
- # Nothing we can sensibly do here
25
- return
26
-
27
- # Ensure state exists
28
- if current_video.state is None:
29
- try:
30
- current_video.get_or_create_state()
31
- except Exception as e:
32
- logger.warning(
33
- "Video state not found for video %s during error cleanup: %s",
34
- getattr(current_video, "uuid", None),
35
- e,
36
- )
37
- return
38
-
39
- current_video.state = current_video.get_or_create_state()
40
-
41
- # Try to restore original raw file
42
- try:
43
- if original_file_path is not None:
44
- original_path = Path(original_file_path)
45
- if not original_path.exists():
46
- raise AssertionError("Original file path does not exist")
47
-
48
- logger.info("Marked video import as failed in state")
49
- raw_file_path = getattr(getattr(current_video, "raw_file", None), "path", None)
50
- if raw_file_path and original_file_path:
51
- shutil.copy2(str(raw_file_path), str(original_file_path))
52
- else:
53
- logger.warning("Cannot restore original raw file: path is None")
54
- else:
55
- logger.warning("Original file path is None")
56
- except AssertionError:
57
- logger.warning("Original file path does not exist")
58
-
59
- # Reset state flags if processing had started
60
- try:
61
- from endoreg_db.models.state import VideoState # local import to avoid cycles
62
-
63
- if not isinstance(current_video.state, VideoState):
64
- logger.error("Current video state is not a VideoState instance during cleanup")
65
- raise AssertionError
66
-
67
- if processing_context.get("processing_started"):
68
- current_video.state.frames_extracted = False
69
- current_video.state.frames_initialized = False
70
- current_video.state.video_meta_extracted = False
71
- current_video.state.text_meta_extracted = False
72
- current_video.state.save()
73
- except Exception as e:
74
- logger.warning("Error during video error cleanup: %s", e)
75
- except Exception as outer_exc:
76
- logger.warning("Unexpected error in cleanup_video_on_error: %s", outer_exc)
77
-
78
-
79
- def cleanup_video_processing_context(
80
- *,
81
- processing_context: Dict[str, Any],
82
- processed_files: MutableSet[str],
83
- ) -> None:
84
- """
85
- Cleanup processing context and release file lock for video imports.
86
-
87
- Extracted from VideoImportService._cleanup_processing_context.
88
- """
89
- # DEFENSIVE: ensure dict
90
- if processing_context is None:
91
- processing_context = {}
92
-
93
- # Release file lock if it was acquired
94
- try:
95
- lock_context = processing_context.get("_lock_context")
96
- if lock_context is not None:
97
- try:
98
- lock_context.__exit__(None, None, None)
99
- logger.info("Released file lock")
100
- except Exception as e:
101
- logger.warning("Error releasing file lock during context cleanup: %s", e)
102
- except Exception as e:
103
- logger.warning("Error while handling lock release in context cleanup: %s", e)
104
-
105
- # Remove file from processed_files set if processing failed
106
- try:
107
- file_path = processing_context.get("file_path")
108
- anonymization_completed = processing_context.get("anonymization_completed")
109
-
110
- if file_path and not anonymization_completed:
111
- file_path_str = str(file_path)
112
- if file_path_str in processed_files:
113
- processed_files.remove(file_path_str)
114
- logger.info(
115
- "Removed %s from processed files (failed processing)",
116
- file_path_str,
117
- )
118
- except Exception as e:
119
- logger.warning("Error while cleaning processed_files set: %s", e)