endoreg-db 0.8.6.1__py3-none-any.whl → 0.8.8.9__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/authz/auth.py +74 -0
- endoreg_db/authz/backends.py +168 -0
- endoreg_db/authz/management/commands/list_routes.py +18 -0
- endoreg_db/authz/middleware.py +83 -0
- endoreg_db/authz/permissions.py +127 -0
- endoreg_db/authz/policy.py +218 -0
- endoreg_db/authz/views_auth.py +66 -0
- endoreg_db/config/env.py +13 -8
- endoreg_db/data/__init__.py +2 -11
- endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +3 -3
- endoreg_db/data/event_classification/data.yaml +4 -0
- endoreg_db/data/event_classification_choice/data.yaml +9 -0
- endoreg_db/data/examination/examinations/data.yaml +114 -14
- endoreg_db/data/examination/time-type/data.yaml +0 -3
- endoreg_db/data/examination_indication/endoscopy.yaml +108 -173
- endoreg_db/data/examination_indication_classification/endoscopy.yaml +0 -70
- endoreg_db/data/examination_indication_classification_choice/endoscopy.yaml +33 -37
- endoreg_db/data/finding/00_generic.yaml +35 -0
- endoreg_db/data/finding/00_generic_complication.yaml +9 -0
- endoreg_db/data/finding/01_gastroscopy_baseline.yaml +88 -0
- endoreg_db/data/finding/01_gastroscopy_observation.yaml +113 -0
- endoreg_db/data/finding/02_colonoscopy_baseline.yaml +53 -0
- endoreg_db/data/finding/02_colonoscopy_hidden.yaml +119 -0
- endoreg_db/data/finding/02_colonoscopy_observation.yaml +152 -0
- endoreg_db/data/finding_classification/00_generic.yaml +44 -0
- endoreg_db/data/finding_classification/00_generic_histology.yaml +28 -0
- endoreg_db/data/finding_classification/00_generic_lesion.yaml +52 -0
- endoreg_db/data/finding_classification/02_colonoscopy_baseline.yaml +83 -0
- endoreg_db/data/finding_classification/02_colonoscopy_histology.yaml +13 -0
- endoreg_db/data/finding_classification/02_colonoscopy_other.yaml +12 -0
- endoreg_db/data/finding_classification/02_colonoscopy_polyp.yaml +101 -0
- endoreg_db/data/finding_classification_choice/{yes_no_na.yaml → 00_generic.yaml} +5 -1
- endoreg_db/data/finding_classification_choice/{examination_setting_generic_types.yaml → 00_generic_baseline.yaml} +10 -2
- endoreg_db/data/finding_classification_choice/{complication_generic_types.yaml → 00_generic_complication.yaml} +1 -1
- endoreg_db/data/finding_classification_choice/{histology.yaml → 00_generic_histology.yaml} +1 -4
- endoreg_db/data/finding_classification_choice/00_generic_lesion.yaml +158 -0
- endoreg_db/data/finding_classification_choice/{bowel_preparation.yaml → 02_colonoscopy_bowel_preparation.yaml} +1 -30
- endoreg_db/data/finding_classification_choice/{colonoscopy_not_complete_reason.yaml → 02_colonoscopy_generic.yaml} +1 -1
- endoreg_db/data/finding_classification_choice/{histology_polyp.yaml → 02_colonoscopy_histology.yaml} +1 -1
- endoreg_db/data/finding_classification_choice/{colonoscopy_location.yaml → 02_colonoscopy_location.yaml} +23 -4
- endoreg_db/data/finding_classification_choice/02_colonoscopy_other.yaml +34 -0
- endoreg_db/data/finding_classification_choice/02_colonoscopy_polyp_advanced_imaging.yaml +76 -0
- endoreg_db/data/finding_classification_choice/{colon_lesion_paris.yaml → 02_colonoscopy_polyp_morphology.yaml} +26 -8
- endoreg_db/data/finding_classification_choice/02_colonoscopy_size.yaml +27 -0
- endoreg_db/data/finding_classification_type/{colonoscopy_basic.yaml → 00_generic.yaml} +18 -13
- endoreg_db/data/finding_classification_type/02_colonoscopy.yaml +9 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy.yaml +59 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy_ablation.yaml +44 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy_bleeding.yaml +55 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy_resection.yaml +85 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy_stenosis.yaml +17 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy_stent.yaml +9 -0
- endoreg_db/data/finding_intervention/01_gastroscopy.yaml +19 -0
- endoreg_db/data/finding_intervention/04_eus.yaml +39 -0
- endoreg_db/data/finding_intervention/05_ercp.yaml +3 -0
- endoreg_db/data/finding_type/data.yaml +8 -12
- endoreg_db/data/requirement/01_patient_data.yaml +93 -0
- endoreg_db/data/requirement/old/colon_polyp_intervention.yaml +49 -0
- endoreg_db/data/requirement/old/coloreg_colon_polyp.yaml +49 -0
- endoreg_db/data/requirement_operator/new_operators.yaml +36 -0
- endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +29 -12
- endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
- endoreg_db/data/requirement_set/{endoscopy_bleeding_risk.yaml → 02_endoscopy_bleeding_risk.yaml} +0 -6
- endoreg_db/data/requirement_set/90_coloreg.yaml +190 -0
- endoreg_db/data/requirement_set/_old_ +109 -0
- endoreg_db/data/requirement_set_type/data.yaml +21 -0
- endoreg_db/data/setup_config.yaml +4 -4
- endoreg_db/data/tag/requirement_set_tags.yaml +21 -0
- endoreg_db/exceptions.py +4 -2
- endoreg_db/forms/examination_form.py +1 -1
- endoreg_db/helpers/data_loader.py +125 -53
- endoreg_db/helpers/default_objects.py +116 -81
- endoreg_db/import_files/__init__.py +27 -0
- endoreg_db/import_files/context/__init__.py +7 -0
- endoreg_db/import_files/context/default_sensitive_meta.py +81 -0
- endoreg_db/import_files/context/ensure_center.py +17 -0
- endoreg_db/import_files/context/file_lock.py +66 -0
- endoreg_db/import_files/context/import_context.py +43 -0
- endoreg_db/import_files/context/validate_directories.py +56 -0
- endoreg_db/import_files/file_storage/__init__.py +15 -0
- endoreg_db/import_files/file_storage/create_report_file.py +76 -0
- endoreg_db/import_files/file_storage/create_video_file.py +75 -0
- endoreg_db/import_files/file_storage/sensitive_meta_storage.py +39 -0
- endoreg_db/import_files/file_storage/state_management.py +400 -0
- endoreg_db/import_files/file_storage/storage.py +36 -0
- endoreg_db/import_files/import_service.md +26 -0
- endoreg_db/import_files/processing/__init__.py +11 -0
- endoreg_db/import_files/processing/report_processing/report_anonymization.py +94 -0
- endoreg_db/import_files/processing/sensitive_meta_adapter.py +51 -0
- endoreg_db/import_files/processing/video_processing/video_anonymization.py +107 -0
- endoreg_db/import_files/processing/video_processing/video_cleanup_on_error.py +119 -0
- endoreg_db/import_files/pseudonymization/fake.py +52 -0
- endoreg_db/import_files/pseudonymization/k_anonymity.py +182 -0
- endoreg_db/import_files/pseudonymization/k_pseudonymity.py +128 -0
- endoreg_db/import_files/report_import_service.py +141 -0
- endoreg_db/import_files/video_import_service.py +150 -0
- endoreg_db/management/commands/create_model_meta_from_huggingface.py +21 -10
- endoreg_db/management/commands/create_multilabel_model_meta.py +299 -129
- endoreg_db/management/commands/import_report.py +130 -65
- endoreg_db/management/commands/import_video.py +9 -10
- endoreg_db/management/commands/import_video_with_classification.py +2 -2
- endoreg_db/management/commands/list_routes.py +18 -0
- endoreg_db/management/commands/load_ai_model_data.py +5 -5
- endoreg_db/management/commands/load_ai_model_label_data.py +9 -7
- endoreg_db/management/commands/load_base_db_data.py +5 -134
- endoreg_db/management/commands/load_center_data.py +12 -12
- endoreg_db/management/commands/load_contraindication_data.py +14 -16
- endoreg_db/management/commands/load_disease_classification_choices_data.py +15 -18
- endoreg_db/management/commands/load_disease_classification_data.py +15 -18
- endoreg_db/management/commands/load_disease_data.py +25 -28
- endoreg_db/management/commands/load_endoscope_data.py +20 -27
- endoreg_db/management/commands/load_event_data.py +14 -16
- endoreg_db/management/commands/load_examination_data.py +31 -44
- endoreg_db/management/commands/load_examination_indication_data.py +20 -21
- endoreg_db/management/commands/load_finding_data.py +52 -80
- endoreg_db/management/commands/load_information_source.py +21 -23
- endoreg_db/management/commands/load_lab_value_data.py +17 -26
- endoreg_db/management/commands/load_medication_data.py +13 -12
- endoreg_db/management/commands/load_organ_data.py +15 -19
- endoreg_db/management/commands/load_pdf_type_data.py +19 -18
- endoreg_db/management/commands/load_profession_data.py +14 -17
- endoreg_db/management/commands/load_qualification_data.py +20 -23
- endoreg_db/management/commands/load_report_reader_flag_data.py +17 -19
- endoreg_db/management/commands/load_requirement_data.py +62 -39
- endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
- endoreg_db/management/commands/load_risk_data.py +7 -6
- endoreg_db/management/commands/load_shift_data.py +20 -23
- endoreg_db/management/commands/load_tag_data.py +8 -11
- endoreg_db/management/commands/load_unit_data.py +17 -19
- endoreg_db/management/commands/setup_endoreg_db.py +3 -3
- endoreg_db/management/commands/start_filewatcher.py +46 -37
- endoreg_db/management/commands/storage_management.py +271 -203
- endoreg_db/management/commands/validate_video_files.py +1 -5
- endoreg_db/migrations/0001_initial.py +297 -250
- endoreg_db/models/__init__.py +78 -123
- endoreg_db/models/administration/__init__.py +21 -42
- endoreg_db/models/administration/ai/active_model.py +2 -2
- endoreg_db/models/administration/ai/ai_model.py +7 -6
- endoreg_db/models/administration/case/__init__.py +1 -15
- endoreg_db/models/administration/case/case.py +3 -3
- endoreg_db/models/administration/case/case_template/__init__.py +2 -14
- endoreg_db/models/administration/case/case_template/case_template.py +2 -124
- endoreg_db/models/administration/case/case_template/case_template_rule.py +2 -268
- endoreg_db/models/administration/case/case_template/case_template_rule_value.py +2 -85
- endoreg_db/models/administration/case/case_template/case_template_type.py +2 -25
- endoreg_db/models/administration/center/center.py +33 -19
- endoreg_db/models/administration/center/center_product.py +12 -9
- endoreg_db/models/administration/center/center_resource.py +25 -19
- endoreg_db/models/administration/center/center_shift.py +21 -17
- endoreg_db/models/administration/center/center_waste.py +16 -8
- endoreg_db/models/administration/person/__init__.py +2 -0
- endoreg_db/models/administration/person/employee/employee.py +10 -5
- endoreg_db/models/administration/person/employee/employee_qualification.py +9 -4
- endoreg_db/models/administration/person/employee/employee_type.py +12 -6
- endoreg_db/models/administration/person/examiner/examiner.py +13 -11
- endoreg_db/models/administration/person/patient/__init__.py +2 -0
- endoreg_db/models/administration/person/patient/patient.py +129 -100
- endoreg_db/models/administration/person/patient/patient_external_id.py +37 -0
- endoreg_db/models/administration/person/person.py +4 -0
- endoreg_db/models/administration/person/profession/__init__.py +8 -4
- endoreg_db/models/administration/person/user/portal_user_information.py +11 -7
- endoreg_db/models/administration/product/product.py +20 -15
- endoreg_db/models/administration/product/product_material.py +17 -18
- endoreg_db/models/administration/product/product_weight.py +12 -8
- endoreg_db/models/administration/product/reference_product.py +23 -55
- endoreg_db/models/administration/qualification/qualification.py +7 -3
- endoreg_db/models/administration/qualification/qualification_type.py +7 -3
- endoreg_db/models/administration/shift/scheduled_days.py +8 -5
- endoreg_db/models/administration/shift/shift.py +16 -12
- endoreg_db/models/administration/shift/shift_type.py +23 -31
- endoreg_db/models/label/__init__.py +8 -9
- endoreg_db/models/label/annotation/image_classification.py +10 -9
- endoreg_db/models/label/annotation/video_segmentation_annotation.py +23 -28
- endoreg_db/models/label/label.py +15 -15
- endoreg_db/models/label/label_set.py +19 -6
- endoreg_db/models/label/label_type.py +1 -1
- endoreg_db/models/label/label_video_segment/_create_from_video.py +5 -8
- endoreg_db/models/label/label_video_segment/label_video_segment.py +98 -102
- endoreg_db/models/label/video_segmentation_label.py +4 -0
- endoreg_db/models/label/video_segmentation_labelset.py +4 -3
- endoreg_db/models/media/frame/frame.py +22 -22
- endoreg_db/models/media/pdf/raw_pdf.py +194 -194
- endoreg_db/models/media/pdf/report_file.py +25 -29
- endoreg_db/models/media/pdf/report_reader/report_reader_config.py +55 -47
- endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
- endoreg_db/models/media/processing_history/__init__.py +5 -0
- endoreg_db/models/media/processing_history/processing_history.py +96 -0
- endoreg_db/models/media/video/__init__.py +1 -0
- endoreg_db/models/media/video/create_from_file.py +139 -77
- endoreg_db/models/media/video/pipe_2.py +8 -9
- endoreg_db/models/media/video/video_file.py +174 -112
- endoreg_db/models/media/video/video_file_ai.py +288 -74
- endoreg_db/models/media/video/video_file_anonymize.py +38 -38
- endoreg_db/models/media/video/video_file_frames/__init__.py +3 -1
- endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +7 -9
- endoreg_db/models/media/video/video_file_frames/_delete_frames.py +9 -8
- endoreg_db/models/media/video/video_file_frames/_extract_frames.py +38 -45
- endoreg_db/models/media/video/video_file_frames/_get_frame.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +4 -18
- endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -3
- endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +7 -6
- endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_get_frames.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +15 -25
- endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +26 -23
- endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +23 -14
- endoreg_db/models/media/video/video_file_io.py +113 -61
- endoreg_db/models/media/video/video_file_meta/get_crop_template.py +3 -3
- endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +5 -3
- endoreg_db/models/media/video/video_file_meta/get_fps.py +37 -34
- endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +19 -25
- endoreg_db/models/media/video/video_file_meta/text_meta.py +41 -38
- endoreg_db/models/media/video/video_file_meta/video_meta.py +14 -7
- endoreg_db/models/media/video/video_file_segments.py +24 -17
- endoreg_db/models/media/video/video_metadata.py +19 -35
- endoreg_db/models/media/video/video_processing.py +96 -95
- endoreg_db/models/medical/contraindication/README.md +1 -0
- endoreg_db/models/medical/contraindication/__init__.py +13 -3
- endoreg_db/models/medical/disease.py +22 -16
- endoreg_db/models/medical/event.py +31 -18
- endoreg_db/models/medical/examination/__init__.py +13 -6
- endoreg_db/models/medical/examination/examination.py +39 -20
- endoreg_db/models/medical/examination/examination_indication.py +30 -95
- endoreg_db/models/medical/examination/examination_time.py +23 -8
- endoreg_db/models/medical/examination/examination_time_type.py +9 -6
- endoreg_db/models/medical/examination/examination_type.py +3 -4
- endoreg_db/models/medical/finding/finding.py +32 -40
- endoreg_db/models/medical/finding/finding_classification.py +42 -72
- endoreg_db/models/medical/finding/finding_intervention.py +25 -22
- endoreg_db/models/medical/finding/finding_type.py +13 -12
- endoreg_db/models/medical/hardware/endoscope.py +26 -26
- endoreg_db/models/medical/hardware/endoscopy_processor.py +2 -2
- endoreg_db/models/medical/laboratory/lab_value.py +62 -91
- endoreg_db/models/medical/medication/medication.py +22 -10
- endoreg_db/models/medical/medication/medication_indication.py +29 -3
- endoreg_db/models/medical/medication/medication_indication_type.py +25 -14
- endoreg_db/models/medical/medication/medication_intake_time.py +31 -19
- endoreg_db/models/medical/medication/medication_schedule.py +27 -16
- endoreg_db/models/medical/organ/__init__.py +15 -12
- endoreg_db/models/medical/patient/medication_examples.py +6 -6
- endoreg_db/models/medical/patient/patient_disease.py +20 -23
- endoreg_db/models/medical/patient/patient_event.py +19 -22
- endoreg_db/models/medical/patient/patient_examination.py +48 -54
- endoreg_db/models/medical/patient/patient_examination_indication.py +16 -14
- endoreg_db/models/medical/patient/patient_finding.py +122 -139
- endoreg_db/models/medical/patient/patient_finding_classification.py +44 -49
- endoreg_db/models/medical/patient/patient_finding_intervention.py +8 -19
- endoreg_db/models/medical/patient/patient_lab_sample.py +28 -23
- endoreg_db/models/medical/patient/patient_lab_value.py +82 -89
- endoreg_db/models/medical/patient/patient_medication.py +27 -38
- endoreg_db/models/medical/patient/patient_medication_schedule.py +28 -36
- endoreg_db/models/medical/risk/risk.py +7 -6
- endoreg_db/models/medical/risk/risk_type.py +8 -5
- endoreg_db/models/metadata/model_meta.py +60 -29
- endoreg_db/models/metadata/model_meta_logic.py +125 -18
- endoreg_db/models/metadata/pdf_meta.py +31 -24
- endoreg_db/models/metadata/sensitive_meta.py +105 -85
- endoreg_db/models/metadata/sensitive_meta_logic.py +198 -103
- endoreg_db/models/metadata/video_meta.py +51 -31
- endoreg_db/models/metadata/video_prediction_logic.py +16 -23
- endoreg_db/models/metadata/video_prediction_meta.py +29 -33
- endoreg_db/models/other/distribution/date_value_distribution.py +89 -29
- endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +21 -5
- endoreg_db/models/other/distribution/numeric_value_distribution.py +114 -53
- endoreg_db/models/other/distribution/single_categorical_value_distribution.py +4 -3
- endoreg_db/models/other/emission/emission_factor.py +18 -8
- endoreg_db/models/other/gender.py +10 -5
- endoreg_db/models/other/information_source.py +50 -29
- endoreg_db/models/other/material.py +9 -5
- endoreg_db/models/other/resource.py +6 -4
- endoreg_db/models/other/tag.py +10 -5
- endoreg_db/models/other/transport_route.py +13 -8
- endoreg_db/models/other/unit.py +10 -6
- endoreg_db/models/other/waste.py +6 -5
- endoreg_db/models/report/report.py +6 -0
- endoreg_db/models/requirement/requirement.py +329 -361
- endoreg_db/models/requirement/requirement_error.py +85 -0
- endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
- endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +3 -6
- endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +90 -64
- endoreg_db/models/requirement/requirement_operator.py +103 -112
- endoreg_db/models/requirement/requirement_set.py +74 -57
- endoreg_db/models/state/__init__.py +4 -4
- endoreg_db/models/state/abstract.py +2 -2
- endoreg_db/models/state/anonymization.py +12 -0
- endoreg_db/models/state/audit_ledger.py +49 -51
- endoreg_db/models/state/label_video_segment.py +9 -0
- endoreg_db/models/state/raw_pdf.py +101 -68
- endoreg_db/models/state/sensitive_meta.py +6 -2
- endoreg_db/models/state/video.py +110 -90
- endoreg_db/models/upload_job.py +35 -34
- endoreg_db/models/utils.py +28 -25
- endoreg_db/queries/__init__.py +3 -1
- endoreg_db/root_urls.py +21 -2
- endoreg_db/schemas/examination_evaluation.py +1 -1
- endoreg_db/serializers/__init__.py +2 -10
- endoreg_db/serializers/anonymization.py +18 -10
- endoreg_db/serializers/label_video_segment/label_video_segment.py +2 -29
- endoreg_db/serializers/meta/__init__.py +1 -6
- endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
- endoreg_db/serializers/misc/file_overview.py +11 -99
- endoreg_db/serializers/misc/sensitive_patient_data.py +50 -26
- endoreg_db/serializers/patient_examination/patient_examination.py +3 -3
- endoreg_db/serializers/pdf/anony_text_validation.py +39 -23
- endoreg_db/serializers/requirements/requirement_sets.py +92 -22
- endoreg_db/serializers/video/segmentation.py +2 -1
- endoreg_db/serializers/video/video_file_list.py +65 -34
- endoreg_db/serializers/video/video_processing_history.py +20 -5
- endoreg_db/services/__old/pdf_import.py +1487 -0
- endoreg_db/services/__old/video_import.py +1306 -0
- endoreg_db/services/anonymization.py +128 -89
- endoreg_db/services/lookup_service.py +65 -52
- endoreg_db/services/lookup_store.py +2 -2
- endoreg_db/services/pdf_import.py +0 -1382
- endoreg_db/services/report_import.py +10 -0
- endoreg_db/services/video_import.py +6 -1255
- endoreg_db/tasks/upload_tasks.py +79 -70
- endoreg_db/tasks/video_ingest.py +8 -4
- endoreg_db/urls/__init__.py +5 -32
- endoreg_db/urls/ai.py +32 -0
- endoreg_db/urls/media.py +121 -83
- endoreg_db/urls/root_urls.py +29 -0
- endoreg_db/utils/__init__.py +15 -5
- endoreg_db/utils/ai/multilabel_classification_net.py +116 -20
- endoreg_db/utils/case_generator/__init__.py +3 -0
- endoreg_db/utils/dataloader.py +142 -40
- endoreg_db/utils/defaults/set_default_center.py +32 -0
- endoreg_db/utils/names.py +22 -16
- endoreg_db/utils/paths.py +110 -46
- endoreg_db/utils/permissions.py +2 -1
- endoreg_db/utils/pipelines/Readme.md +1 -1
- endoreg_db/utils/pipelines/process_video_dir.py +1 -1
- endoreg_db/utils/requirement_operator_logic/_old/model_evaluators.py +655 -0
- endoreg_db/utils/requirement_operator_logic/new_operator_logic.py +97 -0
- endoreg_db/utils/setup_config.py +8 -5
- endoreg_db/utils/storage.py +115 -0
- endoreg_db/utils/validate_endo_roi.py +8 -2
- endoreg_db/utils/video/ffmpeg_wrapper.py +184 -188
- endoreg_db/views/__init__.py +85 -183
- endoreg_db/views/ai/__init__.py +8 -0
- endoreg_db/views/ai/label.py +155 -0
- endoreg_db/views/anonymization/media_management.py +202 -166
- endoreg_db/views/anonymization/overview.py +99 -67
- endoreg_db/views/anonymization/validate.py +182 -44
- endoreg_db/views/media/__init__.py +7 -20
- endoreg_db/views/media/pdf_media.py +197 -174
- endoreg_db/views/media/sensitive_metadata.py +193 -138
- endoreg_db/views/media/video_media.py +89 -82
- endoreg_db/views/meta/__init__.py +0 -8
- endoreg_db/views/misc/__init__.py +1 -7
- endoreg_db/views/misc/upload_views.py +94 -93
- endoreg_db/views/patient/patient.py +5 -4
- endoreg_db/views/report/__init__.py +5 -7
- endoreg_db/views/{pdf → report}/reimport.py +22 -22
- endoreg_db/views/{pdf/pdf_stream.py → report/report_stream.py} +46 -39
- endoreg_db/views/requirement/evaluate.py +188 -187
- endoreg_db/views/requirement/lookup.py +17 -3
- endoreg_db/views/requirement/lookup_store.py +22 -90
- endoreg_db/views/requirement/requirement_utils.py +89 -0
- endoreg_db/views/video/__init__.py +23 -24
- endoreg_db/views/video/correction.py +201 -172
- endoreg_db/views/video/reimport.py +1 -1
- endoreg_db/views/{media/video_segments.py → video/segments_crud.py} +77 -40
- endoreg_db/views/video/{video_meta.py → video_meta_stats.py} +2 -2
- endoreg_db/views/video/video_stream.py +7 -8
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/METADATA +7 -3
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/RECORD +391 -413
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/WHEEL +1 -1
- endoreg_db/data/finding/anatomy_colon.yaml +0 -128
- endoreg_db/data/finding/colonoscopy.yaml +0 -40
- endoreg_db/data/finding/colonoscopy_bowel_prep.yaml +0 -56
- endoreg_db/data/finding/complication.yaml +0 -16
- endoreg_db/data/finding/data.yaml +0 -105
- endoreg_db/data/finding/examination_setting.yaml +0 -16
- endoreg_db/data/finding/medication_related.yaml +0 -18
- endoreg_db/data/finding/outcome.yaml +0 -12
- endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +0 -95
- endoreg_db/data/finding_classification/colonoscopy_jnet.yaml +0 -22
- endoreg_db/data/finding_classification/colonoscopy_kudo.yaml +0 -25
- endoreg_db/data/finding_classification/colonoscopy_lesion_circularity.yaml +0 -20
- endoreg_db/data/finding_classification/colonoscopy_lesion_planarity.yaml +0 -24
- endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +0 -68
- endoreg_db/data/finding_classification/colonoscopy_lesion_surface.yaml +0 -20
- endoreg_db/data/finding_classification/colonoscopy_location.yaml +0 -80
- endoreg_db/data/finding_classification/colonoscopy_lst.yaml +0 -21
- endoreg_db/data/finding_classification/colonoscopy_nice.yaml +0 -20
- endoreg_db/data/finding_classification/colonoscopy_paris.yaml +0 -26
- endoreg_db/data/finding_classification/colonoscopy_sano.yaml +0 -22
- endoreg_db/data/finding_classification/colonoscopy_summary.yaml +0 -53
- endoreg_db/data/finding_classification/complication_generic.yaml +0 -25
- endoreg_db/data/finding_classification/examination_setting_generic.yaml +0 -40
- endoreg_db/data/finding_classification/histology_colo.yaml +0 -51
- endoreg_db/data/finding_classification/intervention_required.yaml +0 -26
- endoreg_db/data/finding_classification/medication_related.yaml +0 -23
- endoreg_db/data/finding_classification/visualized.yaml +0 -33
- endoreg_db/data/finding_classification_choice/colon_lesion_circularity_default.yaml +0 -32
- endoreg_db/data/finding_classification_choice/colon_lesion_jnet.yaml +0 -15
- endoreg_db/data/finding_classification_choice/colon_lesion_kudo.yaml +0 -23
- endoreg_db/data/finding_classification_choice/colon_lesion_lst.yaml +0 -15
- endoreg_db/data/finding_classification_choice/colon_lesion_nice.yaml +0 -17
- endoreg_db/data/finding_classification_choice/colon_lesion_planarity_default.yaml +0 -49
- endoreg_db/data/finding_classification_choice/colon_lesion_sano.yaml +0 -14
- endoreg_db/data/finding_classification_choice/colon_lesion_surface_intact_default.yaml +0 -36
- endoreg_db/data/finding_classification_choice/colonoscopy_size.yaml +0 -82
- endoreg_db/data/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +0 -15
- endoreg_db/data/finding_classification_choice/outcome.yaml +0 -19
- endoreg_db/data/finding_intervention/endoscopy.yaml +0 -43
- endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +0 -168
- endoreg_db/data/finding_intervention/endoscopy_egd.yaml +0 -128
- endoreg_db/data/finding_intervention/endoscopy_ercp.yaml +0 -32
- endoreg_db/data/finding_intervention/endoscopy_eus_lower.yaml +0 -9
- endoreg_db/data/finding_intervention/endoscopy_eus_upper.yaml +0 -36
- endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +0 -79
- endoreg_db/data/requirement/age.yaml +0 -26
- endoreg_db/data/requirement/gender.yaml +0 -25
- endoreg_db/management/commands/init_default_ai_model.py +0 -112
- endoreg_db/management/commands/reset_celery_schedule.py +0 -9
- endoreg_db/management/commands/validate_video.py +0 -204
- endoreg_db/migrations/0002_add_video_correction_models.py +0 -52
- endoreg_db/migrations/0003_add_center_display_name.py +0 -30
- endoreg_db/models/administration/permissions/__init__.py +0 -44
- endoreg_db/models/rule/__init__.py +0 -13
- endoreg_db/models/rule/rule.py +0 -27
- endoreg_db/models/rule/rule_applicator.py +0 -224
- endoreg_db/models/rule/rule_attribute_dtype.py +0 -17
- endoreg_db/models/rule/rule_type.py +0 -20
- endoreg_db/models/rule/ruleset.py +0 -17
- endoreg_db/renames.yml +0 -8
- endoreg_db/serializers/_old/raw_pdf_meta_validation.py +0 -223
- endoreg_db/serializers/_old/raw_video_meta_validation.py +0 -179
- endoreg_db/serializers/_old/video.py +0 -71
- endoreg_db/serializers/meta/pdf_file_meta_extraction.py +0 -115
- endoreg_db/serializers/meta/report_meta.py +0 -53
- endoreg_db/serializers/report/__init__.py +0 -9
- endoreg_db/serializers/report/mixins.py +0 -45
- endoreg_db/serializers/report/report.py +0 -105
- endoreg_db/serializers/report/report_list.py +0 -22
- endoreg_db/serializers/report/secure_file_url.py +0 -26
- endoreg_db/serializers/video/video_metadata.py +0 -105
- endoreg_db/services/requirements_object.py +0 -147
- endoreg_db/services/storage_aware_video_processor.py +0 -344
- endoreg_db/urls/files.py +0 -6
- endoreg_db/urls/label_video_segment_validate.py +0 -33
- endoreg_db/urls/label_video_segments.py +0 -46
- endoreg_db/urls/report.py +0 -48
- endoreg_db/urls/video.py +0 -61
- endoreg_db/utils/case_generator/case_generator.py +0 -159
- endoreg_db/utils/case_generator/utils.py +0 -30
- endoreg_db/utils/requirement_operator_logic/model_evaluators.py +0 -368
- endoreg_db/views/label/__init__.py +0 -5
- endoreg_db/views/label/label.py +0 -15
- endoreg_db/views/label_video_segment/__init__.py +0 -16
- endoreg_db/views/label_video_segment/create_lvs_from_annotation.py +0 -44
- endoreg_db/views/label_video_segment/get_lvs_by_name_and_video.py +0 -50
- endoreg_db/views/label_video_segment/label_video_segment.py +0 -77
- endoreg_db/views/label_video_segment/label_video_segment_by_label.py +0 -174
- endoreg_db/views/label_video_segment/label_video_segment_detail.py +0 -73
- endoreg_db/views/label_video_segment/update_lvs_from_annotation.py +0 -46
- endoreg_db/views/label_video_segment/validate.py +0 -226
- endoreg_db/views/media/segments.py +0 -71
- endoreg_db/views/meta/available_files_list.py +0 -146
- endoreg_db/views/meta/report_meta.py +0 -53
- endoreg_db/views/meta/sensitive_meta_detail.py +0 -148
- endoreg_db/views/misc/secure_file_serving_view.py +0 -80
- endoreg_db/views/misc/secure_file_url_view.py +0 -84
- endoreg_db/views/misc/secure_url_validate.py +0 -79
- endoreg_db/views/patient_examination/DEPRECATED_video_backup.py +0 -164
- endoreg_db/views/patient_finding_location/__init__.py +0 -5
- endoreg_db/views/patient_finding_location/pfl_create.py +0 -70
- endoreg_db/views/patient_finding_morphology/__init__.py +0 -5
- endoreg_db/views/patient_finding_morphology/pfm_create.py +0 -70
- endoreg_db/views/pdf/__init__.py +0 -8
- endoreg_db/views/report/report_list.py +0 -112
- endoreg_db/views/report/report_with_secure_url.py +0 -28
- endoreg_db/views/report/start_examination.py +0 -7
- endoreg_db/views/video/segmentation.py +0 -274
- endoreg_db/views/video/task_status.py +0 -49
- endoreg_db/views/video/timeline.py +0 -46
- endoreg_db/views/video/video_analyze.py +0 -52
- endoreg_db/views.py +0 -0
- /endoreg_db/data/requirement/{colonoscopy_baseline_austria.yaml → old/colonoscopy_baseline_austria.yaml} +0 -0
- /endoreg_db/data/requirement/{disease_cardiovascular.yaml → old/disease_cardiovascular.yaml} +0 -0
- /endoreg_db/data/requirement/{disease_classification_choice_cardiovascular.yaml → old/disease_classification_choice_cardiovascular.yaml} +0 -0
- /endoreg_db/data/requirement/{disease_hepatology.yaml → old/disease_hepatology.yaml} +0 -0
- /endoreg_db/data/requirement/{disease_misc.yaml → old/disease_misc.yaml} +0 -0
- /endoreg_db/data/requirement/{disease_renal.yaml → old/disease_renal.yaml} +0 -0
- /endoreg_db/data/requirement/{endoscopy_bleeding_risk.yaml → old/endoscopy_bleeding_risk.yaml} +0 -0
- /endoreg_db/data/requirement/{event_cardiology.yaml → old/event_cardiology.yaml} +0 -0
- /endoreg_db/data/requirement/{event_requirements.yaml → old/event_requirements.yaml} +0 -0
- /endoreg_db/data/requirement/{finding_colon_polyp.yaml → old/finding_colon_polyp.yaml} +0 -0
- /endoreg_db/{migrations/__init__.py → data/requirement/old/gender.yaml} +0 -0
- /endoreg_db/data/requirement/{lab_value.yaml → old/lab_value.yaml} +0 -0
- /endoreg_db/data/requirement/{medication.yaml → old/medication.yaml} +0 -0
- /endoreg_db/data/requirement_operator/{age.yaml → _old/age.yaml} +0 -0
- /endoreg_db/data/requirement_operator/{lab_operators.yaml → _old/lab_operators.yaml} +0 -0
- /endoreg_db/data/requirement_operator/{model_operators.yaml → _old/model_operators.yaml} +0 -0
- /endoreg_db/{models/media/video/refactor_plan.md → import_files/pseudonymization/__init__.py} +0 -0
- /endoreg_db/{models/media/video/video_file_frames.py → import_files/pseudonymization/pseudonymize.py} +0 -0
- /endoreg_db/models/{metadata/frame_ocr_result.py → report/__init__.py} +0 -0
- /endoreg_db/{urls/sensitive_meta.py → models/report/images.py} +0 -0
- /endoreg_db/utils/requirement_operator_logic/{lab_value_operators.py → _old/lab_value_operators.py} +0 -0
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import TYPE_CHECKING, Optional, Dict
|
|
3
|
-
import cv2
|
|
4
2
|
from pathlib import Path
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
5
6
|
|
|
6
7
|
if TYPE_CHECKING:
|
|
7
8
|
from ..video_file import VideoFile
|
|
8
9
|
|
|
10
|
+
|
|
9
11
|
def _validate_video_path(video_path: Path):
|
|
10
12
|
"""
|
|
11
13
|
Validates that the provided path is an existing video file.
|
|
12
|
-
|
|
14
|
+
|
|
13
15
|
Raises:
|
|
14
16
|
TypeError: If `video_path` is not a Path object.
|
|
15
17
|
FileNotFoundError: If the file does not exist at the specified path.
|
|
@@ -24,45 +26,53 @@ def _validate_video_path(video_path: Path):
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
def _get_fps(video: "VideoFile") -> float:
|
|
28
32
|
"""
|
|
29
33
|
Determine and return the frames per second (FPS) of a video associated with a VideoFile instance.
|
|
30
|
-
|
|
34
|
+
|
|
31
35
|
Attempts to retrieve FPS from the instance itself, its linked VideoMeta, or by direct analysis of the raw video file using OpenCV. Updates and saves the FPS value to the instance if successfully determined. Raises a ValueError if FPS cannot be determined by any method.
|
|
32
|
-
|
|
36
|
+
|
|
33
37
|
Returns:
|
|
34
38
|
float: The frames per second (FPS) of the video.
|
|
35
|
-
|
|
39
|
+
|
|
36
40
|
Raises:
|
|
37
41
|
ValueError: If the FPS cannot be determined from any available source.
|
|
38
42
|
"""
|
|
39
43
|
from .video_meta import _update_video_meta
|
|
44
|
+
|
|
40
45
|
if video.fps is not None:
|
|
41
46
|
return video.fps
|
|
42
47
|
|
|
43
48
|
logger.debug("FPS not set on instance %s, checking VideoMeta.", video.uuid)
|
|
44
49
|
|
|
45
|
-
|
|
46
50
|
if not video.video_meta:
|
|
47
51
|
logger.info("VideoMeta not linked for %s, attempting update.", video.uuid)
|
|
48
52
|
|
|
49
|
-
_update_video_meta(video, save_instance=True)
|
|
53
|
+
_update_video_meta(video, save_instance=True) # Call the helper function
|
|
50
54
|
|
|
51
55
|
# Check again after potential update
|
|
52
56
|
if video.fps is not None:
|
|
53
57
|
return video.fps
|
|
54
58
|
elif video.video_meta and video.video_meta.fps is not None:
|
|
55
59
|
logger.info("Retrieved FPS %.2f from VideoMeta for %s.", video.video_meta.fps, video.uuid)
|
|
56
|
-
|
|
60
|
+
_fps = video.video_meta.fps
|
|
61
|
+
try:
|
|
62
|
+
_fps = float(_fps)
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
logger.warning("Invalid FPS value %.2f in VideoMeta for video %s.", video.video_meta.fps, video.uuid)
|
|
65
|
+
raise ValueError(f"Could not determine FPS for video {video.uuid} due to invalid VideoMeta FPS value.")
|
|
66
|
+
video.fps = _fps
|
|
57
67
|
# Avoid saving if called from within the save method itself
|
|
58
|
-
if not getattr(video,
|
|
68
|
+
if not getattr(video, "_saving", False):
|
|
59
69
|
video.save(update_fields=["fps"])
|
|
60
|
-
return
|
|
70
|
+
return _fps
|
|
61
71
|
else:
|
|
62
72
|
logger.warning("Could not determine FPS from VideoMeta for video %s. Trying direct raw file access.", video.uuid)
|
|
63
73
|
try:
|
|
64
74
|
if video.has_raw:
|
|
65
|
-
video_path = video.get_raw_file_path()
|
|
75
|
+
video_path = video.get_raw_file_path() # Use helper
|
|
66
76
|
if video_path and video_path.exists():
|
|
67
77
|
cap = cv2.VideoCapture(video_path.as_posix())
|
|
68
78
|
if not cap.isOpened():
|
|
@@ -77,11 +87,11 @@ def _get_fps(video: "VideoFile") -> float:
|
|
|
77
87
|
finally:
|
|
78
88
|
cap.release()
|
|
79
89
|
if fps and fps > 0:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
video.fps = fps
|
|
91
|
+
logger.info("Determined FPS %.2f directly from file for %s.", video.fps, video.uuid)
|
|
92
|
+
if not getattr(video, "_saving", False):
|
|
93
|
+
video.save(update_fields=["fps"])
|
|
94
|
+
return fps
|
|
85
95
|
else:
|
|
86
96
|
logger.warning("Could not determine a valid FPS for video file %s.", video_path)
|
|
87
97
|
elif video_path:
|
|
@@ -92,40 +102,33 @@ def _get_fps(video: "VideoFile") -> float:
|
|
|
92
102
|
logger.warning("Raw file not available for direct FPS check.")
|
|
93
103
|
|
|
94
104
|
except Exception as e:
|
|
95
|
-
logger.error("Error getting FPS directly from file %s: %s", video.raw_file.name if video.has_raw else
|
|
96
|
-
|
|
97
|
-
raise ValueError(
|
|
98
|
-
f"Could not determine FPS for video {video.uuid}. "
|
|
99
|
-
"Ensure the video file is valid and accessible."
|
|
100
|
-
)
|
|
105
|
+
logger.error("Error getting FPS directly from file %s: %s", video.raw_file.name if video.has_raw else "N/A", e)
|
|
101
106
|
|
|
107
|
+
raise ValueError(f"Could not determine FPS for video {video.uuid}. Ensure the video file is valid and accessible.")
|
|
102
108
|
|
|
103
109
|
|
|
104
110
|
# TODO Refactor to utils / check if similar function exists in utils
|
|
105
|
-
def _get_fps_from_property(cap) -> float:
|
|
111
|
+
def _get_fps_from_property(cap: cv2.VideoCapture) -> float:
|
|
106
112
|
"""
|
|
107
113
|
Retrieve the frames per second (FPS) from an OpenCV video capture object using the appropriate property for the OpenCV version.
|
|
108
|
-
|
|
114
|
+
|
|
109
115
|
Parameters:
|
|
110
116
|
cap: An OpenCV video capture object.
|
|
111
|
-
|
|
117
|
+
|
|
112
118
|
Returns:
|
|
113
119
|
float: The FPS value obtained from the video capture properties, or 0.0 if unavailable.
|
|
114
120
|
"""
|
|
115
|
-
|
|
116
|
-
return cap.get(cv2.CAP_PROP_FPS)
|
|
117
|
-
# For older OpenCV versions
|
|
118
|
-
return cap.get(cv2.cv.CV_CAP_PROP_FPS) # type: ignore
|
|
121
|
+
return cap.get(cv2.CAP_PROP_FPS)
|
|
119
122
|
|
|
120
123
|
|
|
121
124
|
def _calculate_fps_manually(cap, video_path: Path) -> float:
|
|
122
125
|
"""
|
|
123
126
|
Estimate the frames per second (FPS) of a video by reading all frames and dividing the total frame count by the elapsed time.
|
|
124
|
-
|
|
127
|
+
|
|
125
128
|
Parameters:
|
|
126
129
|
cap: An OpenCV video capture object positioned at the start of the video.
|
|
127
130
|
video_path (Path): Path to the video file, used for logging.
|
|
128
|
-
|
|
131
|
+
|
|
129
132
|
Returns:
|
|
130
133
|
float: The estimated FPS, or 0.0 if the duration is zero or calculation fails.
|
|
131
134
|
"""
|
|
@@ -142,6 +145,6 @@ def _calculate_fps_manually(cap, video_path: Path) -> float:
|
|
|
142
145
|
seconds = (end_time - start_time) / cv2.getTickFrequency()
|
|
143
146
|
if seconds > 0:
|
|
144
147
|
return num_frames / seconds
|
|
145
|
-
|
|
148
|
+
|
|
146
149
|
logger.error(f"Manual FPS calculation failed for {video_path} due to zero duration.")
|
|
147
|
-
return 0.0
|
|
150
|
+
return 0.0
|
|
@@ -2,30 +2,33 @@
|
|
|
2
2
|
import logging
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
5
6
|
import cv2
|
|
7
|
+
|
|
6
8
|
# --- End Add Imports ---
|
|
7
9
|
|
|
8
10
|
if TYPE_CHECKING:
|
|
9
|
-
from ..video_file import VideoFile
|
|
11
|
+
from ..video_file import VideoFile # Correct import path
|
|
10
12
|
|
|
11
13
|
# --- Add Logger ---
|
|
12
14
|
logger = logging.getLogger(__name__)
|
|
13
15
|
# --- End Add Logger ---
|
|
14
16
|
|
|
17
|
+
|
|
15
18
|
def _initialize_video_specs(video: "VideoFile", use_raw: bool = True) -> bool:
|
|
16
19
|
"""
|
|
17
20
|
Initializes video specifications for a VideoFile object by reading from the video file.
|
|
18
|
-
|
|
21
|
+
|
|
19
22
|
Attempts to populate missing values for fps, width, height, frame count, and duration using OpenCV. Selects the raw file if available and requested, otherwise uses the active file. Updates only unset fields if valid values are obtained. Returns True if successful or if no updates are needed. Raises FileNotFoundError if the video file cannot be found, or RuntimeError if the file cannot be opened or properties cannot be read.
|
|
20
23
|
"""
|
|
21
24
|
video_path: Optional[Path] = None
|
|
22
25
|
target_file_name: Optional[str] = None
|
|
23
26
|
|
|
24
27
|
if use_raw and video.has_raw:
|
|
25
|
-
video_path = video.get_raw_file_path()
|
|
28
|
+
video_path = video.get_raw_file_path() # Use IO helper
|
|
26
29
|
target_file_name = video.raw_file.name
|
|
27
|
-
elif video.active_file:
|
|
28
|
-
video_path = video.active_file_path
|
|
30
|
+
elif video.active_file: # Fallback to active file if raw not requested or available
|
|
31
|
+
video_path = video.active_file_path # Use property relying on IO helpers
|
|
29
32
|
target_file_name = video.active_file.name
|
|
30
33
|
else:
|
|
31
34
|
logger.error("No suitable video file found for spec initialization for %s.", video.uuid)
|
|
@@ -44,7 +47,7 @@ def _initialize_video_specs(video: "VideoFile", use_raw: bool = True) -> bool:
|
|
|
44
47
|
video_cap = cv2.VideoCapture(video_path.as_posix())
|
|
45
48
|
if not video_cap.isOpened():
|
|
46
49
|
# Raise exception
|
|
47
|
-
video_cap.release()
|
|
50
|
+
video_cap.release() # Ensure release
|
|
48
51
|
raise RuntimeError(f"Could not open video file {video_path} with OpenCV for spec initialization (Video: {video.uuid}).")
|
|
49
52
|
|
|
50
53
|
updated = False
|
|
@@ -67,14 +70,14 @@ def _initialize_video_specs(video: "VideoFile", use_raw: bool = True) -> bool:
|
|
|
67
70
|
logger.error("Error getting properties from OpenCV for %s (Video: %s): %s", video_path, video.uuid, cv_err, exc_info=True)
|
|
68
71
|
raise RuntimeError(f"OpenCV failed to get properties for {video_path}") from cv_err
|
|
69
72
|
finally:
|
|
70
|
-
video_cap.release()
|
|
73
|
+
video_cap.release() # Ensure release after getting props
|
|
71
74
|
|
|
72
75
|
# --- Update FPS ---
|
|
73
76
|
if current_fps is None and file_fps and file_fps > 0:
|
|
74
77
|
video.fps = file_fps
|
|
75
78
|
fields_to_update.append("fps")
|
|
76
79
|
updated = True
|
|
77
|
-
current_fps = file_fps
|
|
80
|
+
current_fps = file_fps # Update local var for duration calc
|
|
78
81
|
|
|
79
82
|
# --- Update Width ---
|
|
80
83
|
if current_width is None and file_width > 0:
|
|
@@ -93,20 +96,16 @@ def _initialize_video_specs(video: "VideoFile", use_raw: bool = True) -> bool:
|
|
|
93
96
|
video.frame_count = file_frame_count
|
|
94
97
|
fields_to_update.append("frame_count")
|
|
95
98
|
updated = True
|
|
96
|
-
elif file_frame_count is None or file_frame_count <= 0:
|
|
97
|
-
logger.warning(
|
|
98
|
-
"Invalid frame count (value: %s) obtained from OpenCV for %s. Video frame_count not updated.",
|
|
99
|
-
file_frame_count, video_path
|
|
100
|
-
)
|
|
99
|
+
elif file_frame_count is None or file_frame_count <= 0: # Log if not updated due to invalid file_frame_count
|
|
100
|
+
logger.warning("Invalid frame count (value: %s) obtained from OpenCV for %s. Video frame_count not updated.", file_frame_count, video_path)
|
|
101
101
|
|
|
102
102
|
# --- Update Duration ---
|
|
103
|
-
if current_duration is None:
|
|
103
|
+
if current_duration is None: # Only if duration isn't already set
|
|
104
104
|
# Use the potentially updated video.frame_count and current_fps (which reflects video.fps or file_fps)
|
|
105
105
|
final_frame_count_for_duration = video.frame_count
|
|
106
|
-
final_fps_for_duration = current_fps
|
|
106
|
+
final_fps_for_duration = current_fps # This is video.fps after potential update from file_fps
|
|
107
107
|
|
|
108
|
-
if
|
|
109
|
-
final_fps_for_duration and final_fps_for_duration > 0):
|
|
108
|
+
if final_frame_count_for_duration and final_frame_count_for_duration > 0 and final_fps_for_duration and final_fps_for_duration > 0:
|
|
110
109
|
video.duration = final_frame_count_for_duration / final_fps_for_duration
|
|
111
110
|
fields_to_update.append("duration")
|
|
112
111
|
updated = True
|
|
@@ -114,15 +113,10 @@ def _initialize_video_specs(video: "VideoFile", use_raw: bool = True) -> bool:
|
|
|
114
113
|
# Log if duration could not be calculated, indicating which component was missing/invalid
|
|
115
114
|
if not (final_frame_count_for_duration and final_frame_count_for_duration > 0):
|
|
116
115
|
logger.warning(
|
|
117
|
-
"Duration not calculated for %s: frame count is unavailable or invalid (value: %s).",
|
|
118
|
-
video_path, final_frame_count_for_duration
|
|
116
|
+
"Duration not calculated for %s: frame count is unavailable or invalid (value: %s).", video_path, final_frame_count_for_duration
|
|
119
117
|
)
|
|
120
118
|
if not (final_fps_for_duration and final_fps_for_duration > 0):
|
|
121
|
-
logger.warning(
|
|
122
|
-
"Duration not calculated for %s: FPS is unavailable or invalid (value: %s).",
|
|
123
|
-
video_path, final_fps_for_duration
|
|
124
|
-
)
|
|
125
|
-
|
|
119
|
+
logger.warning("Duration not calculated for %s: FPS is unavailable or invalid (value: %s).", video_path, final_fps_for_duration)
|
|
126
120
|
|
|
127
121
|
# --- Save if updated ---
|
|
128
122
|
if updated:
|
|
@@ -137,7 +131,7 @@ def _initialize_video_specs(video: "VideoFile", use_raw: bool = True) -> bool:
|
|
|
137
131
|
# Log and re-raise exception
|
|
138
132
|
logger.error("Error initializing video specs for %s from file %s: %s", video.uuid, video_path, e, exc_info=True)
|
|
139
133
|
# Ensure capture is released in case of unexpected error
|
|
140
|
-
if
|
|
134
|
+
if "video_cap" in locals() and video_cap.isOpened():
|
|
141
135
|
video_cap.release()
|
|
142
136
|
# Re-raise as RuntimeError
|
|
143
137
|
raise RuntimeError(f"Failed to initialize video specs for {video.uuid} from {video_path}") from e
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from typing import TYPE_CHECKING, Optional
|
|
3
|
+
|
|
4
|
+
# --- End Fix ---
|
|
5
|
+
from django.db import transaction
|
|
6
|
+
|
|
3
7
|
# --- Fix Imports ---
|
|
4
8
|
from ....metadata import SensitiveMeta
|
|
5
9
|
from ....metadata.sensitive_meta_logic import update_or_create_sensitive_meta_from_dict
|
|
6
|
-
# --- End Fix ---
|
|
7
|
-
from django.db import transaction
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
12
|
from ..video_file import VideoFile
|
|
@@ -38,77 +40,78 @@ def _update_text_metadata(
|
|
|
38
40
|
logger.warning(f"Frames not extracted for video {video.uuid}. Attempting automatic frame extraction...")
|
|
39
41
|
try:
|
|
40
42
|
success = video.extract_frames(overwrite=False)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
raise ValueError(f"Cannot update text metadata for video {video.uuid}:
|
|
43
|
+
except (FileNotFoundError, RuntimeError, ValueError, OSError) as exc:
|
|
44
|
+
logger.error(
|
|
45
|
+
"Failed to extract frames for video %s: %s",
|
|
46
|
+
video.uuid,
|
|
47
|
+
exc,
|
|
48
|
+
exc_info=True,
|
|
49
|
+
)
|
|
50
|
+
raise ValueError(f"Cannot update text metadata for video {video.uuid}: Frames not extracted and automatic extraction failed") from exc
|
|
51
|
+
|
|
52
|
+
if not success:
|
|
53
|
+
raise ValueError(f"Cannot update text metadata for video {video.uuid}: Frame extraction returned False")
|
|
54
|
+
|
|
55
|
+
state.refresh_from_db()
|
|
56
|
+
if not state.frames_extracted:
|
|
57
|
+
raise ValueError(f"Cannot update text metadata for video {video.uuid}: Frame extraction completed but state was not updated")
|
|
58
|
+
|
|
59
|
+
logger.info(f"Successfully extracted frames for video {video.uuid}")
|
|
56
60
|
|
|
57
61
|
if state.text_meta_extracted and not overwrite:
|
|
58
|
-
logger.info("Text already extracted for video %s and overwrite=False. Skipping.", video.uuid)
|
|
59
|
-
return video.sensitive_meta
|
|
62
|
+
logger.info("Text already extracted for video %s and overwrite=False. Skipping.", video.uuid) # Changed to info
|
|
63
|
+
return video.sensitive_meta # Return existing meta if available
|
|
60
64
|
# --- End Pre-condition Checks ---
|
|
61
65
|
|
|
62
66
|
# Extract text using the AI helper function
|
|
63
67
|
# _extract_text_from_video_frames raises ValueError on pre-condition failure
|
|
64
68
|
try:
|
|
65
69
|
if not extracted_data_dict:
|
|
66
|
-
extracted_data_dict = video.extract_text_from_frames(
|
|
67
|
-
frame_fraction=ocr_frame_fraction, cap=cap
|
|
68
|
-
)
|
|
70
|
+
extracted_data_dict = video.extract_text_from_frames(frame_fraction=ocr_frame_fraction, cap=cap)
|
|
69
71
|
except Exception as text_extract_e:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
logger.error("Failed during text extraction step for video %s: %s", video.uuid, text_extract_e, exc_info=True)
|
|
73
|
+
raise RuntimeError(f"Text extraction failed for video {video.uuid}") from text_extract_e
|
|
72
74
|
|
|
73
75
|
# --- Atomic Update Block ---
|
|
74
76
|
try:
|
|
75
77
|
with transaction.atomic():
|
|
76
78
|
# Refresh state in case it changed
|
|
77
79
|
state.refresh_from_db()
|
|
78
|
-
sensitive_meta_instance = video.sensitive_meta
|
|
80
|
+
sensitive_meta_instance = video.sensitive_meta # Get current instance
|
|
79
81
|
|
|
80
82
|
if not extracted_data_dict:
|
|
81
83
|
logger.warning("No text extracted for video %s; skipping SensitiveMeta update.", video.uuid)
|
|
82
84
|
# Mark state as retrieved even if no data found, to avoid re-running unless overwrite=True
|
|
83
85
|
if not state.text_meta_extracted:
|
|
84
86
|
state.text_meta_extracted = True
|
|
85
|
-
state.save(update_fields=[
|
|
86
|
-
return sensitive_meta_instance
|
|
87
|
+
state.save(update_fields=["text_meta_extracted"])
|
|
88
|
+
return sensitive_meta_instance # Return existing meta if available
|
|
87
89
|
|
|
88
90
|
# Add center info if not already present in extracted data
|
|
89
|
-
if
|
|
90
|
-
extracted_data_dict[
|
|
91
|
+
if "center_name" not in extracted_data_dict and video.center:
|
|
92
|
+
extracted_data_dict["center_name"] = video.center.name
|
|
91
93
|
logger.debug("Data for SensitiveMeta update for video %s: %s", video.uuid, extracted_data_dict)
|
|
92
94
|
|
|
93
95
|
# Pass the Class, the data dict, and the current instance (or None)
|
|
94
96
|
# This function might raise exceptions if data is invalid
|
|
95
97
|
sensitive_meta, created = update_or_create_sensitive_meta_from_dict(
|
|
96
|
-
SensitiveMeta,
|
|
98
|
+
SensitiveMeta, # Pass the class
|
|
97
99
|
extracted_data_dict,
|
|
98
|
-
instance=sensitive_meta_instance
|
|
100
|
+
instance=sensitive_meta_instance, # Pass current instance via keyword
|
|
99
101
|
)
|
|
100
102
|
|
|
101
103
|
# Update VideoFile fields if necessary
|
|
102
104
|
update_fields_video = []
|
|
103
|
-
if created or sensitive_meta != sensitive_meta_instance:
|
|
105
|
+
if created or sensitive_meta != sensitive_meta_instance: # Check if relation needs update
|
|
104
106
|
video.sensitive_meta = sensitive_meta
|
|
105
|
-
update_fields_video.append(
|
|
107
|
+
update_fields_video.append("sensitive_meta")
|
|
106
108
|
|
|
107
|
-
if not video.date and sensitive_meta and extracted_data_dict.get(
|
|
108
|
-
extracted_date = extracted_data_dict.get(
|
|
109
|
-
if extracted_date:
|
|
109
|
+
if not video.date and sensitive_meta and extracted_data_dict.get("date"):
|
|
110
|
+
extracted_date = extracted_data_dict.get("date")
|
|
111
|
+
if extracted_date: # Ensure date is not None or empty
|
|
110
112
|
video.date = extracted_date
|
|
111
113
|
update_fields_video.append("date")
|
|
114
|
+
|
|
112
115
|
|
|
113
116
|
# Save VideoFile if fields changed
|
|
114
117
|
if update_fields_video:
|
|
@@ -117,14 +120,14 @@ def _update_text_metadata(
|
|
|
117
120
|
# Update state
|
|
118
121
|
if not state.text_meta_extracted:
|
|
119
122
|
state.text_meta_extracted = True
|
|
120
|
-
state.save(update_fields=[
|
|
123
|
+
state.save(update_fields=["text_meta_extracted"])
|
|
121
124
|
|
|
122
125
|
# Mark sensitive meta as processed when updated via text metadata
|
|
123
126
|
if sensitive_meta:
|
|
124
127
|
state.mark_sensitive_meta_processed(save=True)
|
|
125
128
|
logger.info(f"Marked sensitive_meta_processed=True for video {video.uuid} after text metadata update")
|
|
126
129
|
|
|
127
|
-
logger.info("Successfully updated/created SensitiveMeta and state for video %s.", video.uuid)
|
|
130
|
+
logger.info("Successfully updated/created SensitiveMeta and state for video %s.", video.uuid) # Changed to info
|
|
128
131
|
return sensitive_meta
|
|
129
132
|
|
|
130
133
|
except Exception as e:
|
|
@@ -6,31 +6,36 @@ if TYPE_CHECKING:
|
|
|
6
6
|
|
|
7
7
|
logger = logging.getLogger(__name__)
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
def _update_video_meta(video: "VideoFile", save_instance: bool = True):
|
|
10
11
|
"""
|
|
11
12
|
Updates or creates the technical VideoMeta from the raw video file.
|
|
12
13
|
Raises FileNotFoundError or ValueError on pre-condition failure, RuntimeError on processing failure.
|
|
13
14
|
"""
|
|
14
|
-
from ....metadata import VideoMeta
|
|
15
|
+
from ....metadata import VideoMeta # Local import
|
|
15
16
|
|
|
16
17
|
logger.debug("Updating technical VideoMeta for video %s (from raw file).", video.uuid)
|
|
17
18
|
|
|
18
19
|
if not video.has_raw:
|
|
19
20
|
# DEFENSIVE: Log warning and skip instead of crashing
|
|
20
|
-
logger.warning(
|
|
21
|
+
logger.warning(
|
|
22
|
+
f"Raw video file path not available for {video.uuid}. Skipping VideoMeta update - this may indicate the video was processed and raw file moved."
|
|
23
|
+
)
|
|
21
24
|
return # Graceful skip instead of FileNotFoundError
|
|
22
25
|
|
|
23
|
-
raw_video_path = video.get_raw_file_path()
|
|
26
|
+
raw_video_path = video.get_raw_file_path() # Use helper
|
|
24
27
|
if not raw_video_path or not raw_video_path.exists():
|
|
25
28
|
# DEFENSIVE: Log warning and skip instead of crashing production pipeline
|
|
26
|
-
logger.warning(
|
|
29
|
+
logger.warning(
|
|
30
|
+
f"Raw video file path {raw_video_path} does not exist for video {video.uuid}. Skipping VideoMeta update - this typically happens after video processing when raw files are moved to processed location."
|
|
31
|
+
)
|
|
27
32
|
return # Graceful skip instead of FileNotFoundError that crashes production
|
|
28
33
|
|
|
29
34
|
try:
|
|
30
35
|
vm = video.video_meta
|
|
31
36
|
if vm:
|
|
32
37
|
logger.info("Updating existing VideoMeta (PK: %s) for video %s.", vm.pk, video.uuid)
|
|
33
|
-
vm.update_meta(raw_video_path)
|
|
38
|
+
vm.update_meta(raw_video_path) # Assuming this method exists and raises on error
|
|
34
39
|
vm.save()
|
|
35
40
|
else:
|
|
36
41
|
if not video.center or not video.processor:
|
|
@@ -43,9 +48,11 @@ def _update_video_meta(video: "VideoFile", save_instance: bool = True):
|
|
|
43
48
|
video_path=raw_video_path,
|
|
44
49
|
center=video.center,
|
|
45
50
|
processor=video.processor,
|
|
46
|
-
save_instance=True
|
|
51
|
+
save_instance=True, # Let create_from_file handle saving
|
|
47
52
|
)
|
|
48
|
-
|
|
53
|
+
vm = video.video_meta
|
|
54
|
+
assert vm is not None # For type checker
|
|
55
|
+
logger.info("Created and linked VideoMeta (PK: %s) for video %s.", vm.pk, video.uuid)
|
|
49
56
|
|
|
50
57
|
# Save the VideoFile instance itself if requested and if video_meta was linked/updated
|
|
51
58
|
if save_instance:
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import TYPE_CHECKING, List, Dict, Tuple, Set
|
|
3
|
-
from icecream import ic
|
|
4
2
|
from pathlib import Path
|
|
3
|
+
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
|
|
4
|
+
|
|
5
5
|
from django.db.models import Q # Import Q for complex queries
|
|
6
|
+
from icecream import ic
|
|
6
7
|
|
|
7
8
|
if TYPE_CHECKING:
|
|
8
|
-
from .
|
|
9
|
+
from django.db.models import QuerySet
|
|
10
|
+
|
|
9
11
|
from ...label import LabelVideoSegment
|
|
10
12
|
from ...metadata import VideoPredictionMeta
|
|
11
13
|
from ..frame import Frame
|
|
12
|
-
from
|
|
14
|
+
from .video_file import VideoFile
|
|
13
15
|
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
18
|
+
|
|
16
19
|
def _convert_sequences_to_db_segments(
|
|
17
20
|
video: "VideoFile",
|
|
18
21
|
sequences: Dict[str, List[Tuple[int, int]]],
|
|
@@ -72,14 +75,14 @@ def _convert_sequences_to_db_segments(
|
|
|
72
75
|
logger.error("Error bulk creating segments for label '%s': %s", label_name, e, exc_info=True)
|
|
73
76
|
error_count += len(segments_to_create)
|
|
74
77
|
|
|
75
|
-
newly_created_segments = LabelVideoSegment.objects.filter(
|
|
76
|
-
video_file=video,
|
|
77
|
-
prediction_meta=video_prediction_meta,
|
|
78
|
-
label__name__in=processed_labels
|
|
79
|
-
)
|
|
78
|
+
newly_created_segments = LabelVideoSegment.objects.filter(video_file=video, prediction_meta=video_prediction_meta, label__name__in=processed_labels)
|
|
80
79
|
|
|
81
|
-
logger.info(
|
|
82
|
-
|
|
80
|
+
logger.info(
|
|
81
|
+
"Attempting to create state objects for %d potentially new segments (Video: %s, PredictionMeta: %s)",
|
|
82
|
+
newly_created_segments.count(),
|
|
83
|
+
video.uuid,
|
|
84
|
+
video_prediction_meta.pk,
|
|
85
|
+
)
|
|
83
86
|
|
|
84
87
|
for segment in newly_created_segments:
|
|
85
88
|
try:
|
|
@@ -92,7 +95,12 @@ def _convert_sequences_to_db_segments(
|
|
|
92
95
|
|
|
93
96
|
logger.info(
|
|
94
97
|
"LabelVideoSegment conversion finished for video %s. Segments Created: %d, Skipped: %d, Errors: %d. States Created: %d, State Errors: %d",
|
|
95
|
-
video.uuid,
|
|
98
|
+
video.uuid,
|
|
99
|
+
created_count,
|
|
100
|
+
skipped_count,
|
|
101
|
+
error_count,
|
|
102
|
+
state_created_count,
|
|
103
|
+
state_error_count,
|
|
96
104
|
)
|
|
97
105
|
|
|
98
106
|
|
|
@@ -160,15 +168,14 @@ def _get_outside_frames(video: "VideoFile", outside_label_name: str = "outside")
|
|
|
160
168
|
|
|
161
169
|
q_objects: Q | None = None
|
|
162
170
|
for segment in outside_segments:
|
|
163
|
-
clause = Q(frame_number__gte=segment.start_frame_number,
|
|
164
|
-
frame_number__lt=segment.end_frame_number)
|
|
171
|
+
clause = Q(frame_number__gte=segment.start_frame_number, frame_number__lt=segment.end_frame_number)
|
|
165
172
|
q_objects = clause if q_objects is None else q_objects | clause
|
|
166
173
|
|
|
167
174
|
if q_objects is None:
|
|
168
175
|
return Frame.objects.none()
|
|
169
176
|
|
|
170
177
|
try:
|
|
171
|
-
return video.frames.filter(q_objects).distinct().order_by(
|
|
178
|
+
return video.frames.filter(q_objects).distinct().order_by("frame_number")
|
|
172
179
|
except Exception as e:
|
|
173
180
|
logger.error("Error filtering outside frames for video %s: %s", video.uuid, e, exc_info=True)
|
|
174
181
|
return Frame.objects.none()
|
|
@@ -177,11 +184,12 @@ def _get_outside_frames(video: "VideoFile", outside_label_name: str = "outside")
|
|
|
177
184
|
def _get_outside_frame_paths(video: "VideoFile", outside_label_name: str = "outside") -> List["Path"]:
|
|
178
185
|
"""Gets the file paths of frames that fall within 'outside' segments."""
|
|
179
186
|
from pathlib import Path # Local import
|
|
187
|
+
|
|
180
188
|
frames = _get_outside_frames(video, outside_label_name=outside_label_name)
|
|
181
189
|
frame_paths = []
|
|
182
190
|
for frame in frames:
|
|
183
191
|
try:
|
|
184
|
-
frame_paths.append(Path(frame.
|
|
192
|
+
frame_paths.append(Path(frame.relative_path))
|
|
185
193
|
except Exception as e:
|
|
186
194
|
logger.warning("Could not get path for frame %s (Number: %d): %s", frame.pk, frame.frame_number, e)
|
|
187
195
|
ic(f"Could not get path for frame {frame.pk}: {e}")
|
|
@@ -206,4 +214,3 @@ def _label_segments_to_frame_annotations(video: "VideoFile"):
|
|
|
206
214
|
logger.info("Processed %d segments for frame annotations for video %s", processed_count, video.uuid)
|
|
207
215
|
except AttributeError:
|
|
208
216
|
logger.error("Could not generate frame annotations for video %s. 'label_video_segments' related manager not found.", video.uuid)
|
|
209
|
-
|
|
@@ -4,59 +4,43 @@ Video Metadata Model
|
|
|
4
4
|
Stores analysis results for videos (sensitive frames, detection statistics).
|
|
5
5
|
Created as part of Phase 1.1: Video Correction API Endpoints.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from django.db import models
|
|
9
|
+
|
|
8
10
|
from .video_file import VideoFile
|
|
9
11
|
|
|
12
|
+
|
|
10
13
|
class VideoMetadata(models.Model):
|
|
11
14
|
"""
|
|
12
15
|
Stores analysis results for videos after sensitive frame detection.
|
|
13
|
-
|
|
16
|
+
|
|
14
17
|
This model holds the output of frame analysis operations (MiniCPM, OCR+LLM)
|
|
15
18
|
and provides metrics for the correction UI.
|
|
16
19
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
related_name='metadata',
|
|
21
|
-
help_text="Video file this metadata belongs to"
|
|
22
|
-
)
|
|
23
|
-
|
|
20
|
+
|
|
21
|
+
video = models.OneToOneField(VideoFile, on_delete=models.CASCADE, related_name="metadata", help_text="Video file this metadata belongs to")
|
|
22
|
+
|
|
24
23
|
# Analysis Results
|
|
25
|
-
sensitive_frame_count = models.IntegerField(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
sensitive_ratio = models.FloatField(
|
|
31
|
-
null=True,
|
|
32
|
-
blank=True,
|
|
33
|
-
help_text="Ratio of sensitive frames to total frames (0.0-1.0)"
|
|
34
|
-
)
|
|
35
|
-
sensitive_frame_ids = models.TextField(
|
|
36
|
-
null=True,
|
|
37
|
-
blank=True,
|
|
38
|
-
help_text="JSON array of sensitive frame indices (0-based)"
|
|
39
|
-
)
|
|
40
|
-
|
|
24
|
+
sensitive_frame_count = models.IntegerField(null=True, blank=True, help_text="Number of frames detected as containing sensitive information")
|
|
25
|
+
sensitive_ratio = models.FloatField(null=True, blank=True, help_text="Ratio of sensitive frames to total frames (0.0-1.0)")
|
|
26
|
+
sensitive_frame_ids = models.TextField(null=True, blank=True, help_text="JSON array of sensitive frame indices (0-based)")
|
|
27
|
+
|
|
41
28
|
# Metadata
|
|
42
|
-
analyzed_at = models.DateTimeField(
|
|
43
|
-
|
|
44
|
-
help_text="Timestamp of last analysis"
|
|
45
|
-
)
|
|
46
|
-
|
|
29
|
+
analyzed_at = models.DateTimeField(auto_now=True, help_text="Timestamp of last analysis")
|
|
30
|
+
|
|
47
31
|
class Meta:
|
|
48
|
-
db_table =
|
|
49
|
-
verbose_name =
|
|
50
|
-
verbose_name_plural =
|
|
51
|
-
|
|
32
|
+
db_table = "video_metadata"
|
|
33
|
+
verbose_name = "Video Metadata"
|
|
34
|
+
verbose_name_plural = "Video Metadata"
|
|
35
|
+
|
|
52
36
|
def __str__(self):
|
|
53
37
|
return f"Metadata for {self.video.uuid} ({self.sensitive_frame_count or 0} sensitive frames)"
|
|
54
|
-
|
|
38
|
+
|
|
55
39
|
@property
|
|
56
40
|
def has_analysis(self) -> bool:
|
|
57
41
|
"""Check if this video has been analyzed."""
|
|
58
42
|
return self.sensitive_frame_count is not None
|
|
59
|
-
|
|
43
|
+
|
|
60
44
|
@property
|
|
61
45
|
def sensitive_percentage(self) -> float:
|
|
62
46
|
"""Get sensitivity as percentage (0-100)."""
|