endoreg-db 0.8.6.1__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.

Files changed (360) hide show
  1. endoreg_db/authz/auth.py +74 -0
  2. endoreg_db/authz/backends.py +168 -0
  3. endoreg_db/authz/management/commands/list_routes.py +18 -0
  4. endoreg_db/authz/middleware.py +83 -0
  5. endoreg_db/authz/permissions.py +127 -0
  6. endoreg_db/authz/policy.py +218 -0
  7. endoreg_db/authz/views_auth.py +66 -0
  8. endoreg_db/config/env.py +13 -8
  9. endoreg_db/data/__init__.py +8 -31
  10. endoreg_db/data/_examples/disease.yaml +55 -0
  11. endoreg_db/data/_examples/disease_classification.yaml +13 -0
  12. endoreg_db/data/_examples/disease_classification_choice.yaml +62 -0
  13. endoreg_db/data/_examples/event.yaml +64 -0
  14. endoreg_db/data/_examples/examination.yaml +72 -0
  15. endoreg_db/data/_examples/finding/anatomy_colon.yaml +128 -0
  16. endoreg_db/data/_examples/finding/colonoscopy.yaml +40 -0
  17. endoreg_db/data/_examples/finding/colonoscopy_bowel_prep.yaml +56 -0
  18. endoreg_db/data/_examples/finding/complication.yaml +16 -0
  19. endoreg_db/data/_examples/finding/data.yaml +105 -0
  20. endoreg_db/data/_examples/finding/examination_setting.yaml +16 -0
  21. endoreg_db/data/_examples/finding/medication_related.yaml +18 -0
  22. endoreg_db/data/_examples/finding/outcome.yaml +12 -0
  23. endoreg_db/data/_examples/finding_classification/colonoscopy_bowel_preparation.yaml +68 -0
  24. endoreg_db/data/_examples/finding_classification/colonoscopy_jnet.yaml +22 -0
  25. endoreg_db/data/_examples/finding_classification/colonoscopy_kudo.yaml +25 -0
  26. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
  27. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
  28. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_size.yaml +68 -0
  29. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
  30. endoreg_db/data/_examples/finding_classification/colonoscopy_location.yaml +80 -0
  31. endoreg_db/data/_examples/finding_classification/colonoscopy_lst.yaml +21 -0
  32. endoreg_db/data/_examples/finding_classification/colonoscopy_nice.yaml +20 -0
  33. endoreg_db/data/_examples/finding_classification/colonoscopy_paris.yaml +26 -0
  34. endoreg_db/data/_examples/finding_classification/colonoscopy_sano.yaml +22 -0
  35. endoreg_db/data/_examples/finding_classification/colonoscopy_summary.yaml +53 -0
  36. endoreg_db/data/_examples/finding_classification/complication_generic.yaml +25 -0
  37. endoreg_db/data/_examples/finding_classification/examination_setting_generic.yaml +40 -0
  38. endoreg_db/data/_examples/finding_classification/histology_colo.yaml +51 -0
  39. endoreg_db/data/_examples/finding_classification/intervention_required.yaml +26 -0
  40. endoreg_db/data/_examples/finding_classification/medication_related.yaml +23 -0
  41. endoreg_db/data/_examples/finding_classification/visualized.yaml +33 -0
  42. endoreg_db/data/_examples/finding_classification_choice/bowel_preparation.yaml +78 -0
  43. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_circularity_default.yaml +32 -0
  44. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
  45. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
  46. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_lst.yaml +15 -0
  47. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_nice.yaml +17 -0
  48. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_paris.yaml +57 -0
  49. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_planarity_default.yaml +49 -0
  50. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_sano.yaml +14 -0
  51. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_surface_intact_default.yaml +36 -0
  52. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_location.yaml +229 -0
  53. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
  54. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_size.yaml +82 -0
  55. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
  56. endoreg_db/data/_examples/finding_classification_choice/complication_generic_types.yaml +15 -0
  57. endoreg_db/data/_examples/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
  58. endoreg_db/data/_examples/finding_classification_choice/histology.yaml +24 -0
  59. endoreg_db/data/_examples/finding_classification_choice/histology_polyp.yaml +20 -0
  60. endoreg_db/data/_examples/finding_classification_choice/outcome.yaml +19 -0
  61. endoreg_db/data/_examples/finding_classification_choice/yes_no_na.yaml +11 -0
  62. endoreg_db/data/_examples/finding_classification_type/colonoscopy_basic.yaml +48 -0
  63. endoreg_db/data/_examples/finding_intervention/endoscopy.yaml +43 -0
  64. endoreg_db/data/_examples/finding_intervention/endoscopy_colonoscopy.yaml +168 -0
  65. endoreg_db/data/_examples/finding_intervention/endoscopy_egd.yaml +128 -0
  66. endoreg_db/data/_examples/finding_intervention/endoscopy_ercp.yaml +32 -0
  67. endoreg_db/data/_examples/finding_intervention/endoscopy_eus_lower.yaml +9 -0
  68. endoreg_db/data/_examples/finding_intervention/endoscopy_eus_upper.yaml +36 -0
  69. endoreg_db/data/_examples/finding_intervention_type/endoscopy.yaml +15 -0
  70. endoreg_db/data/_examples/finding_type/data.yaml +43 -0
  71. endoreg_db/data/_examples/requirement/age.yaml +26 -0
  72. endoreg_db/data/_examples/requirement/colonoscopy_baseline_austria.yaml +45 -0
  73. endoreg_db/data/_examples/requirement/disease_cardiovascular.yaml +79 -0
  74. endoreg_db/data/_examples/requirement/disease_classification_choice_cardiovascular.yaml +41 -0
  75. endoreg_db/data/_examples/requirement/disease_hepatology.yaml +12 -0
  76. endoreg_db/data/_examples/requirement/disease_misc.yaml +12 -0
  77. endoreg_db/data/_examples/requirement/disease_renal.yaml +96 -0
  78. endoreg_db/data/_examples/requirement/endoscopy_bleeding_risk.yaml +59 -0
  79. endoreg_db/data/_examples/requirement/event_cardiology.yaml +251 -0
  80. endoreg_db/data/_examples/requirement/event_requirements.yaml +145 -0
  81. endoreg_db/data/_examples/requirement/finding_colon_polyp.yaml +50 -0
  82. endoreg_db/data/_examples/requirement/gender.yaml +25 -0
  83. endoreg_db/data/_examples/requirement/lab_value.yaml +441 -0
  84. endoreg_db/data/_examples/requirement/medication.yaml +93 -0
  85. endoreg_db/data/_examples/requirement_operator/age.yaml +13 -0
  86. endoreg_db/data/_examples/requirement_operator/lab_operators.yaml +129 -0
  87. endoreg_db/data/_examples/requirement_operator/model_operators.yaml +96 -0
  88. endoreg_db/data/_examples/requirement_set/01_endoscopy_generic.yaml +48 -0
  89. endoreg_db/data/_examples/requirement_set/colonoscopy_austria_screening.yaml +57 -0
  90. endoreg_db/data/_examples/yaml_examples.xlsx +0 -0
  91. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +4 -3
  92. endoreg_db/data/event_classification/data.yaml +4 -0
  93. endoreg_db/data/event_classification_choice/data.yaml +9 -0
  94. endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +43 -70
  95. endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +22 -52
  96. endoreg_db/data/finding_classification/colonoscopy_location.yaml +31 -62
  97. endoreg_db/data/finding_classification/histology_colo.yaml +28 -36
  98. endoreg_db/data/requirement/colon_polyp_intervention.yaml +49 -0
  99. endoreg_db/data/requirement/coloreg_colon_polyp.yaml +49 -0
  100. endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +31 -12
  101. endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
  102. endoreg_db/data/requirement_set/02_endoscopy_bleeding_risk.yaml +46 -0
  103. endoreg_db/data/requirement_set/90_coloreg.yaml +178 -0
  104. endoreg_db/data/requirement_set/_old_ +109 -0
  105. endoreg_db/data/requirement_set_type/data.yaml +21 -0
  106. endoreg_db/data/setup_config.yaml +4 -4
  107. endoreg_db/data/tag/requirement_set_tags.yaml +21 -0
  108. endoreg_db/exceptions.py +5 -2
  109. endoreg_db/helpers/data_loader.py +1 -1
  110. endoreg_db/management/commands/create_model_meta_from_huggingface.py +21 -10
  111. endoreg_db/management/commands/create_multilabel_model_meta.py +299 -129
  112. endoreg_db/management/commands/import_video.py +9 -10
  113. endoreg_db/management/commands/import_video_with_classification.py +1 -1
  114. endoreg_db/management/commands/init_default_ai_model.py +1 -1
  115. endoreg_db/management/commands/list_routes.py +18 -0
  116. endoreg_db/management/commands/load_center_data.py +12 -12
  117. endoreg_db/management/commands/load_requirement_data.py +60 -31
  118. endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
  119. endoreg_db/management/commands/setup_endoreg_db.py +3 -3
  120. endoreg_db/management/commands/storage_management.py +271 -203
  121. endoreg_db/migrations/0001_initial.py +1799 -1300
  122. endoreg_db/migrations/0002_requirementset_depends_on.py +18 -0
  123. endoreg_db/migrations/_old/0001_initial.py +1857 -0
  124. endoreg_db/migrations/_old/0004_employee_city_employee_post_code_employee_street_and_more.py +68 -0
  125. endoreg_db/migrations/_old/0004_remove_casetemplate_rules_and_more.py +77 -0
  126. endoreg_db/migrations/_old/0005_merge_20251111_1003.py +14 -0
  127. endoreg_db/migrations/_old/0006_sensitivemeta_anonymized_text_and_more.py +68 -0
  128. endoreg_db/migrations/_old/0007_remove_rule_attribute_dtype_remove_rule_rule_type_and_more.py +89 -0
  129. endoreg_db/migrations/_old/0008_remove_event_event_classification_and_more.py +27 -0
  130. endoreg_db/migrations/_old/0009_alter_modelmeta_options_and_more.py +21 -0
  131. endoreg_db/models/__init__.py +78 -123
  132. endoreg_db/models/administration/__init__.py +21 -42
  133. endoreg_db/models/administration/ai/active_model.py +2 -2
  134. endoreg_db/models/administration/ai/ai_model.py +7 -6
  135. endoreg_db/models/administration/case/__init__.py +1 -15
  136. endoreg_db/models/administration/case/case.py +3 -3
  137. endoreg_db/models/administration/case/case_template/__init__.py +2 -14
  138. endoreg_db/models/administration/case/case_template/case_template.py +2 -124
  139. endoreg_db/models/administration/case/case_template/case_template_rule.py +2 -268
  140. endoreg_db/models/administration/case/case_template/case_template_rule_value.py +2 -85
  141. endoreg_db/models/administration/case/case_template/case_template_type.py +2 -25
  142. endoreg_db/models/administration/center/center.py +33 -19
  143. endoreg_db/models/administration/center/center_product.py +12 -9
  144. endoreg_db/models/administration/center/center_resource.py +25 -19
  145. endoreg_db/models/administration/center/center_shift.py +21 -17
  146. endoreg_db/models/administration/center/center_waste.py +16 -8
  147. endoreg_db/models/administration/person/__init__.py +2 -0
  148. endoreg_db/models/administration/person/employee/employee.py +10 -5
  149. endoreg_db/models/administration/person/employee/employee_qualification.py +9 -4
  150. endoreg_db/models/administration/person/employee/employee_type.py +12 -6
  151. endoreg_db/models/administration/person/examiner/examiner.py +13 -11
  152. endoreg_db/models/administration/person/patient/__init__.py +2 -0
  153. endoreg_db/models/administration/person/patient/patient.py +103 -100
  154. endoreg_db/models/administration/person/patient/patient_external_id.py +37 -0
  155. endoreg_db/models/administration/person/person.py +4 -0
  156. endoreg_db/models/administration/person/profession/__init__.py +8 -4
  157. endoreg_db/models/administration/person/user/portal_user_information.py +11 -7
  158. endoreg_db/models/administration/product/product.py +20 -15
  159. endoreg_db/models/administration/product/product_material.py +17 -18
  160. endoreg_db/models/administration/product/product_weight.py +12 -8
  161. endoreg_db/models/administration/product/reference_product.py +23 -55
  162. endoreg_db/models/administration/qualification/qualification.py +7 -3
  163. endoreg_db/models/administration/qualification/qualification_type.py +7 -3
  164. endoreg_db/models/administration/shift/scheduled_days.py +8 -5
  165. endoreg_db/models/administration/shift/shift.py +16 -12
  166. endoreg_db/models/administration/shift/shift_type.py +23 -31
  167. endoreg_db/models/label/__init__.py +7 -8
  168. endoreg_db/models/label/annotation/image_classification.py +10 -9
  169. endoreg_db/models/label/annotation/video_segmentation_annotation.py +8 -5
  170. endoreg_db/models/label/label.py +15 -15
  171. endoreg_db/models/label/label_set.py +19 -6
  172. endoreg_db/models/label/label_type.py +1 -1
  173. endoreg_db/models/label/label_video_segment/_create_from_video.py +5 -8
  174. endoreg_db/models/label/label_video_segment/label_video_segment.py +76 -102
  175. endoreg_db/models/label/video_segmentation_label.py +4 -0
  176. endoreg_db/models/label/video_segmentation_labelset.py +4 -3
  177. endoreg_db/models/media/frame/frame.py +22 -22
  178. endoreg_db/models/media/pdf/raw_pdf.py +110 -182
  179. endoreg_db/models/media/pdf/report_file.py +25 -29
  180. endoreg_db/models/media/pdf/report_reader/report_reader_config.py +30 -46
  181. endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
  182. endoreg_db/models/media/video/__init__.py +1 -0
  183. endoreg_db/models/media/video/create_from_file.py +48 -56
  184. endoreg_db/models/media/video/pipe_2.py +8 -9
  185. endoreg_db/models/media/video/video_file.py +150 -108
  186. endoreg_db/models/media/video/video_file_ai.py +288 -74
  187. endoreg_db/models/media/video/video_file_anonymize.py +38 -38
  188. endoreg_db/models/media/video/video_file_frames/__init__.py +3 -1
  189. endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -8
  190. endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +7 -9
  191. endoreg_db/models/media/video/video_file_frames/_delete_frames.py +9 -8
  192. endoreg_db/models/media/video/video_file_frames/_extract_frames.py +38 -45
  193. endoreg_db/models/media/video/video_file_frames/_get_frame.py +6 -8
  194. endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +4 -18
  195. endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -3
  196. endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +7 -6
  197. endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +6 -8
  198. endoreg_db/models/media/video/video_file_frames/_get_frames.py +6 -8
  199. endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +15 -25
  200. endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +26 -23
  201. endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +23 -14
  202. endoreg_db/models/media/video/video_file_io.py +109 -62
  203. endoreg_db/models/media/video/video_file_meta/get_crop_template.py +3 -3
  204. endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +5 -3
  205. endoreg_db/models/media/video/video_file_meta/get_fps.py +37 -34
  206. endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +19 -25
  207. endoreg_db/models/media/video/video_file_meta/text_meta.py +41 -38
  208. endoreg_db/models/media/video/video_file_meta/video_meta.py +14 -7
  209. endoreg_db/models/media/video/video_file_segments.py +24 -17
  210. endoreg_db/models/media/video/video_metadata.py +19 -35
  211. endoreg_db/models/media/video/video_processing.py +96 -95
  212. endoreg_db/models/medical/contraindication/__init__.py +13 -3
  213. endoreg_db/models/medical/disease.py +22 -16
  214. endoreg_db/models/medical/event.py +31 -18
  215. endoreg_db/models/medical/examination/__init__.py +13 -6
  216. endoreg_db/models/medical/examination/examination.py +17 -18
  217. endoreg_db/models/medical/examination/examination_indication.py +26 -25
  218. endoreg_db/models/medical/examination/examination_time.py +16 -6
  219. endoreg_db/models/medical/examination/examination_time_type.py +9 -6
  220. endoreg_db/models/medical/examination/examination_type.py +3 -4
  221. endoreg_db/models/medical/finding/finding.py +38 -39
  222. endoreg_db/models/medical/finding/finding_classification.py +37 -48
  223. endoreg_db/models/medical/finding/finding_intervention.py +27 -22
  224. endoreg_db/models/medical/finding/finding_type.py +13 -12
  225. endoreg_db/models/medical/hardware/endoscope.py +20 -26
  226. endoreg_db/models/medical/hardware/endoscopy_processor.py +2 -2
  227. endoreg_db/models/medical/laboratory/lab_value.py +62 -91
  228. endoreg_db/models/medical/medication/medication.py +22 -10
  229. endoreg_db/models/medical/medication/medication_indication.py +29 -3
  230. endoreg_db/models/medical/medication/medication_indication_type.py +25 -14
  231. endoreg_db/models/medical/medication/medication_intake_time.py +31 -19
  232. endoreg_db/models/medical/medication/medication_schedule.py +27 -16
  233. endoreg_db/models/medical/organ/__init__.py +15 -12
  234. endoreg_db/models/medical/patient/medication_examples.py +1 -5
  235. endoreg_db/models/medical/patient/patient_disease.py +20 -23
  236. endoreg_db/models/medical/patient/patient_event.py +19 -22
  237. endoreg_db/models/medical/patient/patient_examination.py +48 -54
  238. endoreg_db/models/medical/patient/patient_examination_indication.py +16 -14
  239. endoreg_db/models/medical/patient/patient_finding.py +122 -139
  240. endoreg_db/models/medical/patient/patient_finding_classification.py +44 -49
  241. endoreg_db/models/medical/patient/patient_finding_intervention.py +8 -19
  242. endoreg_db/models/medical/patient/patient_lab_sample.py +28 -23
  243. endoreg_db/models/medical/patient/patient_lab_value.py +82 -89
  244. endoreg_db/models/medical/patient/patient_medication.py +27 -38
  245. endoreg_db/models/medical/patient/patient_medication_schedule.py +28 -36
  246. endoreg_db/models/medical/risk/risk.py +7 -6
  247. endoreg_db/models/medical/risk/risk_type.py +8 -5
  248. endoreg_db/models/metadata/model_meta.py +60 -29
  249. endoreg_db/models/metadata/model_meta_logic.py +125 -18
  250. endoreg_db/models/metadata/pdf_meta.py +19 -24
  251. endoreg_db/models/metadata/sensitive_meta.py +102 -85
  252. endoreg_db/models/metadata/sensitive_meta_logic.py +192 -173
  253. endoreg_db/models/metadata/video_meta.py +51 -31
  254. endoreg_db/models/metadata/video_prediction_logic.py +16 -23
  255. endoreg_db/models/metadata/video_prediction_meta.py +29 -33
  256. endoreg_db/models/other/distribution/date_value_distribution.py +89 -29
  257. endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +21 -5
  258. endoreg_db/models/other/distribution/numeric_value_distribution.py +114 -53
  259. endoreg_db/models/other/distribution/single_categorical_value_distribution.py +4 -3
  260. endoreg_db/models/other/emission/emission_factor.py +18 -8
  261. endoreg_db/models/other/gender.py +10 -5
  262. endoreg_db/models/other/information_source.py +25 -25
  263. endoreg_db/models/other/material.py +9 -5
  264. endoreg_db/models/other/resource.py +6 -4
  265. endoreg_db/models/other/tag.py +10 -5
  266. endoreg_db/models/other/transport_route.py +13 -8
  267. endoreg_db/models/other/unit.py +10 -6
  268. endoreg_db/models/other/waste.py +6 -5
  269. endoreg_db/models/requirement/requirement.py +580 -272
  270. endoreg_db/models/requirement/requirement_error.py +85 -0
  271. endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
  272. endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +3 -6
  273. endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +90 -64
  274. endoreg_db/models/requirement/requirement_operator.py +36 -33
  275. endoreg_db/models/requirement/requirement_set.py +74 -57
  276. endoreg_db/models/state/__init__.py +4 -4
  277. endoreg_db/models/state/abstract.py +2 -2
  278. endoreg_db/models/state/anonymization.py +12 -0
  279. endoreg_db/models/state/audit_ledger.py +46 -47
  280. endoreg_db/models/state/label_video_segment.py +9 -0
  281. endoreg_db/models/state/raw_pdf.py +40 -46
  282. endoreg_db/models/state/sensitive_meta.py +6 -2
  283. endoreg_db/models/state/video.py +58 -53
  284. endoreg_db/models/upload_job.py +32 -55
  285. endoreg_db/models/utils.py +1 -2
  286. endoreg_db/root_urls.py +21 -2
  287. endoreg_db/serializers/__init__.py +0 -2
  288. endoreg_db/serializers/anonymization.py +18 -10
  289. endoreg_db/serializers/meta/report_meta.py +1 -1
  290. endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
  291. endoreg_db/serializers/misc/file_overview.py +11 -99
  292. endoreg_db/serializers/requirements/requirement_sets.py +92 -22
  293. endoreg_db/serializers/video/segmentation.py +2 -1
  294. endoreg_db/serializers/video/video_processing_history.py +20 -5
  295. endoreg_db/services/anonymization.py +75 -73
  296. endoreg_db/services/lookup_service.py +37 -24
  297. endoreg_db/services/pdf_import.py +166 -68
  298. endoreg_db/services/storage_aware_video_processor.py +140 -114
  299. endoreg_db/services/video_import.py +193 -283
  300. endoreg_db/urls/__init__.py +7 -20
  301. endoreg_db/urls/media.py +108 -67
  302. endoreg_db/urls/root_urls.py +29 -0
  303. endoreg_db/utils/__init__.py +15 -5
  304. endoreg_db/utils/ai/multilabel_classification_net.py +116 -20
  305. endoreg_db/utils/case_generator/__init__.py +3 -0
  306. endoreg_db/utils/dataloader.py +88 -16
  307. endoreg_db/utils/defaults/set_default_center.py +32 -0
  308. endoreg_db/utils/names.py +22 -16
  309. endoreg_db/utils/permissions.py +2 -1
  310. endoreg_db/utils/pipelines/process_video_dir.py +1 -1
  311. endoreg_db/utils/requirement_operator_logic/model_evaluators.py +414 -127
  312. endoreg_db/utils/setup_config.py +8 -5
  313. endoreg_db/utils/storage.py +115 -0
  314. endoreg_db/utils/validate_endo_roi.py +8 -2
  315. endoreg_db/utils/video/ffmpeg_wrapper.py +184 -188
  316. endoreg_db/views/__init__.py +0 -10
  317. endoreg_db/views/anonymization/media_management.py +198 -163
  318. endoreg_db/views/anonymization/overview.py +4 -1
  319. endoreg_db/views/anonymization/validate.py +174 -40
  320. endoreg_db/views/media/__init__.py +2 -0
  321. endoreg_db/views/media/pdf_media.py +131 -152
  322. endoreg_db/views/media/sensitive_metadata.py +46 -6
  323. endoreg_db/views/media/video_media.py +89 -82
  324. endoreg_db/views/media/video_segments.py +2 -3
  325. endoreg_db/views/meta/sensitive_meta_detail.py +0 -63
  326. endoreg_db/views/patient/patient.py +5 -4
  327. endoreg_db/views/pdf/pdf_stream.py +20 -21
  328. endoreg_db/views/pdf/reimport.py +11 -32
  329. endoreg_db/views/requirement/evaluate.py +188 -187
  330. endoreg_db/views/requirement/lookup.py +17 -3
  331. endoreg_db/views/requirement/requirement_utils.py +89 -0
  332. endoreg_db/views/video/__init__.py +0 -2
  333. endoreg_db/views/video/correction.py +2 -2
  334. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/METADATA +7 -3
  335. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/RECORD +341 -245
  336. endoreg_db/models/administration/permissions/__init__.py +0 -44
  337. endoreg_db/models/media/video/video_file_frames.py +0 -0
  338. endoreg_db/models/metadata/frame_ocr_result.py +0 -0
  339. endoreg_db/models/rule/__init__.py +0 -13
  340. endoreg_db/models/rule/rule.py +0 -27
  341. endoreg_db/models/rule/rule_applicator.py +0 -224
  342. endoreg_db/models/rule/rule_attribute_dtype.py +0 -17
  343. endoreg_db/models/rule/rule_type.py +0 -20
  344. endoreg_db/models/rule/ruleset.py +0 -17
  345. endoreg_db/serializers/video/video_metadata.py +0 -105
  346. endoreg_db/urls/report.py +0 -48
  347. endoreg_db/urls/video.py +0 -61
  348. endoreg_db/utils/case_generator/case_generator.py +0 -159
  349. endoreg_db/utils/case_generator/utils.py +0 -30
  350. endoreg_db/views/report/__init__.py +0 -9
  351. endoreg_db/views/report/report_list.py +0 -112
  352. endoreg_db/views/report/report_with_secure_url.py +0 -28
  353. endoreg_db/views/report/start_examination.py +0 -7
  354. endoreg_db/views.py +0 -0
  355. /endoreg_db/data/{requirement_set → _examples/requirement_set}/endoscopy_bleeding_risk.yaml +0 -0
  356. /endoreg_db/migrations/{0002_add_video_correction_models.py → _old/0002_add_video_correction_models.py} +0 -0
  357. /endoreg_db/migrations/{0003_add_center_display_name.py → _old/0003_add_center_display_name.py} +0 -0
  358. /endoreg_db/{models/media/video/refactor_plan.md → views/pdf/pdf_stream_views.py} +0 -0
  359. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/WHEEL +0 -0
  360. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -11,30 +11,26 @@ Changelog:
11
11
 
12
12
  import logging
13
13
  import os
14
- import random
15
14
  import shutil
16
- import sys
17
15
  import time
18
16
  from contextlib import contextmanager
19
17
  from datetime import date
20
18
  from pathlib import Path
21
19
  from typing import Any, Dict, List, Optional, Tuple, Union
22
-
20
+ import subprocess
23
21
  from django.db import transaction
24
22
  from django.db.models.fields.files import FieldFile
25
- from lx_anonymizer import FrameCleaner
26
- from moviepy import video
27
23
 
28
24
  from endoreg_db.models import EndoscopyProcessor, SensitiveMeta, VideoFile
29
25
  from endoreg_db.models.media.video.video_file_anonymize import _cleanup_raw_assets
26
+ from endoreg_db.utils import ensure_local_file, storage_file_exists
30
27
  from endoreg_db.utils.hashs import get_video_hash
31
28
  from endoreg_db.utils.paths import ANONYM_VIDEO_DIR, STORAGE_DIR, VIDEO_DIR
29
+ from endoreg_db.models.state import VideoState
32
30
 
33
31
  # File lock configuration (matches PDF import)
34
32
  STALE_LOCK_SECONDS = 6000 # 100 minutes - reclaim locks older than this
35
- MAX_LOCK_WAIT_SECONDS = (
36
- 90 # New: wait up to 90s for a non-stale lock to clear before skipping
37
- )
33
+ MAX_LOCK_WAIT_SECONDS = 90 # New: wait up to 90s for a non-stale lock to clear before skipping
38
34
 
39
35
  logger = logging.getLogger(__name__)
40
36
 
@@ -63,10 +59,7 @@ class VideoImportService:
63
59
  # Ensure anonym_video directory exists before listing files
64
60
  anonym_video_dir = Path(ANONYM_VIDEO_DIR)
65
61
  if anonym_video_dir.exists():
66
- self.processed_files = set(
67
- str(anonym_video_dir / file)
68
- for file in os.listdir(ANONYM_VIDEO_DIR)
69
- )
62
+ self.processed_files = set(str(anonym_video_dir / file) for file in os.listdir(ANONYM_VIDEO_DIR))
70
63
  else:
71
64
  logger.info(f"Creating anonym_videos directory: {anonym_video_dir}")
72
65
  anonym_video_dir.mkdir(parents=True, exist_ok=True)
@@ -80,12 +73,13 @@ class VideoImportService:
80
73
  self.processing_context: Dict[str, Any] = {}
81
74
 
82
75
  self.delete_source = True
76
+ self.original_file_path = None
83
77
 
84
78
  self.logger = logging.getLogger(__name__)
79
+
80
+ self.current_video_id = Optional[int]
85
81
 
86
- self.cleaner = (
87
- None # This gets instantiated in the perform_frame_cleaning method
88
- )
82
+ self.cleaner = None # This gets instantiated in the perform_frame_cleaning method
89
83
 
90
84
  def _require_current_video(self) -> VideoFile:
91
85
  """Return the current VideoFile or raise if it has not been initialized."""
@@ -131,9 +125,7 @@ class VideoImportService:
131
125
  )
132
126
  lock_path.unlink()
133
127
  except Exception as e:
134
- logger.warning(
135
- "Failed to remove stale lock %s: %s", lock_path, e
136
- )
128
+ logger.warning("Failed to remove stale lock %s: %s", lock_path, e)
137
129
  # Loop continues and retries acquire immediately
138
130
  continue
139
131
 
@@ -176,9 +168,7 @@ class VideoImportService:
176
168
 
177
169
  try:
178
170
  # Initialize processing context
179
- self._initialize_processing_context(
180
- file_path, center_name, processor_name, save_video, delete_source
181
- )
171
+ self._initialize_processing_context(file_path, center_name, processor_name, save_video, delete_source)
182
172
 
183
173
  # Validate and prepare file (may raise ValueError if another worker holds a non-stale lock)
184
174
  try:
@@ -212,15 +202,11 @@ class VideoImportService:
212
202
 
213
203
  except Exception as e:
214
204
  # Safe file path access - handles cases where processing_context wasn't initialized
215
- safe_file_path = getattr(self, "processing_context", {}).get(
216
- "file_path", file_path
217
- )
205
+ safe_file_path = getattr(self, "processing_context", {}).get("file_path", file_path)
218
206
  # Debug: Log context state for troubleshooting
219
207
  context_keys = list(getattr(self, "processing_context", {}).keys())
220
208
  self.logger.debug(f"Context keys during error: {context_keys}")
221
- self.logger.error(
222
- f"Video import and anonymization failed for {safe_file_path}: {e}"
223
- )
209
+ self.logger.error(f"Video import and anonymization failed for {safe_file_path}: {e}")
224
210
  self._cleanup_on_error()
225
211
  raise
226
212
  finally:
@@ -246,6 +232,7 @@ class VideoImportService:
246
232
  "anonymization_completed": False,
247
233
  "error_reason": None,
248
234
  }
235
+ self.original_file_path = str(file_path)
249
236
 
250
237
  self.logger.info(f"Initialized processing context for: {file_path}")
251
238
 
@@ -297,6 +284,7 @@ class VideoImportService:
297
284
  delete_source=self.processing_context["delete_source"],
298
285
  save_video_file=self.processing_context["save_video"],
299
286
  )
287
+ self.current_video_id = self.current_video.pk
300
288
 
301
289
  if not self.current_video:
302
290
  raise RuntimeError("Failed to create VideoFile instance")
@@ -358,9 +346,7 @@ class VideoImportService:
358
346
 
359
347
  timestamp = int(time.time())
360
348
  filename = f"video_{timestamp}{suffix}"
361
- self.logger.warning(
362
- "No UUID available, using timestamp-based filename: %s", filename
363
- )
349
+ self.logger.warning("No UUID available, using timestamp-based filename: %s", filename)
364
350
  stored_raw_path = videos_dir / filename
365
351
  self.logger.debug("Using UUID-based raw filename: %s", filename)
366
352
 
@@ -377,9 +363,7 @@ class VideoImportService:
377
363
  except Exception:
378
364
  shutil.copy2(source_path, stored_raw_path)
379
365
  os.remove(source_path)
380
- self.logger.info(
381
- "Copied & removed raw video to: %s", stored_raw_path
382
- )
366
+ self.logger.info("Copied & removed raw video to: %s", stored_raw_path)
383
367
  else:
384
368
  shutil.copy2(source_path, stored_raw_path)
385
369
  self.logger.info("Copied raw video to: %s", stored_raw_path)
@@ -409,8 +393,6 @@ class VideoImportService:
409
393
  # Initialize video specifications
410
394
  video.initialize_video_specs()
411
395
 
412
-
413
-
414
396
  # Extract frames BEFORE processing to prevent pipeline 1 conflicts
415
397
  self.logger.info("Pre-extracting frames to avoid pipeline conflicts...")
416
398
  try:
@@ -418,7 +400,7 @@ class VideoImportService:
418
400
  if frames_extracted:
419
401
  self.processing_context["frames_extracted"] = True
420
402
  self.logger.info("Frame extraction completed successfully")
421
- # Initialize frame objects in database
403
+ # Initialize frame objects in database
422
404
  video.initialize_frames(video.get_frame_paths())
423
405
 
424
406
  # CRITICAL: Immediately save the frames_extracted state to database
@@ -432,9 +414,7 @@ class VideoImportService:
432
414
  self.logger.warning("Frame extraction failed, but continuing...")
433
415
  self.processing_context["frames_extracted"] = False
434
416
  except Exception as e:
435
- self.logger.warning(
436
- f"Frame extraction failed during setup: {e}, but continuing..."
437
- )
417
+ self.logger.warning(f"Frame extraction failed during setup: {e}, but continuing...")
438
418
  self.processing_context["frames_extracted"] = False
439
419
 
440
420
  # Ensure default patient data
@@ -445,32 +425,22 @@ class VideoImportService:
445
425
  def _process_frames_and_metadata(self):
446
426
  """Process frames and extract metadata with anonymization."""
447
427
  # Check frame cleaning availability
448
- frame_cleaning_available, frame_cleaner = (
449
- self._ensure_frame_cleaning_available()
450
- )
428
+ frame_cleaning_available, frame_cleaner = self._ensure_frame_cleaning_available()
451
429
  video = self._require_current_video()
452
430
 
453
431
  raw_file_field = video.raw_file
454
- has_raw_file = isinstance(raw_file_field, FieldFile) and bool(
455
- raw_file_field.name
456
- )
432
+ has_raw_file = isinstance(raw_file_field, FieldFile) and bool(raw_file_field.name)
457
433
 
458
434
  if not (frame_cleaning_available and has_raw_file):
459
- self.logger.warning(
460
- "Frame cleaning not available or conditions not met, using fallback anonymization."
461
- )
435
+ self.logger.warning("Frame cleaning not available or conditions not met, using fallback anonymization.")
462
436
  self._fallback_anonymize_video()
463
437
  return
464
438
 
465
439
  try:
466
- self.logger.info(
467
- "Starting frame-level anonymization with processor ROI masking..."
468
- )
440
+ self.logger.info("Starting frame-level anonymization with processor ROI masking...")
469
441
 
470
442
  # Get processor ROI information
471
- endoscope_data_roi_nested, endoscope_image_roi = (
472
- self._get_processor_roi_info()
473
- )
443
+ endoscope_data_roi_nested, endoscope_image_roi = self._get_processor_roi_info()
474
444
 
475
445
  # Perform frame cleaning with timeout to prevent blocking
476
446
  from concurrent.futures import ThreadPoolExecutor
@@ -484,21 +454,12 @@ class VideoImportService:
484
454
  )
485
455
  try:
486
456
  # Increased timeout to better accommodate ffmpeg + OCR
487
- future.result(timeout=50000)
457
+ future.result(timeout=5000)
488
458
  self.processing_context["anonymization_completed"] = True
489
- self.logger.info(
490
- "Frame cleaning completed successfully within timeout"
491
- )
459
+ self.logger.info("Frame cleaning completed successfully within timeout")
492
460
  except FutureTimeoutError:
493
- self.logger.warning(
494
- "Frame cleaning timed out; entering grace period check for cleaned output"
495
- )
461
+ self.logger.warning("Frame cleaning timed out; entering grace period check for cleaned output")
496
462
  # Grace period: detect if cleaned file appears shortly after timeout
497
- raw_video_path = self.processing_context.get("raw_video_path")
498
- video_filename = self.processing_context.get(
499
- "video_filename",
500
- Path(raw_video_path).name if raw_video_path else "video.mp4",
501
- )
502
463
  grace_seconds = 60
503
464
  expected_cleaned_path: Optional[Path] = None
504
465
  processed_field = video.processed_file
@@ -511,12 +472,8 @@ class VideoImportService:
511
472
  if expected_cleaned_path is not None:
512
473
  for _ in range(grace_seconds):
513
474
  if expected_cleaned_path.exists():
514
- self.processing_context["cleaned_video_path"] = (
515
- expected_cleaned_path
516
- )
517
- self.processing_context["anonymization_completed"] = (
518
- True
519
- )
475
+ self.processing_context["cleaned_video_path"] = expected_cleaned_path
476
+ self.processing_context["anonymization_completed"] = True
520
477
  self.logger.info(
521
478
  "Detected cleaned video during grace period: %s",
522
479
  expected_cleaned_path,
@@ -527,26 +484,18 @@ class VideoImportService:
527
484
  else:
528
485
  self._fallback_anonymize_video()
529
486
  if not found:
530
- raise TimeoutError(
531
- "Frame cleaning operation timed out - likely Ollama connection issue"
532
- )
487
+ raise TimeoutError("Frame cleaning operation timed out - likely Ollama connection issue")
533
488
 
534
489
  except Exception as e:
535
- self.logger.warning(
536
- "Frame cleaning failed (reason: %s), falling back to simple copy", e
537
- )
490
+ self.logger.warning("Frame cleaning failed (reason: %s), falling back to simple copy", e)
538
491
  # Try fallback anonymization when frame cleaning fails
539
492
  try:
540
493
  self._fallback_anonymize_video()
541
494
  except Exception as fallback_error:
542
- self.logger.error(
543
- "Fallback anonymization also failed: %s", fallback_error
544
- )
495
+ self.logger.error("Fallback anonymization also failed: %s", fallback_error)
545
496
  # If even fallback fails, mark as not anonymized but continue import
546
497
  self.processing_context["anonymization_completed"] = False
547
- self.processing_context["error_reason"] = (
548
- f"Frame cleaning failed: {e}, Fallback failed: {fallback_error}"
549
- )
498
+ self.processing_context["error_reason"] = f"Frame cleaning failed: {e}, Fallback failed: {fallback_error}"
550
499
 
551
500
  def _save_anonymized_video(self):
552
501
  original_raw_file_path_to_delete = None
@@ -555,24 +504,14 @@ class VideoImportService:
555
504
  anonymized_video_path = video.get_target_anonymized_video_path()
556
505
 
557
506
  if not anonymized_video_path.exists():
558
- raise RuntimeError(
559
- f"Processed video file not found after assembly for {video.uuid}: {anonymized_video_path}"
560
- )
507
+ raise RuntimeError(f"Processed video file not found after assembly for {video.uuid}: {anonymized_video_path}")
561
508
 
562
509
  new_processed_hash = get_video_hash(anonymized_video_path)
563
- if (
564
- video.__class__.objects.filter(processed_video_hash=new_processed_hash)
565
- .exclude(pk=video.pk)
566
- .exists()
567
- ):
568
- raise ValueError(
569
- f"Processed video hash {new_processed_hash} already exists for another video (Video: {video.uuid})."
570
- )
510
+ if video.__class__.objects.filter(processed_video_hash=new_processed_hash).exclude(pk=video.pk).exists():
511
+ raise ValueError(f"Processed video hash {new_processed_hash} already exists for another video (Video: {video.uuid}).")
571
512
 
572
513
  video.processed_video_hash = new_processed_hash
573
- video.processed_file.name = anonymized_video_path.relative_to(
574
- STORAGE_DIR
575
- ).as_posix()
514
+ video.processed_file.name = anonymized_video_path.relative_to(STORAGE_DIR).as_posix()
576
515
 
577
516
  update_fields = [
578
517
  "processed_video_hash",
@@ -584,7 +523,7 @@ class VideoImportService:
584
523
  original_raw_file_path_to_delete = video.get_raw_file_path()
585
524
  original_raw_frame_dir_to_delete = video.get_frame_dir_path()
586
525
 
587
- video.raw_file.name = None # type: ignore[assignment]
526
+ video.raw_file.name = ""
588
527
 
589
528
  update_fields.extend(["raw_file", "video_hash"])
590
529
 
@@ -597,10 +536,18 @@ class VideoImportService:
597
536
  )
598
537
 
599
538
  video.save(update_fields=update_fields)
600
- video.state.mark_anonymized(save=True)
601
- video.refresh_from_db()
602
- self.current_video = video
603
- return True
539
+ if not isinstance(video.state, VideoState):
540
+ try:
541
+ video.get_or_create_state()
542
+ except ValueError as e:
543
+ raise RuntimeError(f"Video state not found for video {video.uuid}. Error {e}")
544
+
545
+ else:
546
+ video.state.mark_anonymized(save=True)
547
+ video.refresh_from_db()
548
+ self.current_video = video
549
+
550
+ return True
604
551
 
605
552
  def _fallback_anonymize_video(self):
606
553
  """
@@ -610,23 +557,15 @@ class VideoImportService:
610
557
  self.logger.info("Attempting fallback video anonymization...")
611
558
  video = self.current_video
612
559
  if video is None:
613
- self.logger.warning(
614
- "No VideoFile instance available for fallback anonymization"
615
- )
560
+ self.logger.warning("No VideoFile instance available for fallback anonymization")
616
561
 
617
562
  # Strategy 2: Simple copy (no processing, just copy raw to processed)
618
- self.logger.info(
619
- "Using simple copy fallback (raw video will be used as 'processed' video)"
620
- )
563
+ self.logger.info("Using simple copy fallback (raw video will be used as 'processed' video)")
621
564
  self.processing_context["anonymization_completed"] = False
622
565
  self.processing_context["use_raw_as_processed"] = True
623
- self.logger.warning(
624
- "Fallback: Video will be imported without anonymization (raw copy used)"
625
- )
566
+ self.logger.warning("Fallback: Video will be imported without anonymization (raw copy used)")
626
567
  except Exception as e:
627
- self.logger.error(
628
- f"Error during fallback anonymization: {e}", exc_info=True
629
- )
568
+ self.logger.error(f"Error during fallback anonymization: {e}", exc_info=True)
630
569
  self.processing_context["anonymization_completed"] = False
631
570
  self.processing_context["error_reason"] = str(e)
632
571
 
@@ -660,18 +599,12 @@ class VideoImportService:
660
599
  state.text_meta_extracted = True
661
600
 
662
601
  # ✅ FIX: Only mark as processed if anonymization actually completed
663
- anonymization_completed = self.processing_context.get(
664
- "anonymization_completed", False
665
- )
602
+ anonymization_completed = self.processing_context.get("anonymization_completed", False)
666
603
  if anonymization_completed:
667
604
  state.mark_sensitive_meta_processed(save=False)
668
- self.logger.info(
669
- "Anonymization completed - marking sensitive meta as processed"
670
- )
605
+ self.logger.info("Anonymization completed - marking sensitive meta as processed")
671
606
  else:
672
- self.logger.warning(
673
- f"Anonymization NOT completed - NOT marking as processed. Reason: {self.processing_context.get('error_reason', 'Unknown')}"
674
- )
607
+ self.logger.warning(f"Anonymization NOT completed - NOT marking as processed. Reason: {self.processing_context.get('error_reason', 'Unknown')}")
675
608
  # Explicitly mark as NOT processed
676
609
  state.sensitive_meta_processed = False
677
610
 
@@ -703,9 +636,7 @@ class VideoImportService:
703
636
  processed_video_path = Path(raw_video_path).parent / processed_filename
704
637
  try:
705
638
  shutil.copy2(str(raw_video_path), str(processed_video_path))
706
- self.logger.info(
707
- "Copied raw video for processing: %s", processed_video_path
708
- )
639
+ self.logger.info("Copied raw video for processing: %s", processed_video_path)
709
640
  except Exception as exc:
710
641
  self.logger.error("Failed to copy raw video: %s", exc)
711
642
  processed_video_path = None
@@ -725,16 +656,10 @@ class VideoImportService:
725
656
  relative_path = anonym_target_path.relative_to(storage_root)
726
657
  video.processed_file.name = str(relative_path)
727
658
  video.save(update_fields=["processed_file"])
728
- self.logger.info(
729
- "Updated processed_file path to: %s", relative_path
730
- )
659
+ self.logger.info("Updated processed_file path to: %s", relative_path)
731
660
  except Exception as exc:
732
- self.logger.error(
733
- "Failed to update processed_file path: %s", exc
734
- )
735
- video.processed_file.name = (
736
- f"anonym_videos/{anonym_video_filename}"
737
- )
661
+ self.logger.error("Failed to update processed_file path: %s", exc)
662
+ video.processed_file.name = f"anonym_videos/{anonym_video_filename}"
738
663
  video.save(update_fields=["processed_file"])
739
664
  self.logger.info(
740
665
  "Updated processed_file path using fallback: %s",
@@ -748,21 +673,15 @@ class VideoImportService:
748
673
  anonym_target_path,
749
674
  )
750
675
  except Exception as exc:
751
- self.logger.error(
752
- "Failed to move processed video to anonym_videos: %s", exc
753
- )
676
+ self.logger.error("Failed to move processed video to anonym_videos: %s", exc)
754
677
  else:
755
- self.logger.warning(
756
- "No processed video available - processed_file will remain empty"
757
- )
678
+ self.logger.warning("No processed video available - processed_file will remain empty")
758
679
 
759
680
  try:
760
681
  from endoreg_db.utils.paths import RAW_FRAME_DIR
761
682
 
762
683
  shutil.rmtree(RAW_FRAME_DIR, ignore_errors=True)
763
- self.logger.debug(
764
- "Cleaned up temporary frames directory: %s", RAW_FRAME_DIR
765
- )
684
+ self.logger.debug("Cleaned up temporary frames directory: %s", RAW_FRAME_DIR)
766
685
  except Exception as exc:
767
686
  self.logger.warning("Failed to remove directory %s: %s", RAW_FRAME_DIR, exc)
768
687
 
@@ -772,14 +691,10 @@ class VideoImportService:
772
691
  os.remove(source_path)
773
692
  self.logger.info("Removed remaining source file: %s", source_path)
774
693
  except Exception as exc:
775
- self.logger.warning(
776
- "Failed to remove source file %s: %s", source_path, exc
777
- )
694
+ self.logger.warning("Failed to remove source file %s: %s", source_path, exc)
778
695
 
779
- if not video.processed_file or not Path(video.processed_file.path).exists():
780
- self.logger.warning(
781
- "No processed_file found after cleanup - video will be unprocessed"
782
- )
696
+ if not video.processed_file or not storage_file_exists(video.processed_file):
697
+ self.logger.warning("No processed_file found after cleanup - video will be unprocessed")
783
698
  try:
784
699
  video.anonymize(delete_original_raw=self.delete_source)
785
700
  video.save(update_fields=["processed_file"])
@@ -794,14 +709,16 @@ class VideoImportService:
794
709
 
795
710
  with transaction.atomic():
796
711
  video.refresh_from_db()
797
- if hasattr(video, "state") and self.processing_context.get(
798
- "anonymization_completed"
799
- ):
712
+ if hasattr(video, "state") and self.processing_context.get("anonymization_completed"):
713
+ if not isinstance(video.state, VideoState):
714
+ try:
715
+ video.get_or_create_state()
716
+ except:
717
+ raise RuntimeError(f"Video state not found for video {video.uuid}")
718
+
800
719
  video.state.mark_sensitive_meta_processed(save=True)
801
720
 
802
- self.logger.info(
803
- "Import and anonymization completed for VideoFile UUID: %s", video.uuid
804
- )
721
+ self.logger.info("Import and anonymization completed for VideoFile UUID: %s", video.uuid)
805
722
  self.logger.info("Raw video stored in: /data/videos")
806
723
  self.logger.info("Processed video stored in: /data/anonym_videos")
807
724
 
@@ -813,52 +730,77 @@ class VideoImportService:
813
730
  """Create or move a sensitive copy of the raw video file inside storage."""
814
731
 
815
732
  video = video_instance or self._require_current_video()
816
-
817
733
  raw_field: FieldFile | None = getattr(video, "raw_file", None)
818
- source_path: Path | None = None
819
- try:
820
- if raw_field and raw_field.path:
821
- source_path = Path(raw_field.path)
822
- except Exception:
823
- source_path = None
824
734
 
825
- if source_path is None and file_path is not None:
826
- source_path = Path(file_path)
735
+ def copy_into_sensitive(source: Path) -> Path:
736
+ target_dir = VIDEO_DIR / "sensitive"
737
+ if not target_dir.exists():
738
+ self.logger.info("Creating sensitive file directory: %s", target_dir)
739
+ os.makedirs(target_dir, exist_ok=True)
827
740
 
828
- if source_path is None:
829
- raise ValueError("No file path available for creating sensitive file")
830
- if not raw_field:
831
- raise ValueError(
832
- "VideoFile must have a raw_file to create a sensitive file"
833
- )
741
+ target_name = source.name or "raw_video"
742
+ target_file_path = target_dir / target_name
834
743
 
835
- target_dir = VIDEO_DIR / "sensitive"
836
- if not target_dir.exists():
837
- self.logger.info("Creating sensitive file directory: %s", target_dir)
838
- os.makedirs(target_dir, exist_ok=True)
744
+ if source != target_file_path:
745
+ try:
746
+ shutil.copy2(source, target_file_path)
747
+ self.logger.info("Copied raw file to sensitive directory: %s", target_file_path)
748
+ except Exception as exc:
749
+ self.logger.warning("Failed to copy raw file to sensitive dir: %s", exc)
750
+ shutil.copy(source, target_file_path)
751
+ self.logger.info(
752
+ "Fallback copy succeeded for sensitive directory: %s",
753
+ target_file_path,
754
+ )
755
+ else:
756
+ self.logger.debug(
757
+ "Source path already in sensitive directory: %s",
758
+ target_file_path,
759
+ )
839
760
 
840
- target_file_path = target_dir / source_path.name
841
- try:
842
- shutil.move(str(source_path), str(target_file_path))
843
- self.logger.info(
844
- "Moved raw file to sensitive directory: %s", target_file_path
845
- )
846
- except Exception as exc:
847
- self.logger.warning(
848
- "Failed to move raw file to sensitive dir, copying instead: %s", exc
849
- )
850
- shutil.copy(str(source_path), str(target_file_path))
761
+ return target_file_path
762
+
763
+ target_file_path: Path | None = None
764
+
765
+ # Prefer an on-disk path from the FieldFile when available
766
+ if raw_field:
851
767
  try:
852
- os.remove(source_path)
853
- except FileNotFoundError:
854
- pass
768
+ local_candidate = Path(raw_field.path)
769
+ if local_candidate.exists():
770
+ target_file_path = copy_into_sensitive(local_candidate)
771
+ except Exception:
772
+ target_file_path = None
773
+
774
+ if target_file_path is None and storage_file_exists(raw_field):
775
+ try:
776
+ with ensure_local_file(raw_field) as temp_source:
777
+ target_file_path = copy_into_sensitive(Path(temp_source))
778
+ except Exception as exc:
779
+ self.logger.warning("Failed to download raw_field for sensitive copy: %s", exc)
780
+
781
+ if target_file_path is None and file_path is not None:
782
+ file_candidate = Path(file_path)
783
+ if file_candidate.exists():
784
+ target_file_path = copy_into_sensitive(file_candidate)
785
+
786
+ if target_file_path is None:
787
+ context_path = self.processing_context.get("raw_video_path")
788
+ if context_path:
789
+ context_candidate = Path(context_path)
790
+ if context_candidate.exists():
791
+ target_file_path = copy_into_sensitive(context_candidate)
792
+
793
+ if target_file_path is None:
794
+ raise ValueError("No file path available for creating sensitive file")
795
+ if not raw_field:
796
+ raise ValueError("VideoFile must have a raw_file to create a sensitive file")
855
797
 
856
798
  try:
857
799
  from endoreg_db.utils import data_paths
858
800
 
859
801
  storage_root = data_paths["storage"]
860
802
  relative_path = target_file_path.relative_to(storage_root)
861
- video.raw_file.name = str(relative_path)
803
+ video.raw_file.name = relative_path.as_posix()
862
804
  video.save(update_fields=["raw_file"])
863
805
  self.logger.info(
864
806
  "Updated video.raw_file to point to sensitive location: %s",
@@ -876,9 +818,7 @@ class VideoImportService:
876
818
  self.processing_context["raw_video_path"] = target_file_path
877
819
  self.processing_context["video_filename"] = target_file_path.name
878
820
 
879
- self.logger.info(
880
- "Created sensitive file for %s at %s", video.uuid, target_file_path
881
- )
821
+ self.logger.info("Created sensitive file for %s at %s", video.uuid, target_file_path)
882
822
  return target_file_path
883
823
 
884
824
  def _get_processor_roi_info(
@@ -894,9 +834,7 @@ class VideoImportService:
894
834
  video_meta = getattr(video, "video_meta", None)
895
835
  processor = getattr(video_meta, "processor", None) if video_meta else None
896
836
  if processor:
897
- assert isinstance(processor, EndoscopyProcessor), (
898
- "Processor is not of type EndoscopyProcessor"
899
- )
837
+ assert isinstance(processor, EndoscopyProcessor), "Processor is not of type EndoscopyProcessor"
900
838
  endoscope_image_roi = processor.get_roi_endoscope_image()
901
839
  endoscope_data_roi_nested = processor.get_sensitive_rois()
902
840
  self.logger.info(
@@ -924,34 +862,26 @@ class VideoImportService:
924
862
 
925
863
  return endoscope_data_roi_nested, endoscope_image_roi
926
864
 
927
- def _ensure_default_patient_data(
928
- self, video_instance: VideoFile | None = None
929
- ) -> None:
865
+ def _ensure_default_patient_data(self, video_instance: VideoFile | None = None) -> None:
930
866
  """Ensure minimum patient data is present on the video's SensitiveMeta."""
931
867
 
932
868
  video = video_instance or self._require_current_video()
933
869
 
934
870
  sensitive_meta = getattr(video, "sensitive_meta", None)
935
871
  if not sensitive_meta:
936
- self.logger.info(
937
- "No SensitiveMeta found for video %s, creating default", video.uuid
938
- )
872
+ self.logger.info("No SensitiveMeta found for video %s, creating default", video.uuid)
939
873
  default_data = {
940
874
  "patient_first_name": "Patient",
941
875
  "patient_last_name": "Unknown",
942
876
  "patient_dob": date(1990, 1, 1),
943
877
  "examination_date": date.today(),
944
- "center_name": video.center.name
945
- if video.center
946
- else "university_hospital_wuerzburg",
878
+ "center_name": video.center.name if video.center else "university_hospital_wuerzburg",
947
879
  }
948
880
  try:
949
881
  sensitive_meta = SensitiveMeta.create_from_dict(default_data)
950
882
  video.sensitive_meta = sensitive_meta
951
883
  video.save(update_fields=["sensitive_meta"])
952
- self.logger.info(
953
- "Created default SensitiveMeta for video %s", video.uuid
954
- )
884
+ self.logger.info("Created default SensitiveMeta for video %s", video.uuid)
955
885
  except Exception as exc:
956
886
  self.logger.error(
957
887
  "Failed to create default SensitiveMeta for video %s: %s",
@@ -960,32 +890,9 @@ class VideoImportService:
960
890
  )
961
891
  return
962
892
  else:
963
- update_data: Dict[str, Any] = {}
964
- if not sensitive_meta.patient_first_name:
965
- update_data["patient_first_name"] = "Patient"
966
- if not sensitive_meta.patient_last_name:
967
- update_data["patient_last_name"] = "Unknown"
968
- if not sensitive_meta.patient_dob:
969
- update_data["patient_dob"] = date(1990, 1, 1)
970
- if not sensitive_meta.examination_date:
971
- update_data["examination_date"] = date.today()
972
-
973
- if update_data:
974
- try:
975
- sensitive_meta.update_from_dict(update_data)
976
- state = video.get_or_create_state()
977
- state.mark_sensitive_meta_processed(save=True)
978
- self.logger.info(
979
- "Updated missing SensitiveMeta fields for video %s: %s",
980
- video.uuid,
981
- list(update_data.keys()),
982
- )
983
- except Exception as exc:
984
- self.logger.error(
985
- "Failed to update SensitiveMeta for video %s: %s",
986
- video.uuid,
987
- exc,
988
- )
893
+ state = video.get_or_create_state()
894
+ state.mark_sensitive_meta_processed(save=True)
895
+
989
896
 
990
897
  def _ensure_frame_cleaning_available(self):
991
898
  """
@@ -995,25 +902,24 @@ class VideoImportService:
995
902
  Tuple of (availability_flag, FrameCleaner_class, ReportReader_class)
996
903
  """
997
904
  try:
998
- # Check if we can find lx-anonymizer
999
- from lx_anonymizer import FrameCleaner # type: ignore[import]
1000
-
1001
- if FrameCleaner:
1002
- return True, FrameCleaner()
1003
-
905
+ from lx_anonymizer import FrameCleaner
1004
906
  except Exception as e:
1005
- self.logger.warning(
1006
- f"Frame cleaning not available: {e} Please install or update lx_anonymizer."
1007
- )
907
+ self.logger.warning(f"Frame cleaning not available: {e} Please install or update lx_anonymizer.")
908
+ _available = False
909
+ FrameCleaner = None
1008
910
 
1009
- return False, None
911
+ assert FrameCleaner is not None
912
+ frame_cleaner = FrameCleaner()
913
+ _available = True
914
+
915
+ return _available, frame_cleaner
1010
916
 
1011
917
  def _perform_frame_cleaning(self, endoscope_data_roi_nested, endoscope_image_roi):
1012
918
  """Perform frame cleaning and anonymization."""
1013
919
  # Instantiate frame cleaner
1014
920
  is_available, frame_cleaner = self._ensure_frame_cleaning_available()
1015
921
 
1016
- if not is_available:
922
+ if not is_available or frame_cleaner is None:
1017
923
  raise RuntimeError("Frame cleaning not available")
1018
924
 
1019
925
  # Prepare parameters for frame cleaning
@@ -1030,9 +936,7 @@ class VideoImportService:
1030
936
  video = self._require_current_video()
1031
937
  # Ensure raw_video_path is not None
1032
938
  if not raw_video_path:
1033
- raise RuntimeError(
1034
- "raw_video_path is None, cannot construct cleaned_video_path"
1035
- )
939
+ raise RuntimeError("raw_video_path is None, cannot construct cleaned_video_path")
1036
940
  suffix = Path(raw_video_path).suffix or ".mp4"
1037
941
  cleaned_filename = f"cleaned_{video.uuid}{suffix}"
1038
942
  cleaned_video_path = Path(raw_video_path).parent / cleaned_filename
@@ -1053,13 +957,9 @@ class VideoImportService:
1053
957
 
1054
958
  # Update sensitive metadata with extracted information
1055
959
  self._update_sensitive_metadata(extracted_metadata)
1056
- self.logger.info(
1057
- f"Extracted metadata from frame cleaning: {extracted_metadata}"
1058
- )
960
+ self.logger.info(f"Extracted metadata from frame cleaning: {extracted_metadata}")
1059
961
 
1060
- self.logger.info(
1061
- f"Frame cleaning with ROI masking completed: {actual_cleaned_path}"
1062
- )
962
+ self.logger.info(f"Frame cleaning with ROI masking completed: {actual_cleaned_path}")
1063
963
  self.logger.info("Cleaned video will be moved to anonym_videos during cleanup")
1064
964
 
1065
965
  def _update_sensitive_metadata(self, extracted_metadata: Dict[str, Any]):
@@ -1096,21 +996,15 @@ class VideoImportService:
1096
996
 
1097
997
  center_obj = Center.objects.get(name=center_name)
1098
998
  metadata_to_update["center"] = center_obj
1099
- self.logger.debug(
1100
- "Loaded center object '%s' from center_name", center_name
1101
- )
999
+ self.logger.debug("Loaded center object '%s' from center_name", center_name)
1102
1000
  metadata_to_update.pop("center_name", None)
1103
1001
  except Center.DoesNotExist:
1104
- self.logger.error(
1105
- "Center '%s' not found in database", center_name
1106
- )
1002
+ self.logger.error("Center '%s' not found in database", center_name)
1107
1003
  return
1108
1004
 
1109
1005
  try:
1110
1006
  sm.update_from_dict(metadata_to_update)
1111
- updated_fields = list(
1112
- extracted_metadata.keys()
1113
- ) # Only log originally extracted fields
1007
+ updated_fields = list(extracted_metadata.keys()) # Only log originally extracted fields
1114
1008
  except KeyError as e:
1115
1009
  self.logger.warning(f"Failed to update SensitiveMeta field {e}")
1116
1010
  return
@@ -1126,9 +1020,7 @@ class VideoImportService:
1126
1020
 
1127
1021
  state = video.get_or_create_state()
1128
1022
  state.mark_sensitive_meta_processed(save=True)
1129
- self.logger.info(
1130
- "Marked sensitive metadata as processed for video %s", video.uuid
1131
- )
1023
+ self.logger.info("Marked sensitive metadata as processed for video %s", video.uuid)
1132
1024
  except Exception as e:
1133
1025
  self.logger.error(f"Failed to save SensitiveMeta: {e}")
1134
1026
  raise # Re-raise to trigger fallback in calling method
@@ -1144,18 +1036,9 @@ class VideoImportService:
1144
1036
  video = self._require_current_video()
1145
1037
 
1146
1038
  raw_field: FieldFile | None = getattr(video, "raw_file", None)
1147
- raw_exists = False
1148
- if raw_field and getattr(raw_field, "path", None):
1149
- try:
1150
- raw_exists = Path(raw_field.path).exists()
1151
- except (ValueError, OSError):
1152
- raw_exists = False
1153
-
1154
- video_processing_complete = (
1155
- video.sensitive_meta is not None
1156
- and video.video_meta is not None
1157
- and raw_exists
1158
- )
1039
+ raw_exists = storage_file_exists(raw_field)
1040
+
1041
+ video_processing_complete = video.sensitive_meta is not None and video.video_meta is not None and raw_exists
1159
1042
 
1160
1043
  if video_processing_complete:
1161
1044
  self.logger.info(
@@ -1189,15 +1072,44 @@ class VideoImportService:
1189
1072
  def _cleanup_on_error(self):
1190
1073
  """Cleanup processing context on error."""
1191
1074
  if self.current_video and hasattr(self.current_video, "state"):
1075
+ if self.current_video.state is None:
1076
+ try:
1077
+ self.current_video.get_or_create_state()
1078
+ except Exception as e:
1079
+ self.logger.warning(f"Video state not found for video {self.current_video.uuid} during error cleanup {e}")
1080
+ return
1081
+ self.current_video.state = self.current_video.get_or_create_state()
1082
+ try:
1083
+ if self.original_file_path is not None:
1084
+ assert Path(self.original_file_path).exists()
1085
+ else:
1086
+ self.logger.warning("Original file path is None")
1087
+ self.logger.info("Marked video import as failed in state")
1088
+ raw_file_path = getattr(self.current_video.raw_file, "path", None)
1089
+ original_file_path = self.original_file_path
1090
+ if raw_file_path and original_file_path:
1091
+ shutil.copy2(str(raw_file_path), str(original_file_path))
1092
+ else:
1093
+ self.logger.warning("Cannot restore original raw file: path is None")
1094
+ except AssertionError:
1095
+ self.logger.warning("Original file path does not exist")
1192
1096
  try:
1097
+
1098
+ if not isinstance(self.current_video.state, VideoState):
1099
+ logger.error("Current video is none after Assertion for Video File")
1100
+ raise AssertionError
1101
+
1102
+
1193
1103
  if self.processing_context.get("processing_started"):
1194
1104
  self.current_video.state.frames_extracted = False
1195
1105
  self.current_video.state.frames_initialized = False
1196
1106
  self.current_video.state.video_meta_extracted = False
1197
1107
  self.current_video.state.text_meta_extracted = False
1198
1108
  self.current_video.state.save()
1109
+
1199
1110
  except Exception as e:
1200
1111
  self.logger.warning(f"Error during cleanup: {e}")
1112
+
1201
1113
 
1202
1114
  def _cleanup_processing_context(self):
1203
1115
  """
@@ -1226,9 +1138,7 @@ class VideoImportService:
1226
1138
  file_path_str = str(file_path)
1227
1139
  if file_path_str in self.processed_files:
1228
1140
  self.processed_files.remove(file_path_str)
1229
- self.logger.info(
1230
- f"Removed {file_path_str} from processed files (failed processing)"
1231
- )
1141
+ self.logger.info(f"Removed {file_path_str} from processed files (failed processing)")
1232
1142
 
1233
1143
  except Exception as e:
1234
1144
  self.logger.warning(f"Error during context cleanup: {e}")