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.
- endoreg_db/import_files/file_storage/state_management.py +132 -36
- endoreg_db/migrations/__init__.py +0 -0
- endoreg_db/models/state/video.py +1 -0
- endoreg_db/utils/video/ffmpeg_wrapper.py +217 -52
- {endoreg_db-0.8.8.9.dist-info → endoreg_db-0.8.9.2.dist-info}/METADATA +1 -1
- {endoreg_db-0.8.8.9.dist-info → endoreg_db-0.8.9.2.dist-info}/RECORD +8 -8
- endoreg_db/import_files/processing/video_processing/video_cleanup_on_error.py +0 -119
- {endoreg_db-0.8.8.9.dist-info → endoreg_db-0.8.9.2.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.8.9.dist-info → endoreg_db-0.8.9.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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(
|
|
407
|
+
logger.error("Error during safety copy / restore of original file: %s", e, exc_info=True)
|
|
385
408
|
raise
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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(
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
endoreg_db/models/state/video.py
CHANGED
|
@@ -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(
|
|
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 = [
|
|
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(
|
|
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(
|
|
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 [
|
|
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 =
|
|
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(
|
|
329
|
+
logger.info(
|
|
330
|
+
"Determined video dimensions from first frame: %dx%d", width, height
|
|
331
|
+
)
|
|
296
332
|
except Exception as e:
|
|
297
|
-
logger.error(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.",
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
801
|
-
|
|
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(
|
|
804
|
-
|
|
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(
|
|
817
|
-
|
|
818
|
-
|
|
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
|
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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.
|
|
769
|
-
endoreg_db-0.8.
|
|
770
|
-
endoreg_db-0.8.
|
|
771
|
-
endoreg_db-0.8.
|
|
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)
|
|
File without changes
|
|
File without changes
|