endoreg-db 0.8.4.4__py3-none-any.whl → 0.8.8.0__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 +8 -31
- endoreg_db/data/_examples/disease.yaml +55 -0
- endoreg_db/data/_examples/disease_classification.yaml +13 -0
- endoreg_db/data/_examples/disease_classification_choice.yaml +62 -0
- endoreg_db/data/_examples/event.yaml +64 -0
- endoreg_db/data/_examples/examination.yaml +72 -0
- endoreg_db/data/_examples/finding/anatomy_colon.yaml +128 -0
- endoreg_db/data/_examples/finding/colonoscopy.yaml +40 -0
- endoreg_db/data/_examples/finding/colonoscopy_bowel_prep.yaml +56 -0
- endoreg_db/data/_examples/finding/complication.yaml +16 -0
- endoreg_db/data/_examples/finding/data.yaml +105 -0
- endoreg_db/data/_examples/finding/examination_setting.yaml +16 -0
- endoreg_db/data/_examples/finding/medication_related.yaml +18 -0
- endoreg_db/data/_examples/finding/outcome.yaml +12 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_bowel_preparation.yaml +68 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_jnet.yaml +22 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_kudo.yaml +25 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_size.yaml +68 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_location.yaml +80 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lst.yaml +21 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_nice.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_paris.yaml +26 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_sano.yaml +22 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_summary.yaml +53 -0
- endoreg_db/data/_examples/finding_classification/complication_generic.yaml +25 -0
- endoreg_db/data/_examples/finding_classification/examination_setting_generic.yaml +40 -0
- endoreg_db/data/_examples/finding_classification/histology_colo.yaml +51 -0
- endoreg_db/data/_examples/finding_classification/intervention_required.yaml +26 -0
- endoreg_db/data/_examples/finding_classification/medication_related.yaml +23 -0
- endoreg_db/data/_examples/finding_classification/visualized.yaml +33 -0
- endoreg_db/data/_examples/finding_classification_choice/bowel_preparation.yaml +78 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_circularity_default.yaml +32 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_lst.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_nice.yaml +17 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_paris.yaml +57 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_planarity_default.yaml +49 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_sano.yaml +14 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_surface_intact_default.yaml +36 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_location.yaml +229 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_size.yaml +82 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/complication_generic_types.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/histology.yaml +24 -0
- endoreg_db/data/_examples/finding_classification_choice/histology_polyp.yaml +20 -0
- endoreg_db/data/_examples/finding_classification_choice/outcome.yaml +19 -0
- endoreg_db/data/_examples/finding_classification_choice/yes_no_na.yaml +11 -0
- endoreg_db/data/_examples/finding_classification_type/colonoscopy_basic.yaml +48 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy.yaml +43 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_colonoscopy.yaml +168 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_egd.yaml +128 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_ercp.yaml +32 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_eus_lower.yaml +9 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_eus_upper.yaml +36 -0
- endoreg_db/data/_examples/finding_intervention_type/endoscopy.yaml +15 -0
- endoreg_db/data/_examples/finding_type/data.yaml +43 -0
- endoreg_db/data/_examples/requirement/age.yaml +26 -0
- endoreg_db/data/_examples/requirement/colonoscopy_baseline_austria.yaml +45 -0
- endoreg_db/data/_examples/requirement/disease_cardiovascular.yaml +79 -0
- endoreg_db/data/_examples/requirement/disease_classification_choice_cardiovascular.yaml +41 -0
- endoreg_db/data/_examples/requirement/disease_hepatology.yaml +12 -0
- endoreg_db/data/_examples/requirement/disease_misc.yaml +12 -0
- endoreg_db/data/_examples/requirement/disease_renal.yaml +96 -0
- endoreg_db/data/_examples/requirement/endoscopy_bleeding_risk.yaml +59 -0
- endoreg_db/data/_examples/requirement/event_cardiology.yaml +251 -0
- endoreg_db/data/_examples/requirement/event_requirements.yaml +145 -0
- endoreg_db/data/_examples/requirement/finding_colon_polyp.yaml +50 -0
- endoreg_db/data/_examples/requirement/gender.yaml +25 -0
- endoreg_db/data/_examples/requirement/lab_value.yaml +441 -0
- endoreg_db/data/_examples/requirement/medication.yaml +93 -0
- endoreg_db/data/_examples/requirement_operator/age.yaml +13 -0
- endoreg_db/data/_examples/requirement_operator/lab_operators.yaml +129 -0
- endoreg_db/data/_examples/requirement_operator/model_operators.yaml +96 -0
- endoreg_db/data/_examples/requirement_set/01_endoscopy_generic.yaml +48 -0
- endoreg_db/data/_examples/requirement_set/colonoscopy_austria_screening.yaml +57 -0
- endoreg_db/data/_examples/yaml_examples.xlsx +0 -0
- endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +4 -3
- endoreg_db/data/event_classification/data.yaml +4 -0
- endoreg_db/data/event_classification_choice/data.yaml +9 -0
- endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +43 -70
- endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +22 -52
- endoreg_db/data/finding_classification/colonoscopy_location.yaml +31 -62
- endoreg_db/data/finding_classification/histology_colo.yaml +28 -36
- endoreg_db/data/requirement/colon_polyp_intervention.yaml +49 -0
- endoreg_db/data/requirement/coloreg_colon_polyp.yaml +49 -0
- endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +31 -12
- endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
- endoreg_db/data/requirement_set/02_endoscopy_bleeding_risk.yaml +46 -0
- endoreg_db/data/requirement_set/90_coloreg.yaml +178 -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 +5 -2
- endoreg_db/helpers/data_loader.py +1 -1
- 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_video.py +9 -10
- endoreg_db/management/commands/import_video_with_classification.py +1 -1
- endoreg_db/management/commands/init_default_ai_model.py +1 -1
- endoreg_db/management/commands/list_routes.py +18 -0
- endoreg_db/management/commands/load_ai_model_data.py +2 -1
- endoreg_db/management/commands/load_center_data.py +12 -12
- endoreg_db/management/commands/load_requirement_data.py +60 -31
- endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
- endoreg_db/management/commands/setup_endoreg_db.py +14 -10
- endoreg_db/management/commands/storage_management.py +271 -203
- endoreg_db/migrations/0001_initial.py +1799 -1300
- endoreg_db/migrations/0002_requirementset_depends_on.py +18 -0
- endoreg_db/migrations/_old/0001_initial.py +1857 -0
- endoreg_db/migrations/_old/0004_employee_city_employee_post_code_employee_street_and_more.py +68 -0
- endoreg_db/migrations/_old/0004_remove_casetemplate_rules_and_more.py +77 -0
- endoreg_db/migrations/_old/0005_merge_20251111_1003.py +14 -0
- endoreg_db/migrations/_old/0006_sensitivemeta_anonymized_text_and_more.py +68 -0
- endoreg_db/migrations/_old/0007_remove_rule_attribute_dtype_remove_rule_rule_type_and_more.py +89 -0
- endoreg_db/migrations/_old/0008_remove_event_event_classification_and_more.py +27 -0
- endoreg_db/migrations/_old/0009_alter_modelmeta_options_and_more.py +21 -0
- 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 +103 -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 +7 -8
- endoreg_db/models/label/annotation/image_classification.py +10 -9
- endoreg_db/models/label/annotation/video_segmentation_annotation.py +8 -5
- 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 +76 -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 +249 -177
- endoreg_db/models/media/pdf/report_file.py +25 -29
- endoreg_db/models/media/pdf/report_reader/report_reader_config.py +30 -46
- endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
- endoreg_db/models/media/video/__init__.py +1 -0
- endoreg_db/models/media/video/create_from_file.py +48 -56
- endoreg_db/models/media/video/pipe_1.py +30 -33
- endoreg_db/models/media/video/pipe_2.py +8 -9
- endoreg_db/models/media/video/video_file.py +359 -204
- 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 +109 -62
- 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/__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 +17 -18
- endoreg_db/models/medical/examination/examination_indication.py +26 -25
- endoreg_db/models/medical/examination/examination_time.py +16 -6
- 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 +38 -39
- endoreg_db/models/medical/finding/finding_classification.py +37 -48
- endoreg_db/models/medical/finding/finding_intervention.py +27 -22
- endoreg_db/models/medical/finding/finding_type.py +13 -12
- endoreg_db/models/medical/hardware/endoscope.py +20 -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 +1 -5
- 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 +139 -18
- endoreg_db/models/metadata/pdf_meta.py +19 -24
- endoreg_db/models/metadata/sensitive_meta.py +102 -85
- endoreg_db/models/metadata/sensitive_meta_logic.py +383 -43
- 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 +25 -25
- 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/requirement/requirement.py +580 -272
- 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 +36 -33
- 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 +46 -47
- endoreg_db/models/state/label_video_segment.py +9 -0
- endoreg_db/models/state/raw_pdf.py +40 -46
- endoreg_db/models/state/sensitive_meta.py +6 -2
- endoreg_db/models/state/video.py +58 -53
- endoreg_db/models/upload_job.py +32 -55
- endoreg_db/models/utils.py +1 -2
- endoreg_db/root_urls.py +21 -2
- endoreg_db/serializers/__init__.py +26 -57
- endoreg_db/serializers/anonymization.py +18 -10
- endoreg_db/serializers/meta/report_meta.py +1 -1
- endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
- endoreg_db/serializers/misc/__init__.py +1 -1
- endoreg_db/serializers/misc/file_overview.py +33 -91
- endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
- endoreg_db/serializers/requirements/requirement_sets.py +92 -22
- endoreg_db/serializers/video/segmentation.py +2 -1
- endoreg_db/serializers/video/video_processing_history.py +20 -5
- endoreg_db/serializers/video_examination.py +198 -0
- endoreg_db/services/anonymization.py +75 -73
- endoreg_db/services/lookup_service.py +256 -73
- endoreg_db/services/lookup_store.py +174 -30
- endoreg_db/services/pdf_import.py +711 -310
- endoreg_db/services/storage_aware_video_processor.py +140 -114
- endoreg_db/services/video_import.py +266 -117
- endoreg_db/urls/__init__.py +27 -27
- endoreg_db/urls/label_video_segments.py +2 -0
- endoreg_db/urls/media.py +108 -66
- 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 +88 -16
- endoreg_db/utils/defaults/set_default_center.py +32 -0
- endoreg_db/utils/names.py +22 -16
- endoreg_db/utils/permissions.py +2 -1
- endoreg_db/utils/pipelines/process_video_dir.py +1 -1
- endoreg_db/utils/requirement_operator_logic/model_evaluators.py +414 -127
- 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 +5 -12
- endoreg_db/views/anonymization/media_management.py +198 -163
- endoreg_db/views/anonymization/overview.py +4 -1
- endoreg_db/views/anonymization/validate.py +174 -40
- endoreg_db/views/media/__init__.py +2 -0
- endoreg_db/views/media/pdf_media.py +131 -150
- endoreg_db/views/media/sensitive_metadata.py +46 -6
- endoreg_db/views/media/video_media.py +89 -82
- endoreg_db/views/media/video_segments.py +187 -260
- endoreg_db/views/meta/sensitive_meta_detail.py +0 -63
- endoreg_db/views/patient/patient.py +5 -4
- endoreg_db/views/pdf/__init__.py +5 -8
- endoreg_db/views/pdf/pdf_stream.py +186 -0
- endoreg_db/views/pdf/pdf_stream_views.py +0 -127
- endoreg_db/views/pdf/reimport.py +86 -91
- endoreg_db/views/requirement/evaluate.py +188 -187
- endoreg_db/views/requirement/lookup.py +186 -288
- endoreg_db/views/requirement/requirement_utils.py +89 -0
- endoreg_db/views/video/__init__.py +0 -4
- endoreg_db/views/video/correction.py +2 -2
- endoreg_db/views/video/video_examination_viewset.py +202 -289
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/METADATA +7 -3
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/RECORD +350 -255
- endoreg_db/models/administration/permissions/__init__.py +0 -44
- endoreg_db/models/media/video/refactor_plan.md +0 -0
- endoreg_db/models/media/video/video_file_frames.py +0 -0
- endoreg_db/models/metadata/frame_ocr_result.py +0 -0
- 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/serializers/video/video_metadata.py +0 -105
- 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/views/pdf/pdf_media.py +0 -239
- endoreg_db/views/report/__init__.py +0 -9
- 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/video_media.py +0 -158
- endoreg_db/views.py +0 -0
- /endoreg_db/data/{requirement_set → _examples/requirement_set}/endoscopy_bleeding_risk.yaml +0 -0
- /endoreg_db/migrations/{0002_add_video_correction_models.py → _old/0002_add_video_correction_models.py} +0 -0
- /endoreg_db/migrations/{0003_add_center_display_name.py → _old/0003_add_center_display_name.py} +0 -0
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import subprocess
|
|
3
1
|
import json
|
|
4
2
|
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
5
6
|
from functools import lru_cache
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import
|
|
8
|
+
from typing import Dict, List, Optional, Tuple
|
|
9
|
+
|
|
8
10
|
import cv2
|
|
9
11
|
from tqdm import tqdm
|
|
10
|
-
import shutil
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger("ffmpeg_wrapper")
|
|
13
14
|
|
|
@@ -28,13 +29,9 @@ def _resolve_ffmpeg_executable() -> Optional[str]:
|
|
|
28
29
|
|
|
29
30
|
# 2) Django settings overrides (if Django is configured)
|
|
30
31
|
try:
|
|
31
|
-
from django.conf import settings
|
|
32
|
+
from django.conf import settings
|
|
32
33
|
|
|
33
|
-
env_candidates.extend(
|
|
34
|
-
getattr(settings, attr)
|
|
35
|
-
for attr in ("FFMPEG_EXECUTABLE", "FFMPEG_BINARY", "FFMPEG_PATH")
|
|
36
|
-
if hasattr(settings, attr)
|
|
37
|
-
)
|
|
34
|
+
env_candidates.extend(getattr(settings, attr) for attr in ("FFMPEG_EXECUTABLE", "FFMPEG_BINARY", "FFMPEG_PATH") if hasattr(settings, attr))
|
|
38
35
|
except Exception:
|
|
39
36
|
# Django might not be configured for every consumer
|
|
40
37
|
pass
|
|
@@ -76,35 +73,27 @@ def _resolve_ffmpeg_executable() -> Optional[str]:
|
|
|
76
73
|
|
|
77
74
|
return None
|
|
78
75
|
|
|
76
|
+
|
|
79
77
|
def _detect_nvenc_support() -> bool:
|
|
80
78
|
"""
|
|
81
79
|
Detect if NVIDIA NVENC hardware acceleration is available.
|
|
82
|
-
|
|
80
|
+
|
|
83
81
|
Returns:
|
|
84
82
|
True if NVENC is available, False otherwise
|
|
85
83
|
"""
|
|
86
84
|
try:
|
|
87
85
|
# Test NVENC availability with a minimal command (minimum size for NVENC)
|
|
88
|
-
cmd = [
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
result = subprocess.run(
|
|
94
|
-
cmd,
|
|
95
|
-
capture_output=True,
|
|
96
|
-
text=True,
|
|
97
|
-
timeout=15,
|
|
98
|
-
check=False
|
|
99
|
-
)
|
|
100
|
-
|
|
86
|
+
cmd = ["ffmpeg", "-f", "lavfi", "-i", "testsrc=duration=1:size=256x256:rate=1", "-c:v", "h264_nvenc", "-preset", "p1", "-f", "null", "-"]
|
|
87
|
+
|
|
88
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15, check=False)
|
|
89
|
+
|
|
101
90
|
if result.returncode == 0:
|
|
102
91
|
logger.debug("NVENC h264 encoding test successful")
|
|
103
92
|
return True
|
|
104
93
|
else:
|
|
105
94
|
logger.debug(f"NVENC test failed: {result.stderr}")
|
|
106
95
|
return False
|
|
107
|
-
|
|
96
|
+
|
|
108
97
|
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
109
98
|
logger.debug(f"NVENC detection failed: {e}")
|
|
110
99
|
return False
|
|
@@ -112,129 +101,132 @@ def _detect_nvenc_support() -> bool:
|
|
|
112
101
|
logger.warning(f"Unexpected error during NVENC detection: {e}")
|
|
113
102
|
return False
|
|
114
103
|
|
|
104
|
+
|
|
115
105
|
def _get_preferred_encoder() -> Dict[str, str]:
|
|
116
106
|
"""
|
|
117
107
|
Get the preferred video encoder configuration based on available hardware.
|
|
118
|
-
|
|
108
|
+
|
|
119
109
|
Returns:
|
|
120
110
|
Dictionary with encoder configuration
|
|
121
111
|
"""
|
|
122
112
|
global _nvenc_available, _preferred_encoder
|
|
123
|
-
|
|
113
|
+
|
|
124
114
|
if _nvenc_available is None:
|
|
125
115
|
_nvenc_available = _detect_nvenc_support()
|
|
126
|
-
|
|
116
|
+
|
|
127
117
|
if _preferred_encoder is None:
|
|
128
118
|
if _nvenc_available:
|
|
129
119
|
_preferred_encoder = {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
120
|
+
"name": "h264_nvenc",
|
|
121
|
+
"preset_param": "-preset",
|
|
122
|
+
"preset_value": "p4", # Medium quality/speed for NVENC
|
|
123
|
+
"quality_param": "-cq",
|
|
124
|
+
"quality_value": "20", # NVENC CQ mode
|
|
125
|
+
"type": "nvenc",
|
|
126
|
+
"fallback_preset": "p1", # Fastest NVENC preset for fallback
|
|
137
127
|
}
|
|
138
128
|
logger.info("Hardware acceleration: NVENC available")
|
|
139
129
|
else:
|
|
140
130
|
_preferred_encoder = {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
"name": "libx264",
|
|
132
|
+
"preset_param": "-preset",
|
|
133
|
+
"preset_value": "medium", # CPU preset
|
|
134
|
+
"quality_param": "-crf",
|
|
135
|
+
"quality_value": "23", # CPU CRF mode
|
|
136
|
+
"type": "cpu",
|
|
137
|
+
"fallback_preset": "ultrafast", # Fastest CPU preset for fallback
|
|
148
138
|
}
|
|
149
139
|
logger.info("Hardware acceleration: NVENC not available, using CPU")
|
|
150
|
-
|
|
140
|
+
|
|
151
141
|
return _preferred_encoder
|
|
152
142
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
custom_crf: Optional[int] = None) -> Tuple[List[str], str]:
|
|
143
|
+
|
|
144
|
+
def _build_encoder_args(quality_mode: str = "balanced", fallback: bool = False, custom_crf: Optional[int] = None) -> Tuple[List[str], str]:
|
|
156
145
|
"""
|
|
157
146
|
Build encoder command arguments based on available hardware and quality requirements.
|
|
158
|
-
|
|
147
|
+
|
|
159
148
|
Args:
|
|
160
149
|
quality_mode: 'fast', 'balanced', or 'quality'
|
|
161
150
|
fallback: Whether to use fallback settings for compatibility
|
|
162
151
|
custom_crf: Override quality setting (for backward compatibility)
|
|
163
|
-
|
|
152
|
+
|
|
164
153
|
Returns:
|
|
165
154
|
Tuple of (encoder_args, encoder_type)
|
|
166
155
|
"""
|
|
167
156
|
encoder = _get_preferred_encoder()
|
|
168
|
-
|
|
169
|
-
if encoder[
|
|
157
|
+
|
|
158
|
+
if encoder["type"] == "nvenc":
|
|
170
159
|
# NVIDIA NVENC configuration
|
|
171
160
|
if fallback:
|
|
172
|
-
preset = encoder[
|
|
173
|
-
quality =
|
|
174
|
-
elif quality_mode ==
|
|
175
|
-
preset =
|
|
176
|
-
quality =
|
|
177
|
-
elif quality_mode ==
|
|
178
|
-
preset =
|
|
179
|
-
quality =
|
|
161
|
+
preset = encoder["fallback_preset"] # p1 - fastest
|
|
162
|
+
quality = "28" # Lower quality for speed
|
|
163
|
+
elif quality_mode == "fast":
|
|
164
|
+
preset = "p2" # Faster preset
|
|
165
|
+
quality = "25"
|
|
166
|
+
elif quality_mode == "quality":
|
|
167
|
+
preset = "p6" # Higher quality preset
|
|
168
|
+
quality = "18"
|
|
180
169
|
else: # balanced
|
|
181
|
-
preset = encoder[
|
|
182
|
-
quality = encoder[
|
|
183
|
-
|
|
170
|
+
preset = encoder["preset_value"] # p4
|
|
171
|
+
quality = encoder["quality_value"] # 20
|
|
172
|
+
|
|
184
173
|
# Override with custom CRF if provided (for backward compatibility)
|
|
185
174
|
if custom_crf is not None:
|
|
186
175
|
quality = str(custom_crf)
|
|
187
|
-
|
|
176
|
+
|
|
188
177
|
return [
|
|
189
|
-
|
|
190
|
-
encoder[
|
|
191
|
-
encoder[
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
178
|
+
"-c:v",
|
|
179
|
+
encoder["name"],
|
|
180
|
+
encoder["preset_param"],
|
|
181
|
+
preset,
|
|
182
|
+
encoder["quality_param"],
|
|
183
|
+
quality,
|
|
184
|
+
"-gpu",
|
|
185
|
+
"0", # Use first GPU
|
|
186
|
+
"-rc",
|
|
187
|
+
"vbr", # Variable bitrate
|
|
188
|
+
"-profile:v",
|
|
189
|
+
"high",
|
|
190
|
+
], encoder["type"]
|
|
196
191
|
else:
|
|
197
192
|
# CPU libx264 configuration
|
|
198
193
|
if fallback:
|
|
199
|
-
preset = encoder[
|
|
200
|
-
quality =
|
|
201
|
-
elif quality_mode ==
|
|
202
|
-
preset =
|
|
203
|
-
quality =
|
|
204
|
-
elif quality_mode ==
|
|
205
|
-
preset =
|
|
206
|
-
quality =
|
|
194
|
+
preset = encoder["fallback_preset"] # ultrafast
|
|
195
|
+
quality = "28" # Lower quality for speed
|
|
196
|
+
elif quality_mode == "fast":
|
|
197
|
+
preset = "faster"
|
|
198
|
+
quality = "20"
|
|
199
|
+
elif quality_mode == "quality":
|
|
200
|
+
preset = "slow"
|
|
201
|
+
quality = "18"
|
|
207
202
|
else: # balanced
|
|
208
|
-
preset = encoder[
|
|
209
|
-
quality = encoder[
|
|
210
|
-
|
|
203
|
+
preset = encoder["preset_value"] # medium
|
|
204
|
+
quality = encoder["quality_value"] # 23
|
|
205
|
+
|
|
211
206
|
# Override with custom CRF if provided (for backward compatibility)
|
|
212
207
|
if custom_crf is not None:
|
|
213
208
|
quality = str(custom_crf)
|
|
214
|
-
|
|
215
|
-
return [
|
|
216
|
-
|
|
217
|
-
encoder['preset_param'], preset,
|
|
218
|
-
encoder['quality_param'], quality,
|
|
219
|
-
'-profile:v', 'high'
|
|
220
|
-
], encoder['type']
|
|
209
|
+
|
|
210
|
+
return ["-c:v", encoder["name"], encoder["preset_param"], preset, encoder["quality_param"], quality, "-profile:v", "high"], encoder["type"]
|
|
211
|
+
|
|
221
212
|
|
|
222
213
|
def is_ffmpeg_available() -> bool:
|
|
223
214
|
"""
|
|
224
215
|
Checks whether the FFmpeg executable is available in the system's PATH.
|
|
225
|
-
|
|
216
|
+
|
|
226
217
|
Returns:
|
|
227
218
|
True if FFmpeg is found in the PATH; otherwise, False.
|
|
228
219
|
"""
|
|
229
220
|
return _resolve_ffmpeg_executable() is not None
|
|
230
221
|
|
|
222
|
+
|
|
231
223
|
def check_ffmpeg_availability():
|
|
232
224
|
"""
|
|
233
225
|
Verifies that FFmpeg is installed and available in the system's PATH.
|
|
234
|
-
|
|
226
|
+
|
|
235
227
|
Raises:
|
|
236
228
|
FileNotFoundError: If FFmpeg is not found.
|
|
237
|
-
|
|
229
|
+
|
|
238
230
|
Returns:
|
|
239
231
|
True if FFmpeg is available.
|
|
240
232
|
"""
|
|
@@ -245,10 +237,11 @@ def check_ffmpeg_availability():
|
|
|
245
237
|
# logger.info("FFmpeg is available.") # Caller can log if needed
|
|
246
238
|
return True
|
|
247
239
|
|
|
240
|
+
|
|
248
241
|
def get_stream_info(file_path: Path) -> Optional[Dict]:
|
|
249
242
|
"""
|
|
250
243
|
Retrieves video stream information from a file using ffprobe.
|
|
251
|
-
|
|
244
|
+
|
|
252
245
|
Runs ffprobe to extract stream metadata in JSON format from the specified video file. Returns a dictionary with stream information, or None if the file does not exist or if an error occurs during execution or parsing.
|
|
253
246
|
"""
|
|
254
247
|
if not file_path.exists():
|
|
@@ -257,8 +250,10 @@ def get_stream_info(file_path: Path) -> Optional[Dict]:
|
|
|
257
250
|
|
|
258
251
|
command = [
|
|
259
252
|
"ffprobe",
|
|
260
|
-
"-v",
|
|
261
|
-
"
|
|
253
|
+
"-v",
|
|
254
|
+
"quiet",
|
|
255
|
+
"-print_format",
|
|
256
|
+
"json",
|
|
262
257
|
"-show_streams",
|
|
263
258
|
str(file_path),
|
|
264
259
|
]
|
|
@@ -276,7 +271,7 @@ def get_stream_info(file_path: Path) -> Optional[Dict]:
|
|
|
276
271
|
return None
|
|
277
272
|
|
|
278
273
|
|
|
279
|
-
def assemble_video_from_frames(
|
|
274
|
+
def assemble_video_from_frames( # Renamed from assemble_video
|
|
280
275
|
frame_paths: List[Path],
|
|
281
276
|
output_path: Path,
|
|
282
277
|
fps: float,
|
|
@@ -302,7 +297,7 @@ def assemble_video_from_frames( # Renamed from assemble_video
|
|
|
302
297
|
logger.error("Error reading first frame to determine dimensions: %s", e, exc_info=True)
|
|
303
298
|
return None
|
|
304
299
|
|
|
305
|
-
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
300
|
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
306
301
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
307
302
|
video_writer = cv2.VideoWriter(str(output_path), fourcc, fps, (width, height))
|
|
308
303
|
|
|
@@ -343,7 +338,7 @@ def transcode_video(
|
|
|
343
338
|
) -> Optional[Path]:
|
|
344
339
|
"""
|
|
345
340
|
Transcodes a video file using FFmpeg with automatic hardware acceleration.
|
|
346
|
-
|
|
341
|
+
|
|
347
342
|
Args:
|
|
348
343
|
input_path: Source video file path
|
|
349
344
|
output_path: Output video file path
|
|
@@ -355,7 +350,7 @@ def transcode_video(
|
|
|
355
350
|
extra_args: Additional FFmpeg arguments
|
|
356
351
|
quality_mode: Quality mode ('fast', 'balanced', 'quality')
|
|
357
352
|
force_cpu: Force CPU encoding even if NVENC is available
|
|
358
|
-
|
|
353
|
+
|
|
359
354
|
Returns:
|
|
360
355
|
Path to transcoded video or None if failed
|
|
361
356
|
"""
|
|
@@ -371,8 +366,8 @@ def transcode_video(
|
|
|
371
366
|
# Force CPU encoding
|
|
372
367
|
encoder_args, encoder_type = _build_encoder_args(quality_mode, fallback=False, custom_crf=crf)
|
|
373
368
|
# Override to use CPU encoder
|
|
374
|
-
encoder_args[1] =
|
|
375
|
-
encoder_args[3] =
|
|
369
|
+
encoder_args[1] = "libx264" # Replace encoder name
|
|
370
|
+
encoder_args[3] = "medium" if preset == "auto" else preset # Replace preset
|
|
376
371
|
if crf is not None:
|
|
377
372
|
encoder_args[5] = str(crf) # Replace quality value
|
|
378
373
|
else:
|
|
@@ -381,28 +376,33 @@ def transcode_video(
|
|
|
381
376
|
else:
|
|
382
377
|
# Manual codec/preset specification (backward compatibility)
|
|
383
378
|
encoder_args = [
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
379
|
+
"-c:v",
|
|
380
|
+
codec,
|
|
381
|
+
"-preset",
|
|
382
|
+
preset,
|
|
383
|
+
"-crf" if codec == "libx264" else "-cq",
|
|
384
|
+
str(crf if crf is not None else 23),
|
|
387
385
|
]
|
|
388
|
-
encoder_type =
|
|
386
|
+
encoder_type = "nvenc" if "nvenc" in codec else "cpu"
|
|
389
387
|
|
|
390
388
|
# Build complete command
|
|
391
389
|
command = [
|
|
392
390
|
"ffmpeg",
|
|
393
|
-
"-i",
|
|
391
|
+
"-i",
|
|
392
|
+
str(input_path),
|
|
394
393
|
*encoder_args,
|
|
395
|
-
"-c:a",
|
|
396
|
-
|
|
394
|
+
"-c:a",
|
|
395
|
+
audio_codec,
|
|
396
|
+
"-b:a",
|
|
397
|
+
audio_bitrate,
|
|
397
398
|
"-y", # Overwrite output file if it exists
|
|
398
399
|
]
|
|
399
|
-
|
|
400
|
+
|
|
400
401
|
if extra_args:
|
|
401
402
|
command.extend(extra_args)
|
|
402
403
|
command.append(str(output_path))
|
|
403
404
|
|
|
404
|
-
logger.info("Starting transcoding: %s -> %s (using %s)",
|
|
405
|
-
input_path.name, output_path.name, encoder_type)
|
|
405
|
+
logger.info("Starting transcoding: %s -> %s (using %s)", input_path.name, output_path.name, encoder_type)
|
|
406
406
|
logger.debug("FFmpeg command: %s", " ".join(command))
|
|
407
407
|
|
|
408
408
|
try:
|
|
@@ -420,18 +420,14 @@ def transcode_video(
|
|
|
420
420
|
logger.info("Transcoding finished successfully: %s", output_path)
|
|
421
421
|
return output_path
|
|
422
422
|
else:
|
|
423
|
-
logger.error("FFmpeg transcoding failed for %s with return code %d.",
|
|
424
|
-
input_path.name, process.returncode)
|
|
423
|
+
logger.error("FFmpeg transcoding failed for %s with return code %d.", input_path.name, process.returncode)
|
|
425
424
|
logger.error("FFmpeg stderr:\n%s", stderr_output)
|
|
426
|
-
|
|
425
|
+
|
|
427
426
|
# Try fallback to CPU if NVENC failed
|
|
428
|
-
if encoder_type ==
|
|
427
|
+
if encoder_type == "nvenc" and not force_cpu:
|
|
429
428
|
logger.warning("NVENC transcoding failed, trying CPU fallback...")
|
|
430
|
-
return _transcode_video_fallback(
|
|
431
|
-
|
|
432
|
-
extra_args, quality_mode, crf
|
|
433
|
-
)
|
|
434
|
-
|
|
429
|
+
return _transcode_video_fallback(input_path, output_path, audio_codec, audio_bitrate, extra_args, quality_mode, crf)
|
|
430
|
+
|
|
435
431
|
# Clean up potentially corrupted output file
|
|
436
432
|
if output_path.exists():
|
|
437
433
|
try:
|
|
@@ -447,18 +443,13 @@ def transcode_video(
|
|
|
447
443
|
logger.error("Error during transcoding of %s: %s", input_path.name, e, exc_info=True)
|
|
448
444
|
return None
|
|
449
445
|
|
|
446
|
+
|
|
450
447
|
def _transcode_video_fallback(
|
|
451
|
-
input_path: Path,
|
|
452
|
-
output_path: Path,
|
|
453
|
-
audio_codec: str,
|
|
454
|
-
audio_bitrate: str,
|
|
455
|
-
extra_args: Optional[List[str]],
|
|
456
|
-
quality_mode: str,
|
|
457
|
-
custom_crf: Optional[int]
|
|
448
|
+
input_path: Path, output_path: Path, audio_codec: str, audio_bitrate: str, extra_args: Optional[List[str]], quality_mode: str, custom_crf: Optional[int]
|
|
458
449
|
) -> Optional[Path]:
|
|
459
450
|
"""
|
|
460
451
|
Fallback transcoding using CPU encoding.
|
|
461
|
-
|
|
452
|
+
|
|
462
453
|
Args:
|
|
463
454
|
input_path: Source video file path
|
|
464
455
|
output_path: Output video file path
|
|
@@ -467,7 +458,7 @@ def _transcode_video_fallback(
|
|
|
467
458
|
extra_args: Additional FFmpeg arguments
|
|
468
459
|
quality_mode: Quality mode
|
|
469
460
|
custom_crf: Custom CRF value
|
|
470
|
-
|
|
461
|
+
|
|
471
462
|
Returns:
|
|
472
463
|
Path to transcoded video or None if failed
|
|
473
464
|
"""
|
|
@@ -475,24 +466,27 @@ def _transcode_video_fallback(
|
|
|
475
466
|
# Build CPU encoder arguments
|
|
476
467
|
encoder_args, _ = _build_encoder_args(quality_mode, fallback=True, custom_crf=custom_crf)
|
|
477
468
|
# Force CPU encoder
|
|
478
|
-
encoder_args[1] =
|
|
479
|
-
|
|
469
|
+
encoder_args[1] = "libx264"
|
|
470
|
+
|
|
480
471
|
command = [
|
|
481
472
|
"ffmpeg",
|
|
482
|
-
"-i",
|
|
473
|
+
"-i",
|
|
474
|
+
str(input_path),
|
|
483
475
|
*encoder_args,
|
|
484
|
-
"-c:a",
|
|
485
|
-
|
|
476
|
+
"-c:a",
|
|
477
|
+
audio_codec,
|
|
478
|
+
"-b:a",
|
|
479
|
+
audio_bitrate,
|
|
486
480
|
"-y",
|
|
487
481
|
]
|
|
488
|
-
|
|
482
|
+
|
|
489
483
|
if extra_args:
|
|
490
484
|
command.extend(extra_args)
|
|
491
485
|
command.append(str(output_path))
|
|
492
486
|
|
|
493
487
|
logger.info("CPU fallback transcoding: %s -> %s", input_path.name, output_path.name)
|
|
494
488
|
logger.debug("Fallback FFmpeg command: %s", " ".join(command))
|
|
495
|
-
|
|
489
|
+
|
|
496
490
|
process = subprocess.Popen(command, stderr=subprocess.PIPE, text=True, universal_newlines=True)
|
|
497
491
|
stderr_output = ""
|
|
498
492
|
if process.stderr:
|
|
@@ -508,7 +502,7 @@ def _transcode_video_fallback(
|
|
|
508
502
|
logger.error("CPU fallback transcoding also failed for %s", input_path.name)
|
|
509
503
|
logger.error("Fallback stderr:\n%s", stderr_output)
|
|
510
504
|
return None
|
|
511
|
-
|
|
505
|
+
|
|
512
506
|
except Exception as e:
|
|
513
507
|
logger.error("Error during CPU fallback transcoding: %s", e, exc_info=True)
|
|
514
508
|
return None
|
|
@@ -551,12 +545,13 @@ def _transcode_video_fallback(
|
|
|
551
545
|
logger.error("Error during transcoding of %s: %s", input_path.name, e, exc_info=True)
|
|
552
546
|
return None
|
|
553
547
|
|
|
548
|
+
|
|
554
549
|
def transcode_videofile_if_required(
|
|
555
550
|
input_path: Path,
|
|
556
551
|
output_path: Path,
|
|
557
552
|
required_codec: str = "h264",
|
|
558
|
-
required_pixel_format: str = "yuv420p",
|
|
559
|
-
**transcode_options
|
|
553
|
+
required_pixel_format: str = "yuv420p", # Changed default from yuvj420p
|
|
554
|
+
**transcode_options, # Pass other options to transcode_video
|
|
560
555
|
) -> Optional[Path]:
|
|
561
556
|
"""
|
|
562
557
|
Checks if a video needs transcoding based on codec and pixel format,
|
|
@@ -578,7 +573,7 @@ def transcode_videofile_if_required(
|
|
|
578
573
|
codec_name = video_stream.get("codec_name")
|
|
579
574
|
pixel_format = video_stream.get("pix_fmt")
|
|
580
575
|
# Check color range as well, default is usually 'tv' (limited)
|
|
581
|
-
color_range = video_stream.get("color_range", "tv")
|
|
576
|
+
color_range = video_stream.get("color_range", "tv") # Default to tv if not specified
|
|
582
577
|
|
|
583
578
|
needs_transcoding = False
|
|
584
579
|
transcode_reason = []
|
|
@@ -597,44 +592,44 @@ def transcode_videofile_if_required(
|
|
|
597
592
|
if needs_transcoding:
|
|
598
593
|
logger.info("Transcoding %s to %s due to: %s", input_path.name, output_path.name, "; ".join(transcode_reason))
|
|
599
594
|
# Ensure codec and pixel format are set in options if not already present
|
|
600
|
-
transcode_options.setdefault(
|
|
601
|
-
transcode_options.setdefault(
|
|
595
|
+
transcode_options.setdefault("codec", "libx264" if required_codec == "h264" else required_codec)
|
|
596
|
+
transcode_options.setdefault("extra_args", [])
|
|
602
597
|
|
|
603
598
|
# Ensure pixel format and color range are correctly set in extra_args
|
|
604
|
-
extra_args = transcode_options[
|
|
605
|
-
if
|
|
606
|
-
extra_args.extend([
|
|
599
|
+
extra_args = transcode_options["extra_args"]
|
|
600
|
+
if "-pix_fmt" not in extra_args:
|
|
601
|
+
extra_args.extend(["-pix_fmt", required_pixel_format])
|
|
607
602
|
else:
|
|
608
603
|
# If pix_fmt is already set, ensure it's the required one
|
|
609
604
|
try:
|
|
610
|
-
pix_fmt_index = extra_args.index(
|
|
605
|
+
pix_fmt_index = extra_args.index("-pix_fmt")
|
|
611
606
|
if extra_args[pix_fmt_index + 1] != required_pixel_format:
|
|
612
607
|
logger.warning("Overriding existing -pix_fmt '%s' with '%s'", extra_args[pix_fmt_index + 1], required_pixel_format)
|
|
613
608
|
extra_args[pix_fmt_index + 1] = required_pixel_format
|
|
614
609
|
except (ValueError, IndexError):
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
610
|
+
# Should not happen if '-pix_fmt' is in extra_args, but handle defensively
|
|
611
|
+
logger.error("Error processing existing -pix_fmt argument. Appending required format.")
|
|
612
|
+
extra_args.extend(["-pix_fmt", required_pixel_format])
|
|
618
613
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
extra_args.extend(['-color_range', 'pc'])
|
|
614
|
+
if "-color_range" not in extra_args:
|
|
615
|
+
# Add color range 'pc' (which corresponds to 2 or 'jpeg') for yuv420p
|
|
616
|
+
extra_args.extend(["-color_range", "pc"])
|
|
623
617
|
else:
|
|
624
618
|
# If color_range is already set, ensure it's 'pc'
|
|
625
|
-
|
|
626
|
-
color_range_index = extra_args.index(
|
|
627
|
-
if extra_args[color_range_index + 1] !=
|
|
619
|
+
try:
|
|
620
|
+
color_range_index = extra_args.index("-color_range")
|
|
621
|
+
if extra_args[color_range_index + 1] != "pc":
|
|
628
622
|
logger.warning("Overriding existing -color_range '%s' with 'pc'", extra_args[color_range_index + 1])
|
|
629
|
-
extra_args[color_range_index + 1] =
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
623
|
+
extra_args[color_range_index + 1] = "pc"
|
|
624
|
+
except (ValueError, IndexError):
|
|
625
|
+
logger.error("Error processing existing -color_range argument. Appending 'pc'.")
|
|
626
|
+
extra_args.extend(["-color_range", "pc"])
|
|
634
627
|
|
|
635
628
|
return transcode_video(input_path, output_path, **transcode_options)
|
|
636
629
|
else:
|
|
637
|
-
logger.info(
|
|
630
|
+
logger.info(
|
|
631
|
+
"Video %s already meets requirements (%s, %s, color_range=pc). No transcoding needed.", input_path.name, required_codec, required_pixel_format
|
|
632
|
+
)
|
|
638
633
|
# If no transcoding is needed, should we copy/link or just return the original path?
|
|
639
634
|
# For simplicity, let's assume the caller handles the file location.
|
|
640
635
|
# If the output_path is different, we might need to copy.
|
|
@@ -648,15 +643,10 @@ def transcode_videofile_if_required(
|
|
|
648
643
|
except Exception as e:
|
|
649
644
|
logger.error("Failed to copy %s to %s: %s", input_path.name, output_path.name, e)
|
|
650
645
|
return None
|
|
651
|
-
return input_path
|
|
646
|
+
return input_path # Return original path if no copy needed
|
|
652
647
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
output_dir: Path,
|
|
656
|
-
quality: int,
|
|
657
|
-
ext: str = "jpg",
|
|
658
|
-
fps: Optional[float] = None
|
|
659
|
-
) -> List[Path]:
|
|
648
|
+
|
|
649
|
+
def extract_frames(video_path: Path, output_dir: Path, quality: int, ext: str = "jpg", fps: Optional[float] = None) -> List[Path]:
|
|
660
650
|
"""
|
|
661
651
|
Extracts frames from a video file using FFmpeg.
|
|
662
652
|
|
|
@@ -681,9 +671,11 @@ def extract_frames(
|
|
|
681
671
|
output_pattern = output_dir / f"frame_%07d.{ext}"
|
|
682
672
|
|
|
683
673
|
cmd = [
|
|
684
|
-
ffmpeg_executable,
|
|
685
|
-
"-i",
|
|
686
|
-
|
|
674
|
+
ffmpeg_executable, # Use the found executable path
|
|
675
|
+
"-i",
|
|
676
|
+
str(video_path),
|
|
677
|
+
"-qscale:v",
|
|
678
|
+
str(quality), # Video quality scale
|
|
687
679
|
]
|
|
688
680
|
|
|
689
681
|
if fps is not None:
|
|
@@ -713,27 +705,27 @@ def extract_frames(
|
|
|
713
705
|
logger.error("An unexpected error occurred during FFmpeg execution: %s", e, exc_info=True)
|
|
714
706
|
return []
|
|
715
707
|
|
|
716
|
-
|
|
717
708
|
# Collect paths of extracted frames
|
|
718
709
|
extracted_files = sorted(output_dir.glob(f"frame_*.{ext}"))
|
|
719
710
|
return extracted_files
|
|
720
711
|
|
|
712
|
+
|
|
721
713
|
def extract_frame_range(
|
|
722
714
|
video_path: Path,
|
|
723
715
|
output_dir: Path,
|
|
724
716
|
start_frame: int,
|
|
725
|
-
end_frame: int,
|
|
717
|
+
end_frame: int, # Exclusive end frame number
|
|
726
718
|
quality: int,
|
|
727
719
|
ext: str = "jpg",
|
|
728
720
|
) -> List[Path]:
|
|
729
721
|
"""
|
|
730
722
|
Extracts a specific range of frames from a video using FFmpeg.
|
|
731
|
-
|
|
723
|
+
|
|
732
724
|
Frames from start_frame (inclusive) to end_frame (exclusive) are saved as images
|
|
733
725
|
in the output directory, following the naming pattern 'frame_%07d.ext'. The
|
|
734
726
|
function ensures only the requested frames are returned, and cleans up partial
|
|
735
727
|
results on failure.
|
|
736
|
-
|
|
728
|
+
|
|
737
729
|
Args:
|
|
738
730
|
video_path: Path to the input video file.
|
|
739
731
|
output_dir: Directory where extracted frames will be saved.
|
|
@@ -741,10 +733,10 @@ def extract_frame_range(
|
|
|
741
733
|
end_frame: Index at which to stop extraction (exclusive, 0-based).
|
|
742
734
|
quality: JPEG quality factor (1-31, lower is better).
|
|
743
735
|
ext: File extension for output images (e.g., 'jpg', 'png').
|
|
744
|
-
|
|
736
|
+
|
|
745
737
|
Returns:
|
|
746
738
|
List of Paths to the extracted frame image files within the specified range.
|
|
747
|
-
|
|
739
|
+
|
|
748
740
|
Raises:
|
|
749
741
|
FileNotFoundError: If the FFmpeg executable is not found.
|
|
750
742
|
ValueError: If start_frame is greater than or equal to end_frame.
|
|
@@ -767,15 +759,19 @@ def extract_frame_range(
|
|
|
767
759
|
# Use select filter for precise frame range extraction
|
|
768
760
|
# 'select' uses 0-based indexing 'n'
|
|
769
761
|
# We want frames where start_frame <= n < end_frame
|
|
770
|
-
select_filter = f"select='between(n,{start_frame},{end_frame-1})'"
|
|
762
|
+
select_filter = f"select='between(n,{start_frame},{end_frame - 1})'"
|
|
771
763
|
|
|
772
764
|
cmd = [
|
|
773
765
|
ffmpeg_executable,
|
|
774
|
-
"-i",
|
|
775
|
-
|
|
776
|
-
"-
|
|
777
|
-
|
|
778
|
-
"-
|
|
766
|
+
"-i",
|
|
767
|
+
str(video_path),
|
|
768
|
+
"-vf",
|
|
769
|
+
select_filter,
|
|
770
|
+
"-vsync",
|
|
771
|
+
"vfr", # Variable frame rate sync to handle selected frames
|
|
772
|
+
"-qscale:v",
|
|
773
|
+
str(quality),
|
|
774
|
+
"-copyts", # Attempt to copy timestamps if needed, might not be accurate with select
|
|
779
775
|
str(output_pattern),
|
|
780
776
|
]
|
|
781
777
|
|
|
@@ -819,17 +815,17 @@ def extract_frame_range(
|
|
|
819
815
|
# This might happen if ffmpeg fails silently for some frames or if the video ends early.
|
|
820
816
|
logger.warning("Expected frame file %s not found after extraction.", frame_file)
|
|
821
817
|
|
|
822
|
-
|
|
823
818
|
logger.info("Found %d extracted frame files in range [%d, %d) for video %s.", len(extracted_files), start_frame, end_frame, video_path.name)
|
|
824
819
|
return extracted_files
|
|
825
820
|
|
|
821
|
+
|
|
826
822
|
__all__ = [
|
|
827
|
-
"is_ffmpeg_available",
|
|
828
|
-
"check_ffmpeg_availability",
|
|
823
|
+
"is_ffmpeg_available", # ADDED
|
|
824
|
+
"check_ffmpeg_availability", # ADDED
|
|
829
825
|
"get_stream_info",
|
|
830
|
-
"assemble_video_from_frames",
|
|
826
|
+
"assemble_video_from_frames", # Updated name
|
|
831
827
|
"transcode_video",
|
|
832
828
|
"transcode_videofile_if_required",
|
|
833
829
|
"extract_frames",
|
|
834
|
-
"extract_frame_range",
|
|
830
|
+
"extract_frame_range", # Add new function to __all__
|
|
835
831
|
]
|