endoreg-db 0.8.9.2__py3-none-any.whl → 0.8.9.10__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/admin.py +10 -5
- endoreg_db/apps.py +4 -7
- endoreg_db/authz/auth.py +1 -0
- endoreg_db/authz/backends.py +1 -1
- endoreg_db/authz/management/commands/list_routes.py +2 -0
- endoreg_db/authz/middleware.py +8 -7
- endoreg_db/authz/permissions.py +21 -10
- endoreg_db/authz/policy.py +14 -19
- endoreg_db/authz/views_auth.py +14 -10
- endoreg_db/codemods/rename_datetime_fields.py +8 -1
- endoreg_db/exceptions.py +5 -2
- endoreg_db/forms/__init__.py +0 -1
- endoreg_db/forms/examination_form.py +4 -3
- endoreg_db/forms/patient_finding_intervention_form.py +30 -8
- endoreg_db/forms/patient_form.py +9 -13
- endoreg_db/forms/questionnaires/__init__.py +1 -1
- endoreg_db/forms/settings/__init__.py +4 -1
- endoreg_db/forms/unit.py +2 -1
- endoreg_db/helpers/count_db.py +17 -14
- endoreg_db/helpers/default_objects.py +2 -1
- endoreg_db/helpers/download_segmentation_model.py +4 -3
- endoreg_db/helpers/interact.py +0 -5
- endoreg_db/helpers/test_video_helper.py +33 -25
- endoreg_db/import_files/__init__.py +1 -1
- endoreg_db/import_files/context/__init__.py +1 -1
- endoreg_db/import_files/context/default_sensitive_meta.py +11 -9
- endoreg_db/import_files/context/ensure_center.py +4 -4
- endoreg_db/import_files/context/file_lock.py +3 -3
- endoreg_db/import_files/context/import_context.py +11 -12
- endoreg_db/import_files/context/validate_directories.py +1 -0
- endoreg_db/import_files/file_storage/create_report_file.py +57 -34
- endoreg_db/import_files/file_storage/create_video_file.py +64 -35
- endoreg_db/import_files/file_storage/sensitive_meta_storage.py +5 -2
- endoreg_db/import_files/file_storage/state_management.py +89 -122
- endoreg_db/import_files/file_storage/storage.py +5 -1
- endoreg_db/import_files/processing/report_processing/report_anonymization.py +24 -19
- endoreg_db/import_files/processing/sensitive_meta_adapter.py +3 -3
- endoreg_db/import_files/processing/video_processing/video_anonymization.py +18 -18
- endoreg_db/import_files/pseudonymization/k_anonymity.py +8 -9
- endoreg_db/import_files/pseudonymization/k_pseudonymity.py +16 -5
- endoreg_db/import_files/report_import_service.py +36 -30
- endoreg_db/import_files/video_import_service.py +27 -23
- endoreg_db/logger_conf.py +56 -40
- endoreg_db/management/__init__.py +1 -1
- endoreg_db/management/commands/__init__.py +1 -1
- endoreg_db/management/commands/check_auth.py +45 -38
- endoreg_db/management/commands/create_model_meta_from_huggingface.py +53 -2
- endoreg_db/management/commands/create_multilabel_model_meta.py +54 -19
- endoreg_db/management/commands/fix_missing_patient_data.py +105 -71
- endoreg_db/management/commands/fix_video_paths.py +75 -54
- endoreg_db/management/commands/import_report.py +1 -3
- endoreg_db/management/commands/list_routes.py +2 -0
- endoreg_db/management/commands/load_ai_model_data.py +8 -2
- endoreg_db/management/commands/load_ai_model_label_data.py +0 -1
- endoreg_db/management/commands/load_center_data.py +3 -3
- endoreg_db/management/commands/load_distribution_data.py +35 -38
- endoreg_db/management/commands/load_endoscope_data.py +0 -3
- endoreg_db/management/commands/load_examination_data.py +20 -4
- endoreg_db/management/commands/load_finding_data.py +18 -3
- endoreg_db/management/commands/load_gender_data.py +17 -24
- endoreg_db/management/commands/load_green_endoscopy_wuerzburg_data.py +95 -85
- endoreg_db/management/commands/load_information_source.py +0 -3
- endoreg_db/management/commands/load_lab_value_data.py +14 -3
- endoreg_db/management/commands/load_legacy_data.py +303 -0
- endoreg_db/management/commands/load_name_data.py +1 -2
- endoreg_db/management/commands/load_pdf_type_data.py +4 -8
- endoreg_db/management/commands/load_profession_data.py +0 -1
- endoreg_db/management/commands/load_report_reader_flag_data.py +0 -4
- endoreg_db/management/commands/load_requirement_data.py +6 -2
- endoreg_db/management/commands/load_unit_data.py +0 -4
- endoreg_db/management/commands/load_user_groups.py +5 -7
- endoreg_db/management/commands/model_input.py +169 -0
- endoreg_db/management/commands/register_ai_model.py +22 -16
- endoreg_db/management/commands/setup_endoreg_db.py +110 -32
- endoreg_db/management/commands/storage_management.py +14 -8
- endoreg_db/management/commands/summarize_db_content.py +154 -63
- endoreg_db/management/commands/train_image_multilabel_model.py +144 -0
- endoreg_db/management/commands/validate_video_files.py +82 -50
- endoreg_db/management/commands/video_validation.py +4 -6
- endoreg_db/migrations/0001_initial.py +112 -63
- endoreg_db/models/__init__.py +8 -0
- endoreg_db/models/administration/ai/active_model.py +5 -5
- endoreg_db/models/administration/ai/ai_model.py +41 -18
- endoreg_db/models/administration/ai/model_type.py +1 -0
- endoreg_db/models/administration/case/case.py +22 -22
- endoreg_db/models/administration/center/__init__.py +5 -5
- endoreg_db/models/administration/center/center.py +6 -2
- endoreg_db/models/administration/center/center_resource.py +18 -4
- endoreg_db/models/administration/center/center_shift.py +3 -1
- endoreg_db/models/administration/center/center_waste.py +6 -2
- endoreg_db/models/administration/person/__init__.py +1 -1
- endoreg_db/models/administration/person/employee/__init__.py +1 -1
- endoreg_db/models/administration/person/employee/employee_type.py +3 -1
- endoreg_db/models/administration/person/examiner/__init__.py +1 -1
- endoreg_db/models/administration/person/examiner/examiner.py +10 -2
- endoreg_db/models/administration/person/names/first_name.py +6 -4
- endoreg_db/models/administration/person/names/last_name.py +4 -3
- endoreg_db/models/administration/person/patient/__init__.py +1 -1
- endoreg_db/models/administration/person/patient/patient.py +0 -1
- endoreg_db/models/administration/person/patient/patient_external_id.py +0 -1
- endoreg_db/models/administration/person/person.py +1 -1
- endoreg_db/models/administration/product/__init__.py +7 -6
- endoreg_db/models/administration/product/product.py +6 -2
- endoreg_db/models/administration/product/product_group.py +9 -7
- endoreg_db/models/administration/product/product_material.py +9 -2
- endoreg_db/models/administration/product/reference_product.py +64 -15
- endoreg_db/models/administration/qualification/qualification.py +3 -1
- endoreg_db/models/administration/shift/shift.py +3 -1
- endoreg_db/models/administration/shift/shift_type.py +12 -4
- endoreg_db/models/aidataset/__init__.py +5 -0
- endoreg_db/models/aidataset/aidataset.py +193 -0
- endoreg_db/models/label/__init__.py +1 -1
- endoreg_db/models/label/label.py +10 -2
- endoreg_db/models/label/label_set.py +3 -1
- endoreg_db/models/label/label_video_segment/_create_from_video.py +6 -2
- endoreg_db/models/label/label_video_segment/label_video_segment.py +148 -44
- endoreg_db/models/media/__init__.py +12 -5
- endoreg_db/models/media/frame/__init__.py +1 -1
- endoreg_db/models/media/frame/frame.py +34 -8
- endoreg_db/models/media/pdf/__init__.py +2 -1
- endoreg_db/models/media/pdf/raw_pdf.py +11 -4
- endoreg_db/models/media/pdf/report_file.py +6 -2
- endoreg_db/models/media/pdf/report_reader/__init__.py +3 -3
- endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +15 -5
- endoreg_db/models/media/video/create_from_file.py +20 -41
- endoreg_db/models/media/video/pipe_1.py +75 -30
- endoreg_db/models/media/video/pipe_2.py +37 -12
- endoreg_db/models/media/video/video_file.py +36 -24
- endoreg_db/models/media/video/video_file_ai.py +235 -70
- endoreg_db/models/media/video/video_file_anonymize.py +240 -65
- endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -1
- endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +3 -1
- endoreg_db/models/media/video/video_file_frames/_delete_frames.py +30 -9
- endoreg_db/models/media/video/video_file_frames/_extract_frames.py +95 -29
- endoreg_db/models/media/video/video_file_frames/_get_frame.py +13 -3
- endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -1
- endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +15 -3
- endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +15 -3
- endoreg_db/models/media/video/video_file_frames/_get_frames.py +7 -2
- endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +109 -23
- endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +111 -27
- endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +46 -13
- endoreg_db/models/media/video/video_file_io.py +85 -33
- endoreg_db/models/media/video/video_file_meta/__init__.py +6 -6
- endoreg_db/models/media/video/video_file_meta/get_crop_template.py +17 -4
- endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +28 -7
- endoreg_db/models/media/video/video_file_meta/get_fps.py +46 -13
- endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +81 -20
- endoreg_db/models/media/video/video_file_meta/text_meta.py +61 -20
- endoreg_db/models/media/video/video_file_meta/video_meta.py +40 -12
- endoreg_db/models/media/video/video_file_segments.py +118 -27
- endoreg_db/models/media/video/video_metadata.py +25 -6
- endoreg_db/models/media/video/video_processing.py +54 -15
- endoreg_db/models/medical/__init__.py +3 -13
- endoreg_db/models/medical/contraindication/__init__.py +3 -1
- endoreg_db/models/medical/disease.py +18 -6
- endoreg_db/models/medical/event.py +6 -2
- endoreg_db/models/medical/examination/__init__.py +5 -1
- endoreg_db/models/medical/examination/examination.py +22 -6
- endoreg_db/models/medical/examination/examination_indication.py +23 -7
- endoreg_db/models/medical/examination/examination_time.py +6 -2
- endoreg_db/models/medical/finding/__init__.py +3 -1
- endoreg_db/models/medical/finding/finding.py +37 -12
- endoreg_db/models/medical/finding/finding_classification.py +27 -8
- endoreg_db/models/medical/finding/finding_intervention.py +19 -6
- endoreg_db/models/medical/finding/finding_type.py +3 -1
- endoreg_db/models/medical/hardware/__init__.py +1 -1
- endoreg_db/models/medical/hardware/endoscope.py +14 -2
- endoreg_db/models/medical/laboratory/__init__.py +1 -1
- endoreg_db/models/medical/laboratory/lab_value.py +139 -39
- endoreg_db/models/medical/medication/__init__.py +7 -3
- endoreg_db/models/medical/medication/medication.py +3 -1
- endoreg_db/models/medical/medication/medication_indication.py +3 -1
- endoreg_db/models/medical/medication/medication_indication_type.py +11 -3
- endoreg_db/models/medical/medication/medication_intake_time.py +3 -1
- endoreg_db/models/medical/medication/medication_schedule.py +3 -1
- endoreg_db/models/medical/patient/__init__.py +2 -10
- endoreg_db/models/medical/patient/medication_examples.py +3 -14
- endoreg_db/models/medical/patient/patient_disease.py +17 -5
- endoreg_db/models/medical/patient/patient_event.py +12 -4
- endoreg_db/models/medical/patient/patient_examination.py +52 -15
- endoreg_db/models/medical/patient/patient_examination_indication.py +15 -4
- endoreg_db/models/medical/patient/patient_finding.py +105 -29
- endoreg_db/models/medical/patient/patient_finding_classification.py +41 -12
- endoreg_db/models/medical/patient/patient_finding_intervention.py +11 -3
- endoreg_db/models/medical/patient/patient_lab_sample.py +6 -2
- endoreg_db/models/medical/patient/patient_lab_value.py +42 -10
- endoreg_db/models/medical/patient/patient_medication.py +25 -7
- endoreg_db/models/medical/patient/patient_medication_schedule.py +34 -10
- endoreg_db/models/metadata/model_meta.py +40 -12
- endoreg_db/models/metadata/model_meta_logic.py +51 -16
- endoreg_db/models/metadata/sensitive_meta.py +65 -28
- endoreg_db/models/metadata/sensitive_meta_logic.py +28 -26
- endoreg_db/models/metadata/video_meta.py +146 -39
- endoreg_db/models/metadata/video_prediction_logic.py +70 -21
- endoreg_db/models/metadata/video_prediction_meta.py +80 -27
- endoreg_db/models/operation_log.py +63 -0
- endoreg_db/models/other/__init__.py +10 -10
- endoreg_db/models/other/distribution/__init__.py +9 -7
- endoreg_db/models/other/distribution/base_value_distribution.py +3 -1
- endoreg_db/models/other/distribution/date_value_distribution.py +19 -5
- endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +3 -1
- endoreg_db/models/other/distribution/numeric_value_distribution.py +34 -9
- endoreg_db/models/other/emission/__init__.py +1 -1
- endoreg_db/models/other/emission/emission_factor.py +9 -3
- endoreg_db/models/other/information_source.py +15 -5
- endoreg_db/models/other/material.py +3 -1
- endoreg_db/models/other/transport_route.py +3 -1
- endoreg_db/models/other/unit.py +6 -2
- endoreg_db/models/report/report.py +0 -1
- endoreg_db/models/requirement/requirement.py +84 -27
- endoreg_db/models/requirement/requirement_error.py +5 -6
- endoreg_db/models/requirement/requirement_evaluation/__init__.py +1 -1
- endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +8 -8
- endoreg_db/models/requirement/requirement_evaluation/get_values.py +3 -3
- endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +24 -8
- endoreg_db/models/requirement/requirement_operator.py +28 -8
- endoreg_db/models/requirement/requirement_set.py +34 -11
- endoreg_db/models/state/__init__.py +1 -0
- endoreg_db/models/state/audit_ledger.py +9 -2
- endoreg_db/models/{media → state}/processing_history/__init__.py +1 -3
- endoreg_db/models/state/processing_history/processing_history.py +136 -0
- endoreg_db/models/state/raw_pdf.py +0 -1
- endoreg_db/models/state/video.py +2 -4
- endoreg_db/models/utils.py +4 -2
- endoreg_db/queries/__init__.py +2 -6
- endoreg_db/queries/annotations/__init__.py +1 -3
- endoreg_db/queries/annotations/legacy.py +37 -26
- endoreg_db/root_urls.py +3 -4
- endoreg_db/schemas/examination_evaluation.py +3 -0
- endoreg_db/serializers/Frames_NICE_and_PARIS_classifications.py +249 -163
- endoreg_db/serializers/__init__.py +2 -8
- endoreg_db/serializers/administration/__init__.py +1 -2
- endoreg_db/serializers/administration/ai/__init__.py +0 -1
- endoreg_db/serializers/administration/ai/active_model.py +3 -1
- endoreg_db/serializers/administration/ai/ai_model.py +5 -3
- endoreg_db/serializers/administration/ai/model_type.py +3 -1
- endoreg_db/serializers/administration/center.py +7 -2
- endoreg_db/serializers/administration/gender.py +4 -2
- endoreg_db/serializers/anonymization.py +13 -13
- endoreg_db/serializers/evaluation/examination_evaluation.py +0 -1
- endoreg_db/serializers/examination/__init__.py +1 -1
- endoreg_db/serializers/examination/base.py +12 -13
- endoreg_db/serializers/examination/dropdown.py +6 -7
- endoreg_db/serializers/examination_serializer.py +3 -6
- endoreg_db/serializers/finding/__init__.py +1 -1
- endoreg_db/serializers/finding/finding.py +14 -7
- endoreg_db/serializers/finding_classification/__init__.py +3 -3
- endoreg_db/serializers/finding_classification/choice.py +3 -3
- endoreg_db/serializers/finding_classification/classification.py +2 -4
- endoreg_db/serializers/label_video_segment/__init__.py +5 -3
- endoreg_db/serializers/{label → label_video_segment}/image_classification_annotation.py +5 -5
- endoreg_db/serializers/label_video_segment/label/__init__.py +6 -0
- endoreg_db/serializers/{label → label_video_segment/label}/label.py +1 -1
- endoreg_db/serializers/label_video_segment/label_video_segment.py +338 -228
- endoreg_db/serializers/meta/__init__.py +1 -2
- endoreg_db/serializers/meta/sensitive_meta_detail.py +28 -13
- endoreg_db/serializers/meta/sensitive_meta_update.py +51 -46
- endoreg_db/serializers/meta/sensitive_meta_verification.py +19 -16
- endoreg_db/serializers/misc/__init__.py +2 -2
- endoreg_db/serializers/misc/file_overview.py +11 -7
- endoreg_db/serializers/misc/stats.py +10 -8
- endoreg_db/serializers/misc/translatable_field_mix_in.py +6 -6
- endoreg_db/serializers/misc/upload_job.py +32 -29
- endoreg_db/serializers/patient/__init__.py +2 -1
- endoreg_db/serializers/patient/patient.py +32 -15
- endoreg_db/serializers/patient/patient_dropdown.py +11 -3
- endoreg_db/serializers/patient_examination/__init__.py +1 -1
- endoreg_db/serializers/patient_examination/patient_examination.py +67 -40
- endoreg_db/serializers/patient_finding/__init__.py +1 -1
- endoreg_db/serializers/patient_finding/patient_finding.py +2 -1
- endoreg_db/serializers/patient_finding/patient_finding_classification.py +17 -9
- endoreg_db/serializers/patient_finding/patient_finding_detail.py +26 -17
- endoreg_db/serializers/patient_finding/patient_finding_intervention.py +7 -5
- endoreg_db/serializers/patient_finding/patient_finding_list.py +10 -11
- endoreg_db/serializers/patient_finding/patient_finding_write.py +36 -27
- endoreg_db/serializers/pdf/__init__.py +1 -3
- endoreg_db/serializers/requirements/requirement_schema.py +1 -6
- endoreg_db/serializers/sensitive_meta_serializer.py +100 -81
- endoreg_db/serializers/video/__init__.py +2 -2
- endoreg_db/serializers/video/{segmentation.py → video_file.py} +66 -47
- endoreg_db/serializers/video/video_file_brief.py +6 -2
- endoreg_db/serializers/video/video_file_detail.py +36 -23
- endoreg_db/serializers/video/video_file_list.py +4 -2
- endoreg_db/serializers/video/video_processing_history.py +54 -50
- endoreg_db/services/__init__.py +1 -1
- endoreg_db/services/anonymization.py +2 -2
- endoreg_db/services/examination_evaluation.py +40 -17
- endoreg_db/services/model_meta_from_hf.py +76 -0
- endoreg_db/services/polling_coordinator.py +101 -70
- endoreg_db/services/pseudonym_service.py +27 -22
- endoreg_db/services/report_import.py +6 -3
- endoreg_db/services/segment_sync.py +75 -59
- endoreg_db/services/video_import.py +6 -7
- endoreg_db/urls/__init__.py +2 -2
- endoreg_db/urls/ai.py +7 -25
- endoreg_db/urls/anonymization.py +61 -15
- endoreg_db/urls/auth.py +4 -4
- endoreg_db/urls/classification.py +4 -9
- endoreg_db/urls/examination.py +27 -18
- endoreg_db/urls/media.py +27 -34
- endoreg_db/urls/patient.py +11 -7
- endoreg_db/urls/requirements.py +3 -1
- endoreg_db/urls/root_urls.py +2 -3
- endoreg_db/urls/stats.py +24 -16
- endoreg_db/urls/upload.py +3 -11
- endoreg_db/utils/__init__.py +14 -15
- endoreg_db/utils/ai/__init__.py +1 -1
- endoreg_db/utils/ai/data_loader_for_model_input.py +262 -0
- endoreg_db/utils/ai/data_loader_for_model_training.py +262 -0
- endoreg_db/utils/ai/get.py +2 -1
- endoreg_db/utils/ai/inference_dataset.py +14 -15
- endoreg_db/utils/ai/model_training/config.py +117 -0
- endoreg_db/utils/ai/model_training/dataset.py +74 -0
- endoreg_db/utils/ai/model_training/losses.py +68 -0
- endoreg_db/utils/ai/model_training/metrics.py +78 -0
- endoreg_db/utils/ai/model_training/model_backbones.py +155 -0
- endoreg_db/utils/ai/model_training/model_gastronet_resnet.py +118 -0
- endoreg_db/utils/ai/model_training/trainer_gastronet_multilabel.py +771 -0
- endoreg_db/utils/ai/multilabel_classification_net.py +21 -6
- endoreg_db/utils/ai/predict.py +4 -4
- endoreg_db/utils/ai/preprocess.py +19 -11
- endoreg_db/utils/calc_duration_seconds.py +4 -4
- endoreg_db/utils/case_generator/lab_sample_factory.py +3 -4
- endoreg_db/utils/check_video_files.py +74 -47
- endoreg_db/utils/cropping.py +10 -9
- endoreg_db/utils/dataloader.py +11 -3
- endoreg_db/utils/dates.py +3 -4
- endoreg_db/utils/defaults/set_default_center.py +7 -6
- endoreg_db/utils/env.py +6 -2
- endoreg_db/utils/extract_specific_frames.py +24 -9
- endoreg_db/utils/file_operations.py +30 -18
- endoreg_db/utils/fix_video_path_direct.py +57 -41
- endoreg_db/utils/frame_anonymization_utils.py +157 -157
- endoreg_db/utils/hashs.py +3 -18
- endoreg_db/utils/links/requirement_link.py +96 -52
- endoreg_db/utils/ocr.py +30 -25
- endoreg_db/utils/operation_log.py +61 -0
- endoreg_db/utils/parse_and_generate_yaml.py +12 -13
- endoreg_db/utils/paths.py +6 -6
- endoreg_db/utils/permissions.py +40 -24
- endoreg_db/utils/pipelines/process_video_dir.py +50 -26
- endoreg_db/utils/product/sum_emissions.py +5 -3
- endoreg_db/utils/product/sum_weights.py +4 -2
- endoreg_db/utils/pydantic_models/__init__.py +3 -4
- endoreg_db/utils/requirement_operator_logic/_old/lab_value_operators.py +207 -107
- endoreg_db/utils/requirement_operator_logic/_old/model_evaluators.py +252 -65
- endoreg_db/utils/requirement_operator_logic/new_operator_logic.py +27 -10
- endoreg_db/utils/setup_config.py +21 -5
- endoreg_db/utils/storage.py +3 -1
- endoreg_db/utils/translation.py +19 -15
- endoreg_db/utils/uuid.py +1 -0
- endoreg_db/utils/validate_endo_roi.py +12 -4
- endoreg_db/utils/validate_subcategory_dict.py +26 -24
- endoreg_db/utils/validate_video_detailed.py +207 -149
- endoreg_db/utils/video/__init__.py +7 -3
- endoreg_db/utils/video/extract_frames.py +30 -18
- endoreg_db/utils/video/names.py +11 -6
- endoreg_db/utils/video/streaming_processor.py +175 -101
- endoreg_db/utils/video/video_splitter.py +30 -19
- endoreg_db/views/Frames_NICE_and_PARIS_classifications_views.py +59 -50
- endoreg_db/views/__init__.py +0 -20
- endoreg_db/views/anonymization/__init__.py +6 -2
- endoreg_db/views/anonymization/media_management.py +2 -6
- endoreg_db/views/anonymization/overview.py +34 -1
- endoreg_db/views/anonymization/validate.py +79 -18
- endoreg_db/views/auth/__init__.py +1 -1
- endoreg_db/views/auth/keycloak.py +16 -14
- endoreg_db/views/examination/__init__.py +12 -15
- endoreg_db/views/examination/examination.py +5 -5
- endoreg_db/views/examination/examination_manifest_cache.py +5 -5
- endoreg_db/views/examination/get_finding_classification_choices.py +8 -5
- endoreg_db/views/examination/get_finding_classifications.py +9 -7
- endoreg_db/views/examination/get_findings.py +8 -10
- endoreg_db/views/examination/get_instruments.py +3 -2
- endoreg_db/views/examination/get_interventions.py +1 -1
- endoreg_db/views/finding/__init__.py +2 -2
- endoreg_db/views/finding/finding.py +58 -54
- endoreg_db/views/finding/get_classifications.py +1 -1
- endoreg_db/views/finding/get_interventions.py +1 -1
- endoreg_db/views/finding_classification/__init__.py +5 -5
- endoreg_db/views/finding_classification/finding_classification.py +5 -6
- endoreg_db/views/finding_classification/get_classification_choices.py +3 -4
- endoreg_db/views/media/__init__.py +13 -13
- endoreg_db/views/media/pdf_media.py +9 -9
- endoreg_db/views/media/sensitive_metadata.py +10 -7
- endoreg_db/views/media/video_media.py +4 -4
- endoreg_db/views/meta/__init__.py +1 -1
- endoreg_db/views/meta/sensitive_meta_list.py +20 -22
- endoreg_db/views/meta/sensitive_meta_verification.py +14 -11
- endoreg_db/views/misc/__init__.py +6 -34
- endoreg_db/views/misc/center.py +2 -1
- endoreg_db/views/misc/csrf.py +2 -1
- endoreg_db/views/misc/gender.py +2 -1
- endoreg_db/views/misc/stats.py +141 -106
- endoreg_db/views/patient/__init__.py +1 -3
- endoreg_db/views/patient/patient.py +141 -99
- endoreg_db/views/patient_examination/__init__.py +5 -5
- endoreg_db/views/patient_examination/patient_examination.py +43 -42
- endoreg_db/views/patient_examination/patient_examination_create.py +10 -15
- endoreg_db/views/patient_examination/patient_examination_detail.py +12 -15
- endoreg_db/views/patient_examination/patient_examination_list.py +21 -17
- endoreg_db/views/patient_examination/video.py +114 -80
- endoreg_db/views/patient_finding/__init__.py +1 -1
- endoreg_db/views/patient_finding/patient_finding.py +17 -10
- endoreg_db/views/patient_finding/patient_finding_optimized.py +127 -95
- endoreg_db/views/patient_finding_classification/__init__.py +1 -1
- endoreg_db/views/patient_finding_classification/pfc_create.py +35 -27
- endoreg_db/views/report/reimport.py +1 -1
- endoreg_db/views/report/report_stream.py +5 -8
- endoreg_db/views/requirement/__init__.py +2 -1
- endoreg_db/views/requirement/evaluate.py +7 -9
- endoreg_db/views/requirement/lookup.py +2 -3
- endoreg_db/views/requirement/lookup_store.py +0 -1
- endoreg_db/views/requirement/requirement_utils.py +2 -4
- endoreg_db/views/stats/__init__.py +4 -4
- endoreg_db/views/stats/stats_views.py +152 -115
- endoreg_db/views/video/__init__.py +18 -27
- endoreg_db/views/{ai → video/ai}/__init__.py +2 -2
- endoreg_db/views/{ai → video/ai}/label.py +20 -16
- endoreg_db/views/video/correction.py +5 -6
- endoreg_db/views/video/reimport.py +134 -99
- endoreg_db/views/video/segments_crud.py +134 -44
- endoreg_db/views/video/video_apply_mask.py +13 -12
- endoreg_db/views/video/video_correction.py +2 -1
- endoreg_db/views/video/video_download_processed.py +15 -15
- endoreg_db/views/video/video_meta_stats.py +7 -6
- endoreg_db/views/video/video_processing_history.py +3 -2
- endoreg_db/views/video/video_remove_frames.py +13 -12
- endoreg_db/views/video/video_stream.py +110 -82
- {endoreg_db-0.8.9.2.dist-info → endoreg_db-0.8.9.10.dist-info}/METADATA +9 -3
- {endoreg_db-0.8.9.2.dist-info → endoreg_db-0.8.9.10.dist-info}/RECORD +434 -431
- endoreg_db/management/commands/import_fallback_video.py +0 -203
- endoreg_db/management/commands/import_video.py +0 -422
- endoreg_db/management/commands/import_video_with_classification.py +0 -367
- endoreg_db/models/media/processing_history/processing_history.py +0 -96
- endoreg_db/serializers/label/__init__.py +0 -7
- endoreg_db/serializers/label_video_segment/_lvs_create.py +0 -149
- endoreg_db/serializers/label_video_segment/_lvs_update.py +0 -138
- endoreg_db/serializers/label_video_segment/_lvs_validate.py +0 -149
- endoreg_db/serializers/label_video_segment/label_video_segment_annotation.py +0 -99
- endoreg_db/serializers/label_video_segment/label_video_segment_update.py +0 -163
- endoreg_db/services/__old/pdf_import.py +0 -1487
- endoreg_db/services/__old/video_import.py +0 -1306
- endoreg_db/tasks/upload_tasks.py +0 -216
- endoreg_db/tasks/video_ingest.py +0 -161
- endoreg_db/tasks/video_processing_tasks.py +0 -327
- endoreg_db/views/misc/translation.py +0 -182
- {endoreg_db-0.8.9.2.dist-info → endoreg_db-0.8.9.10.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.9.2.dist-info → endoreg_db-0.8.9.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import shutil
|
|
3
2
|
from pathlib import Path
|
|
4
|
-
from
|
|
5
|
-
|
|
3
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
4
|
+
|
|
6
5
|
from django.db import transaction
|
|
6
|
+
from icecream import ic
|
|
7
7
|
from tqdm import tqdm
|
|
8
|
-
from typing import TYPE_CHECKING, Union, List, Optional
|
|
9
8
|
|
|
10
9
|
if TYPE_CHECKING:
|
|
11
10
|
from ...models.media import VideoFile
|
|
12
11
|
|
|
13
|
-
from django.core.files import File
|
|
14
12
|
import io
|
|
13
|
+
|
|
14
|
+
from django.core.files import File
|
|
15
|
+
|
|
15
16
|
from .ffmpeg_wrapper import extract_frames as ffmpeg_extract_frames
|
|
16
17
|
|
|
17
18
|
|
|
@@ -28,27 +29,33 @@ def prepare_bulk_frames(frame_paths: List[Path]):
|
|
|
28
29
|
yield frame_number, file_obj
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
def extract_frames(
|
|
32
|
+
def extract_frames(
|
|
33
|
+
video_path: Path,
|
|
34
|
+
output_dir: Path,
|
|
35
|
+
quality: int,
|
|
36
|
+
ext: str = "jpg",
|
|
37
|
+
fps: Optional[float] = None,
|
|
38
|
+
) -> List[Path]:
|
|
32
39
|
"""Extracts frames from a video file using ffmpeg_wrapper."""
|
|
33
40
|
# Ensure output directory exists
|
|
34
41
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
35
42
|
return ffmpeg_extract_frames(video_path, output_dir, quality, ext, fps)
|
|
36
43
|
|
|
37
44
|
|
|
38
|
-
def initialize_frame_objects(
|
|
39
|
-
video: "VideoFile", extracted_paths: List[Path]
|
|
40
|
-
):
|
|
45
|
+
def initialize_frame_objects(video: "VideoFile", extracted_paths: List[Path]):
|
|
41
46
|
"""
|
|
42
47
|
Initialize frame objects for the extracted frames and update state.
|
|
43
48
|
"""
|
|
44
49
|
state = video.get_or_create_state()
|
|
45
50
|
# Check state before proceeding
|
|
46
51
|
if state.frames_initialized:
|
|
47
|
-
ic(f"Frames already initialized for video {video.
|
|
52
|
+
ic(f"Frames already initialized for video {video.video_hash}, skipping.")
|
|
48
53
|
return
|
|
49
54
|
|
|
50
55
|
if not extracted_paths:
|
|
51
|
-
ic(
|
|
56
|
+
ic(
|
|
57
|
+
f"No extracted paths provided for video {video.video_hash}, cannot initialize frames."
|
|
58
|
+
)
|
|
52
59
|
return
|
|
53
60
|
|
|
54
61
|
video.frame_count = len(extracted_paths)
|
|
@@ -58,13 +65,19 @@ def initialize_frame_objects(
|
|
|
58
65
|
# Prepare frame data (relative paths for storage)
|
|
59
66
|
frame_dir = video.get_frame_dir_path()
|
|
60
67
|
if not frame_dir:
|
|
61
|
-
|
|
68
|
+
raise ValueError(f"Frame directory not set for video {video.video_hash}")
|
|
62
69
|
|
|
63
|
-
storage_base_path = Path(
|
|
70
|
+
storage_base_path = Path(
|
|
71
|
+
video._meta.get_field("raw_file").storage.location
|
|
72
|
+
) # Get storage root
|
|
64
73
|
|
|
65
74
|
for i, path in tqdm(enumerate(extracted_paths, start=1)):
|
|
66
|
-
frame_number =
|
|
67
|
-
|
|
75
|
+
frame_number = (
|
|
76
|
+
int(path.stem.split("_")[1]) - 1
|
|
77
|
+
) # Assuming frame_0000001.jpg is frame_number 0
|
|
78
|
+
relative_path = path.relative_to(
|
|
79
|
+
storage_base_path
|
|
80
|
+
).as_posix() # Path relative to MEDIA_ROOT
|
|
68
81
|
|
|
69
82
|
# Create Frame instance (without saving yet)
|
|
70
83
|
frame_obj_instance = video.create_frame_object(
|
|
@@ -83,6 +96,5 @@ def initialize_frame_objects(
|
|
|
83
96
|
|
|
84
97
|
# Update state and save VideoFile (to save frame_count)
|
|
85
98
|
state.frames_initialized = True
|
|
86
|
-
state.save(update_fields=[
|
|
87
|
-
video.save(update_fields=[
|
|
88
|
-
|
|
99
|
+
state.save(update_fields=["frames_initialized"])
|
|
100
|
+
video.save(update_fields=["frame_count"]) # Save frame_count on VideoFile
|
endoreg_db/utils/video/names.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
def get_video_key(
|
|
5
|
+
examination_alias: str, content: str, is_anonymous: bool = False
|
|
6
|
+
) -> str:
|
|
4
7
|
"""
|
|
5
8
|
Generates a video key based on the examination alias, content, and anonymity status.
|
|
6
9
|
"""
|
|
@@ -8,8 +11,9 @@ def get_video_key(examination_alias:str, content:str, is_anonymous:bool=False) -
|
|
|
8
11
|
return f"{examination_alias}-{content}-anonymous"
|
|
9
12
|
else:
|
|
10
13
|
return f"{examination_alias}-{content}-non_anonymous"
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def identify_video_key(video_key: str) -> str:
|
|
13
17
|
"""
|
|
14
18
|
Identifies the video key based on the provided string.
|
|
15
19
|
"""
|
|
@@ -22,7 +26,9 @@ def identify_video_key(video_key:str) -> str:
|
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
def get_video_key_regex_by_examination_alias(
|
|
25
|
-
examination_alias:Optional[str]=
|
|
29
|
+
examination_alias: Optional[str] = None,
|
|
30
|
+
content: Optional[str] = None,
|
|
31
|
+
is_anonymous: Optional[bool] = None,
|
|
26
32
|
):
|
|
27
33
|
"""
|
|
28
34
|
Generates a regex pattern to match video keys based on examination alias, content, and anonymity status.
|
|
@@ -37,6 +43,5 @@ def get_video_key_regex_by_examination_alias(
|
|
|
37
43
|
pattern += "anonymous" if is_anonymous else "non_anonymous"
|
|
38
44
|
else:
|
|
39
45
|
pattern += "(anonymous|non_anonymous)"
|
|
40
|
-
|
|
41
|
-
return f"^{pattern}$"
|
|
42
46
|
|
|
47
|
+
return f"^{pattern}$"
|
|
@@ -9,93 +9,132 @@ from ...exceptions import InsufficientStorageError, VideoProcessingError
|
|
|
9
9
|
|
|
10
10
|
logger = logging.getLogger(__name__)
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
class StreamingVideoProcessor:
|
|
13
14
|
"""
|
|
14
15
|
Streaming video processor for memory-efficient video anonymization.
|
|
15
16
|
Processes videos in chunks to reduce memory usage and improve performance.
|
|
16
17
|
"""
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
def __init__(self, chunk_duration: int = 30, temp_dir: Optional[Path] = None):
|
|
19
20
|
"""
|
|
20
21
|
Initialize the streaming processor.
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
Args:
|
|
23
24
|
chunk_duration: Duration of each chunk in seconds
|
|
24
25
|
temp_dir: Temporary directory for processing chunks
|
|
25
26
|
"""
|
|
26
27
|
self.chunk_duration = chunk_duration
|
|
27
|
-
self.temp_dir =
|
|
28
|
+
self.temp_dir = (
|
|
29
|
+
Path(temp_dir)
|
|
30
|
+
if temp_dir
|
|
31
|
+
else Path(tempfile.gettempdir()) / "video_streaming"
|
|
32
|
+
)
|
|
28
33
|
self.temp_dir.mkdir(parents=True, exist_ok=True)
|
|
29
|
-
|
|
34
|
+
|
|
30
35
|
def check_ffmpeg_available(self) -> bool:
|
|
31
36
|
"""Check if FFmpeg is available in the system."""
|
|
32
37
|
try:
|
|
33
|
-
subprocess.run(
|
|
34
|
-
|
|
38
|
+
subprocess.run(
|
|
39
|
+
["ffmpeg", "-version"], capture_output=True, check=True, timeout=10
|
|
40
|
+
)
|
|
35
41
|
return True
|
|
36
|
-
except (
|
|
42
|
+
except (
|
|
43
|
+
subprocess.CalledProcessError,
|
|
44
|
+
FileNotFoundError,
|
|
45
|
+
subprocess.TimeoutExpired,
|
|
46
|
+
):
|
|
37
47
|
return False
|
|
38
|
-
|
|
48
|
+
|
|
39
49
|
def get_video_duration(self, video_path: Path) -> float:
|
|
40
50
|
"""Get video duration in seconds using FFprobe."""
|
|
41
51
|
try:
|
|
42
52
|
cmd = [
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
"ffprobe",
|
|
54
|
+
"-v",
|
|
55
|
+
"quiet",
|
|
56
|
+
"-show_entries",
|
|
57
|
+
"format=duration",
|
|
58
|
+
"-of",
|
|
59
|
+
"csv=p=0",
|
|
60
|
+
str(video_path),
|
|
45
61
|
]
|
|
46
|
-
result = subprocess.run(
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
cmd, capture_output=True, text=True, check=True, timeout=30
|
|
64
|
+
)
|
|
47
65
|
return float(result.stdout.strip())
|
|
48
|
-
except (
|
|
66
|
+
except (
|
|
67
|
+
subprocess.CalledProcessError,
|
|
68
|
+
ValueError,
|
|
69
|
+
subprocess.TimeoutExpired,
|
|
70
|
+
) as e:
|
|
49
71
|
logger.error(f"Failed to get video duration for {video_path}: {e}")
|
|
50
72
|
raise VideoProcessingError(f"Could not determine video duration: {e}")
|
|
51
|
-
|
|
52
|
-
def split_video_chunks(
|
|
73
|
+
|
|
74
|
+
def split_video_chunks(
|
|
75
|
+
self, video_path: Path
|
|
76
|
+
) -> Iterator[Tuple[Path, float, float]]:
|
|
53
77
|
"""
|
|
54
78
|
Split video into chunks for streaming processing.
|
|
55
|
-
|
|
79
|
+
|
|
56
80
|
Args:
|
|
57
81
|
video_path: Path to the input video
|
|
58
|
-
|
|
82
|
+
|
|
59
83
|
Yields:
|
|
60
84
|
Tuple of (chunk_path, start_time, end_time)
|
|
61
85
|
"""
|
|
62
86
|
if not self.check_ffmpeg_available():
|
|
63
87
|
raise VideoProcessingError("FFmpeg not available for video processing")
|
|
64
|
-
|
|
88
|
+
|
|
65
89
|
try:
|
|
66
90
|
total_duration = self.get_video_duration(video_path)
|
|
67
|
-
logger.info(
|
|
68
|
-
|
|
91
|
+
logger.info(
|
|
92
|
+
f"Video duration: {total_duration:.2f}s, splitting into {self.chunk_duration}s chunks"
|
|
93
|
+
)
|
|
94
|
+
|
|
69
95
|
chunk_count = 0
|
|
70
96
|
for start_time in range(0, int(total_duration), self.chunk_duration):
|
|
71
97
|
end_time = min(start_time + self.chunk_duration, total_duration)
|
|
72
|
-
|
|
98
|
+
|
|
73
99
|
# Create chunk filename
|
|
74
|
-
chunk_filename =
|
|
100
|
+
chunk_filename = (
|
|
101
|
+
f"chunk_{chunk_count:04d}_{start_time}_{int(end_time)}.mp4"
|
|
102
|
+
)
|
|
75
103
|
chunk_path = self.temp_dir / chunk_filename
|
|
76
|
-
|
|
104
|
+
|
|
77
105
|
# Extract chunk using FFmpeg
|
|
78
106
|
cmd = [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
"ffmpeg",
|
|
108
|
+
"-y", # Overwrite output files
|
|
109
|
+
"-ss",
|
|
110
|
+
str(start_time), # Start time
|
|
111
|
+
"-i",
|
|
112
|
+
str(video_path), # Input file
|
|
113
|
+
"-t",
|
|
114
|
+
str(end_time - start_time), # Duration
|
|
115
|
+
"-c",
|
|
116
|
+
"copy", # Copy streams without re-encoding for speed
|
|
117
|
+
"-avoid_negative_ts",
|
|
118
|
+
"make_zero", # Handle timestamp issues
|
|
119
|
+
str(chunk_path),
|
|
86
120
|
]
|
|
87
|
-
|
|
121
|
+
|
|
88
122
|
try:
|
|
89
|
-
logger.debug(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
logger.debug(
|
|
124
|
+
f"Creating chunk {chunk_count}: {start_time}s-{end_time}s"
|
|
125
|
+
)
|
|
126
|
+
result = subprocess.run(
|
|
127
|
+
cmd, capture_output=True, text=True, check=True, timeout=300
|
|
128
|
+
) # 5 minute timeout per chunk
|
|
129
|
+
|
|
93
130
|
if chunk_path.exists() and chunk_path.stat().st_size > 0:
|
|
94
131
|
yield chunk_path, start_time, end_time
|
|
95
132
|
chunk_count += 1
|
|
96
133
|
else:
|
|
97
|
-
logger.warning(
|
|
98
|
-
|
|
134
|
+
logger.warning(
|
|
135
|
+
f"Chunk {chunk_count} was not created or is empty"
|
|
136
|
+
)
|
|
137
|
+
|
|
99
138
|
except subprocess.CalledProcessError as e:
|
|
100
139
|
logger.error(f"FFmpeg failed for chunk {chunk_count}: {e.stderr}")
|
|
101
140
|
# Skip this chunk but continue with others
|
|
@@ -103,102 +142,119 @@ class StreamingVideoProcessor:
|
|
|
103
142
|
except subprocess.TimeoutExpired:
|
|
104
143
|
logger.error(f"FFmpeg timeout for chunk {chunk_count}")
|
|
105
144
|
continue
|
|
106
|
-
|
|
145
|
+
|
|
107
146
|
except Exception as e:
|
|
108
147
|
logger.error(f"Error in video chunking: {e}")
|
|
109
148
|
raise VideoProcessingError(f"Video chunking failed: {e}")
|
|
110
|
-
|
|
111
|
-
def process_chunk_anonymization(
|
|
149
|
+
|
|
150
|
+
def process_chunk_anonymization(
|
|
151
|
+
self, chunk_path: Path, anonymizer_func, **kwargs
|
|
152
|
+
) -> Path:
|
|
112
153
|
"""
|
|
113
154
|
Process a single chunk with the anonymization function.
|
|
114
|
-
|
|
155
|
+
|
|
115
156
|
Args:
|
|
116
157
|
chunk_path: Path to the video chunk
|
|
117
158
|
anonymizer_func: Function to anonymize the chunk
|
|
118
159
|
**kwargs: Additional arguments for the anonymizer
|
|
119
|
-
|
|
160
|
+
|
|
120
161
|
Returns:
|
|
121
162
|
Path to the anonymized chunk
|
|
122
163
|
"""
|
|
123
164
|
try:
|
|
124
|
-
output_path = chunk_path.with_suffix(
|
|
125
|
-
|
|
165
|
+
output_path = chunk_path.with_suffix(".anonymized.mp4")
|
|
166
|
+
|
|
126
167
|
# Call the anonymization function
|
|
127
168
|
result = anonymizer_func(chunk_path, output_path, **kwargs)
|
|
128
|
-
|
|
169
|
+
|
|
129
170
|
if isinstance(result, Path):
|
|
130
171
|
return result
|
|
131
172
|
elif result is True and output_path.exists():
|
|
132
173
|
return output_path
|
|
133
174
|
else:
|
|
134
|
-
raise VideoProcessingError(
|
|
135
|
-
|
|
175
|
+
raise VideoProcessingError(
|
|
176
|
+
f"Anonymization failed for chunk {chunk_path}"
|
|
177
|
+
)
|
|
178
|
+
|
|
136
179
|
except Exception as e:
|
|
137
180
|
logger.error(f"Chunk anonymization failed for {chunk_path}: {e}")
|
|
138
181
|
raise VideoProcessingError(f"Chunk processing failed: {e}")
|
|
139
|
-
|
|
182
|
+
|
|
140
183
|
def merge_chunks(self, chunk_paths: list[Path], output_path: Path) -> Path:
|
|
141
184
|
"""
|
|
142
185
|
Merge anonymized chunks back into a single video.
|
|
143
|
-
|
|
186
|
+
|
|
144
187
|
Args:
|
|
145
188
|
chunk_paths: List of paths to anonymized chunks
|
|
146
189
|
output_path: Path for the final merged video
|
|
147
|
-
|
|
190
|
+
|
|
148
191
|
Returns:
|
|
149
192
|
Path to the merged video
|
|
150
193
|
"""
|
|
151
194
|
if not chunk_paths:
|
|
152
195
|
raise VideoProcessingError("No chunks to merge")
|
|
153
|
-
|
|
196
|
+
|
|
154
197
|
try:
|
|
155
198
|
# Check storage space before merging
|
|
156
|
-
total_chunk_size = sum(
|
|
199
|
+
total_chunk_size = sum(
|
|
200
|
+
chunk.stat().st_size for chunk in chunk_paths if chunk.exists()
|
|
201
|
+
)
|
|
157
202
|
free_space = shutil.disk_usage(output_path.parent).free
|
|
158
|
-
|
|
203
|
+
|
|
159
204
|
if free_space < total_chunk_size * 1.2: # 20% safety margin
|
|
160
205
|
raise InsufficientStorageError(
|
|
161
206
|
f"Insufficient space for merging. Required: {total_chunk_size * 1.2 / 1e9:.1f} GB, "
|
|
162
207
|
f"Available: {free_space / 1e9:.1f} GB",
|
|
163
208
|
required_space=int(total_chunk_size * 1.2),
|
|
164
|
-
available_space=free_space
|
|
209
|
+
available_space=free_space,
|
|
165
210
|
)
|
|
166
|
-
|
|
211
|
+
|
|
167
212
|
# Create concat file for FFmpeg
|
|
168
213
|
concat_file = self.temp_dir / f"concat_{output_path.stem}.txt"
|
|
169
|
-
|
|
170
|
-
with open(concat_file,
|
|
214
|
+
|
|
215
|
+
with open(concat_file, "w") as f:
|
|
171
216
|
for chunk_path in chunk_paths:
|
|
172
217
|
if chunk_path.exists():
|
|
173
218
|
# Use relative paths in concat file for better portability
|
|
174
219
|
f.write(f"file '{chunk_path.name}'\n")
|
|
175
|
-
|
|
220
|
+
|
|
176
221
|
# Merge using FFmpeg concat demuxer
|
|
177
222
|
cmd = [
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
223
|
+
"ffmpeg",
|
|
224
|
+
"-y", # Overwrite output
|
|
225
|
+
"-f",
|
|
226
|
+
"concat", # Use concat demuxer
|
|
227
|
+
"-safe",
|
|
228
|
+
"0", # Allow unsafe file paths
|
|
229
|
+
"-i",
|
|
230
|
+
str(concat_file), # Input concat file
|
|
231
|
+
"-c",
|
|
232
|
+
"copy", # Copy streams without re-encoding
|
|
233
|
+
str(output_path),
|
|
184
234
|
]
|
|
185
|
-
|
|
235
|
+
|
|
186
236
|
logger.info(f"Merging {len(chunk_paths)} chunks into {output_path}")
|
|
187
|
-
|
|
237
|
+
|
|
188
238
|
# Change working directory to temp_dir for relative paths
|
|
189
|
-
result = subprocess.run(
|
|
190
|
-
|
|
191
|
-
|
|
239
|
+
result = subprocess.run(
|
|
240
|
+
cmd,
|
|
241
|
+
cwd=str(self.temp_dir),
|
|
242
|
+
capture_output=True,
|
|
243
|
+
text=True,
|
|
244
|
+
check=True,
|
|
245
|
+
timeout=600,
|
|
246
|
+
)
|
|
247
|
+
|
|
192
248
|
if not output_path.exists() or output_path.stat().st_size == 0:
|
|
193
249
|
raise VideoProcessingError("Merged video was not created or is empty")
|
|
194
|
-
|
|
250
|
+
|
|
195
251
|
logger.info(f"Successfully merged video: {output_path}")
|
|
196
|
-
|
|
252
|
+
|
|
197
253
|
# Clean up concat file
|
|
198
254
|
concat_file.unlink(missing_ok=True)
|
|
199
|
-
|
|
255
|
+
|
|
200
256
|
return output_path
|
|
201
|
-
|
|
257
|
+
|
|
202
258
|
except subprocess.CalledProcessError as e:
|
|
203
259
|
logger.error(f"FFmpeg merge failed: {e.stderr}")
|
|
204
260
|
raise VideoProcessingError(f"Video merging failed: {e.stderr}")
|
|
@@ -208,80 +264,98 @@ class StreamingVideoProcessor:
|
|
|
208
264
|
except Exception as e:
|
|
209
265
|
logger.error(f"Unexpected error during merge: {e}")
|
|
210
266
|
raise VideoProcessingError(f"Video merging failed: {e}")
|
|
211
|
-
|
|
212
|
-
def process_video_streaming(
|
|
213
|
-
|
|
267
|
+
|
|
268
|
+
def process_video_streaming(
|
|
269
|
+
self,
|
|
270
|
+
input_path: Path,
|
|
271
|
+
output_path: Path,
|
|
272
|
+
anonymizer_func,
|
|
273
|
+
progress_callback=None,
|
|
274
|
+
**kwargs,
|
|
275
|
+
) -> Path:
|
|
214
276
|
"""
|
|
215
277
|
Process a video using streaming approach for memory efficiency.
|
|
216
|
-
|
|
278
|
+
|
|
217
279
|
Args:
|
|
218
280
|
input_path: Path to input video
|
|
219
281
|
output_path: Path for output video
|
|
220
282
|
anonymizer_func: Function to anonymize video chunks
|
|
221
283
|
progress_callback: Optional callback for progress updates
|
|
222
284
|
**kwargs: Additional arguments for anonymizer
|
|
223
|
-
|
|
285
|
+
|
|
224
286
|
Returns:
|
|
225
287
|
Path to the processed video
|
|
226
288
|
"""
|
|
227
289
|
processed_chunks = []
|
|
228
290
|
total_chunks = 0
|
|
229
|
-
|
|
291
|
+
|
|
230
292
|
try:
|
|
231
|
-
logger.info(
|
|
232
|
-
|
|
293
|
+
logger.info(
|
|
294
|
+
f"Starting streaming video processing: {input_path} -> {output_path}"
|
|
295
|
+
)
|
|
296
|
+
|
|
233
297
|
# First pass: count total chunks for progress tracking
|
|
234
298
|
chunk_list = list(self.split_video_chunks(input_path))
|
|
235
299
|
total_chunks = len(chunk_list)
|
|
236
|
-
|
|
300
|
+
|
|
237
301
|
if total_chunks == 0:
|
|
238
|
-
raise VideoProcessingError(
|
|
239
|
-
|
|
302
|
+
raise VideoProcessingError(
|
|
303
|
+
"No chunks were created from the input video"
|
|
304
|
+
)
|
|
305
|
+
|
|
240
306
|
logger.info(f"Processing {total_chunks} chunks")
|
|
241
|
-
|
|
307
|
+
|
|
242
308
|
# Process each chunk
|
|
243
309
|
for i, (chunk_path, start_time, end_time) in enumerate(chunk_list):
|
|
244
310
|
try:
|
|
245
|
-
logger.debug(
|
|
246
|
-
|
|
311
|
+
logger.debug(
|
|
312
|
+
f"Processing chunk {i + 1}/{total_chunks}: {chunk_path}"
|
|
313
|
+
)
|
|
314
|
+
|
|
247
315
|
# Process the chunk
|
|
248
316
|
processed_chunk = self.process_chunk_anonymization(
|
|
249
317
|
chunk_path, anonymizer_func, **kwargs
|
|
250
318
|
)
|
|
251
319
|
processed_chunks.append(processed_chunk)
|
|
252
|
-
|
|
320
|
+
|
|
253
321
|
# Update progress
|
|
254
322
|
if progress_callback:
|
|
255
|
-
progress = int(
|
|
256
|
-
|
|
257
|
-
|
|
323
|
+
progress = int(
|
|
324
|
+
(i + 1) / total_chunks * 80
|
|
325
|
+
) # Reserve 20% for merging
|
|
326
|
+
progress_callback(
|
|
327
|
+
progress, f"Processed chunk {i + 1}/{total_chunks}"
|
|
328
|
+
)
|
|
329
|
+
|
|
258
330
|
# Clean up original chunk to save space
|
|
259
331
|
chunk_path.unlink(missing_ok=True)
|
|
260
|
-
|
|
332
|
+
|
|
261
333
|
except Exception as e:
|
|
262
334
|
logger.error(f"Failed to process chunk {i}: {e}")
|
|
263
335
|
# Clean up failed chunk
|
|
264
336
|
chunk_path.unlink(missing_ok=True)
|
|
265
337
|
# Continue with other chunks
|
|
266
338
|
continue
|
|
267
|
-
|
|
339
|
+
|
|
268
340
|
if not processed_chunks:
|
|
269
341
|
raise VideoProcessingError("No chunks were successfully processed")
|
|
270
|
-
|
|
342
|
+
|
|
271
343
|
# Update progress for merging phase
|
|
272
344
|
if progress_callback:
|
|
273
|
-
progress_callback(
|
|
274
|
-
|
|
345
|
+
progress_callback(
|
|
346
|
+
80, f"Merging {len(processed_chunks)} processed chunks..."
|
|
347
|
+
)
|
|
348
|
+
|
|
275
349
|
# Merge processed chunks
|
|
276
350
|
final_output = self.merge_chunks(processed_chunks, output_path)
|
|
277
|
-
|
|
351
|
+
|
|
278
352
|
# Final progress update
|
|
279
353
|
if progress_callback:
|
|
280
354
|
progress_callback(100, "Video processing completed")
|
|
281
|
-
|
|
355
|
+
|
|
282
356
|
logger.info(f"Streaming video processing completed: {final_output}")
|
|
283
357
|
return final_output
|
|
284
|
-
|
|
358
|
+
|
|
285
359
|
except (InsufficientStorageError, VideoProcessingError):
|
|
286
360
|
# Re-raise these specific errors as-is
|
|
287
361
|
raise
|
|
@@ -291,7 +365,7 @@ class StreamingVideoProcessor:
|
|
|
291
365
|
finally:
|
|
292
366
|
# Clean up all temporary chunks
|
|
293
367
|
self.cleanup_chunks(processed_chunks)
|
|
294
|
-
|
|
368
|
+
|
|
295
369
|
def cleanup_chunks(self, chunk_paths: list[Path]) -> None:
|
|
296
370
|
"""Clean up temporary chunk files."""
|
|
297
371
|
for chunk_path in chunk_paths:
|
|
@@ -301,7 +375,7 @@ class StreamingVideoProcessor:
|
|
|
301
375
|
logger.debug(f"Cleaned up chunk: {chunk_path}")
|
|
302
376
|
except Exception as e:
|
|
303
377
|
logger.warning(f"Failed to clean up chunk {chunk_path}: {e}")
|
|
304
|
-
|
|
378
|
+
|
|
305
379
|
def cleanup_temp_dir(self) -> None:
|
|
306
380
|
"""Clean up the entire temporary directory."""
|
|
307
381
|
try:
|
|
@@ -309,4 +383,4 @@ class StreamingVideoProcessor:
|
|
|
309
383
|
shutil.rmtree(self.temp_dir)
|
|
310
384
|
logger.debug(f"Cleaned up temp directory: {self.temp_dir}")
|
|
311
385
|
except Exception as e:
|
|
312
|
-
logger.warning(f"Failed to clean up temp directory {self.temp_dir}: {e}")
|
|
386
|
+
logger.warning(f"Failed to clean up temp directory {self.temp_dir}: {e}")
|