endoreg-db 0.6.4__py3-none-any.whl → 0.8.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/admin.py +26 -26
- endoreg_db/api_urls.py +4 -0
- endoreg_db/apps.py +12 -0
- endoreg_db/assets/dummy_model.ckpt +1 -0
- endoreg_db/codemods/readme.md +88 -0
- endoreg_db/codemods/rename_datetime_fields.py +92 -0
- endoreg_db/config/env.py +101 -0
- endoreg_db/data/__init__.py +12 -0
- endoreg_db/data/ai_model/data.yaml +1 -1
- endoreg_db/data/ai_model_label/label/polyp_classification.yaml +52 -0
- endoreg_db/data/ai_model_label/label-set/data.yaml +20 -1
- endoreg_db/data/ai_model_label/label-set/polyp_classifications.yaml +25 -0
- endoreg_db/data/center/data.yaml +13 -12
- endoreg_db/data/center_shift/ukw.yaml +9 -0
- endoreg_db/data/db_summary.csv +58 -0
- endoreg_db/data/db_summary.xlsx +0 -0
- endoreg_db/data/disease/misc.yaml +1 -2
- endoreg_db/data/endoscopy_processor/data.yaml +3 -0
- endoreg_db/data/event/cardiology.yaml +0 -13
- endoreg_db/data/examination/examinations/data.yaml +14 -9
- endoreg_db/data/examination_indication/endoscopy.yaml +30 -30
- endoreg_db/data/examination_indication_classification/endoscopy.yaml +11 -11
- endoreg_db/data/examination_requirement_set/colonoscopy.yaml +15 -0
- endoreg_db/data/finding/anatomy_colon.yaml +128 -0
- endoreg_db/data/finding/colonoscopy.yaml +40 -0
- endoreg_db/data/finding/colonoscopy_bowel_prep.yaml +56 -0
- endoreg_db/data/finding/complication.yaml +16 -0
- endoreg_db/data/finding/data.yaml +3 -46
- endoreg_db/data/finding/examination_setting.yaml +16 -0
- endoreg_db/data/finding/medication_related.yaml +18 -0
- endoreg_db/data/finding/outcome.yaml +12 -0
- endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +95 -0
- endoreg_db/data/finding_classification/colonoscopy_jnet.yaml +22 -0
- endoreg_db/data/finding_classification/colonoscopy_kudo.yaml +25 -0
- endoreg_db/data/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
- endoreg_db/data/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
- endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +68 -0
- endoreg_db/data/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
- endoreg_db/data/finding_classification/colonoscopy_location.yaml +80 -0
- endoreg_db/data/finding_classification/colonoscopy_lst.yaml +21 -0
- endoreg_db/data/finding_classification/colonoscopy_nice.yaml +20 -0
- endoreg_db/data/finding_classification/colonoscopy_paris.yaml +26 -0
- endoreg_db/data/finding_classification/colonoscopy_sano.yaml +22 -0
- endoreg_db/data/finding_classification/colonoscopy_summary.yaml +53 -0
- endoreg_db/data/finding_classification/complication_generic.yaml +25 -0
- endoreg_db/data/finding_classification/examination_setting_generic.yaml +40 -0
- endoreg_db/data/finding_classification/histology_colo.yaml +51 -0
- endoreg_db/data/finding_classification/intervention_required.yaml +26 -0
- endoreg_db/data/finding_classification/medication_related.yaml +23 -0
- endoreg_db/data/finding_classification/visualized.yaml +33 -0
- endoreg_db/data/finding_classification_choice/bowel_preparation.yaml +78 -0
- endoreg_db/data/{finding_morphology_classification_choice → finding_classification_choice}/colon_lesion_circularity_default.yaml +0 -2
- endoreg_db/data/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_lst.yaml +15 -0
- endoreg_db/data/{finding_morphology_classification_choice → finding_classification_choice}/colon_lesion_nice.yaml +4 -7
- endoreg_db/data/{finding_morphology_classification_choice → finding_classification_choice}/colon_lesion_paris.yaml +0 -8
- endoreg_db/data/{finding_morphology_classification_choice → finding_classification_choice}/colon_lesion_planarity_default.yaml +6 -13
- endoreg_db/data/finding_classification_choice/colon_lesion_sano.yaml +14 -0
- endoreg_db/data/{finding_morphology_classification_choice → finding_classification_choice}/colon_lesion_surface_intact_default.yaml +3 -6
- endoreg_db/data/{finding_location_classification_choice/colonoscopy.yaml → finding_classification_choice/colonoscopy_location.yaml} +11 -22
- endoreg_db/data/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
- endoreg_db/data/finding_classification_choice/colonoscopy_size.yaml +82 -0
- endoreg_db/data/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
- endoreg_db/data/finding_classification_choice/complication_generic_types.yaml +15 -0
- endoreg_db/data/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
- endoreg_db/data/finding_classification_choice/histology.yaml +24 -0
- endoreg_db/data/finding_classification_choice/histology_polyp.yaml +20 -0
- endoreg_db/data/finding_classification_choice/outcome.yaml +19 -0
- endoreg_db/data/finding_classification_choice/yes_no_na.yaml +11 -0
- endoreg_db/data/finding_classification_type/colonoscopy_basic.yaml +48 -0
- endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +8 -3
- endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +6 -6
- endoreg_db/data/finding_type/data.yaml +23 -10
- endoreg_db/data/gender/data.yaml +8 -1
- endoreg_db/data/information_source/annotation.yaml +6 -0
- endoreg_db/data/information_source/prediction.yaml +7 -0
- endoreg_db/data/information_source_type/data.yaml +8 -0
- endoreg_db/data/lab_value/misc.yaml +43 -0
- endoreg_db/data/medication/anticoagulation.yaml +5 -5
- endoreg_db/data/medication/tah.yaml +5 -5
- endoreg_db/data/medication_intake_time/base.yaml +4 -4
- endoreg_db/data/names_first/first_names.yaml +3 -0
- endoreg_db/data/pdf_type/data.yaml +26 -2
- endoreg_db/data/qualification/endoscopy.yaml +36 -0
- endoreg_db/data/qualification/m2.yaml +39 -0
- endoreg_db/data/qualification/outpatient_clinic.yaml +35 -0
- endoreg_db/data/qualification/sonography.yaml +36 -0
- endoreg_db/data/qualification_type/base.yaml +29 -0
- endoreg_db/data/report_reader_flag/rkh-histology-generic.yaml +10 -0
- endoreg_db/data/report_reader_flag/ukw-histology-generic.yaml +5 -0
- endoreg_db/data/requirement/age.yaml +26 -0
- endoreg_db/data/requirement/colonoscopy_baseline_austria.yaml +45 -0
- endoreg_db/data/requirement/disease_cardiovascular.yaml +6 -6
- endoreg_db/data/requirement/disease_classification_choice_cardiovascular.yaml +9 -6
- endoreg_db/data/requirement/disease_hepatology.yaml +1 -1
- endoreg_db/data/requirement/disease_misc.yaml +3 -3
- endoreg_db/data/requirement/disease_renal.yaml +18 -2
- endoreg_db/data/requirement/{colonoscopy_indications.yaml → endoscopy_bleeding_risk.yaml} +6 -3
- endoreg_db/data/requirement/event_cardiology.yaml +17 -17
- endoreg_db/data/requirement/event_requirements.yaml +145 -0
- endoreg_db/data/requirement/finding_colon_polyp.yaml +50 -0
- endoreg_db/data/requirement/gender.yaml +25 -0
- endoreg_db/data/requirement/lab_value.yaml +352 -31
- endoreg_db/data/requirement/medication.yaml +93 -0
- endoreg_db/data/requirement_operator/age.yaml +13 -0
- endoreg_db/data/requirement_operator/lab_operators.yaml +36 -35
- endoreg_db/data/requirement_operator/model_operators.yaml +13 -7
- endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +48 -0
- endoreg_db/data/requirement_set/colonoscopy_austria_screening.yaml +57 -0
- endoreg_db/data/requirement_set/endoscopy_bleeding_risk.yaml +42 -2
- endoreg_db/data/requirement_type/requirement_types.yaml +82 -0
- endoreg_db/data/shift/endoscopy.yaml +21 -0
- endoreg_db/data/shift_type/base.yaml +35 -0
- endoreg_db/data/tag/requirement_set_tags.yaml +11 -0
- endoreg_db/data/unit/concentration.yaml +23 -0
- endoreg_db/exceptions.py +19 -0
- endoreg_db/forms/patient_finding_intervention_form.py +4 -5
- endoreg_db/forms/patient_form.py +7 -6
- endoreg_db/forms/questionnaires/__init__.py +1 -1
- endoreg_db/forms/questionnaires/tto_questionnaire.py +19 -19
- endoreg_db/helpers/count_db.py +45 -0
- endoreg_db/helpers/data_loader.py +208 -0
- endoreg_db/helpers/default_objects.py +359 -0
- endoreg_db/helpers/download_segmentation_model.py +31 -0
- endoreg_db/helpers/interact.py +6 -0
- endoreg_db/helpers/test_video_helper.py +119 -0
- endoreg_db/logger_conf.py +140 -0
- endoreg_db/management/__init__.py +1 -0
- endoreg_db/management/commands/__init__.py +1 -0
- endoreg_db/management/commands/anonymize_video.py +0 -0
- endoreg_db/management/commands/check_auth.py +125 -0
- endoreg_db/management/commands/create_multilabel_model_meta.py +214 -0
- endoreg_db/management/commands/fix_missing_patient_data.py +172 -0
- endoreg_db/management/commands/fix_video_paths.py +165 -0
- endoreg_db/management/commands/import_fallback_video.py +203 -0
- endoreg_db/management/commands/import_report.py +298 -0
- endoreg_db/management/commands/import_video.py +422 -0
- endoreg_db/management/commands/import_video_with_classification.py +367 -0
- endoreg_db/management/commands/init_default_ai_model.py +112 -0
- endoreg_db/management/commands/load_ai_model_data.py +2 -7
- endoreg_db/management/commands/load_base_db_data.py +1 -0
- endoreg_db/management/commands/load_endoscope_data.py +2 -2
- endoreg_db/management/commands/load_examination_indication_data.py +2 -3
- endoreg_db/management/commands/load_finding_data.py +49 -92
- endoreg_db/management/commands/load_green_endoscopy_wuerzburg_data.py +0 -1
- endoreg_db/management/commands/load_information_source.py +13 -7
- endoreg_db/management/commands/load_name_data.py +37 -0
- endoreg_db/management/commands/load_qualification_data.py +59 -0
- endoreg_db/management/commands/load_requirement_data.py +30 -6
- endoreg_db/management/commands/load_shift_data.py +60 -0
- endoreg_db/management/commands/load_tag_data.py +57 -0
- endoreg_db/management/commands/register_ai_model.py +1 -1
- endoreg_db/management/commands/start_filewatcher.py +106 -0
- endoreg_db/management/commands/storage_management.py +548 -0
- endoreg_db/management/commands/summarize_db_content.py +189 -0
- endoreg_db/management/commands/validate_video.py +204 -0
- endoreg_db/management/commands/validate_video_files.py +161 -0
- endoreg_db/management/commands/video_validation.py +22 -0
- endoreg_db/migrations/0001_initial.py +625 -813
- endoreg_db/migrations/0002_add_video_correction_models.py +52 -0
- endoreg_db/models/__init__.py +270 -307
- endoreg_db/models/administration/__init__.py +116 -0
- endoreg_db/models/{ai_model → administration/ai}/__init__.py +6 -1
- endoreg_db/models/administration/ai/active_model.py +35 -0
- endoreg_db/models/administration/ai/ai_model.py +156 -0
- endoreg_db/models/{ai_model → administration/ai}/model_type.py +6 -1
- endoreg_db/models/administration/case/__init__.py +19 -0
- endoreg_db/models/administration/case/case.py +114 -0
- endoreg_db/models/{case_template → administration/case/case_template}/case_template.py +3 -3
- endoreg_db/models/{case_template → administration/case/case_template}/case_template_rule.py +3 -10
- endoreg_db/models/{case_template → administration/case/case_template}/case_template_rule_value.py +2 -4
- endoreg_db/models/{case_template → administration/case/case_template}/case_template_type.py +1 -3
- endoreg_db/models/{center → administration/center}/__init__.py +3 -1
- endoreg_db/models/administration/center/center.py +61 -0
- endoreg_db/models/administration/center/center_product.py +64 -0
- endoreg_db/models/{center → administration/center}/center_resource.py +19 -3
- endoreg_db/models/administration/center/center_shift.py +88 -0
- endoreg_db/models/administration/center/center_waste.py +30 -0
- endoreg_db/models/administration/permissions/__init__.py +44 -0
- endoreg_db/models/administration/person/__init__.py +24 -0
- endoreg_db/models/administration/person/employee/__init__.py +3 -0
- endoreg_db/models/administration/person/employee/employee.py +35 -0
- endoreg_db/models/administration/person/employee/employee_qualification.py +39 -0
- endoreg_db/models/administration/person/employee/employee_type.py +42 -0
- endoreg_db/models/administration/person/examiner/__init__.py +4 -0
- endoreg_db/models/administration/person/examiner/examiner.py +54 -0
- endoreg_db/models/administration/person/names/__init__.py +0 -0
- endoreg_db/models/{persons → administration/person/names}/first_name.py +1 -1
- endoreg_db/models/{persons → administration/person/names}/last_name.py +2 -3
- endoreg_db/models/administration/person/patient/__init__.py +5 -0
- endoreg_db/models/administration/person/patient/patient.py +460 -0
- endoreg_db/models/administration/person/profession/__init__.py +24 -0
- endoreg_db/models/administration/person/user/__init__.py +5 -0
- endoreg_db/models/administration/person/user/portal_user_information.py +37 -0
- endoreg_db/models/administration/product/product.py +97 -0
- endoreg_db/models/administration/product/product_group.py +39 -0
- endoreg_db/models/administration/product/product_material.py +54 -0
- endoreg_db/models/{product → administration/product}/product_weight.py +9 -0
- endoreg_db/models/{product → administration/product}/reference_product.py +26 -11
- endoreg_db/models/administration/qualification/__init__.py +7 -0
- endoreg_db/models/administration/qualification/qualification.py +37 -0
- endoreg_db/models/administration/qualification/qualification_type.py +35 -0
- endoreg_db/models/administration/shift/__init__.py +9 -0
- endoreg_db/models/administration/shift/scheduled_days.py +69 -0
- endoreg_db/models/administration/shift/shift.py +51 -0
- endoreg_db/models/administration/shift/shift_type.py +108 -0
- endoreg_db/models/label/__init__.py +24 -1
- endoreg_db/models/label/annotation/__init__.py +12 -0
- endoreg_db/models/label/annotation/image_classification.py +84 -0
- endoreg_db/models/label/annotation/video_segmentation_annotation.py +66 -0
- endoreg_db/models/label/label.py +45 -74
- endoreg_db/models/label/label_set.py +53 -0
- endoreg_db/models/label/label_type.py +29 -0
- endoreg_db/models/label/label_video_segment/__init__.py +3 -0
- endoreg_db/models/label/label_video_segment/_create_from_video.py +41 -0
- endoreg_db/models/label/label_video_segment/label_video_segment.py +511 -0
- endoreg_db/models/label/video_segmentation_label.py +31 -0
- endoreg_db/models/{annotation → label}/video_segmentation_labelset.py +7 -0
- endoreg_db/models/media/__init__.py +14 -0
- endoreg_db/models/media/frame/__init__.py +3 -0
- endoreg_db/models/media/frame/frame.py +111 -0
- endoreg_db/models/media/pdf/__init__.py +11 -0
- endoreg_db/models/media/pdf/raw_pdf.py +608 -0
- endoreg_db/models/media/pdf/report_file.py +162 -0
- endoreg_db/models/media/pdf/report_reader/report_reader_config.py +77 -0
- endoreg_db/models/media/video/__init__.py +4 -0
- endoreg_db/models/media/video/create_from_file.py +336 -0
- endoreg_db/models/media/video/pipe_1.py +207 -0
- endoreg_db/models/media/video/pipe_2.py +105 -0
- endoreg_db/models/media/video/refactor_plan.md +0 -0
- endoreg_db/models/media/video/video_file.py +680 -0
- endoreg_db/models/media/video/video_file_ai.py +443 -0
- endoreg_db/models/media/video/video_file_anonymize.py +348 -0
- endoreg_db/models/media/video/video_file_frames/__init__.py +47 -0
- endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +22 -0
- endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +23 -0
- endoreg_db/models/media/video/video_file_frames/_delete_frames.py +104 -0
- endoreg_db/models/media/video/video_file_frames/_extract_frames.py +174 -0
- endoreg_db/models/media/video/video_file_frames/_get_frame.py +28 -0
- endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +27 -0
- endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +20 -0
- endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +27 -0
- endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +34 -0
- endoreg_db/models/media/video/video_file_frames/_get_frames.py +27 -0
- endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +129 -0
- endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +129 -0
- endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +65 -0
- endoreg_db/models/media/video/video_file_frames.py +0 -0
- endoreg_db/models/media/video/video_file_io.py +166 -0
- endoreg_db/models/media/video/video_file_meta/__init__.py +22 -0
- endoreg_db/models/media/video/video_file_meta/get_crop_template.py +45 -0
- endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +39 -0
- endoreg_db/models/media/video/video_file_meta/get_fps.py +147 -0
- endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +143 -0
- endoreg_db/models/media/video/video_file_meta/text_meta.py +134 -0
- endoreg_db/models/media/video/video_file_meta/video_meta.py +70 -0
- endoreg_db/models/media/video/video_file_meta.py +11 -0
- endoreg_db/models/media/video/video_file_segments.py +209 -0
- endoreg_db/models/medical/__init__.py +146 -0
- endoreg_db/models/{contraindication → medical/contraindication}/__init__.py +1 -5
- endoreg_db/models/{disease.py → medical/disease.py} +60 -52
- endoreg_db/models/{event.py → medical/event.py} +31 -54
- endoreg_db/models/{examination → medical/examination}/__init__.py +1 -1
- endoreg_db/models/medical/examination/examination.py +148 -0
- endoreg_db/models/{examination → medical/examination}/examination_indication.py +64 -35
- endoreg_db/models/{examination → medical/examination}/examination_time.py +0 -4
- endoreg_db/models/{examination → medical/examination}/examination_time_type.py +1 -8
- endoreg_db/models/{examination → medical/examination}/examination_type.py +1 -7
- endoreg_db/models/medical/finding/__init__.py +18 -0
- endoreg_db/models/medical/finding/finding.py +96 -0
- endoreg_db/models/medical/finding/finding_classification.py +142 -0
- endoreg_db/models/{finding → medical/finding}/finding_intervention.py +2 -10
- endoreg_db/models/medical/finding/finding_type.py +35 -0
- endoreg_db/models/medical/hardware/__init__.py +8 -0
- endoreg_db/models/{hardware → medical/hardware}/endoscope.py +28 -23
- endoreg_db/models/medical/laboratory/__init__.py +5 -0
- endoreg_db/models/medical/laboratory/lab_value.py +419 -0
- endoreg_db/models/{medication → medical/medication}/medication.py +1 -3
- endoreg_db/models/{medication → medical/medication}/medication_indication_type.py +8 -3
- endoreg_db/models/{medication → medical/medication}/medication_intake_time.py +21 -3
- endoreg_db/models/{medication → medical/medication}/medication_schedule.py +13 -5
- endoreg_db/models/{organ → medical/organ}/__init__.py +3 -6
- endoreg_db/models/medical/patient/__init__.py +56 -0
- endoreg_db/models/medical/patient/medication_examples.py +38 -0
- endoreg_db/models/medical/patient/patient_disease.py +63 -0
- endoreg_db/models/medical/patient/patient_event.py +75 -0
- endoreg_db/models/medical/patient/patient_examination.py +249 -0
- endoreg_db/models/{persons → medical}/patient/patient_examination_indication.py +21 -9
- endoreg_db/models/medical/patient/patient_finding.py +357 -0
- endoreg_db/models/medical/patient/patient_finding_classification.py +207 -0
- endoreg_db/models/{patient → medical/patient}/patient_finding_intervention.py +15 -1
- endoreg_db/models/medical/patient/patient_lab_sample.py +148 -0
- endoreg_db/models/{persons → medical}/patient/patient_lab_value.py +40 -15
- endoreg_db/models/medical/patient/patient_medication.py +104 -0
- endoreg_db/models/medical/patient/patient_medication_schedule.py +136 -0
- endoreg_db/models/{risk → medical/risk}/risk_type.py +0 -4
- endoreg_db/models/{data_file/metadata → metadata}/__init__.py +6 -0
- endoreg_db/models/metadata/frame_ocr_result.py +0 -0
- endoreg_db/models/metadata/model_meta.py +193 -0
- endoreg_db/models/metadata/model_meta_logic.py +236 -0
- endoreg_db/models/{data_file/metadata → metadata}/pdf_meta.py +28 -13
- endoreg_db/models/metadata/sensitive_meta.py +288 -0
- endoreg_db/models/metadata/sensitive_meta_logic.py +643 -0
- endoreg_db/models/metadata/video_meta.py +332 -0
- endoreg_db/models/metadata/video_prediction_logic.py +190 -0
- endoreg_db/models/metadata/video_prediction_meta.py +270 -0
- endoreg_db/models/other/__init__.py +17 -0
- endoreg_db/models/other/distribution/date_value_distribution.py +0 -2
- endoreg_db/models/other/distribution/numeric_value_distribution.py +30 -2
- endoreg_db/models/{emission → other/emission}/emission_factor.py +15 -6
- endoreg_db/models/{persons → other}/gender.py +8 -3
- endoreg_db/models/other/information_source.py +159 -0
- endoreg_db/models/other/material.py +10 -2
- endoreg_db/models/other/resource.py +6 -2
- endoreg_db/models/other/tag.py +27 -0
- endoreg_db/models/other/transport_route.py +13 -2
- endoreg_db/models/{unit.py → other/unit.py} +16 -6
- endoreg_db/models/other/waste.py +10 -3
- endoreg_db/models/requirement/requirement.py +556 -114
- endoreg_db/models/requirement/requirement_evaluation/__init__.py +4 -132
- endoreg_db/models/requirement/requirement_evaluation/get_values.py +40 -0
- endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +9 -0
- endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +80 -87
- endoreg_db/models/requirement/requirement_operator.py +132 -14
- endoreg_db/models/requirement/requirement_set.py +181 -21
- endoreg_db/models/rule/__init__.py +13 -0
- endoreg_db/models/{rules → rule}/rule.py +6 -3
- endoreg_db/models/{rules → rule}/rule_attribute_dtype.py +0 -2
- endoreg_db/models/{rules → rule}/rule_type.py +0 -2
- endoreg_db/models/{rules → rule}/ruleset.py +0 -2
- endoreg_db/models/state/__init__.py +12 -0
- endoreg_db/models/state/abstract.py +11 -0
- endoreg_db/models/state/audit_ledger.py +150 -0
- endoreg_db/models/state/label_video_segment.py +22 -0
- endoreg_db/models/state/raw_pdf.py +187 -0
- endoreg_db/models/state/sensitive_meta.py +46 -0
- endoreg_db/models/state/video.py +232 -0
- endoreg_db/models/upload_job.py +99 -0
- endoreg_db/models/utils.py +135 -0
- endoreg_db/models/video_metadata.py +66 -0
- endoreg_db/models/video_processing.py +153 -0
- endoreg_db/renames.yml +8 -0
- endoreg_db/root_urls.py +9 -0
- endoreg_db/schemas/__init__.py +0 -0
- endoreg_db/schemas/examination_evaluation.py +27 -0
- endoreg_db/serializers/Frames_NICE_and_PARIS_classifications.py +775 -0
- endoreg_db/serializers/__init__.py +147 -10
- endoreg_db/serializers/{raw_pdf_meta_validation.py → _old/raw_pdf_meta_validation.py} +3 -3
- endoreg_db/serializers/{raw_video_meta_validation.py → _old/raw_video_meta_validation.py} +18 -14
- endoreg_db/serializers/_old/video.py +71 -0
- endoreg_db/serializers/administration/__init__.py +14 -0
- endoreg_db/serializers/administration/ai/__init__.py +10 -0
- endoreg_db/serializers/administration/ai/active_model.py +10 -0
- endoreg_db/serializers/administration/ai/ai_model.py +18 -0
- endoreg_db/serializers/administration/ai/model_type.py +10 -0
- endoreg_db/serializers/administration/center.py +9 -0
- endoreg_db/serializers/administration/gender.py +9 -0
- endoreg_db/serializers/anonymization.py +69 -0
- endoreg_db/serializers/evaluation/examination_evaluation.py +1 -0
- endoreg_db/serializers/examination/__init__.py +10 -0
- endoreg_db/serializers/examination/base.py +46 -0
- endoreg_db/serializers/examination/dropdown.py +21 -0
- endoreg_db/serializers/examination_serializer.py +12 -0
- endoreg_db/serializers/finding/__init__.py +5 -0
- endoreg_db/serializers/finding/finding.py +54 -0
- endoreg_db/serializers/finding_classification/__init__.py +7 -0
- endoreg_db/serializers/finding_classification/choice.py +19 -0
- endoreg_db/serializers/finding_classification/classification.py +13 -0
- endoreg_db/serializers/label/__init__.py +7 -0
- endoreg_db/serializers/label/image_classification_annotation.py +62 -0
- endoreg_db/serializers/label/label.py +15 -0
- endoreg_db/serializers/label_video_segment/__init__.py +7 -0
- endoreg_db/serializers/label_video_segment/_lvs_create.py +149 -0
- endoreg_db/serializers/label_video_segment/_lvs_update.py +138 -0
- endoreg_db/serializers/label_video_segment/_lvs_validate.py +149 -0
- endoreg_db/serializers/label_video_segment/label_video_segment.py +344 -0
- endoreg_db/serializers/label_video_segment/label_video_segment_annotation.py +99 -0
- endoreg_db/serializers/label_video_segment/label_video_segment_update.py +163 -0
- endoreg_db/serializers/meta/__init__.py +19 -0
- endoreg_db/serializers/meta/pdf_file_meta_extraction.py +115 -0
- endoreg_db/serializers/meta/report_meta.py +53 -0
- endoreg_db/serializers/meta/sensitive_meta_detail.py +162 -0
- endoreg_db/serializers/meta/sensitive_meta_update.py +148 -0
- endoreg_db/serializers/meta/sensitive_meta_verification.py +59 -0
- endoreg_db/serializers/meta/video_meta.py +39 -0
- endoreg_db/serializers/misc/__init__.py +14 -0
- endoreg_db/serializers/misc/file_overview.py +152 -0
- endoreg_db/serializers/misc/stats.py +33 -0
- endoreg_db/serializers/misc/translatable_field_mix_in.py +44 -0
- endoreg_db/serializers/misc/upload_job.py +71 -0
- endoreg_db/serializers/misc/vop_patient_data.py +120 -0
- endoreg_db/serializers/patient/__init__.py +11 -0
- endoreg_db/serializers/patient/patient.py +86 -0
- endoreg_db/serializers/patient/patient_dropdown.py +27 -0
- endoreg_db/serializers/patient_examination/__init__.py +7 -0
- endoreg_db/serializers/patient_examination/patient_examination.py +141 -0
- endoreg_db/serializers/patient_finding/__init__.py +15 -0
- endoreg_db/serializers/patient_finding/patient_finding.py +31 -0
- endoreg_db/serializers/patient_finding/patient_finding_classification.py +39 -0
- endoreg_db/serializers/patient_finding/patient_finding_detail.py +53 -0
- endoreg_db/serializers/patient_finding/patient_finding_intervention.py +26 -0
- endoreg_db/serializers/patient_finding/patient_finding_list.py +41 -0
- endoreg_db/serializers/patient_finding/patient_finding_write.py +126 -0
- endoreg_db/serializers/pdf/__init__.py +5 -0
- endoreg_db/serializers/pdf/anony_text_validation.py +85 -0
- endoreg_db/serializers/report/__init__.py +9 -0
- endoreg_db/serializers/report/mixins.py +45 -0
- endoreg_db/serializers/report/report.py +105 -0
- endoreg_db/serializers/report/report_list.py +22 -0
- endoreg_db/serializers/report/secure_file_url.py +26 -0
- endoreg_db/serializers/requirements/requirement_schema.py +25 -0
- endoreg_db/serializers/requirements/requirement_sets.py +29 -0
- endoreg_db/serializers/sensitive_meta_serializer.py +282 -0
- endoreg_db/serializers/video/__init__.py +7 -0
- endoreg_db/serializers/video/segmentation.py +263 -0
- endoreg_db/serializers/video/video_file_brief.py +10 -0
- endoreg_db/serializers/video/video_file_detail.py +83 -0
- endoreg_db/serializers/video/video_file_list.py +67 -0
- endoreg_db/serializers/video/video_metadata.py +105 -0
- endoreg_db/serializers/video/video_processing_history.py +153 -0
- endoreg_db/services/__init__.py +5 -0
- endoreg_db/services/anonymization.py +223 -0
- endoreg_db/services/examination_evaluation.py +149 -0
- endoreg_db/services/finding_description_service.py +0 -0
- endoreg_db/services/lookup_service.py +241 -0
- endoreg_db/services/lookup_store.py +122 -0
- endoreg_db/services/ollama_api_docs.py +1528 -0
- endoreg_db/services/pdf_import.py +963 -0
- endoreg_db/services/polling_coordinator.py +288 -0
- endoreg_db/services/pseudonym_service.py +89 -0
- endoreg_db/services/requirements_object.py +147 -0
- endoreg_db/services/segment_sync.py +155 -0
- endoreg_db/services/storage_aware_video_processor.py +344 -0
- endoreg_db/services/video_import.py +1118 -0
- endoreg_db/tasks/upload_tasks.py +207 -0
- endoreg_db/tasks/video_ingest.py +157 -0
- endoreg_db/tasks/video_processing_tasks.py +327 -0
- endoreg_db/urls/__init__.py +70 -0
- endoreg_db/urls/anonymization.py +32 -0
- endoreg_db/urls/auth.py +16 -0
- endoreg_db/urls/classification.py +39 -0
- endoreg_db/urls/examination.py +54 -0
- endoreg_db/urls/files.py +6 -0
- endoreg_db/urls/label_video_segment_validate.py +33 -0
- endoreg_db/urls/label_video_segments.py +44 -0
- endoreg_db/urls/media.py +229 -0
- endoreg_db/urls/patient.py +19 -0
- endoreg_db/urls/report.py +48 -0
- endoreg_db/urls/requirements.py +13 -0
- endoreg_db/urls/stats.py +46 -0
- endoreg_db/urls/upload.py +20 -0
- endoreg_db/urls/video.py +61 -0
- endoreg_db/urls.py +6 -283
- endoreg_db/utils/__init__.py +66 -57
- endoreg_db/utils/ai/__init__.py +9 -0
- endoreg_db/{models/ai_model/utils.py → utils/ai/get.py} +1 -4
- endoreg_db/{models/ai_model/lightning → utils/ai}/inference_dataset.py +0 -1
- endoreg_db/{models/ai_model/lightning → utils/ai}/multilabel_classification_net.py +14 -10
- endoreg_db/{models/ai_model/lightning → utils/ai}/postprocess.py +15 -5
- endoreg_db/utils/ai/predict.py +291 -0
- endoreg_db/{models/ai_model/lightning → utils/ai}/preprocess.py +1 -1
- endoreg_db/utils/calc_duration_seconds.py +24 -0
- endoreg_db/utils/case_generator/__init__.py +0 -0
- endoreg_db/utils/check_video_files.py +148 -0
- endoreg_db/utils/dataloader.py +50 -12
- endoreg_db/utils/dates.py +21 -0
- endoreg_db/utils/env.py +33 -0
- endoreg_db/utils/extract_specific_frames.py +72 -0
- endoreg_db/utils/file_operations.py +29 -1
- endoreg_db/utils/fix_video_path_direct.py +141 -0
- endoreg_db/utils/frame_anonymization_utils.py +463 -0
- endoreg_db/utils/links/__init__.py +0 -0
- endoreg_db/utils/links/requirement_link.py +193 -0
- endoreg_db/utils/mime_types.py +0 -0
- endoreg_db/utils/names.py +2 -0
- endoreg_db/utils/paths.py +100 -82
- endoreg_db/utils/permissions.py +143 -0
- endoreg_db/utils/pipelines/Readme.md +235 -0
- endoreg_db/utils/pipelines/__init__.py +0 -0
- endoreg_db/utils/pipelines/process_video_dir.py +120 -0
- endoreg_db/utils/product/__init__.py +0 -0
- endoreg_db/utils/product/sum_emissions.py +20 -0
- endoreg_db/utils/product/sum_weights.py +18 -0
- endoreg_db/utils/pydantic_models/db_config.py +1 -1
- endoreg_db/utils/requirement_helpers.py +0 -0
- endoreg_db/utils/requirement_operator_logic/__init__.py +0 -0
- endoreg_db/utils/requirement_operator_logic/lab_value_operators.py +578 -0
- endoreg_db/utils/requirement_operator_logic/model_evaluators.py +368 -0
- endoreg_db/utils/translation.py +27 -0
- endoreg_db/utils/validate_video_detailed.py +357 -0
- endoreg_db/utils/video/__init__.py +19 -6
- endoreg_db/utils/video/extract_frames.py +37 -70
- endoreg_db/utils/video/ffmpeg_wrapper.py +772 -0
- endoreg_db/utils/video/names.py +42 -0
- endoreg_db/utils/video/streaming_processor.py +312 -0
- endoreg_db/utils/video/video_splitter.py +94 -0
- endoreg_db/views/Frames_NICE_and_PARIS_classifications_views.py +238 -0
- endoreg_db/views/__init__.py +282 -2
- endoreg_db/views/anonymization/__init__.py +27 -0
- endoreg_db/views/anonymization/media_management.py +454 -0
- endoreg_db/views/anonymization/overview.py +216 -0
- endoreg_db/views/anonymization/validate.py +66 -0
- endoreg_db/views/auth/__init__.py +13 -0
- endoreg_db/views/{views.py → auth/keycloak.py} +19 -13
- endoreg_db/views/examination/__init__.py +33 -0
- endoreg_db/views/examination/examination.py +37 -0
- endoreg_db/views/examination/examination_manifest_cache.py +26 -0
- endoreg_db/views/examination/get_finding_classification_choices.py +59 -0
- endoreg_db/views/examination/get_finding_classifications.py +36 -0
- endoreg_db/views/examination/get_findings.py +41 -0
- endoreg_db/views/examination/get_instruments.py +18 -0
- endoreg_db/views/examination/get_interventions.py +14 -0
- endoreg_db/views/finding/__init__.py +9 -0
- endoreg_db/views/finding/finding.py +112 -0
- endoreg_db/views/finding/get_classifications.py +14 -0
- endoreg_db/views/finding/get_interventions.py +17 -0
- endoreg_db/views/finding_classification/__init__.py +13 -0
- endoreg_db/views/finding_classification/base.py +0 -0
- endoreg_db/views/finding_classification/finding_classification.py +42 -0
- endoreg_db/views/finding_classification/get_classification_choices.py +55 -0
- endoreg_db/views/label/__init__.py +5 -0
- endoreg_db/views/label/label.py +15 -0
- endoreg_db/views/label_video_segment/__init__.py +16 -0
- endoreg_db/views/label_video_segment/create_lvs_from_annotation.py +44 -0
- endoreg_db/views/label_video_segment/get_lvs_by_name_and_video.py +50 -0
- endoreg_db/views/label_video_segment/label_video_segment.py +77 -0
- endoreg_db/views/label_video_segment/label_video_segment_by_label.py +174 -0
- endoreg_db/views/label_video_segment/label_video_segment_detail.py +73 -0
- endoreg_db/views/label_video_segment/update_lvs_from_annotation.py +46 -0
- endoreg_db/views/label_video_segment/validate.py +226 -0
- endoreg_db/views/media/__init__.py +45 -0
- endoreg_db/views/media/pdf_media.py +386 -0
- endoreg_db/views/media/segments.py +71 -0
- endoreg_db/views/media/sensitive_metadata.py +314 -0
- endoreg_db/views/media/video_media.py +272 -0
- endoreg_db/views/media/video_segments.py +596 -0
- endoreg_db/views/meta/__init__.py +15 -0
- endoreg_db/views/meta/available_files_list.py +146 -0
- endoreg_db/views/meta/report_meta.py +53 -0
- endoreg_db/views/meta/sensitive_meta_detail.py +148 -0
- endoreg_db/views/meta/sensitive_meta_list.py +104 -0
- endoreg_db/views/meta/sensitive_meta_verification.py +71 -0
- endoreg_db/views/misc/__init__.py +63 -0
- endoreg_db/views/misc/center.py +13 -0
- endoreg_db/views/misc/gender.py +14 -0
- endoreg_db/views/misc/secure_file_serving_view.py +80 -0
- endoreg_db/views/misc/secure_file_url_view.py +84 -0
- endoreg_db/views/misc/secure_url_validate.py +79 -0
- endoreg_db/views/misc/stats.py +220 -0
- endoreg_db/views/misc/translation.py +182 -0
- endoreg_db/views/misc/upload_views.py +240 -0
- endoreg_db/views/patient/__init__.py +5 -0
- endoreg_db/views/patient/patient.py +210 -0
- endoreg_db/views/patient_examination/DEPRECATED_video_backup.py +164 -0
- endoreg_db/views/patient_examination/__init__.py +11 -0
- endoreg_db/views/patient_examination/patient_examination.py +140 -0
- endoreg_db/views/patient_examination/patient_examination_create.py +63 -0
- endoreg_db/views/patient_examination/patient_examination_detail.py +66 -0
- endoreg_db/views/patient_examination/patient_examination_list.py +68 -0
- endoreg_db/views/patient_examination/video.py +194 -0
- endoreg_db/views/patient_finding/__init__.py +7 -0
- endoreg_db/views/patient_finding/base.py +0 -0
- endoreg_db/views/patient_finding/patient_finding.py +64 -0
- endoreg_db/views/patient_finding/patient_finding_optimized.py +259 -0
- endoreg_db/views/patient_finding_classification/__init__.py +5 -0
- endoreg_db/views/patient_finding_classification/pfc_create.py +67 -0
- endoreg_db/views/patient_finding_location/__init__.py +5 -0
- endoreg_db/views/patient_finding_location/pfl_create.py +70 -0
- endoreg_db/views/patient_finding_morphology/__init__.py +5 -0
- endoreg_db/views/patient_finding_morphology/pfm_create.py +70 -0
- endoreg_db/views/pdf/__init__.py +11 -0
- endoreg_db/views/pdf/pdf_media.py +239 -0
- endoreg_db/views/pdf/pdf_stream_views.py +127 -0
- endoreg_db/views/pdf/reimport.py +161 -0
- endoreg_db/views/report/__init__.py +9 -0
- endoreg_db/views/report/report_list.py +112 -0
- endoreg_db/views/report/report_with_secure_url.py +28 -0
- endoreg_db/views/report/start_examination.py +7 -0
- endoreg_db/views/requirement/__init__.py +10 -0
- endoreg_db/views/requirement/evaluate.py +279 -0
- endoreg_db/views/requirement/lookup.py +483 -0
- endoreg_db/views/requirement/lookup_store.py +252 -0
- endoreg_db/views/requirement_lookup/lookup.py +0 -0
- endoreg_db/views/requirement_lookup/lookup_store.py +0 -0
- endoreg_db/views/stats/__init__.py +13 -0
- endoreg_db/views/stats/stats_views.py +229 -0
- endoreg_db/views/video/__init__.py +64 -0
- endoreg_db/views/video/correction.py +672 -0
- endoreg_db/views/video/reimport.py +195 -0
- endoreg_db/views/video/segmentation.py +274 -0
- endoreg_db/views/video/task_status.py +49 -0
- endoreg_db/views/{views_for_timeline.py → video/timeline.py} +3 -3
- endoreg_db/views/video/video_analyze.py +52 -0
- endoreg_db/views/video/video_apply_mask.py +48 -0
- endoreg_db/views/video/video_correction.py +21 -0
- endoreg_db/views/video/video_download_processed.py +58 -0
- endoreg_db/views/video/video_examination_viewset.py +329 -0
- endoreg_db/views/video/video_media.py +158 -0
- endoreg_db/views/video/video_meta.py +29 -0
- endoreg_db/views/video/video_processing_history.py +24 -0
- endoreg_db/views/video/video_remove_frames.py +48 -0
- endoreg_db/views/video/video_reprocess.py +40 -0
- endoreg_db/views/video/video_stream.py +306 -0
- endoreg_db-0.8.2.dist-info/METADATA +384 -0
- endoreg_db-0.8.2.dist-info/RECORD +790 -0
- endoreg_db/data/agl_service/data.yaml +0 -19
- endoreg_db/data/finding_location_classification/colonoscopy.yaml +0 -46
- endoreg_db/data/finding_morphology_classification/colonoscopy.yaml +0 -48
- endoreg_db/data/finding_morphology_classification_choice/colonoscopy_size.yaml +0 -57
- endoreg_db/management/commands/_load_model_template.py +0 -41
- endoreg_db/management/commands/delete_all.py +0 -18
- endoreg_db/management/commands/fetch_legacy_image_dataset.py +0 -32
- endoreg_db/management/commands/fix_auth_permission.py +0 -20
- endoreg_db/management/commands/load_active_model_data.py +0 -45
- endoreg_db/management/commands/load_g_play_data.py +0 -113
- endoreg_db/management/commands/load_logging_data.py +0 -39
- endoreg_db/management/commands/load_lx_data.py +0 -64
- endoreg_db/management/commands/load_medication_indication_data.py +0 -63
- endoreg_db/management/commands/load_medication_indication_type_data.py +0 -41
- endoreg_db/management/commands/load_medication_intake_time_data.py +0 -41
- endoreg_db/management/commands/load_medication_schedule_data.py +0 -55
- endoreg_db/management/commands/load_network_data.py +0 -57
- endoreg_db/migrations/0002_alter_frame_image_alter_rawframe_image.py +0 -23
- endoreg_db/migrations/0003_alter_frame_image_alter_rawframe_image.py +0 -23
- endoreg_db/migrations/0004_alter_rawvideofile_file_alter_video_file.py +0 -25
- endoreg_db/migrations/0005_rawvideofile_frame_count_and_more.py +0 -33
- endoreg_db/migrations/0006_frame_extracted_rawframe_extracted.py +0 -23
- endoreg_db/migrations/0007_rename_pseudo_patient_video_patient_and_more.py +0 -24
- endoreg_db/migrations/0008_remove_reportfile_patient_examination_and_more.py +0 -48
- endoreg_db/migrations/0009_requirementoperator_requirementsettype_and_more.py +0 -154
- endoreg_db/models/ai_model/active_model.py +0 -9
- endoreg_db/models/ai_model/ai_model.py +0 -90
- endoreg_db/models/ai_model/lightning/__init__.py +0 -3
- endoreg_db/models/ai_model/lightning/predict.py +0 -172
- endoreg_db/models/ai_model/lightning/prediction_visualizer.py +0 -55
- endoreg_db/models/ai_model/lightning/run_visualizer.py +0 -21
- endoreg_db/models/ai_model/model_meta.py +0 -240
- endoreg_db/models/annotation/__init__.py +0 -32
- endoreg_db/models/annotation/anonymized_image_annotation.py +0 -115
- endoreg_db/models/annotation/binary_classification_annotation_task.py +0 -117
- endoreg_db/models/annotation/image_classification.py +0 -86
- endoreg_db/models/annotation/video_segmentation_annotation.py +0 -52
- endoreg_db/models/case/__init__.py +0 -1
- endoreg_db/models/case/case.py +0 -34
- endoreg_db/models/center/center.py +0 -51
- endoreg_db/models/center/center_product.py +0 -33
- endoreg_db/models/center/center_waste.py +0 -16
- endoreg_db/models/data_file/__init__.py +0 -39
- endoreg_db/models/data_file/base_classes/__init__.py +0 -7
- endoreg_db/models/data_file/base_classes/abstract_frame.py +0 -98
- endoreg_db/models/data_file/base_classes/abstract_pdf.py +0 -127
- endoreg_db/models/data_file/base_classes/abstract_video.py +0 -806
- endoreg_db/models/data_file/base_classes/frame_helpers.py +0 -17
- endoreg_db/models/data_file/base_classes/prepare_bulk_frames.py +0 -19
- endoreg_db/models/data_file/base_classes/utils.py +0 -58
- endoreg_db/models/data_file/frame.py +0 -29
- endoreg_db/models/data_file/import_classes/__init__.py +0 -18
- endoreg_db/models/data_file/import_classes/processing_functions/__init__.py +0 -35
- endoreg_db/models/data_file/import_classes/processing_functions/pdf.py +0 -28
- endoreg_db/models/data_file/import_classes/processing_functions/video.py +0 -260
- endoreg_db/models/data_file/import_classes/raw_pdf.py +0 -254
- endoreg_db/models/data_file/import_classes/raw_video.py +0 -290
- endoreg_db/models/data_file/metadata/sensitive_meta.py +0 -290
- endoreg_db/models/data_file/metadata/video_meta.py +0 -199
- endoreg_db/models/data_file/report_file.py +0 -56
- endoreg_db/models/data_file/video/__init__.py +0 -11
- endoreg_db/models/data_file/video/import_meta.py +0 -25
- endoreg_db/models/data_file/video/video.py +0 -196
- endoreg_db/models/data_file/video_segment.py +0 -214
- endoreg_db/models/examination/examination.py +0 -67
- endoreg_db/models/finding/__init__.py +0 -11
- endoreg_db/models/finding/finding.py +0 -75
- endoreg_db/models/finding/finding_location_classification.py +0 -94
- endoreg_db/models/finding/finding_morphology_classification.py +0 -89
- endoreg_db/models/finding/finding_type.py +0 -22
- endoreg_db/models/hardware/__init__.py +0 -2
- endoreg_db/models/information_source.py +0 -65
- endoreg_db/models/laboratory/__init__.py +0 -1
- endoreg_db/models/laboratory/lab_value.py +0 -162
- endoreg_db/models/logging/__init__.py +0 -11
- endoreg_db/models/logging/agl_service.py +0 -19
- endoreg_db/models/logging/base.py +0 -22
- endoreg_db/models/logging/log_type.py +0 -23
- endoreg_db/models/logging/network_device.py +0 -27
- endoreg_db/models/lx/__init__.py +0 -4
- endoreg_db/models/lx/client.py +0 -57
- endoreg_db/models/lx/identity.py +0 -34
- endoreg_db/models/lx/permission.py +0 -18
- endoreg_db/models/lx/user.py +0 -16
- endoreg_db/models/network/__init__.py +0 -9
- endoreg_db/models/network/agl_service.py +0 -38
- endoreg_db/models/network/network_device.py +0 -58
- endoreg_db/models/network/network_device_type.py +0 -23
- endoreg_db/models/other/distribution.py +0 -5
- endoreg_db/models/patient/__init__.py +0 -24
- endoreg_db/models/patient/patient_examination.py +0 -182
- endoreg_db/models/patient/patient_finding.py +0 -143
- endoreg_db/models/patient/patient_finding_location.py +0 -120
- endoreg_db/models/patient/patient_finding_morphology.py +0 -166
- endoreg_db/models/permissions/__init__.py +0 -44
- endoreg_db/models/persons/__init__.py +0 -34
- endoreg_db/models/persons/examiner/__init__.py +0 -2
- endoreg_db/models/persons/examiner/examiner.py +0 -60
- endoreg_db/models/persons/examiner/examiner_type.py +0 -2
- endoreg_db/models/persons/patient/__init__.py +0 -8
- endoreg_db/models/persons/patient/patient.py +0 -389
- endoreg_db/models/persons/patient/patient_disease.py +0 -22
- endoreg_db/models/persons/patient/patient_event.py +0 -52
- endoreg_db/models/persons/patient/patient_lab_sample.py +0 -108
- endoreg_db/models/persons/patient/patient_medication.py +0 -59
- endoreg_db/models/persons/patient/patient_medication_schedule.py +0 -88
- endoreg_db/models/persons/portal_user_information.py +0 -27
- endoreg_db/models/prediction/__init__.py +0 -8
- endoreg_db/models/prediction/image_classification.py +0 -51
- endoreg_db/models/prediction/video_prediction_meta.py +0 -306
- endoreg_db/models/product/product.py +0 -110
- endoreg_db/models/product/product_group.py +0 -27
- endoreg_db/models/product/product_material.py +0 -28
- endoreg_db/models/questionnaires/__init__.py +0 -114
- endoreg_db/models/quiz/__init__.py +0 -9
- endoreg_db/models/quiz/quiz_answer.py +0 -41
- endoreg_db/models/quiz/quiz_question.py +0 -54
- endoreg_db/models/report_reader/report_reader_config.py +0 -53
- endoreg_db/models/rules/__init__.py +0 -5
- endoreg_db/queries/get/__init__.py +0 -6
- endoreg_db/queries/get/center.py +0 -42
- endoreg_db/queries/get/model.py +0 -13
- endoreg_db/queries/get/patient.py +0 -14
- endoreg_db/queries/get/patient_examination.py +0 -20
- endoreg_db/queries/get/report_file.py +0 -33
- endoreg_db/queries/get/video.py +0 -31
- endoreg_db/serializers/ai_model.py +0 -19
- endoreg_db/serializers/annotation.py +0 -14
- endoreg_db/serializers/center.py +0 -11
- endoreg_db/serializers/examination.py +0 -33
- endoreg_db/serializers/frame.py +0 -9
- endoreg_db/serializers/hardware.py +0 -21
- endoreg_db/serializers/label.py +0 -22
- endoreg_db/serializers/patient.py +0 -33
- endoreg_db/serializers/prediction.py +0 -10
- endoreg_db/serializers/raw_pdf_anony_text_validation.py +0 -137
- endoreg_db/serializers/report_file.py +0 -7
- endoreg_db/serializers/video.py +0 -20
- endoreg_db/serializers/video_segmentation.py +0 -574
- endoreg_db/tests.py +0 -3
- endoreg_db/utils/legacy_ocr.py +0 -201
- endoreg_db/utils/video/transcode_videofile.py +0 -111
- endoreg_db/views/patient_views.py +0 -90
- endoreg_db/views/raw_pdf_anony_text_validation_views.py +0 -95
- endoreg_db/views/raw_pdf_meta_validation_views.py +0 -111
- endoreg_db/views/raw_video_meta_validation_views.py +0 -148
- endoreg_db/views/report_views.py +0 -96
- endoreg_db/views/video_segmentation_views.py +0 -166
- endoreg_db-0.6.4.dist-info/METADATA +0 -161
- endoreg_db-0.6.4.dist-info/RECORD +0 -470
- /endoreg_db/{case_generator/__init__.py → api/serializers/finding_descriptions.py} +0 -0
- /endoreg_db/{queries/get/annotation.py → api/views/finding_descriptions.py} +0 -0
- /endoreg_db/{queries/get/prediction.py → data/shift/m2.yaml} +0 -0
- /endoreg_db/{queries/get/video_import_meta.py → factories/__init__.py} +0 -0
- /endoreg_db/{queries/get/video_prediction_meta.py → helpers/__init__.py} +0 -0
- /endoreg_db/models/{case_template → administration/case/case_template}/__init__.py +0 -0
- /endoreg_db/models/{persons → administration/person}/person.py +0 -0
- /endoreg_db/models/{product → administration/product}/__init__.py +0 -0
- /endoreg_db/models/{report_reader → media/pdf/report_reader}/__init__.py +0 -0
- /endoreg_db/models/{report_reader → media/pdf/report_reader}/report_reader_flag.py +0 -0
- /endoreg_db/models/{hardware → medical/hardware}/endoscopy_processor.py +0 -0
- /endoreg_db/models/{medication → medical/medication}/__init__.py +0 -0
- /endoreg_db/models/{medication → medical/medication}/medication_indication.py +0 -0
- /endoreg_db/models/{risk → medical/risk}/__init__.py +0 -0
- /endoreg_db/models/{risk → medical/risk}/risk.py +0 -0
- /endoreg_db/models/{emission → other/emission}/__init__.py +0 -0
- /endoreg_db/models/{rules → rule}/rule_applicator.py +0 -0
- /endoreg_db/{case_generator → utils/case_generator}/case_generator.py +0 -0
- /endoreg_db/{case_generator → utils/case_generator}/lab_sample_factory.py +0 -0
- /endoreg_db/{case_generator → utils/case_generator}/utils.py +0 -0
- /endoreg_db/views/{csrf.py → misc/csrf.py} +0 -0
- {endoreg_db-0.6.4.dist-info → endoreg_db-0.8.2.dist-info}/WHEEL +0 -0
- {endoreg_db-0.6.4.dist-info → endoreg_db-0.8.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Video import service module.
|
|
3
|
+
|
|
4
|
+
Provides high-level functions for importing and anonymizing video files,
|
|
5
|
+
combining VideoFile creation with frame-level anonymization.
|
|
6
|
+
|
|
7
|
+
Changelog:
|
|
8
|
+
October 14, 2025: Added file locking mechanism to prevent race conditions
|
|
9
|
+
during concurrent video imports (matches PDF import pattern)
|
|
10
|
+
"""
|
|
11
|
+
from datetime import date
|
|
12
|
+
import logging
|
|
13
|
+
import sys
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import time
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Union, Dict, Any, Optional
|
|
20
|
+
from django.db import transaction
|
|
21
|
+
from endoreg_db.models import VideoFile, SensitiveMeta
|
|
22
|
+
from endoreg_db.utils.paths import STORAGE_DIR, RAW_FRAME_DIR, VIDEO_DIR, ANONYM_VIDEO_DIR
|
|
23
|
+
import random
|
|
24
|
+
from lx_anonymizer.ocr import trocr_full_image_ocr
|
|
25
|
+
from numpy import ma
|
|
26
|
+
|
|
27
|
+
# File lock configuration (matches PDF import)
|
|
28
|
+
STALE_LOCK_SECONDS = 600 # 10 minutes - reclaim locks older than this
|
|
29
|
+
MAX_LOCK_WAIT_SECONDS = 90 # New: wait up to 90s for a non-stale lock to clear before skipping
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VideoImportService():
|
|
35
|
+
"""
|
|
36
|
+
Service for importing and anonymizing video files.
|
|
37
|
+
Uses a central video instance pattern for cleaner state management.
|
|
38
|
+
|
|
39
|
+
Features (October 14, 2025):
|
|
40
|
+
- File locking to prevent concurrent processing of the same video
|
|
41
|
+
- Stale lock detection and reclamation (600s timeout)
|
|
42
|
+
- Hash-based duplicate detection
|
|
43
|
+
- Graceful fallback processing without lx_anonymizer
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, project_root: Path = None):
|
|
47
|
+
|
|
48
|
+
# Set up project root path
|
|
49
|
+
if project_root:
|
|
50
|
+
self.project_root = Path(project_root)
|
|
51
|
+
else:
|
|
52
|
+
self.project_root = Path(__file__).parent.parent.parent.parent
|
|
53
|
+
|
|
54
|
+
# Track processed files to prevent duplicates
|
|
55
|
+
self.processed_files = set(str(file) for file in os.listdir(ANONYM_VIDEO_DIR))
|
|
56
|
+
|
|
57
|
+
self.STORAGE_DIR = STORAGE_DIR
|
|
58
|
+
|
|
59
|
+
# Central video instance and processing context
|
|
60
|
+
self.current_video = None
|
|
61
|
+
self.processing_context: Dict[str, Any] = {}
|
|
62
|
+
|
|
63
|
+
self.logger = logging.getLogger(__name__)
|
|
64
|
+
|
|
65
|
+
@contextmanager
|
|
66
|
+
def _file_lock(self, path: Path):
|
|
67
|
+
"""
|
|
68
|
+
Create a file lock to prevent duplicate processing of the same video.
|
|
69
|
+
|
|
70
|
+
This context manager creates a .lock file alongside the video file.
|
|
71
|
+
If the lock file already exists, it checks if it's stale (older than
|
|
72
|
+
STALE_LOCK_SECONDS) and reclaims it if necessary. If it's not stale,
|
|
73
|
+
we now WAIT (up to MAX_LOCK_WAIT_SECONDS) instead of failing immediately.
|
|
74
|
+
"""
|
|
75
|
+
lock_path = Path(str(path) + ".lock")
|
|
76
|
+
fd = None
|
|
77
|
+
try:
|
|
78
|
+
deadline = time.time() + MAX_LOCK_WAIT_SECONDS
|
|
79
|
+
while True:
|
|
80
|
+
try:
|
|
81
|
+
# Atomic create; fail if exists
|
|
82
|
+
fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
|
|
83
|
+
break # acquired
|
|
84
|
+
except FileExistsError:
|
|
85
|
+
# Check for stale lock
|
|
86
|
+
age = None
|
|
87
|
+
try:
|
|
88
|
+
st = os.stat(lock_path)
|
|
89
|
+
age = time.time() - st.st_mtime
|
|
90
|
+
except FileNotFoundError:
|
|
91
|
+
# Race: lock removed between exists and stat; retry acquire in next loop
|
|
92
|
+
age = None
|
|
93
|
+
|
|
94
|
+
if age is not None and age > STALE_LOCK_SECONDS:
|
|
95
|
+
try:
|
|
96
|
+
logger.warning(
|
|
97
|
+
"Stale lock detected for %s (age %.0fs). Reclaiming lock...",
|
|
98
|
+
path, age
|
|
99
|
+
)
|
|
100
|
+
lock_path.unlink()
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.warning("Failed to remove stale lock %s: %s", lock_path, e)
|
|
103
|
+
# Loop continues and retries acquire immediately
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
# Not stale: wait until deadline, then give up gracefully
|
|
107
|
+
if time.time() >= deadline:
|
|
108
|
+
raise ValueError(f"File already being processed: {path}")
|
|
109
|
+
time.sleep(1.0)
|
|
110
|
+
|
|
111
|
+
os.write(fd, b"lock")
|
|
112
|
+
os.close(fd)
|
|
113
|
+
fd = None
|
|
114
|
+
yield
|
|
115
|
+
finally:
|
|
116
|
+
try:
|
|
117
|
+
if fd is not None:
|
|
118
|
+
os.close(fd)
|
|
119
|
+
if lock_path.exists():
|
|
120
|
+
lock_path.unlink()
|
|
121
|
+
except OSError:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
def processed(self) -> bool:
|
|
125
|
+
"""Indicates if the current file has already been processed."""
|
|
126
|
+
return getattr(self, '_processed', False)
|
|
127
|
+
|
|
128
|
+
def import_and_anonymize(
|
|
129
|
+
self,
|
|
130
|
+
file_path: Union[Path, str],
|
|
131
|
+
center_name: str,
|
|
132
|
+
processor_name: str,
|
|
133
|
+
save_video: bool = True,
|
|
134
|
+
delete_source: bool = True,
|
|
135
|
+
) -> "VideoFile":
|
|
136
|
+
"""
|
|
137
|
+
High-level helper that orchestrates the complete video import and anonymization process.
|
|
138
|
+
Uses the central video instance pattern for improved state management.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
# Initialize processing context
|
|
142
|
+
self._initialize_processing_context(file_path, center_name, processor_name,
|
|
143
|
+
save_video, delete_source)
|
|
144
|
+
|
|
145
|
+
# Validate and prepare file (may raise ValueError if another worker holds a non-stale lock)
|
|
146
|
+
try:
|
|
147
|
+
self._validate_and_prepare_file()
|
|
148
|
+
except ValueError as ve:
|
|
149
|
+
# Relaxed behavior: if another process is working on this file, skip cleanly
|
|
150
|
+
if "already being processed" in str(ve):
|
|
151
|
+
self.logger.info(f"Skipping {file_path}: {ve}")
|
|
152
|
+
return None
|
|
153
|
+
raise
|
|
154
|
+
|
|
155
|
+
# Create or retrieve video instance
|
|
156
|
+
self._create_or_retrieve_video_instance()
|
|
157
|
+
|
|
158
|
+
# Setup processing environment
|
|
159
|
+
self._setup_processing_environment()
|
|
160
|
+
|
|
161
|
+
# Process frames and metadata
|
|
162
|
+
self._process_frames_and_metadata()
|
|
163
|
+
|
|
164
|
+
# Finalize processing
|
|
165
|
+
self._finalize_processing()
|
|
166
|
+
|
|
167
|
+
# Move files and cleanup
|
|
168
|
+
self._cleanup_and_archive()
|
|
169
|
+
|
|
170
|
+
return self.current_video
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
self.logger.error(f"Video import and anonymization failed for {file_path}: {e}")
|
|
174
|
+
self._cleanup_on_error()
|
|
175
|
+
raise
|
|
176
|
+
finally:
|
|
177
|
+
self._cleanup_processing_context()
|
|
178
|
+
|
|
179
|
+
def _initialize_processing_context(self, file_path: Union[Path, str], center_name: str,
|
|
180
|
+
processor_name: str, save_video: bool, delete_source: bool):
|
|
181
|
+
"""Initialize the processing context for the current video import."""
|
|
182
|
+
self.processing_context = {
|
|
183
|
+
'file_path': Path(file_path),
|
|
184
|
+
'center_name': center_name,
|
|
185
|
+
'processor_name': processor_name,
|
|
186
|
+
'save_video': save_video,
|
|
187
|
+
'delete_source': delete_source,
|
|
188
|
+
'processing_started': False,
|
|
189
|
+
'frames_extracted': False,
|
|
190
|
+
'anonymization_completed': False,
|
|
191
|
+
'error_reason': None
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
self.logger.info(f"Initialized processing context for: {file_path}")
|
|
195
|
+
|
|
196
|
+
def _validate_and_prepare_file(self):
|
|
197
|
+
"""
|
|
198
|
+
Validate the video file and prepare for processing.
|
|
199
|
+
|
|
200
|
+
Uses file locking to prevent concurrent processing of the same video file.
|
|
201
|
+
This prevents race conditions where multiple workers might try to process
|
|
202
|
+
the same video simultaneously.
|
|
203
|
+
|
|
204
|
+
The lock is acquired here and held for the entire import process.
|
|
205
|
+
See _file_lock() for lock reclamation logic.
|
|
206
|
+
"""
|
|
207
|
+
file_path = self.processing_context['file_path']
|
|
208
|
+
|
|
209
|
+
# Acquire file lock to prevent concurrent processing
|
|
210
|
+
# Lock will be held until finally block in import_and_anonymize()
|
|
211
|
+
self.processing_context['_lock_context'] = self._file_lock(file_path)
|
|
212
|
+
self.processing_context['_lock_context'].__enter__()
|
|
213
|
+
|
|
214
|
+
self.logger.info("Acquired file lock for: %s", file_path)
|
|
215
|
+
|
|
216
|
+
# Check if already processed (memory-based check)
|
|
217
|
+
if str(file_path) in self.processed_files:
|
|
218
|
+
self.logger.info("File %s already processed, skipping", file_path)
|
|
219
|
+
self._processed = True
|
|
220
|
+
raise ValueError(f"File already processed: {file_path}")
|
|
221
|
+
|
|
222
|
+
# Check file exists
|
|
223
|
+
if not file_path.exists():
|
|
224
|
+
raise FileNotFoundError(f"Video file not found: {file_path}")
|
|
225
|
+
|
|
226
|
+
self.logger.info("File validation completed for: %s", file_path)
|
|
227
|
+
|
|
228
|
+
def _create_or_retrieve_video_instance(self):
|
|
229
|
+
"""Create or retrieve the VideoFile instance and move to final storage."""
|
|
230
|
+
# Removed duplicate import of VideoFile (already imported at module level)
|
|
231
|
+
|
|
232
|
+
self.logger.info("Creating VideoFile instance...")
|
|
233
|
+
|
|
234
|
+
self.current_video = VideoFile.create_from_file_initialized(
|
|
235
|
+
file_path=self.processing_context['file_path'],
|
|
236
|
+
center_name=self.processing_context['center_name'],
|
|
237
|
+
processor_name=self.processing_context['processor_name'],
|
|
238
|
+
delete_source=self.processing_context['delete_source'],
|
|
239
|
+
save_video_file=self.processing_context['save_video'],
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if not self.current_video:
|
|
243
|
+
raise RuntimeError("Failed to create VideoFile instance")
|
|
244
|
+
|
|
245
|
+
# Immediately move to final storage locations
|
|
246
|
+
self._move_to_final_storage()
|
|
247
|
+
|
|
248
|
+
self.logger.info("Created VideoFile with UUID: %s", self.current_video.uuid)
|
|
249
|
+
|
|
250
|
+
# Get and mark processing state
|
|
251
|
+
state = VideoFile.get_or_create_state(self.current_video)
|
|
252
|
+
if not state:
|
|
253
|
+
raise RuntimeError("Failed to create VideoFile state")
|
|
254
|
+
|
|
255
|
+
state.mark_processing_started(save=True)
|
|
256
|
+
self.processing_context['processing_started'] = True
|
|
257
|
+
|
|
258
|
+
def _move_to_final_storage(self):
|
|
259
|
+
"""
|
|
260
|
+
Move video from raw_videos to final storage locations.
|
|
261
|
+
- Raw video → /data/videos (raw_file_path)
|
|
262
|
+
- Processed video will later → /data/anonym_videos (file_path)
|
|
263
|
+
"""
|
|
264
|
+
from endoreg_db.utils import data_paths
|
|
265
|
+
|
|
266
|
+
source_path = self.processing_context['file_path']
|
|
267
|
+
|
|
268
|
+
# Define target directories
|
|
269
|
+
videos_dir = data_paths["video"] # /data/videos for raw files
|
|
270
|
+
videos_dir.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
|
|
272
|
+
# Create target path for raw video in /data/videos
|
|
273
|
+
ext = Path(self.current_video.active_file_path).suffix or ".mp4"
|
|
274
|
+
video_filename = f"{self.current_video.uuid}{ext}"
|
|
275
|
+
raw_target_path = videos_dir / video_filename
|
|
276
|
+
|
|
277
|
+
# Move source file to raw video storage
|
|
278
|
+
try:
|
|
279
|
+
shutil.move(str(source_path), str(raw_target_path))
|
|
280
|
+
self.logger.info("Moved raw video to: %s", raw_target_path)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
self.logger.error("Failed to move video to final storage: %s", e)
|
|
283
|
+
raise
|
|
284
|
+
|
|
285
|
+
# Update the raw_file path in database (relative to storage root)
|
|
286
|
+
try:
|
|
287
|
+
storage_root = data_paths["storage"]
|
|
288
|
+
relative_path = raw_target_path.relative_to(storage_root)
|
|
289
|
+
self.current_video.raw_file.name = str(relative_path)
|
|
290
|
+
self.current_video.save(update_fields=['raw_file'])
|
|
291
|
+
self.logger.info("Updated raw_file path to: %s", relative_path)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
self.logger.error("Failed to update raw_file path: %s", e)
|
|
294
|
+
# Fallback to simple relative path
|
|
295
|
+
self.current_video.raw_file.name = f"videos/{video_filename}"
|
|
296
|
+
self.current_video.save(update_fields=['raw_file'])
|
|
297
|
+
self.logger.info("Updated raw_file path using fallback: %s", f"videos/{video_filename}")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# Store paths for later processing
|
|
301
|
+
self.processing_context['raw_video_path'] = raw_target_path
|
|
302
|
+
self.processing_context['video_filename'] = video_filename
|
|
303
|
+
|
|
304
|
+
def _setup_processing_environment(self):
|
|
305
|
+
"""Setup the processing environment without file movement."""
|
|
306
|
+
# Ensure we have a valid video instance
|
|
307
|
+
if not self.current_video:
|
|
308
|
+
raise RuntimeError("No video instance available for processing environment setup")
|
|
309
|
+
|
|
310
|
+
# Initialize video specifications
|
|
311
|
+
self.current_video.initialize_video_specs()
|
|
312
|
+
|
|
313
|
+
# Initialize frame objects in database
|
|
314
|
+
self.current_video.initialize_frames()
|
|
315
|
+
|
|
316
|
+
# Extract frames BEFORE processing to prevent pipeline 1 conflicts
|
|
317
|
+
self.logger.info("Pre-extracting frames to avoid pipeline conflicts...")
|
|
318
|
+
try:
|
|
319
|
+
frames_extracted = self.current_video.extract_frames(overwrite=False)
|
|
320
|
+
if frames_extracted:
|
|
321
|
+
self.processing_context['frames_extracted'] = True
|
|
322
|
+
self.logger.info("Frame extraction completed successfully")
|
|
323
|
+
|
|
324
|
+
# CRITICAL: Immediately save the frames_extracted state to database
|
|
325
|
+
# to prevent refresh_from_db() in pipeline 1 from overriding it
|
|
326
|
+
state = self.current_video.get_or_create_state()
|
|
327
|
+
if not state.frames_extracted:
|
|
328
|
+
state.frames_extracted = True
|
|
329
|
+
state.save(update_fields=['frames_extracted'])
|
|
330
|
+
self.logger.info("Persisted frames_extracted=True to database")
|
|
331
|
+
else:
|
|
332
|
+
self.logger.warning("Frame extraction failed, but continuing...")
|
|
333
|
+
self.processing_context['frames_extracted'] = False
|
|
334
|
+
except Exception as e:
|
|
335
|
+
self.logger.warning(f"Frame extraction failed during setup: {e}, but continuing...")
|
|
336
|
+
self.processing_context['frames_extracted'] = False
|
|
337
|
+
|
|
338
|
+
# Ensure default patient data
|
|
339
|
+
self._ensure_default_patient_data()
|
|
340
|
+
|
|
341
|
+
self.logger.info("Processing environment setup completed")
|
|
342
|
+
|
|
343
|
+
def _process_frames_and_metadata(self):
|
|
344
|
+
"""Process frames and extract metadata with anonymization."""
|
|
345
|
+
# Check frame cleaning availability
|
|
346
|
+
frame_cleaning_available, FrameCleaner, ReportReader = self._ensure_frame_cleaning_available()
|
|
347
|
+
|
|
348
|
+
if not (frame_cleaning_available and self.current_video.raw_file):
|
|
349
|
+
self.logger.warning("Frame cleaning not available or conditions not met, using fallback anonymization.")
|
|
350
|
+
self._fallback_anonymize_video()
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
self.logger.info("Starting frame-level anonymization with processor ROI masking...")
|
|
355
|
+
|
|
356
|
+
# Get processor ROI information
|
|
357
|
+
processor_roi, endoscope_roi = self._get_processor_roi_info()
|
|
358
|
+
|
|
359
|
+
# Perform frame cleaning with timeout to prevent blocking
|
|
360
|
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
|
|
361
|
+
|
|
362
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
363
|
+
future = executor.submit(self._perform_frame_cleaning, FrameCleaner, processor_roi, endoscope_roi)
|
|
364
|
+
try:
|
|
365
|
+
# Increased timeout to better accommodate ffmpeg + OCR
|
|
366
|
+
future.result(timeout=300)
|
|
367
|
+
self.processing_context['anonymization_completed'] = True
|
|
368
|
+
self.logger.info("Frame cleaning completed successfully within timeout")
|
|
369
|
+
except FutureTimeoutError:
|
|
370
|
+
self.logger.warning("Frame cleaning timed out; entering grace period check for cleaned output")
|
|
371
|
+
# Grace period: detect if cleaned file appears shortly after timeout
|
|
372
|
+
raw_video_path = self.processing_context.get('raw_video_path')
|
|
373
|
+
video_filename = self.processing_context.get('video_filename', Path(raw_video_path).name if raw_video_path else "video.mp4")
|
|
374
|
+
grace_seconds = 60
|
|
375
|
+
expected_cleaned = self.current_video.processed_file
|
|
376
|
+
found = False
|
|
377
|
+
if expected_cleaned is not None:
|
|
378
|
+
for _ in range(grace_seconds):
|
|
379
|
+
if expected_cleaned.exists():
|
|
380
|
+
self.processing_context['cleaned_video_path'] = expected_cleaned
|
|
381
|
+
self.processing_context['anonymization_completed'] = True
|
|
382
|
+
self.logger.info("Detected cleaned video during grace period: %s", expected_cleaned)
|
|
383
|
+
found = True
|
|
384
|
+
break
|
|
385
|
+
time.sleep(1)
|
|
386
|
+
else:
|
|
387
|
+
self._fallback_anonymize_video()
|
|
388
|
+
if not found:
|
|
389
|
+
raise TimeoutError("Frame cleaning operation timed out - likely Ollama connection issue")
|
|
390
|
+
|
|
391
|
+
except Exception as e:
|
|
392
|
+
self.logger.warning("Frame cleaning failed (reason: %s), falling back to simple copy", e)
|
|
393
|
+
# Try fallback anonymization when frame cleaning fails
|
|
394
|
+
try:
|
|
395
|
+
self._fallback_anonymize_video()
|
|
396
|
+
except Exception as fallback_error:
|
|
397
|
+
self.logger.error("Fallback anonymization also failed: %s", fallback_error)
|
|
398
|
+
# If even fallback fails, mark as not anonymized but continue import
|
|
399
|
+
self.processing_context['anonymization_completed'] = False
|
|
400
|
+
self.processing_context['error_reason'] = f"Frame cleaning failed: {e}, Fallback failed: {fallback_error}"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _fallback_anonymize_video(self):
|
|
404
|
+
"""
|
|
405
|
+
Fallback to create anonymized video if lx_anonymizer is not available.
|
|
406
|
+
|
|
407
|
+
This method tries multiple fallback strategies:
|
|
408
|
+
1. Use VideoFile.anonymize_video() method if available
|
|
409
|
+
2. Simple copy of raw video to anonym_videos (no processing)
|
|
410
|
+
|
|
411
|
+
The processed video will be marked in processing_context for _cleanup_and_archive().
|
|
412
|
+
"""
|
|
413
|
+
try:
|
|
414
|
+
self.logger.info("Attempting fallback video anonymization...")
|
|
415
|
+
|
|
416
|
+
# Strategy 1: Try VideoFile.pipe_2() method
|
|
417
|
+
if hasattr(self.current_video, 'pipe_2'):
|
|
418
|
+
self.logger.info("Trying VideoFile.pipe_2() method...")
|
|
419
|
+
|
|
420
|
+
# Try to anonymize
|
|
421
|
+
if self.current_video.pipe_2:
|
|
422
|
+
self.logger.info("VideoFile.pipe_2() succeeded")
|
|
423
|
+
self.processing_context['anonymization_completed'] = True
|
|
424
|
+
return
|
|
425
|
+
else:
|
|
426
|
+
self.logger.warning("VideoFile.pipe_2() returned False, trying simple copy fallback")
|
|
427
|
+
else:
|
|
428
|
+
self.logger.warning("VideoFile.pipe_2() method not available")
|
|
429
|
+
|
|
430
|
+
# Strategy 2: Simple copy (no processing, just copy raw to processed)
|
|
431
|
+
self.logger.info("Using simple copy fallback (raw video will be used as 'processed' video)")
|
|
432
|
+
|
|
433
|
+
# The _cleanup_and_archive() method will handle the copy
|
|
434
|
+
# We just need to mark that no real anonymization happened
|
|
435
|
+
self.processing_context['anonymization_completed'] = False
|
|
436
|
+
self.processing_context['use_raw_as_processed'] = True # Signal for cleanup
|
|
437
|
+
|
|
438
|
+
self.logger.warning("Fallback: Video will be imported without anonymization (raw copy used)")
|
|
439
|
+
|
|
440
|
+
except Exception as e:
|
|
441
|
+
self.logger.error(f"Error during fallback anonymization: {e}", exc_info=True)
|
|
442
|
+
self.processing_context['anonymization_completed'] = False
|
|
443
|
+
self.processing_context['error_reason'] = f"Fallback anonymization failed: {e}"
|
|
444
|
+
|
|
445
|
+
def _finalize_processing(self):
|
|
446
|
+
"""Finalize processing and update video state."""
|
|
447
|
+
self.logger.info("Updating video processing state...")
|
|
448
|
+
|
|
449
|
+
with transaction.atomic():
|
|
450
|
+
# Update basic processing states
|
|
451
|
+
# Ensure state exists before accessing it
|
|
452
|
+
|
|
453
|
+
if not self.current_video:
|
|
454
|
+
try:
|
|
455
|
+
self.current_video.refresh_from_db()
|
|
456
|
+
except Exception as e:
|
|
457
|
+
self.logger.error(f"Failed to refresh current_video from DB: {e}")
|
|
458
|
+
if not self.current_video:
|
|
459
|
+
raise RuntimeError("No current video instance available for finalization")
|
|
460
|
+
|
|
461
|
+
if not self.current_video.processed_file:
|
|
462
|
+
self.logger.warning("No processed file available for current video")
|
|
463
|
+
self.current_video.processed_file = None # Ensure field is not None
|
|
464
|
+
self.current_video.mark_sensitive_meta_processed = False
|
|
465
|
+
else:
|
|
466
|
+
self.current_video.mark_sensitive_meta_processed = True
|
|
467
|
+
|
|
468
|
+
state = self.current_video.get_or_create_state()
|
|
469
|
+
if not state:
|
|
470
|
+
raise RuntimeError("Failed to get or create video state")
|
|
471
|
+
|
|
472
|
+
# Only mark frames as extracted if they were successfully extracted
|
|
473
|
+
if self.processing_context.get('frames_extracted', False):
|
|
474
|
+
state.frames_extracted = True
|
|
475
|
+
self.logger.info("Marked frames as extracted in state")
|
|
476
|
+
else:
|
|
477
|
+
self.logger.warning("Frames were not extracted, not updating state")
|
|
478
|
+
|
|
479
|
+
# Always mark these as true (metadata extraction attempts were made)
|
|
480
|
+
state.frames_initialized = True
|
|
481
|
+
state.video_meta_extracted = True
|
|
482
|
+
state.text_meta_extracted = True
|
|
483
|
+
|
|
484
|
+
# ✅ FIX: Only mark as processed if anonymization actually completed
|
|
485
|
+
anonymization_completed = self.processing_context.get('anonymization_completed', False)
|
|
486
|
+
if anonymization_completed:
|
|
487
|
+
state.mark_sensitive_meta_processed(save=False)
|
|
488
|
+
self.logger.info("Anonymization completed - marking sensitive meta as processed")
|
|
489
|
+
else:
|
|
490
|
+
self.logger.warning(
|
|
491
|
+
"Anonymization NOT completed - NOT marking as processed. "
|
|
492
|
+
f"Reason: {self.processing_context.get('error_reason', 'Unknown')}"
|
|
493
|
+
)
|
|
494
|
+
# Explicitly mark as NOT processed
|
|
495
|
+
state.sensitive_meta_processed = False
|
|
496
|
+
|
|
497
|
+
# Save all state changes
|
|
498
|
+
state.save()
|
|
499
|
+
self.logger.info("Video processing state updated")
|
|
500
|
+
# Save all state changes
|
|
501
|
+
self.current_video.state.save()
|
|
502
|
+
self.current_video.save()
|
|
503
|
+
|
|
504
|
+
# Signal completion
|
|
505
|
+
self._signal_completion()
|
|
506
|
+
|
|
507
|
+
def _cleanup_and_archive(self):
|
|
508
|
+
"""Move processed video to anonym_videos and cleanup."""
|
|
509
|
+
from endoreg_db.utils import data_paths
|
|
510
|
+
|
|
511
|
+
# Define target directory for processed videos
|
|
512
|
+
anonym_videos_dir = data_paths["anonym_video"] # /data/anonym_videos
|
|
513
|
+
anonym_videos_dir.mkdir(parents=True, exist_ok=True)
|
|
514
|
+
|
|
515
|
+
# Check if we have a processed/cleaned video
|
|
516
|
+
processed_video_path = None
|
|
517
|
+
|
|
518
|
+
# Look for cleaned video from frame cleaning process
|
|
519
|
+
if 'cleaned_video_path' in self.processing_context:
|
|
520
|
+
processed_video_path = self.processing_context['cleaned_video_path']
|
|
521
|
+
else:
|
|
522
|
+
# If no processing occurred, copy from raw video location
|
|
523
|
+
raw_video_path = self.processing_context.get('raw_video_path')
|
|
524
|
+
if raw_video_path and Path(raw_video_path).exists():
|
|
525
|
+
video_filename = self.processing_context.get('video_filename', Path(raw_video_path).name)
|
|
526
|
+
processed_filename = f"processed_{video_filename}"
|
|
527
|
+
processed_video_path = Path(raw_video_path).parent / processed_filename
|
|
528
|
+
|
|
529
|
+
# Copy raw to processed location (will be moved to anonym_videos)
|
|
530
|
+
try:
|
|
531
|
+
shutil.copy2(str(raw_video_path), str(processed_video_path))
|
|
532
|
+
self.logger.info("Copied raw video for processing: %s", processed_video_path)
|
|
533
|
+
except Exception as e:
|
|
534
|
+
self.logger.error("Failed to copy raw video: %s", e)
|
|
535
|
+
processed_video_path = None # FIXED: Don't use raw as fallback
|
|
536
|
+
|
|
537
|
+
# Move processed video to anonym_videos ONLY if it exists
|
|
538
|
+
if processed_video_path and Path(processed_video_path).exists():
|
|
539
|
+
try:
|
|
540
|
+
# ✅ Clean filename: no original filename leakage
|
|
541
|
+
ext = Path(processed_video_path).suffix or ".mp4"
|
|
542
|
+
anonym_video_filename = f"anonym_{self.current_video.uuid}{ext}"
|
|
543
|
+
anonym_target_path = anonym_videos_dir / anonym_video_filename
|
|
544
|
+
|
|
545
|
+
# Move processed video to anonym_videos/
|
|
546
|
+
shutil.move(str(processed_video_path), str(anonym_target_path))
|
|
547
|
+
self.logger.info("Moved processed video to: %s", anonym_target_path)
|
|
548
|
+
|
|
549
|
+
# Verify the file actually exists before updating database
|
|
550
|
+
if anonym_target_path.exists():
|
|
551
|
+
try:
|
|
552
|
+
storage_root = data_paths["storage"]
|
|
553
|
+
relative_path = anonym_target_path.relative_to(storage_root)
|
|
554
|
+
# Save relative path (e.g. anonym_videos/anonym_<uuid>.mp4)
|
|
555
|
+
self.current_video.processed_file.name = str(relative_path)
|
|
556
|
+
self.current_video.save(update_fields=["processed_file"])
|
|
557
|
+
self.logger.info("Updated processed_file path to: %s", relative_path)
|
|
558
|
+
except Exception as e:
|
|
559
|
+
self.logger.error("Failed to update processed_file path: %s", e)
|
|
560
|
+
# Fallback to simple relative path
|
|
561
|
+
self.current_video.processed_file.name = f"anonym_videos/{anonym_video_filename}"
|
|
562
|
+
self.current_video.save(update_fields=['processed_file'])
|
|
563
|
+
self.logger.info(
|
|
564
|
+
"Updated processed_file path using fallback: %s",
|
|
565
|
+
f"anonym_videos/{anonym_video_filename}",
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
self.processing_context['anonymization_completed'] = True
|
|
569
|
+
else:
|
|
570
|
+
self.logger.warning("Processed video file not found after move: %s", anonym_target_path)
|
|
571
|
+
except Exception as e:
|
|
572
|
+
self.logger.error("Failed to move processed video to anonym_videos: %s", e)
|
|
573
|
+
else:
|
|
574
|
+
self.logger.warning("No processed video available - processed_file will remain empty")
|
|
575
|
+
# Leave processed_file empty/null - frontend should fall back to raw_file
|
|
576
|
+
|
|
577
|
+
# Cleanup temporary directories
|
|
578
|
+
try:
|
|
579
|
+
from endoreg_db.utils.paths import RAW_FRAME_DIR
|
|
580
|
+
shutil.rmtree(RAW_FRAME_DIR, ignore_errors=True)
|
|
581
|
+
self.logger.debug("Cleaned up temporary frames directory: %s", RAW_FRAME_DIR)
|
|
582
|
+
except Exception as e:
|
|
583
|
+
self.logger.warning("Failed to remove directory %s: %s", RAW_FRAME_DIR, e)
|
|
584
|
+
|
|
585
|
+
# Handle source file deletion - this should already be moved, but check raw_videos
|
|
586
|
+
source_path = self.processing_context['file_path']
|
|
587
|
+
if self.processing_context['delete_source'] and Path(source_path).exists():
|
|
588
|
+
try:
|
|
589
|
+
os.remove(source_path)
|
|
590
|
+
self.logger.info("Removed remaining source file: %s", source_path)
|
|
591
|
+
except Exception as e:
|
|
592
|
+
self.logger.warning("Failed to remove source file %s: %s", source_path, e)
|
|
593
|
+
|
|
594
|
+
# Mark as processed (in-memory tracking)
|
|
595
|
+
self.processed_files.add(str(self.processing_context['file_path']))
|
|
596
|
+
|
|
597
|
+
# Refresh from database and finalize state
|
|
598
|
+
with transaction.atomic():
|
|
599
|
+
self.current_video.refresh_from_db()
|
|
600
|
+
if hasattr(self.current_video, 'state') and self.processing_context.get('anonymization_completed'):
|
|
601
|
+
self.current_video.state.mark_sensitive_meta_processed(save=True)
|
|
602
|
+
|
|
603
|
+
self.logger.info("Import and anonymization completed for VideoFile UUID: %s", self.current_video.uuid)
|
|
604
|
+
self.logger.info("Raw video stored in: /data/videos")
|
|
605
|
+
self.logger.info("Processed video stored in: /data/anonym_videos")
|
|
606
|
+
|
|
607
|
+
def _create_sensitive_file(self, video_instance: "VideoFile" = None, file_path: Union[Path, str] = None) -> Path:
|
|
608
|
+
"""
|
|
609
|
+
Create a sensitive file for the given video file by copying the original file and updating the path.
|
|
610
|
+
Uses the central video instance and processing context if parameters not provided.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
video_instance: Optional video instance, defaults to self.current_video
|
|
614
|
+
file_path: Optional file path, defaults to processing_context['file_path']
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
Path: The path to the created sensitive file.
|
|
618
|
+
"""
|
|
619
|
+
video_file = video_instance or self.current_video
|
|
620
|
+
# Always use the currently stored raw file path from the model to avoid deleting external source assets
|
|
621
|
+
source_path = None
|
|
622
|
+
try:
|
|
623
|
+
if video_file and hasattr(video_file, 'raw_file') and video_file.raw_file and hasattr(video_file.raw_file, 'path'):
|
|
624
|
+
source_path = Path(video_file.raw_file.path)
|
|
625
|
+
except Exception:
|
|
626
|
+
source_path = None
|
|
627
|
+
# Fallback only if explicitly provided (do NOT default to processing_context input file)
|
|
628
|
+
if source_path is None and file_path is not None:
|
|
629
|
+
source_path = Path(file_path)
|
|
630
|
+
|
|
631
|
+
if not video_file:
|
|
632
|
+
raise ValueError("No video instance available for creating sensitive file")
|
|
633
|
+
if not source_path:
|
|
634
|
+
raise ValueError("No file path available for creating sensitive file")
|
|
635
|
+
|
|
636
|
+
if not video_file.raw_file:
|
|
637
|
+
raise ValueError("VideoFile must have a raw_file to create a sensitive file")
|
|
638
|
+
|
|
639
|
+
# Ensure the target directory exists
|
|
640
|
+
target_dir = VIDEO_DIR / 'sensitive'
|
|
641
|
+
if not target_dir.exists():
|
|
642
|
+
self.logger.info(f"Creating sensitive file directory: {target_dir}")
|
|
643
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
644
|
+
|
|
645
|
+
# Move the stored raw file into the sensitive directory within storage
|
|
646
|
+
target_file_path = target_dir / source_path.name
|
|
647
|
+
try:
|
|
648
|
+
# Prefer a move within the storage to avoid extra disk usage. This does not touch external input files.
|
|
649
|
+
shutil.move(str(source_path), str(target_file_path))
|
|
650
|
+
self.logger.info(f"Moved raw file to sensitive directory: {target_file_path}")
|
|
651
|
+
except Exception as e:
|
|
652
|
+
# Fallback to copy if move fails (e.g., cross-device or permissions), then remove only the original stored raw file
|
|
653
|
+
self.logger.warning(f"Failed to move raw file to sensitive dir, copying instead: {e}")
|
|
654
|
+
shutil.copy(str(source_path), str(target_file_path))
|
|
655
|
+
try:
|
|
656
|
+
# Remove only the stored raw file copy; never touch external input paths here
|
|
657
|
+
os.remove(source_path)
|
|
658
|
+
except FileNotFoundError:
|
|
659
|
+
pass
|
|
660
|
+
|
|
661
|
+
# Update the model to point to the sensitive file location
|
|
662
|
+
# Use relative path from storage root, like in create_from_file.py
|
|
663
|
+
try:
|
|
664
|
+
from endoreg_db.utils import data_paths
|
|
665
|
+
storage_root = data_paths["storage"]
|
|
666
|
+
relative_path = target_file_path.relative_to(storage_root)
|
|
667
|
+
video_file.raw_file.name = str(relative_path)
|
|
668
|
+
video_file.save(update_fields=['raw_file'])
|
|
669
|
+
self.logger.info(f"Updated video.raw_file to point to sensitive location: {relative_path}")
|
|
670
|
+
except Exception as e:
|
|
671
|
+
# Fallback to absolute path conversion if relative path fails
|
|
672
|
+
self.logger.warning(f"Failed to set relative path, using fallback: {e}")
|
|
673
|
+
video_file.raw_file.name = f"videos/sensitive/{target_file_path.name}"
|
|
674
|
+
video_file.save(update_fields=['raw_file'])
|
|
675
|
+
self.logger.info(f"Updated video.raw_file using fallback method: videos/sensitive/{target_file_path.name}")
|
|
676
|
+
|
|
677
|
+
# Important: Do NOT remove the original input asset passed to the service here.
|
|
678
|
+
# Source file cleanup for external inputs is handled by create_from_file via delete_source flag.
|
|
679
|
+
|
|
680
|
+
self.logger.info(f"Created sensitive file for {video_file.uuid} at {target_file_path}")
|
|
681
|
+
return target_file_path
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _ensure_frame_cleaning_available(self):
|
|
687
|
+
"""
|
|
688
|
+
Ensure frame cleaning modules are available by adding lx-anonymizer to path.
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
Tuple of (availability_flag, FrameCleaner_class, ReportReader_class)
|
|
692
|
+
"""
|
|
693
|
+
try:
|
|
694
|
+
# Check if we can find the lx-anonymizer directory
|
|
695
|
+
from importlib import resources
|
|
696
|
+
lx_anonymizer_path = resources.files("lx_anonymizer")
|
|
697
|
+
|
|
698
|
+
if lx_anonymizer_path.exists():
|
|
699
|
+
# Add to Python path temporarily
|
|
700
|
+
if str(lx_anonymizer_path) not in sys.path:
|
|
701
|
+
sys.path.insert(0, str(lx_anonymizer_path))
|
|
702
|
+
|
|
703
|
+
# Try simple import
|
|
704
|
+
from lx_anonymizer import FrameCleaner, ReportReader
|
|
705
|
+
|
|
706
|
+
self.logger.info("Successfully imported lx_anonymizer modules")
|
|
707
|
+
|
|
708
|
+
# Remove from path to avoid conflicts
|
|
709
|
+
if str(lx_anonymizer_path) in sys.path:
|
|
710
|
+
sys.path.remove(str(lx_anonymizer_path))
|
|
711
|
+
|
|
712
|
+
return True, FrameCleaner, ReportReader
|
|
713
|
+
|
|
714
|
+
else:
|
|
715
|
+
self.logger.warning(f"lx-anonymizer path not found: {lx_anonymizer_path}")
|
|
716
|
+
|
|
717
|
+
except Exception as e:
|
|
718
|
+
self.logger.warning(f"Frame cleaning not available: {e}")
|
|
719
|
+
|
|
720
|
+
return False, None, None
|
|
721
|
+
|
|
722
|
+
def _get_processor_roi_info(self):
|
|
723
|
+
"""Get processor ROI information for masking."""
|
|
724
|
+
processor_roi = None
|
|
725
|
+
endoscope_roi = None
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
if self.current_video.video_meta and self.current_video.video_meta.processor:
|
|
729
|
+
processor = getattr(self.current_video.video_meta, "processor", None)
|
|
730
|
+
|
|
731
|
+
# Get the endoscope ROI for masking
|
|
732
|
+
endoscope_roi = processor.get_roi_endoscope_image()
|
|
733
|
+
|
|
734
|
+
# Get all processor ROIs for comprehensive masking
|
|
735
|
+
processor_roi = {
|
|
736
|
+
'endoscope_image': endoscope_roi,
|
|
737
|
+
'patient_first_name': processor.get_roi_patient_first_name(),
|
|
738
|
+
'patient_last_name': processor.get_roi_patient_last_name(),
|
|
739
|
+
'patient_dob': processor.get_roi_patient_dob(),
|
|
740
|
+
'examination_date': processor.get_roi_examination_date(),
|
|
741
|
+
'examination_time': processor.get_roi_examination_time(),
|
|
742
|
+
'endoscope_type': processor.get_roi_endoscope_type(),
|
|
743
|
+
'endoscopy_sn': processor.get_roi_endoscopy_sn(),
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
self.logger.info(f"Retrieved processor ROI information: endoscope_roi={endoscope_roi}")
|
|
747
|
+
else:
|
|
748
|
+
self.logger.warning(f"No processor found for video {self.current_video.uuid}, proceeding without ROI masking")
|
|
749
|
+
|
|
750
|
+
except Exception as e:
|
|
751
|
+
self.logger.error(f"Failed to retrieve processor ROI information: {e}")
|
|
752
|
+
# Continue without ROI - don't fail the entire import process
|
|
753
|
+
|
|
754
|
+
return processor_roi, endoscope_roi
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _ensure_default_patient_data(self, video_instance: "VideoFile" = None) -> None:
|
|
758
|
+
"""
|
|
759
|
+
Ensure video has minimum required patient data in SensitiveMeta.
|
|
760
|
+
Creates default values if data is missing after OCR processing.
|
|
761
|
+
Uses the central video instance if parameter not provided.
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
video_instance: Optional video instance, defaults to self.current_video
|
|
765
|
+
"""
|
|
766
|
+
video_file = video_instance or self.current_video
|
|
767
|
+
|
|
768
|
+
if not video_file:
|
|
769
|
+
raise ValueError("No video instance available for ensuring patient data")
|
|
770
|
+
|
|
771
|
+
if not video_file.sensitive_meta:
|
|
772
|
+
self.logger.info(f"No SensitiveMeta found for video {video_file.uuid}, creating default")
|
|
773
|
+
|
|
774
|
+
# Create default SensitiveMeta with placeholder data
|
|
775
|
+
default_data = {
|
|
776
|
+
"patient_first_name": "Patient",
|
|
777
|
+
"patient_last_name": "Unknown",
|
|
778
|
+
"patient_dob": date(1990, 1, 1), # Default DOB
|
|
779
|
+
"examination_date": date.today(),
|
|
780
|
+
"center_name": video_file.center.name if video_file.center else "university_hospital_wuerzburg"
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
try:
|
|
784
|
+
sensitive_meta = SensitiveMeta.create_from_dict(default_data)
|
|
785
|
+
video_file.sensitive_meta = sensitive_meta
|
|
786
|
+
video_file.save(update_fields=['sensitive_meta'])
|
|
787
|
+
|
|
788
|
+
# Mark sensitive meta as processed after creating default data
|
|
789
|
+
state = video_file.get_or_create_state()
|
|
790
|
+
state.mark_sensitive_meta_processed(save=True)
|
|
791
|
+
|
|
792
|
+
self.logger.info(f"Created default SensitiveMeta for video {video_file.uuid}")
|
|
793
|
+
except Exception as e:
|
|
794
|
+
self.logger.error(f"Failed to create default SensitiveMeta for video {video_file.uuid}: {e}")
|
|
795
|
+
return
|
|
796
|
+
|
|
797
|
+
else:
|
|
798
|
+
# Update existing SensitiveMeta with missing fields
|
|
799
|
+
update_needed = False
|
|
800
|
+
update_data = {}
|
|
801
|
+
|
|
802
|
+
if not video_file.sensitive_meta.patient_first_name:
|
|
803
|
+
update_data["patient_first_name"] = "Patient"
|
|
804
|
+
update_needed = True
|
|
805
|
+
|
|
806
|
+
if not video_file.sensitive_meta.patient_last_name:
|
|
807
|
+
update_data["patient_last_name"] = "Unknown"
|
|
808
|
+
update_needed = True
|
|
809
|
+
|
|
810
|
+
if not video_file.sensitive_meta.patient_dob:
|
|
811
|
+
update_data["patient_dob"] = date(1990, 1, 1)
|
|
812
|
+
update_needed = True
|
|
813
|
+
|
|
814
|
+
if not video_file.sensitive_meta.examination_date:
|
|
815
|
+
update_data["examination_date"] = date.today()
|
|
816
|
+
update_needed = True
|
|
817
|
+
|
|
818
|
+
if update_needed:
|
|
819
|
+
try:
|
|
820
|
+
video_file.sensitive_meta.update_from_dict(update_data)
|
|
821
|
+
|
|
822
|
+
# Mark sensitive meta as processed after updating missing fields
|
|
823
|
+
state = video_file.get_or_create_state()
|
|
824
|
+
state.mark_sensitive_meta_processed(save=True)
|
|
825
|
+
|
|
826
|
+
self.logger.info(f"Updated missing SensitiveMeta fields for video {video_file.uuid}: {list(update_data.keys())}")
|
|
827
|
+
except Exception as e:
|
|
828
|
+
self.logger.error(f"Failed to update SensitiveMeta for video {video_file.uuid}: {e}")
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _ensure_frame_cleaning_available(self):
|
|
832
|
+
"""
|
|
833
|
+
Ensure frame cleaning modules are available by adding lx-anonymizer to path.
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
Tuple of (availability_flag, FrameCleaner_class, ReportReader_class)
|
|
837
|
+
"""
|
|
838
|
+
try:
|
|
839
|
+
# Check if we can find the lx-anonymizer directory
|
|
840
|
+
from importlib import resources
|
|
841
|
+
lx_anonymizer_path = resources.files("lx_anonymizer")
|
|
842
|
+
|
|
843
|
+
if lx_anonymizer_path.exists():
|
|
844
|
+
# Add to Python path temporarily
|
|
845
|
+
if str(lx_anonymizer_path) not in sys.path:
|
|
846
|
+
sys.path.insert(0, str(lx_anonymizer_path))
|
|
847
|
+
|
|
848
|
+
# Try simple import
|
|
849
|
+
from lx_anonymizer import FrameCleaner, ReportReader
|
|
850
|
+
|
|
851
|
+
self.logger.info("Successfully imported lx_anonymizer modules")
|
|
852
|
+
|
|
853
|
+
# Remove from path to avoid conflicts
|
|
854
|
+
if str(lx_anonymizer_path) in sys.path:
|
|
855
|
+
sys.path.remove(str(lx_anonymizer_path))
|
|
856
|
+
|
|
857
|
+
return True, FrameCleaner, ReportReader
|
|
858
|
+
|
|
859
|
+
else:
|
|
860
|
+
self.logger.warning(f"lx-anonymizer path not found: {lx_anonymizer_path}")
|
|
861
|
+
|
|
862
|
+
except Exception as e:
|
|
863
|
+
self.logger.warning(f"Frame cleaning not available: {e}")
|
|
864
|
+
|
|
865
|
+
return False, None, None
|
|
866
|
+
|
|
867
|
+
def _get_processor_roi_info(self):
|
|
868
|
+
"""Get processor ROI information for masking."""
|
|
869
|
+
processor_roi = None
|
|
870
|
+
endoscope_roi = None
|
|
871
|
+
|
|
872
|
+
try:
|
|
873
|
+
if self.current_video.video_meta and self.current_video.video_meta.processor:
|
|
874
|
+
processor = getattr(self.current_video.video_meta, "processor", None)
|
|
875
|
+
|
|
876
|
+
# Get the endoscope ROI for masking
|
|
877
|
+
endoscope_roi = processor.get_roi_endoscope_image()
|
|
878
|
+
|
|
879
|
+
# Get all processor ROIs for comprehensive masking
|
|
880
|
+
processor_roi = {
|
|
881
|
+
'endoscope_image': endoscope_roi,
|
|
882
|
+
'patient_first_name': processor.get_roi_patient_first_name(),
|
|
883
|
+
'patient_last_name': processor.get_roi_patient_last_name(),
|
|
884
|
+
'patient_dob': processor.get_roi_patient_dob(),
|
|
885
|
+
'examination_date': processor.get_roi_examination_date(),
|
|
886
|
+
'examination_time': processor.get_roi_examination_time(),
|
|
887
|
+
'endoscope_type': processor.get_roi_endoscope_type(),
|
|
888
|
+
'endoscopy_sn': processor.get_roi_endoscopy_sn(),
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
self.logger.info(f"Retrieved processor ROI information: endoscope_roi={endoscope_roi}")
|
|
892
|
+
else:
|
|
893
|
+
self.logger.warning(f"No processor found for video {self.current_video.uuid}, proceeding without ROI masking")
|
|
894
|
+
|
|
895
|
+
except Exception as e:
|
|
896
|
+
self.logger.error(f"Failed to retrieve processor ROI information: {e}")
|
|
897
|
+
# Continue without ROI - don't fail the entire import process
|
|
898
|
+
|
|
899
|
+
return processor_roi, endoscope_roi
|
|
900
|
+
|
|
901
|
+
def _perform_frame_cleaning(self, FrameCleaner, processor_roi, endoscope_roi):
|
|
902
|
+
"""Perform frame cleaning and anonymization."""
|
|
903
|
+
# Instantiate frame cleaner
|
|
904
|
+
frame_cleaner = FrameCleaner()
|
|
905
|
+
|
|
906
|
+
# Prepare parameters for frame cleaning
|
|
907
|
+
raw_video_path = self.processing_context.get('raw_video_path')
|
|
908
|
+
|
|
909
|
+
if not raw_video_path or not Path(raw_video_path).exists():
|
|
910
|
+
raise RuntimeError(f"Raw video path not found: {raw_video_path}")
|
|
911
|
+
|
|
912
|
+
# Get processor name safely
|
|
913
|
+
processor = getattr(self.current_video.video_meta, "processor", None) if self.current_video.video_meta else None
|
|
914
|
+
device_name = processor.name if processor else self.processing_context['processor_name']
|
|
915
|
+
|
|
916
|
+
tmp_dir = RAW_FRAME_DIR
|
|
917
|
+
|
|
918
|
+
# Create temporary output path for cleaned video
|
|
919
|
+
video_filename = self.processing_context.get('video_filename', Path(raw_video_path).name)
|
|
920
|
+
cleaned_filename = f"cleaned_{video_filename}"
|
|
921
|
+
cleaned_video_path = Path(raw_video_path).parent / cleaned_filename
|
|
922
|
+
|
|
923
|
+
# Clean video with ROI masking (heavy I/O operation)
|
|
924
|
+
actual_cleaned_path, extracted_metadata = frame_cleaner.clean_video(
|
|
925
|
+
Path(raw_video_path),
|
|
926
|
+
self.current_video,
|
|
927
|
+
tmp_dir,
|
|
928
|
+
device_name,
|
|
929
|
+
endoscope_roi,
|
|
930
|
+
processor_roi,
|
|
931
|
+
cleaned_video_path
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
# Optional: enrich metadata using TrOCR+LLM on one random extracted frame
|
|
935
|
+
try:
|
|
936
|
+
# Prefer frames belonging to this video (UUID in path), else pick any frame
|
|
937
|
+
frame_candidates = list(RAW_FRAME_DIR.rglob("*.jpg")) + list(RAW_FRAME_DIR.rglob("*.png"))
|
|
938
|
+
video_uuid = str(self.current_video.uuid)
|
|
939
|
+
filtered = [p for p in frame_candidates if video_uuid in str(p)] or frame_candidates
|
|
940
|
+
if filtered:
|
|
941
|
+
sample_frame = random.choice(filtered)
|
|
942
|
+
ocr_text = trocr_full_image_ocr(sample_frame)
|
|
943
|
+
if ocr_text:
|
|
944
|
+
llm_metadata = frame_cleaner.extract_metadata(ocr_text)
|
|
945
|
+
if llm_metadata:
|
|
946
|
+
# Merge with already extracted frame-level metadata
|
|
947
|
+
extracted_metadata = frame_cleaner.frame_metadata_extractor.merge_metadata(
|
|
948
|
+
extracted_metadata or {}, llm_metadata
|
|
949
|
+
)
|
|
950
|
+
self.logger.info("LLM metadata extraction (random frame) successful")
|
|
951
|
+
else:
|
|
952
|
+
self.logger.info("LLM metadata extraction (random frame) found no data")
|
|
953
|
+
else:
|
|
954
|
+
self.logger.info("No text extracted by TrOCR on random frame")
|
|
955
|
+
except Exception as e:
|
|
956
|
+
self.logger.error(f"LLM metadata enrichment step failed: {e}")
|
|
957
|
+
|
|
958
|
+
# Store cleaned video path for later use in _cleanup_and_archive
|
|
959
|
+
self.processing_context['cleaned_video_path'] = actual_cleaned_path
|
|
960
|
+
self.processing_context['extracted_metadata'] = extracted_metadata
|
|
961
|
+
|
|
962
|
+
# Update sensitive metadata with extracted information
|
|
963
|
+
self._update_sensitive_metadata(extracted_metadata)
|
|
964
|
+
self.logger.info(f"Extracted metadata from frame cleaning: {extracted_metadata}")
|
|
965
|
+
|
|
966
|
+
self.logger.info(f"Frame cleaning with ROI masking completed: {actual_cleaned_path}")
|
|
967
|
+
self.logger.info("Cleaned video will be moved to anonym_videos during cleanup")
|
|
968
|
+
|
|
969
|
+
def _update_sensitive_metadata(self, extracted_metadata):
|
|
970
|
+
"""
|
|
971
|
+
Update sensitive metadata with extracted information.
|
|
972
|
+
|
|
973
|
+
SAFETY MECHANISM: Only updates fields that are empty, default values, or explicitly marked as safe to overwrite.
|
|
974
|
+
This prevents accidentally overwriting valuable manually entered or previously extracted data.
|
|
975
|
+
"""
|
|
976
|
+
if not (self.current_video.sensitive_meta and extracted_metadata):
|
|
977
|
+
return
|
|
978
|
+
|
|
979
|
+
sm = self.current_video.sensitive_meta
|
|
980
|
+
updated_fields = []
|
|
981
|
+
|
|
982
|
+
# Map extracted metadata to SensitiveMeta fields
|
|
983
|
+
metadata_mapping = {
|
|
984
|
+
'patient_first_name': 'patient_first_name',
|
|
985
|
+
'patient_last_name': 'patient_last_name',
|
|
986
|
+
'patient_dob': 'patient_dob',
|
|
987
|
+
'examination_date': 'examination_date',
|
|
988
|
+
'endoscope_type': 'endoscope_type'
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
# Define default/placeholder values that are safe to overwrite
|
|
992
|
+
SAFE_TO_OVERWRITE_VALUES = [
|
|
993
|
+
'Vorname unbekannt', # Default first name
|
|
994
|
+
'Nachname unbekannt', # Default last name
|
|
995
|
+
date(1990, 1, 1), # Default DOB
|
|
996
|
+
None, # Empty values
|
|
997
|
+
'', # Empty strings
|
|
998
|
+
'N/A', # Placeholder values
|
|
999
|
+
'Unbekanntes Gerät', # Default device name
|
|
1000
|
+
]
|
|
1001
|
+
|
|
1002
|
+
for meta_key, sm_field in metadata_mapping.items():
|
|
1003
|
+
if extracted_metadata.get(meta_key) and hasattr(sm, sm_field):
|
|
1004
|
+
old_value = getattr(sm, sm_field)
|
|
1005
|
+
new_value = extracted_metadata[meta_key]
|
|
1006
|
+
|
|
1007
|
+
# Enhanced safety check: Only update if current value is safe to overwrite
|
|
1008
|
+
if new_value and (old_value in SAFE_TO_OVERWRITE_VALUES):
|
|
1009
|
+
self.logger.info(f"Updating {sm_field} from '{old_value}' to '{new_value}' for video {self.current_video.uuid}")
|
|
1010
|
+
setattr(sm, sm_field, new_value)
|
|
1011
|
+
updated_fields.append(sm_field)
|
|
1012
|
+
elif new_value and old_value and old_value not in SAFE_TO_OVERWRITE_VALUES:
|
|
1013
|
+
self.logger.info(f"Preserving existing {sm_field} value '{old_value}' (not overwriting with '{new_value}') for video {self.current_video.uuid}")
|
|
1014
|
+
|
|
1015
|
+
if updated_fields:
|
|
1016
|
+
sm.save(update_fields=updated_fields)
|
|
1017
|
+
self.logger.info(f"Updated SensitiveMeta fields for video {self.current_video.uuid}: {updated_fields}")
|
|
1018
|
+
|
|
1019
|
+
# Mark sensitive meta as processed after successful update
|
|
1020
|
+
self.current_video.state.mark_sensitive_meta_processed(save=True)
|
|
1021
|
+
self.logger.info(f"Marked sensitive metadata as processed for video {self.current_video.uuid}")
|
|
1022
|
+
else:
|
|
1023
|
+
self.logger.info(f"No SensitiveMeta fields updated for video {self.current_video.uuid} - all existing values preserved")
|
|
1024
|
+
|
|
1025
|
+
def _signal_completion(self):
|
|
1026
|
+
"""Signal completion to the tracking system."""
|
|
1027
|
+
try:
|
|
1028
|
+
video_processing_complete = (
|
|
1029
|
+
self.current_video.sensitive_meta is not None and
|
|
1030
|
+
self.current_video.video_meta is not None and
|
|
1031
|
+
self.current_video.raw_file and
|
|
1032
|
+
hasattr(self.current_video.raw_file, 'path') and
|
|
1033
|
+
Path(self.current_video.raw_file.path).exists()
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
if video_processing_complete:
|
|
1037
|
+
self.logger.info(f"Video {self.current_video.uuid} processing completed successfully - ready for validation")
|
|
1038
|
+
|
|
1039
|
+
# Update completion flags if they exist
|
|
1040
|
+
completion_fields = []
|
|
1041
|
+
for field_name in ['import_completed', 'processing_complete', 'ready_for_validation']:
|
|
1042
|
+
if hasattr(self.current_video, field_name):
|
|
1043
|
+
setattr(self.current_video, field_name, True)
|
|
1044
|
+
completion_fields.append(field_name)
|
|
1045
|
+
|
|
1046
|
+
if completion_fields:
|
|
1047
|
+
self.current_video.save(update_fields=completion_fields)
|
|
1048
|
+
self.logger.info(f"Updated completion flags: {completion_fields}")
|
|
1049
|
+
else:
|
|
1050
|
+
self.logger.warning(f"Video {self.current_video.uuid} processing incomplete - missing required components")
|
|
1051
|
+
|
|
1052
|
+
except Exception as e:
|
|
1053
|
+
self.logger.warning(f"Failed to signal completion status: {e}")
|
|
1054
|
+
|
|
1055
|
+
def _cleanup_on_error(self):
|
|
1056
|
+
"""Cleanup processing context on error."""
|
|
1057
|
+
if self.current_video and hasattr(self.current_video, 'state'):
|
|
1058
|
+
try:
|
|
1059
|
+
if self.processing_context.get('processing_started'):
|
|
1060
|
+
self.current_video.state.frames_extracted = False
|
|
1061
|
+
self.current_video.state.frames_initialized = False
|
|
1062
|
+
self.current_video.state.video_meta_extracted = False
|
|
1063
|
+
self.current_video.state.text_meta_extracted = False
|
|
1064
|
+
self.current_video.state.save()
|
|
1065
|
+
except Exception as e:
|
|
1066
|
+
self.logger.warning(f"Error during cleanup: {e}")
|
|
1067
|
+
|
|
1068
|
+
def _cleanup_processing_context(self):
|
|
1069
|
+
"""
|
|
1070
|
+
Cleanup processing context and release file lock.
|
|
1071
|
+
|
|
1072
|
+
This method is always called in the finally block of import_and_anonymize()
|
|
1073
|
+
to ensure the file lock is released even if processing fails.
|
|
1074
|
+
"""
|
|
1075
|
+
try:
|
|
1076
|
+
# Release file lock if it was acquired
|
|
1077
|
+
lock_context = self.processing_context.get('_lock_context')
|
|
1078
|
+
if lock_context is not None:
|
|
1079
|
+
try:
|
|
1080
|
+
lock_context.__exit__(None, None, None)
|
|
1081
|
+
self.logger.info("Released file lock")
|
|
1082
|
+
except Exception as e:
|
|
1083
|
+
self.logger.warning(f"Error releasing file lock: {e}")
|
|
1084
|
+
|
|
1085
|
+
# Remove file from processed set if processing failed
|
|
1086
|
+
file_path = self.processing_context.get('file_path')
|
|
1087
|
+
if file_path and not self.processing_context.get('anonymization_completed'):
|
|
1088
|
+
file_path_str = str(file_path)
|
|
1089
|
+
if file_path_str in self.processed_files:
|
|
1090
|
+
self.processed_files.remove(file_path_str)
|
|
1091
|
+
self.logger.info(f"Removed {file_path_str} from processed files (failed processing)")
|
|
1092
|
+
|
|
1093
|
+
except Exception as e:
|
|
1094
|
+
self.logger.warning(f"Error during context cleanup: {e}")
|
|
1095
|
+
finally:
|
|
1096
|
+
# Reset context
|
|
1097
|
+
self.current_video = None
|
|
1098
|
+
self.processing_context = {}
|
|
1099
|
+
|
|
1100
|
+
# Convenience function for callers/tests that expect a module-level import_and_anonymize
|
|
1101
|
+
def import_and_anonymize(
|
|
1102
|
+
file_path,
|
|
1103
|
+
center_name: str,
|
|
1104
|
+
processor_name: str,
|
|
1105
|
+
save_video: bool = True,
|
|
1106
|
+
delete_source: bool = False,
|
|
1107
|
+
) -> "VideoFile":
|
|
1108
|
+
"""Module-level helper that instantiates VideoImportService and runs import_and_anonymize.
|
|
1109
|
+
Kept for backward compatibility with callers that import this function directly.
|
|
1110
|
+
"""
|
|
1111
|
+
service = VideoImportService()
|
|
1112
|
+
return service.import_and_anonymize(
|
|
1113
|
+
file_path=file_path,
|
|
1114
|
+
center_name=center_name,
|
|
1115
|
+
processor_name=processor_name,
|
|
1116
|
+
save_video=save_video,
|
|
1117
|
+
delete_source=delete_source,
|
|
1118
|
+
)
|