endoreg-db 0.4.5__py3-none-any.whl → 0.8.6.3__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.
Files changed (846) hide show
  1. endoreg_db/admin.py +90 -1
  2. endoreg_db/api_urls.py +4 -0
  3. endoreg_db/apps.py +12 -0
  4. endoreg_db/assets/dummy_model.ckpt +1 -0
  5. endoreg_db/codemods/readme.md +88 -0
  6. endoreg_db/codemods/rename_datetime_fields.py +92 -0
  7. endoreg_db/config/env.py +101 -0
  8. endoreg_db/data/__init__.py +76 -4
  9. endoreg_db/data/ai_model/data.yaml +7 -0
  10. endoreg_db/data/{label → ai_model_label}/label/data.yaml +27 -1
  11. endoreg_db/data/ai_model_label/label/polyp_classification.yaml +52 -0
  12. endoreg_db/data/ai_model_label/label-set/data.yaml +40 -0
  13. endoreg_db/data/ai_model_label/label-set/polyp_classifications.yaml +25 -0
  14. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +27 -0
  15. endoreg_db/data/ai_model_video_segmentation_label/base_segmentation.yaml +176 -0
  16. endoreg_db/data/ai_model_video_segmentation_labelset/data.yaml +20 -0
  17. endoreg_db/data/center/data.yaml +40 -9
  18. endoreg_db/data/center_shift/ukw.yaml +9 -0
  19. endoreg_db/data/contraindication/bleeding.yaml +11 -0
  20. endoreg_db/data/db_summary.csv +58 -0
  21. endoreg_db/data/db_summary.xlsx +0 -0
  22. endoreg_db/data/disease/misc.yaml +1 -2
  23. endoreg_db/data/disease_classification/chronic_kidney_disease.yaml +2 -2
  24. endoreg_db/data/disease_classification_choice/chronic_kidney_disease.yaml +6 -6
  25. endoreg_db/data/distribution/numeric/data.yaml +14 -0
  26. endoreg_db/data/endoscope/data.yaml +93 -0
  27. endoreg_db/data/endoscopy_processor/data.yaml +3 -0
  28. endoreg_db/data/event/cardiology.yaml +0 -13
  29. endoreg_db/data/examination/examinations/data.yaml +34 -28
  30. endoreg_db/data/examination/type/data.yaml +12 -0
  31. endoreg_db/data/examination_indication/endoscopy.yaml +424 -0
  32. endoreg_db/data/examination_indication_classification/endoscopy.yaml +160 -0
  33. endoreg_db/data/examination_indication_classification_choice/endoscopy.yaml +101 -0
  34. endoreg_db/data/examination_requirement_set/colonoscopy.yaml +15 -0
  35. endoreg_db/data/finding/anatomy_colon.yaml +128 -0
  36. endoreg_db/data/finding/colonoscopy.yaml +40 -0
  37. endoreg_db/data/finding/colonoscopy_bowel_prep.yaml +56 -0
  38. endoreg_db/data/finding/complication.yaml +16 -0
  39. endoreg_db/data/finding/data.yaml +105 -0
  40. endoreg_db/data/finding/examination_setting.yaml +16 -0
  41. endoreg_db/data/finding/medication_related.yaml +18 -0
  42. endoreg_db/data/finding/outcome.yaml +12 -0
  43. endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +95 -0
  44. endoreg_db/data/finding_classification/colonoscopy_jnet.yaml +22 -0
  45. endoreg_db/data/finding_classification/colonoscopy_kudo.yaml +25 -0
  46. endoreg_db/data/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
  47. endoreg_db/data/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
  48. endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +68 -0
  49. endoreg_db/data/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
  50. endoreg_db/data/finding_classification/colonoscopy_location.yaml +80 -0
  51. endoreg_db/data/finding_classification/colonoscopy_lst.yaml +21 -0
  52. endoreg_db/data/finding_classification/colonoscopy_nice.yaml +20 -0
  53. endoreg_db/data/finding_classification/colonoscopy_paris.yaml +26 -0
  54. endoreg_db/data/finding_classification/colonoscopy_sano.yaml +22 -0
  55. endoreg_db/data/finding_classification/colonoscopy_summary.yaml +53 -0
  56. endoreg_db/data/finding_classification/complication_generic.yaml +25 -0
  57. endoreg_db/data/finding_classification/examination_setting_generic.yaml +40 -0
  58. endoreg_db/data/finding_classification/histology_colo.yaml +51 -0
  59. endoreg_db/data/finding_classification/intervention_required.yaml +26 -0
  60. endoreg_db/data/finding_classification/medication_related.yaml +23 -0
  61. endoreg_db/data/finding_classification/visualized.yaml +33 -0
  62. endoreg_db/data/finding_classification_choice/bowel_preparation.yaml +78 -0
  63. endoreg_db/data/finding_classification_choice/colon_lesion_circularity_default.yaml +32 -0
  64. endoreg_db/data/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
  65. endoreg_db/data/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
  66. endoreg_db/data/finding_classification_choice/colon_lesion_lst.yaml +15 -0
  67. endoreg_db/data/finding_classification_choice/colon_lesion_nice.yaml +17 -0
  68. endoreg_db/data/finding_classification_choice/colon_lesion_paris.yaml +57 -0
  69. endoreg_db/data/finding_classification_choice/colon_lesion_planarity_default.yaml +49 -0
  70. endoreg_db/data/finding_classification_choice/colon_lesion_sano.yaml +14 -0
  71. endoreg_db/data/finding_classification_choice/colon_lesion_surface_intact_default.yaml +36 -0
  72. endoreg_db/data/finding_classification_choice/colonoscopy_location.yaml +229 -0
  73. endoreg_db/data/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
  74. endoreg_db/data/finding_classification_choice/colonoscopy_size.yaml +82 -0
  75. endoreg_db/data/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
  76. endoreg_db/data/finding_classification_choice/complication_generic_types.yaml +15 -0
  77. endoreg_db/data/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
  78. endoreg_db/data/finding_classification_choice/histology.yaml +24 -0
  79. endoreg_db/data/finding_classification_choice/histology_polyp.yaml +20 -0
  80. endoreg_db/data/finding_classification_choice/outcome.yaml +19 -0
  81. endoreg_db/data/finding_classification_choice/yes_no_na.yaml +11 -0
  82. endoreg_db/data/finding_classification_type/colonoscopy_basic.yaml +48 -0
  83. endoreg_db/data/finding_intervention/endoscopy.yaml +43 -0
  84. endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +168 -0
  85. endoreg_db/data/finding_intervention/endoscopy_egd.yaml +128 -0
  86. endoreg_db/data/finding_intervention/endoscopy_ercp.yaml +32 -0
  87. endoreg_db/data/finding_intervention/endoscopy_eus_lower.yaml +9 -0
  88. endoreg_db/data/finding_intervention/endoscopy_eus_upper.yaml +36 -0
  89. endoreg_db/data/finding_intervention_type/endoscopy.yaml +15 -0
  90. endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +79 -0
  91. endoreg_db/data/finding_type/data.yaml +43 -0
  92. endoreg_db/data/gender/data.yaml +24 -0
  93. endoreg_db/data/information_source/annotation.yaml +6 -0
  94. endoreg_db/data/information_source/endoscopy_guidelines.yaml +7 -0
  95. endoreg_db/data/information_source/prediction.yaml +7 -0
  96. endoreg_db/data/information_source_type/data.yaml +8 -0
  97. endoreg_db/data/lab_value/cardiac_enzymes.yaml +7 -1
  98. endoreg_db/data/lab_value/coagulation.yaml +6 -1
  99. endoreg_db/data/lab_value/electrolytes.yaml +39 -1
  100. endoreg_db/data/lab_value/gastrointestinal_function.yaml +12 -0
  101. endoreg_db/data/lab_value/hematology.yaml +17 -2
  102. endoreg_db/data/lab_value/hormones.yaml +6 -0
  103. endoreg_db/data/lab_value/lipids.yaml +12 -3
  104. endoreg_db/data/lab_value/misc.yaml +48 -2
  105. endoreg_db/data/lab_value/renal_function.yaml +2 -1
  106. endoreg_db/data/lx_client_tag/base.yaml +54 -0
  107. endoreg_db/data/lx_client_type/base.yaml +30 -0
  108. endoreg_db/data/lx_permission/base.yaml +24 -0
  109. endoreg_db/data/lx_permission/endoreg.yaml +52 -0
  110. endoreg_db/data/medication/anticoagulation.yaml +5 -5
  111. endoreg_db/data/medication/tah.yaml +5 -5
  112. endoreg_db/data/medication_indication/anticoagulation.yaml +48 -53
  113. endoreg_db/data/medication_intake_time/base.yaml +4 -4
  114. endoreg_db/data/names_first/first_names.yaml +54 -0
  115. endoreg_db/data/names_last/last_names.yaml +51 -0
  116. endoreg_db/data/network_device/data.yaml +30 -0
  117. endoreg_db/data/organ/data.yaml +29 -0
  118. endoreg_db/data/pdf_type/data.yaml +27 -9
  119. endoreg_db/data/qualification/endoscopy.yaml +36 -0
  120. endoreg_db/data/qualification/m2.yaml +39 -0
  121. endoreg_db/data/qualification/outpatient_clinic.yaml +35 -0
  122. endoreg_db/data/qualification/sonography.yaml +36 -0
  123. endoreg_db/data/qualification_type/base.yaml +29 -0
  124. endoreg_db/data/report_reader_flag/rkh-histology-generic.yaml +10 -0
  125. endoreg_db/data/report_reader_flag/ukw-examination-generic.yaml +4 -0
  126. endoreg_db/data/report_reader_flag/ukw-histology-generic.yaml +5 -0
  127. endoreg_db/data/requirement/age.yaml +26 -0
  128. endoreg_db/data/requirement/colonoscopy_baseline_austria.yaml +45 -0
  129. endoreg_db/data/requirement/disease_cardiovascular.yaml +79 -0
  130. endoreg_db/data/requirement/disease_classification_choice_cardiovascular.yaml +41 -0
  131. endoreg_db/data/requirement/disease_hepatology.yaml +12 -0
  132. endoreg_db/data/requirement/disease_misc.yaml +12 -0
  133. endoreg_db/data/requirement/disease_renal.yaml +96 -0
  134. endoreg_db/data/requirement/endoscopy_bleeding_risk.yaml +59 -0
  135. endoreg_db/data/requirement/event_cardiology.yaml +251 -0
  136. endoreg_db/data/requirement/event_requirements.yaml +145 -0
  137. endoreg_db/data/requirement/finding_colon_polyp.yaml +50 -0
  138. endoreg_db/data/requirement/gender.yaml +25 -0
  139. endoreg_db/data/requirement/lab_value.yaml +441 -0
  140. endoreg_db/data/requirement/medication.yaml +93 -0
  141. endoreg_db/data/requirement_operator/age.yaml +13 -0
  142. endoreg_db/data/requirement_operator/lab_operators.yaml +129 -0
  143. endoreg_db/data/requirement_operator/model_operators.yaml +96 -0
  144. endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +48 -0
  145. endoreg_db/data/requirement_set/colonoscopy_austria_screening.yaml +57 -0
  146. endoreg_db/data/requirement_set/endoscopy_bleeding_risk.yaml +52 -0
  147. endoreg_db/data/requirement_set_type/data.yaml +20 -0
  148. endoreg_db/data/requirement_type/requirement_types.yaml +165 -0
  149. endoreg_db/data/risk/bleeding.yaml +26 -0
  150. endoreg_db/data/risk/thrombosis.yaml +37 -0
  151. endoreg_db/data/risk_type/data.yaml +27 -0
  152. endoreg_db/data/setup_config.yaml +38 -0
  153. endoreg_db/data/shift/endoscopy.yaml +21 -0
  154. endoreg_db/data/shift_type/base.yaml +35 -0
  155. endoreg_db/data/tag/requirement_set_tags.yaml +11 -0
  156. endoreg_db/data/unit/concentration.yaml +23 -0
  157. endoreg_db/data/unit/time.yaml +36 -1
  158. endoreg_db/exceptions.py +19 -0
  159. endoreg_db/forms/__init__.py +3 -1
  160. endoreg_db/forms/examination_form.py +11 -0
  161. endoreg_db/forms/patient_finding_intervention_form.py +18 -0
  162. endoreg_db/forms/patient_form.py +27 -0
  163. endoreg_db/forms/questionnaires/__init__.py +1 -1
  164. endoreg_db/forms/questionnaires/tto_questionnaire.py +19 -19
  165. endoreg_db/helpers/count_db.py +45 -0
  166. endoreg_db/helpers/data_loader.py +208 -0
  167. endoreg_db/helpers/default_objects.py +378 -0
  168. endoreg_db/helpers/download_segmentation_model.py +31 -0
  169. endoreg_db/helpers/interact.py +6 -0
  170. endoreg_db/helpers/test_video_helper.py +119 -0
  171. endoreg_db/logger_conf.py +140 -0
  172. endoreg_db/management/__init__.py +1 -0
  173. endoreg_db/management/commands/__init__.py +1 -0
  174. endoreg_db/management/commands/anonymize_video.py +0 -0
  175. endoreg_db/management/commands/check_auth.py +125 -0
  176. endoreg_db/management/commands/create_model_meta_from_huggingface.py +115 -0
  177. endoreg_db/management/commands/create_multilabel_model_meta.py +214 -0
  178. endoreg_db/management/commands/fix_missing_patient_data.py +172 -0
  179. endoreg_db/management/commands/fix_video_paths.py +165 -0
  180. endoreg_db/management/commands/import_fallback_video.py +203 -0
  181. endoreg_db/management/commands/import_report.py +298 -0
  182. endoreg_db/management/commands/import_video.py +423 -0
  183. endoreg_db/management/commands/import_video_with_classification.py +367 -0
  184. endoreg_db/management/commands/init_default_ai_model.py +112 -0
  185. endoreg_db/management/commands/load_ai_model_data.py +58 -26
  186. endoreg_db/management/commands/load_ai_model_label_data.py +59 -0
  187. endoreg_db/management/commands/load_base_db_data.py +174 -118
  188. endoreg_db/management/commands/load_center_data.py +46 -21
  189. endoreg_db/management/commands/{load_logging_data.py → load_contraindication_data.py} +4 -2
  190. endoreg_db/management/commands/load_disease_data.py +29 -7
  191. endoreg_db/management/commands/{load_endoscope_type_data.py → load_endoscope_data.py} +30 -7
  192. endoreg_db/management/commands/load_examination_indication_data.py +86 -0
  193. endoreg_db/management/commands/load_finding_data.py +128 -0
  194. endoreg_db/management/commands/load_green_endoscopy_wuerzburg_data.py +0 -1
  195. endoreg_db/management/commands/load_information_source.py +13 -7
  196. endoreg_db/management/commands/load_lab_value_data.py +3 -3
  197. endoreg_db/management/commands/load_medication_data.py +83 -21
  198. endoreg_db/management/commands/load_name_data.py +37 -0
  199. endoreg_db/management/commands/{load_medication_intake_time_data.py → load_organ_data.py} +7 -5
  200. endoreg_db/management/commands/load_qualification_data.py +59 -0
  201. endoreg_db/management/commands/load_requirement_data.py +180 -0
  202. endoreg_db/management/commands/load_risk_data.py +56 -0
  203. endoreg_db/management/commands/load_shift_data.py +60 -0
  204. endoreg_db/management/commands/load_tag_data.py +57 -0
  205. endoreg_db/management/commands/register_ai_model.py +1 -1
  206. endoreg_db/management/commands/setup_endoreg_db.py +381 -0
  207. endoreg_db/management/commands/start_filewatcher.py +106 -0
  208. endoreg_db/management/commands/storage_management.py +548 -0
  209. endoreg_db/management/commands/summarize_db_content.py +189 -0
  210. endoreg_db/management/commands/validate_video.py +204 -0
  211. endoreg_db/management/commands/validate_video_files.py +161 -0
  212. endoreg_db/management/commands/video_validation.py +22 -0
  213. endoreg_db/mermaid/Overall_flow_patient_finding_intervention.md +10 -0
  214. endoreg_db/mermaid/anonymized_image_annotation.md +20 -0
  215. endoreg_db/mermaid/binary_classification_annotation.md +50 -0
  216. endoreg_db/mermaid/classification.md +8 -0
  217. endoreg_db/mermaid/examination.md +8 -0
  218. endoreg_db/mermaid/findings.md +7 -0
  219. endoreg_db/mermaid/image_classification.md +28 -0
  220. endoreg_db/mermaid/interventions.md +8 -0
  221. endoreg_db/mermaid/morphology.md +8 -0
  222. endoreg_db/mermaid/patient_creation.md +14 -0
  223. endoreg_db/mermaid/video_segmentation_annotation.md +17 -0
  224. endoreg_db/migrations/0001_initial.py +1234 -944
  225. endoreg_db/migrations/0002_add_video_correction_models.py +52 -0
  226. endoreg_db/migrations/0003_add_center_display_name.py +30 -0
  227. endoreg_db/models/__init__.py +339 -53
  228. endoreg_db/models/administration/__init__.py +116 -0
  229. endoreg_db/models/administration/ai/__init__.py +9 -0
  230. endoreg_db/models/administration/ai/active_model.py +35 -0
  231. endoreg_db/models/administration/ai/ai_model.py +156 -0
  232. endoreg_db/models/{ai_model → administration/ai}/model_type.py +19 -4
  233. endoreg_db/models/administration/case/__init__.py +19 -0
  234. endoreg_db/models/administration/case/case.py +114 -0
  235. endoreg_db/models/{case_template → administration/case/case_template}/__init__.py +10 -1
  236. endoreg_db/models/{case_template → administration/case/case_template}/case_template.py +60 -16
  237. endoreg_db/models/{case_template → administration/case/case_template}/case_template_rule.py +6 -13
  238. endoreg_db/models/{case_template → administration/case/case_template}/case_template_rule_value.py +21 -8
  239. endoreg_db/models/{case_template → administration/case/case_template}/case_template_type.py +1 -3
  240. endoreg_db/models/{center → administration/center}/__init__.py +9 -0
  241. endoreg_db/models/administration/center/center.py +67 -0
  242. endoreg_db/models/administration/center/center_product.py +64 -0
  243. endoreg_db/models/administration/center/center_resource.py +49 -0
  244. endoreg_db/models/administration/center/center_shift.py +88 -0
  245. endoreg_db/models/administration/center/center_waste.py +30 -0
  246. endoreg_db/models/administration/permissions/__init__.py +44 -0
  247. endoreg_db/models/administration/person/__init__.py +24 -0
  248. endoreg_db/models/administration/person/employee/__init__.py +3 -0
  249. endoreg_db/models/administration/person/employee/employee.py +35 -0
  250. endoreg_db/models/administration/person/employee/employee_qualification.py +39 -0
  251. endoreg_db/models/administration/person/employee/employee_type.py +42 -0
  252. endoreg_db/models/administration/person/examiner/__init__.py +4 -0
  253. endoreg_db/models/administration/person/examiner/examiner.py +54 -0
  254. endoreg_db/models/administration/person/names/__init__.py +0 -0
  255. endoreg_db/models/{persons → administration/person/names}/first_name.py +1 -1
  256. endoreg_db/models/{persons → administration/person/names}/last_name.py +2 -3
  257. endoreg_db/models/administration/person/patient/__init__.py +5 -0
  258. endoreg_db/models/administration/person/patient/patient.py +460 -0
  259. endoreg_db/models/administration/person/profession/__init__.py +24 -0
  260. endoreg_db/models/administration/person/user/__init__.py +5 -0
  261. endoreg_db/models/administration/person/user/portal_user_information.py +37 -0
  262. endoreg_db/models/administration/product/__init__.py +14 -0
  263. endoreg_db/models/administration/product/product.py +97 -0
  264. endoreg_db/models/administration/product/product_group.py +39 -0
  265. endoreg_db/models/administration/product/product_material.py +54 -0
  266. endoreg_db/models/{product → administration/product}/product_weight.py +21 -0
  267. endoreg_db/models/{product → administration/product}/reference_product.py +44 -13
  268. endoreg_db/models/administration/qualification/__init__.py +7 -0
  269. endoreg_db/models/administration/qualification/qualification.py +37 -0
  270. endoreg_db/models/administration/qualification/qualification_type.py +35 -0
  271. endoreg_db/models/administration/shift/__init__.py +9 -0
  272. endoreg_db/models/administration/shift/scheduled_days.py +69 -0
  273. endoreg_db/models/administration/shift/shift.py +51 -0
  274. endoreg_db/models/administration/shift/shift_type.py +108 -0
  275. endoreg_db/models/label/__init__.py +24 -1
  276. endoreg_db/models/label/annotation/__init__.py +12 -0
  277. endoreg_db/models/label/annotation/image_classification.py +84 -0
  278. endoreg_db/models/label/annotation/video_segmentation_annotation.py +66 -0
  279. endoreg_db/models/label/label.py +53 -54
  280. endoreg_db/models/label/label_set.py +53 -0
  281. endoreg_db/models/label/label_type.py +29 -0
  282. endoreg_db/models/label/label_video_segment/__init__.py +3 -0
  283. endoreg_db/models/label/label_video_segment/_create_from_video.py +41 -0
  284. endoreg_db/models/label/label_video_segment/label_video_segment.py +511 -0
  285. endoreg_db/models/label/video_segmentation_label.py +31 -0
  286. endoreg_db/models/label/video_segmentation_labelset.py +27 -0
  287. endoreg_db/models/media/__init__.py +16 -0
  288. endoreg_db/models/media/frame/__init__.py +3 -0
  289. endoreg_db/models/media/frame/frame.py +111 -0
  290. endoreg_db/models/media/pdf/__init__.py +11 -0
  291. endoreg_db/models/media/pdf/raw_pdf.py +757 -0
  292. endoreg_db/models/media/pdf/report_file.py +162 -0
  293. endoreg_db/models/media/pdf/report_reader/__init__.py +7 -0
  294. endoreg_db/models/media/pdf/report_reader/report_reader_config.py +77 -0
  295. endoreg_db/models/media/video/__init__.py +8 -0
  296. endoreg_db/models/media/video/create_from_file.py +358 -0
  297. endoreg_db/models/media/video/pipe_1.py +213 -0
  298. endoreg_db/models/media/video/pipe_2.py +105 -0
  299. endoreg_db/models/media/video/refactor_plan.md +0 -0
  300. endoreg_db/models/media/video/video_file.py +825 -0
  301. endoreg_db/models/media/video/video_file_ai.py +443 -0
  302. endoreg_db/models/media/video/video_file_anonymize.py +349 -0
  303. endoreg_db/models/media/video/video_file_frames/__init__.py +47 -0
  304. endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +22 -0
  305. endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +23 -0
  306. endoreg_db/models/media/video/video_file_frames/_delete_frames.py +104 -0
  307. endoreg_db/models/media/video/video_file_frames/_extract_frames.py +174 -0
  308. endoreg_db/models/media/video/video_file_frames/_get_frame.py +28 -0
  309. endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +27 -0
  310. endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +20 -0
  311. endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +27 -0
  312. endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +34 -0
  313. endoreg_db/models/media/video/video_file_frames/_get_frames.py +27 -0
  314. endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +129 -0
  315. endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +141 -0
  316. endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +65 -0
  317. endoreg_db/models/media/video/video_file_frames.py +0 -0
  318. endoreg_db/models/media/video/video_file_io.py +168 -0
  319. endoreg_db/models/media/video/video_file_meta/__init__.py +22 -0
  320. endoreg_db/models/media/video/video_file_meta/get_crop_template.py +45 -0
  321. endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +39 -0
  322. endoreg_db/models/media/video/video_file_meta/get_fps.py +147 -0
  323. endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +143 -0
  324. endoreg_db/models/media/video/video_file_meta/text_meta.py +134 -0
  325. endoreg_db/models/media/video/video_file_meta/video_meta.py +70 -0
  326. endoreg_db/models/media/video/video_file_segments.py +209 -0
  327. endoreg_db/models/media/video/video_metadata.py +65 -0
  328. endoreg_db/models/media/video/video_processing.py +152 -0
  329. endoreg_db/models/medical/__init__.py +146 -0
  330. endoreg_db/models/medical/contraindication/__init__.py +17 -0
  331. endoreg_db/models/medical/disease.py +156 -0
  332. endoreg_db/models/medical/event.py +137 -0
  333. endoreg_db/models/medical/examination/__init__.py +9 -0
  334. endoreg_db/models/medical/examination/examination.py +148 -0
  335. endoreg_db/models/medical/examination/examination_indication.py +278 -0
  336. endoreg_db/models/medical/examination/examination_time.py +49 -0
  337. endoreg_db/models/medical/examination/examination_time_type.py +41 -0
  338. endoreg_db/models/medical/examination/examination_type.py +48 -0
  339. endoreg_db/models/medical/finding/__init__.py +18 -0
  340. endoreg_db/models/medical/finding/finding.py +96 -0
  341. endoreg_db/models/medical/finding/finding_classification.py +142 -0
  342. endoreg_db/models/medical/finding/finding_intervention.py +52 -0
  343. endoreg_db/models/medical/finding/finding_type.py +35 -0
  344. endoreg_db/models/medical/hardware/__init__.py +8 -0
  345. endoreg_db/models/medical/hardware/endoscope.py +65 -0
  346. endoreg_db/models/{hardware → medical/hardware}/endoscopy_processor.py +68 -29
  347. endoreg_db/models/medical/laboratory/__init__.py +5 -0
  348. endoreg_db/models/medical/laboratory/lab_value.py +419 -0
  349. endoreg_db/models/medical/medication/__init__.py +19 -0
  350. endoreg_db/models/medical/medication/medication.py +31 -0
  351. endoreg_db/models/medical/medication/medication_indication.py +50 -0
  352. endoreg_db/models/medical/medication/medication_indication_type.py +39 -0
  353. endoreg_db/models/medical/medication/medication_intake_time.py +44 -0
  354. endoreg_db/models/medical/medication/medication_schedule.py +45 -0
  355. endoreg_db/models/medical/organ/__init__.py +35 -0
  356. endoreg_db/models/medical/patient/__init__.py +56 -0
  357. endoreg_db/models/medical/patient/medication_examples.py +38 -0
  358. endoreg_db/models/medical/patient/patient_disease.py +63 -0
  359. endoreg_db/models/medical/patient/patient_event.py +75 -0
  360. endoreg_db/models/medical/patient/patient_examination.py +249 -0
  361. endoreg_db/models/medical/patient/patient_examination_indication.py +44 -0
  362. endoreg_db/models/medical/patient/patient_finding.py +357 -0
  363. endoreg_db/models/medical/patient/patient_finding_classification.py +207 -0
  364. endoreg_db/models/medical/patient/patient_finding_intervention.py +40 -0
  365. endoreg_db/models/medical/patient/patient_lab_sample.py +148 -0
  366. endoreg_db/models/{persons → medical}/patient/patient_lab_value.py +68 -22
  367. endoreg_db/models/medical/patient/patient_medication.py +104 -0
  368. endoreg_db/models/medical/patient/patient_medication_schedule.py +136 -0
  369. endoreg_db/models/medical/risk/__init__.py +7 -0
  370. endoreg_db/models/medical/risk/risk.py +72 -0
  371. endoreg_db/models/medical/risk/risk_type.py +51 -0
  372. endoreg_db/models/metadata/__init__.py +19 -0
  373. endoreg_db/models/metadata/frame_ocr_result.py +0 -0
  374. endoreg_db/models/metadata/model_meta.py +206 -0
  375. endoreg_db/models/metadata/model_meta_logic.py +343 -0
  376. endoreg_db/models/{data_file/metadata → metadata}/pdf_meta.py +32 -13
  377. endoreg_db/models/metadata/sensitive_meta.py +288 -0
  378. endoreg_db/models/metadata/sensitive_meta_logic.py +1048 -0
  379. endoreg_db/models/metadata/video_meta.py +332 -0
  380. endoreg_db/models/metadata/video_prediction_logic.py +190 -0
  381. endoreg_db/models/metadata/video_prediction_meta.py +270 -0
  382. endoreg_db/models/other/__init__.py +36 -1
  383. endoreg_db/models/other/distribution/__init__.py +44 -0
  384. endoreg_db/models/other/distribution/base_value_distribution.py +20 -0
  385. endoreg_db/models/other/distribution/date_value_distribution.py +89 -0
  386. endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +32 -0
  387. endoreg_db/models/other/distribution/numeric_value_distribution.py +125 -0
  388. endoreg_db/models/other/distribution/single_categorical_value_distribution.py +22 -0
  389. endoreg_db/models/other/emission/__init__.py +5 -0
  390. endoreg_db/models/other/emission/emission_factor.py +94 -0
  391. endoreg_db/models/{persons → other}/gender.py +8 -3
  392. endoreg_db/models/other/information_source.py +159 -0
  393. endoreg_db/models/other/material.py +14 -2
  394. endoreg_db/models/other/resource.py +6 -2
  395. endoreg_db/models/other/tag.py +27 -0
  396. endoreg_db/models/other/transport_route.py +15 -3
  397. endoreg_db/models/{unit.py → other/unit.py} +16 -6
  398. endoreg_db/models/other/waste.py +10 -3
  399. endoreg_db/models/requirement/__init__.py +11 -0
  400. endoreg_db/models/requirement/requirement.py +767 -0
  401. endoreg_db/models/requirement/requirement_evaluation/__init__.py +6 -0
  402. endoreg_db/models/requirement/requirement_evaluation/get_values.py +40 -0
  403. endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +9 -0
  404. endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +95 -0
  405. endoreg_db/models/requirement/requirement_operator.py +176 -0
  406. endoreg_db/models/requirement/requirement_set.py +287 -0
  407. endoreg_db/models/rule/__init__.py +13 -0
  408. endoreg_db/models/{rules → rule}/rule.py +6 -3
  409. endoreg_db/models/{rules → rule}/rule_attribute_dtype.py +0 -2
  410. endoreg_db/models/{rules → rule}/rule_type.py +0 -2
  411. endoreg_db/models/{rules → rule}/ruleset.py +0 -2
  412. endoreg_db/models/state/__init__.py +12 -0
  413. endoreg_db/models/state/abstract.py +11 -0
  414. endoreg_db/models/state/audit_ledger.py +150 -0
  415. endoreg_db/models/state/label_video_segment.py +22 -0
  416. endoreg_db/models/state/raw_pdf.py +187 -0
  417. endoreg_db/models/state/sensitive_meta.py +46 -0
  418. endoreg_db/models/state/video.py +232 -0
  419. endoreg_db/models/upload_job.py +99 -0
  420. endoreg_db/models/utils.py +135 -0
  421. endoreg_db/renames.yml +8 -0
  422. endoreg_db/root_urls.py +9 -0
  423. endoreg_db/schemas/__init__.py +0 -0
  424. endoreg_db/schemas/examination_evaluation.py +27 -0
  425. endoreg_db/serializers/Frames_NICE_and_PARIS_classifications.py +775 -0
  426. endoreg_db/serializers/__init__.py +118 -10
  427. endoreg_db/serializers/_old/raw_pdf_meta_validation.py +223 -0
  428. endoreg_db/serializers/_old/raw_video_meta_validation.py +179 -0
  429. endoreg_db/serializers/_old/video.py +71 -0
  430. endoreg_db/serializers/administration/__init__.py +14 -0
  431. endoreg_db/serializers/administration/ai/__init__.py +10 -0
  432. endoreg_db/serializers/administration/ai/active_model.py +10 -0
  433. endoreg_db/serializers/administration/ai/ai_model.py +18 -0
  434. endoreg_db/serializers/administration/ai/model_type.py +10 -0
  435. endoreg_db/serializers/administration/center.py +9 -0
  436. endoreg_db/serializers/administration/gender.py +9 -0
  437. endoreg_db/serializers/anonymization.py +69 -0
  438. endoreg_db/serializers/evaluation/examination_evaluation.py +1 -0
  439. endoreg_db/serializers/examination/__init__.py +10 -0
  440. endoreg_db/serializers/examination/base.py +46 -0
  441. endoreg_db/serializers/examination/dropdown.py +21 -0
  442. endoreg_db/serializers/examination_serializer.py +12 -0
  443. endoreg_db/serializers/finding/__init__.py +5 -0
  444. endoreg_db/serializers/finding/finding.py +54 -0
  445. endoreg_db/serializers/finding_classification/__init__.py +7 -0
  446. endoreg_db/serializers/finding_classification/choice.py +19 -0
  447. endoreg_db/serializers/finding_classification/classification.py +13 -0
  448. endoreg_db/serializers/label/__init__.py +7 -0
  449. endoreg_db/serializers/label/image_classification_annotation.py +62 -0
  450. endoreg_db/serializers/label/label.py +15 -0
  451. endoreg_db/serializers/label_video_segment/__init__.py +7 -0
  452. endoreg_db/serializers/label_video_segment/_lvs_create.py +149 -0
  453. endoreg_db/serializers/label_video_segment/_lvs_update.py +138 -0
  454. endoreg_db/serializers/label_video_segment/_lvs_validate.py +149 -0
  455. endoreg_db/serializers/label_video_segment/label_video_segment.py +344 -0
  456. endoreg_db/serializers/label_video_segment/label_video_segment_annotation.py +99 -0
  457. endoreg_db/serializers/label_video_segment/label_video_segment_update.py +163 -0
  458. endoreg_db/serializers/meta/__init__.py +19 -0
  459. endoreg_db/serializers/meta/pdf_file_meta_extraction.py +115 -0
  460. endoreg_db/serializers/meta/report_meta.py +53 -0
  461. endoreg_db/serializers/meta/sensitive_meta_detail.py +162 -0
  462. endoreg_db/serializers/meta/sensitive_meta_update.py +148 -0
  463. endoreg_db/serializers/meta/sensitive_meta_verification.py +59 -0
  464. endoreg_db/serializers/meta/video_meta.py +39 -0
  465. endoreg_db/serializers/misc/__init__.py +14 -0
  466. endoreg_db/serializers/misc/file_overview.py +182 -0
  467. endoreg_db/serializers/misc/sensitive_patient_data.py +120 -0
  468. endoreg_db/serializers/misc/stats.py +33 -0
  469. endoreg_db/serializers/misc/translatable_field_mix_in.py +44 -0
  470. endoreg_db/serializers/misc/upload_job.py +71 -0
  471. endoreg_db/serializers/patient/__init__.py +11 -0
  472. endoreg_db/serializers/patient/patient.py +86 -0
  473. endoreg_db/serializers/patient/patient_dropdown.py +27 -0
  474. endoreg_db/serializers/patient_examination/__init__.py +7 -0
  475. endoreg_db/serializers/patient_examination/patient_examination.py +141 -0
  476. endoreg_db/serializers/patient_finding/__init__.py +15 -0
  477. endoreg_db/serializers/patient_finding/patient_finding.py +31 -0
  478. endoreg_db/serializers/patient_finding/patient_finding_classification.py +39 -0
  479. endoreg_db/serializers/patient_finding/patient_finding_detail.py +53 -0
  480. endoreg_db/serializers/patient_finding/patient_finding_intervention.py +26 -0
  481. endoreg_db/serializers/patient_finding/patient_finding_list.py +41 -0
  482. endoreg_db/serializers/patient_finding/patient_finding_write.py +126 -0
  483. endoreg_db/serializers/pdf/__init__.py +5 -0
  484. endoreg_db/serializers/pdf/anony_text_validation.py +85 -0
  485. endoreg_db/serializers/report/__init__.py +9 -0
  486. endoreg_db/serializers/report/mixins.py +45 -0
  487. endoreg_db/serializers/report/report.py +105 -0
  488. endoreg_db/serializers/report/report_list.py +22 -0
  489. endoreg_db/serializers/report/secure_file_url.py +26 -0
  490. endoreg_db/serializers/requirements/requirement_schema.py +25 -0
  491. endoreg_db/serializers/requirements/requirement_sets.py +29 -0
  492. endoreg_db/serializers/sensitive_meta_serializer.py +282 -0
  493. endoreg_db/serializers/video/__init__.py +7 -0
  494. endoreg_db/serializers/video/segmentation.py +263 -0
  495. endoreg_db/serializers/video/video_file_brief.py +10 -0
  496. endoreg_db/serializers/video/video_file_detail.py +83 -0
  497. endoreg_db/serializers/video/video_file_list.py +67 -0
  498. endoreg_db/serializers/video/video_metadata.py +105 -0
  499. endoreg_db/serializers/video/video_processing_history.py +153 -0
  500. endoreg_db/serializers/video_examination.py +198 -0
  501. endoreg_db/services/__init__.py +5 -0
  502. endoreg_db/services/anonymization.py +223 -0
  503. endoreg_db/services/examination_evaluation.py +149 -0
  504. endoreg_db/services/finding_description_service.py +0 -0
  505. endoreg_db/services/lookup_service.py +411 -0
  506. endoreg_db/services/lookup_store.py +266 -0
  507. endoreg_db/services/pdf_import.py +1382 -0
  508. endoreg_db/services/polling_coordinator.py +288 -0
  509. endoreg_db/services/pseudonym_service.py +89 -0
  510. endoreg_db/services/requirements_object.py +147 -0
  511. endoreg_db/services/segment_sync.py +155 -0
  512. endoreg_db/services/storage_aware_video_processor.py +344 -0
  513. endoreg_db/services/video_import.py +1259 -0
  514. endoreg_db/tasks/upload_tasks.py +207 -0
  515. endoreg_db/tasks/video_ingest.py +157 -0
  516. endoreg_db/tasks/video_processing_tasks.py +327 -0
  517. endoreg_db/templates/admin/patient_finding_intervention.html +253 -0
  518. endoreg_db/templates/admin/start_examination.html +12 -0
  519. endoreg_db/templates/timeline.html +176 -0
  520. endoreg_db/urls/__init__.py +83 -0
  521. endoreg_db/urls/anonymization.py +32 -0
  522. endoreg_db/urls/auth.py +16 -0
  523. endoreg_db/urls/classification.py +39 -0
  524. endoreg_db/urls/examination.py +54 -0
  525. endoreg_db/urls/files.py +6 -0
  526. endoreg_db/urls/label_video_segment_validate.py +33 -0
  527. endoreg_db/urls/label_video_segments.py +46 -0
  528. endoreg_db/urls/media.py +227 -0
  529. endoreg_db/urls/patient.py +19 -0
  530. endoreg_db/urls/report.py +48 -0
  531. endoreg_db/urls/requirements.py +13 -0
  532. endoreg_db/urls/sensitive_meta.py +0 -0
  533. endoreg_db/urls/stats.py +46 -0
  534. endoreg_db/urls/upload.py +20 -0
  535. endoreg_db/urls/video.py +61 -0
  536. endoreg_db/urls.py +9 -0
  537. endoreg_db/utils/__init__.py +88 -1
  538. endoreg_db/utils/ai/__init__.py +9 -0
  539. endoreg_db/{models/ai_model/utils.py → utils/ai/get.py} +1 -4
  540. endoreg_db/utils/ai/inference_dataset.py +52 -0
  541. endoreg_db/utils/ai/multilabel_classification_net.py +159 -0
  542. endoreg_db/utils/ai/postprocess.py +63 -0
  543. endoreg_db/utils/ai/predict.py +291 -0
  544. endoreg_db/utils/ai/preprocess.py +68 -0
  545. endoreg_db/utils/calc_duration_seconds.py +24 -0
  546. endoreg_db/utils/case_generator/__init__.py +0 -0
  547. endoreg_db/utils/case_generator/case_generator.py +159 -0
  548. endoreg_db/utils/case_generator/lab_sample_factory.py +33 -0
  549. endoreg_db/utils/case_generator/utils.py +30 -0
  550. endoreg_db/utils/check_video_files.py +148 -0
  551. endoreg_db/utils/dataloader.py +118 -35
  552. endoreg_db/utils/dates.py +60 -0
  553. endoreg_db/utils/env.py +33 -0
  554. endoreg_db/utils/extract_specific_frames.py +72 -0
  555. endoreg_db/utils/file_operations.py +29 -1
  556. endoreg_db/utils/fix_video_path_direct.py +141 -0
  557. endoreg_db/utils/frame_anonymization_utils.py +463 -0
  558. endoreg_db/utils/hashs.py +123 -4
  559. endoreg_db/utils/links/__init__.py +0 -0
  560. endoreg_db/utils/links/requirement_link.py +193 -0
  561. endoreg_db/utils/mime_types.py +0 -0
  562. endoreg_db/utils/names.py +76 -0
  563. endoreg_db/utils/parse_and_generate_yaml.py +46 -0
  564. endoreg_db/utils/paths.py +95 -0
  565. endoreg_db/utils/permissions.py +143 -0
  566. endoreg_db/utils/pipelines/Readme.md +235 -0
  567. endoreg_db/utils/pipelines/__init__.py +0 -0
  568. endoreg_db/utils/pipelines/process_video_dir.py +120 -0
  569. endoreg_db/utils/product/__init__.py +0 -0
  570. endoreg_db/utils/product/sum_emissions.py +20 -0
  571. endoreg_db/utils/product/sum_weights.py +18 -0
  572. endoreg_db/utils/pydantic_models/__init__.py +6 -0
  573. endoreg_db/utils/pydantic_models/db_config.py +57 -0
  574. endoreg_db/utils/requirement_helpers.py +0 -0
  575. endoreg_db/utils/requirement_operator_logic/__init__.py +0 -0
  576. endoreg_db/utils/requirement_operator_logic/lab_value_operators.py +578 -0
  577. endoreg_db/utils/requirement_operator_logic/model_evaluators.py +368 -0
  578. endoreg_db/utils/setup_config.py +177 -0
  579. endoreg_db/utils/translation.py +27 -0
  580. endoreg_db/utils/validate_endo_roi.py +19 -0
  581. endoreg_db/utils/validate_subcategory_dict.py +91 -0
  582. endoreg_db/utils/validate_video_detailed.py +357 -0
  583. endoreg_db/utils/video/__init__.py +26 -0
  584. endoreg_db/utils/video/extract_frames.py +88 -0
  585. endoreg_db/utils/video/ffmpeg_wrapper.py +835 -0
  586. endoreg_db/utils/video/names.py +42 -0
  587. endoreg_db/utils/video/streaming_processor.py +312 -0
  588. endoreg_db/utils/video/video_splitter.py +94 -0
  589. endoreg_db/views/Frames_NICE_and_PARIS_classifications_views.py +238 -0
  590. endoreg_db/views/__init__.py +274 -0
  591. endoreg_db/views/anonymization/__init__.py +27 -0
  592. endoreg_db/views/anonymization/media_management.py +454 -0
  593. endoreg_db/views/anonymization/overview.py +216 -0
  594. endoreg_db/views/anonymization/validate.py +107 -0
  595. endoreg_db/views/auth/__init__.py +13 -0
  596. endoreg_db/views/auth/keycloak.py +113 -0
  597. endoreg_db/views/examination/__init__.py +33 -0
  598. endoreg_db/views/examination/examination.py +37 -0
  599. endoreg_db/views/examination/examination_manifest_cache.py +26 -0
  600. endoreg_db/views/examination/get_finding_classification_choices.py +59 -0
  601. endoreg_db/views/examination/get_finding_classifications.py +36 -0
  602. endoreg_db/views/examination/get_findings.py +41 -0
  603. endoreg_db/views/examination/get_instruments.py +18 -0
  604. endoreg_db/views/examination/get_interventions.py +14 -0
  605. endoreg_db/views/finding/__init__.py +9 -0
  606. endoreg_db/views/finding/finding.py +112 -0
  607. endoreg_db/views/finding/get_classifications.py +14 -0
  608. endoreg_db/views/finding/get_interventions.py +17 -0
  609. endoreg_db/views/finding_classification/__init__.py +13 -0
  610. endoreg_db/views/finding_classification/base.py +0 -0
  611. endoreg_db/views/finding_classification/finding_classification.py +42 -0
  612. endoreg_db/views/finding_classification/get_classification_choices.py +55 -0
  613. endoreg_db/views/label/__init__.py +5 -0
  614. endoreg_db/views/label/label.py +15 -0
  615. endoreg_db/views/label_video_segment/__init__.py +16 -0
  616. endoreg_db/views/label_video_segment/create_lvs_from_annotation.py +44 -0
  617. endoreg_db/views/label_video_segment/get_lvs_by_name_and_video.py +50 -0
  618. endoreg_db/views/label_video_segment/label_video_segment.py +77 -0
  619. endoreg_db/views/label_video_segment/label_video_segment_by_label.py +174 -0
  620. endoreg_db/views/label_video_segment/label_video_segment_detail.py +73 -0
  621. endoreg_db/views/label_video_segment/update_lvs_from_annotation.py +46 -0
  622. endoreg_db/views/label_video_segment/validate.py +226 -0
  623. endoreg_db/views/media/__init__.py +45 -0
  624. endoreg_db/views/media/pdf_media.py +388 -0
  625. endoreg_db/views/media/segments.py +71 -0
  626. endoreg_db/views/media/sensitive_metadata.py +314 -0
  627. endoreg_db/views/media/video_media.py +272 -0
  628. endoreg_db/views/media/video_segments.py +524 -0
  629. endoreg_db/views/meta/__init__.py +15 -0
  630. endoreg_db/views/meta/available_files_list.py +146 -0
  631. endoreg_db/views/meta/report_meta.py +53 -0
  632. endoreg_db/views/meta/sensitive_meta_detail.py +148 -0
  633. endoreg_db/views/meta/sensitive_meta_list.py +104 -0
  634. endoreg_db/views/meta/sensitive_meta_verification.py +71 -0
  635. endoreg_db/views/misc/__init__.py +63 -0
  636. endoreg_db/views/misc/center.py +13 -0
  637. endoreg_db/views/misc/csrf.py +7 -0
  638. endoreg_db/views/misc/gender.py +14 -0
  639. endoreg_db/views/misc/secure_file_serving_view.py +80 -0
  640. endoreg_db/views/misc/secure_file_url_view.py +84 -0
  641. endoreg_db/views/misc/secure_url_validate.py +79 -0
  642. endoreg_db/views/misc/stats.py +220 -0
  643. endoreg_db/views/misc/translation.py +182 -0
  644. endoreg_db/views/misc/upload_views.py +240 -0
  645. endoreg_db/views/patient/__init__.py +5 -0
  646. endoreg_db/views/patient/patient.py +210 -0
  647. endoreg_db/views/patient_examination/DEPRECATED_video_backup.py +164 -0
  648. endoreg_db/views/patient_examination/__init__.py +11 -0
  649. endoreg_db/views/patient_examination/patient_examination.py +140 -0
  650. endoreg_db/views/patient_examination/patient_examination_create.py +63 -0
  651. endoreg_db/views/patient_examination/patient_examination_detail.py +66 -0
  652. endoreg_db/views/patient_examination/patient_examination_list.py +68 -0
  653. endoreg_db/views/patient_examination/video.py +194 -0
  654. endoreg_db/views/patient_finding/__init__.py +7 -0
  655. endoreg_db/views/patient_finding/base.py +0 -0
  656. endoreg_db/views/patient_finding/patient_finding.py +64 -0
  657. endoreg_db/views/patient_finding/patient_finding_optimized.py +259 -0
  658. endoreg_db/views/patient_finding_classification/__init__.py +5 -0
  659. endoreg_db/views/patient_finding_classification/pfc_create.py +67 -0
  660. endoreg_db/views/patient_finding_location/__init__.py +5 -0
  661. endoreg_db/views/patient_finding_location/pfl_create.py +70 -0
  662. endoreg_db/views/patient_finding_morphology/__init__.py +5 -0
  663. endoreg_db/views/patient_finding_morphology/pfm_create.py +70 -0
  664. endoreg_db/views/pdf/__init__.py +8 -0
  665. endoreg_db/views/pdf/pdf_stream.py +187 -0
  666. endoreg_db/views/pdf/reimport.py +177 -0
  667. endoreg_db/views/report/__init__.py +9 -0
  668. endoreg_db/views/report/report_list.py +112 -0
  669. endoreg_db/views/report/report_with_secure_url.py +28 -0
  670. endoreg_db/views/report/start_examination.py +7 -0
  671. endoreg_db/views/requirement/__init__.py +10 -0
  672. endoreg_db/views/requirement/evaluate.py +279 -0
  673. endoreg_db/views/requirement/lookup.py +367 -0
  674. endoreg_db/views/requirement/lookup_store.py +252 -0
  675. endoreg_db/views/requirement_lookup/lookup.py +0 -0
  676. endoreg_db/views/requirement_lookup/lookup_store.py +0 -0
  677. endoreg_db/views/stats/__init__.py +13 -0
  678. endoreg_db/views/stats/stats_views.py +229 -0
  679. endoreg_db/views/video/__init__.py +59 -0
  680. endoreg_db/views/video/correction.py +530 -0
  681. endoreg_db/views/video/reimport.py +195 -0
  682. endoreg_db/views/video/segmentation.py +274 -0
  683. endoreg_db/views/video/task_status.py +49 -0
  684. endoreg_db/views/video/timeline.py +46 -0
  685. endoreg_db/views/video/video_analyze.py +52 -0
  686. endoreg_db/views/video/video_apply_mask.py +48 -0
  687. endoreg_db/views/video/video_correction.py +21 -0
  688. endoreg_db/views/video/video_download_processed.py +58 -0
  689. endoreg_db/views/video/video_examination_viewset.py +242 -0
  690. endoreg_db/views/video/video_meta.py +29 -0
  691. endoreg_db/views/video/video_processing_history.py +24 -0
  692. endoreg_db/views/video/video_remove_frames.py +48 -0
  693. endoreg_db/views/video/video_stream.py +306 -0
  694. endoreg_db/views.py +0 -3
  695. endoreg_db-0.8.6.3.dist-info/METADATA +383 -0
  696. endoreg_db-0.8.6.3.dist-info/RECORD +793 -0
  697. {endoreg_db-0.4.5.dist-info → endoreg_db-0.8.6.3.dist-info}/WHEEL +1 -1
  698. endoreg_db/data/active_model/data.yaml +0 -3
  699. endoreg_db/data/agl_service/data.yaml +0 -19
  700. endoreg_db/data/label/label-set/data.yaml +0 -18
  701. endoreg_db/management/commands/_load_model_template.py +0 -41
  702. endoreg_db/management/commands/delete_all.py +0 -18
  703. endoreg_db/management/commands/delete_legacy_images.py +0 -19
  704. endoreg_db/management/commands/delete_legacy_videos.py +0 -17
  705. endoreg_db/management/commands/extract_legacy_video_frames.py +0 -18
  706. endoreg_db/management/commands/fetch_legacy_image_dataset.py +0 -32
  707. endoreg_db/management/commands/fix_auth_permission.py +0 -20
  708. endoreg_db/management/commands/import_legacy_images.py +0 -94
  709. endoreg_db/management/commands/import_legacy_videos.py +0 -76
  710. endoreg_db/management/commands/load_active_model_data.py +0 -45
  711. endoreg_db/management/commands/load_endoscopy_processor_data.py +0 -45
  712. endoreg_db/management/commands/load_g_play_data.py +0 -113
  713. endoreg_db/management/commands/load_label_data.py +0 -67
  714. endoreg_db/management/commands/load_medication_indication_data.py +0 -63
  715. endoreg_db/management/commands/load_medication_indication_type_data.py +0 -41
  716. endoreg_db/management/commands/load_medication_schedule_data.py +0 -55
  717. endoreg_db/management/commands/load_network_data.py +0 -57
  718. endoreg_db/migrations/0002_anonymizedimagelabel_anonymousimageannotation_and_more.py +0 -55
  719. endoreg_db/migrations/0003_anonymousimageannotation_original_image_url_and_more.py +0 -39
  720. endoreg_db/migrations/0004_alter_rawpdffile_file.py +0 -20
  721. endoreg_db/migrations/0005_uploadedfile_alter_rawpdffile_file_anonymizedfile.py +0 -40
  722. endoreg_db/migrations/0006_alter_rawpdffile_file.py +0 -20
  723. endoreg_db/migrations/0007_networkdevicelogentry_datetime_and_more.py +0 -43
  724. endoreg_db/models/ai_model/__init__.py +0 -3
  725. endoreg_db/models/ai_model/active_model.py +0 -9
  726. endoreg_db/models/ai_model/model_meta.py +0 -24
  727. endoreg_db/models/annotation/__init__.py +0 -3
  728. endoreg_db/models/annotation/anonymized_image_annotation.py +0 -60
  729. endoreg_db/models/annotation/binary_classification_annotation_task.py +0 -80
  730. endoreg_db/models/annotation/image_classification.py +0 -27
  731. endoreg_db/models/center/center.py +0 -25
  732. endoreg_db/models/center/center_product.py +0 -34
  733. endoreg_db/models/center/center_resource.py +0 -19
  734. endoreg_db/models/center/center_waste.py +0 -11
  735. endoreg_db/models/data_file/__init__.py +0 -6
  736. endoreg_db/models/data_file/base_classes/__init__.py +0 -2
  737. endoreg_db/models/data_file/base_classes/abstract_frame.py +0 -51
  738. endoreg_db/models/data_file/base_classes/abstract_video.py +0 -201
  739. endoreg_db/models/data_file/frame.py +0 -45
  740. endoreg_db/models/data_file/import_classes/__init__.py +0 -32
  741. endoreg_db/models/data_file/import_classes/processing_functions/__init__.py +0 -35
  742. endoreg_db/models/data_file/import_classes/processing_functions/pdf.py +0 -28
  743. endoreg_db/models/data_file/import_classes/processing_functions/video.py +0 -260
  744. endoreg_db/models/data_file/import_classes/raw_pdf.py +0 -188
  745. endoreg_db/models/data_file/import_classes/raw_video.py +0 -343
  746. endoreg_db/models/data_file/metadata/__init__.py +0 -3
  747. endoreg_db/models/data_file/metadata/sensitive_meta.py +0 -31
  748. endoreg_db/models/data_file/metadata/video_meta.py +0 -133
  749. endoreg_db/models/data_file/report_file.py +0 -89
  750. endoreg_db/models/data_file/video/__init__.py +0 -7
  751. endoreg_db/models/data_file/video/import_meta.py +0 -25
  752. endoreg_db/models/data_file/video/video.py +0 -25
  753. endoreg_db/models/data_file/video_segment.py +0 -107
  754. endoreg_db/models/disease.py +0 -56
  755. endoreg_db/models/emission/__init__.py +0 -1
  756. endoreg_db/models/emission/emission_factor.py +0 -20
  757. endoreg_db/models/event.py +0 -22
  758. endoreg_db/models/examination/__init__.py +0 -4
  759. endoreg_db/models/examination/examination.py +0 -26
  760. endoreg_db/models/examination/examination_time.py +0 -27
  761. endoreg_db/models/examination/examination_time_type.py +0 -24
  762. endoreg_db/models/examination/examination_type.py +0 -18
  763. endoreg_db/models/hardware/__init__.py +0 -2
  764. endoreg_db/models/hardware/endoscope.py +0 -44
  765. endoreg_db/models/information_source.py +0 -29
  766. endoreg_db/models/laboratory/__init__.py +0 -1
  767. endoreg_db/models/laboratory/lab_value.py +0 -102
  768. endoreg_db/models/legacy_data/__init__.py +0 -3
  769. endoreg_db/models/legacy_data/image.py +0 -34
  770. endoreg_db/models/logging/__init__.py +0 -4
  771. endoreg_db/models/logging/agl_service.py +0 -19
  772. endoreg_db/models/logging/base.py +0 -22
  773. endoreg_db/models/logging/log_type.py +0 -23
  774. endoreg_db/models/logging/network_device.py +0 -24
  775. endoreg_db/models/medication/__init__.py +0 -1
  776. endoreg_db/models/medication/medication.py +0 -148
  777. endoreg_db/models/network/__init__.py +0 -3
  778. endoreg_db/models/network/agl_service.py +0 -38
  779. endoreg_db/models/network/network_device.py +0 -53
  780. endoreg_db/models/network/network_device_type.py +0 -23
  781. endoreg_db/models/other/distribution.py +0 -215
  782. endoreg_db/models/patient_examination/__init__.py +0 -35
  783. endoreg_db/models/permissions/__init__.py +0 -44
  784. endoreg_db/models/persons/__init__.py +0 -7
  785. endoreg_db/models/persons/examiner/__init__.py +0 -2
  786. endoreg_db/models/persons/examiner/examiner.py +0 -16
  787. endoreg_db/models/persons/examiner/examiner_type.py +0 -2
  788. endoreg_db/models/persons/patient/__init__.py +0 -8
  789. endoreg_db/models/persons/patient/case/case.py +0 -30
  790. endoreg_db/models/persons/patient/patient.py +0 -216
  791. endoreg_db/models/persons/patient/patient_disease.py +0 -16
  792. endoreg_db/models/persons/patient/patient_event.py +0 -22
  793. endoreg_db/models/persons/patient/patient_lab_sample.py +0 -106
  794. endoreg_db/models/persons/patient/patient_medication.py +0 -44
  795. endoreg_db/models/persons/patient/patient_medication_schedule.py +0 -28
  796. endoreg_db/models/persons/portal_user_information.py +0 -27
  797. endoreg_db/models/prediction/__init__.py +0 -2
  798. endoreg_db/models/prediction/image_classification.py +0 -37
  799. endoreg_db/models/prediction/video_prediction_meta.py +0 -244
  800. endoreg_db/models/product/__init__.py +0 -5
  801. endoreg_db/models/product/product.py +0 -97
  802. endoreg_db/models/product/product_group.py +0 -19
  803. endoreg_db/models/product/product_material.py +0 -24
  804. endoreg_db/models/questionnaires/__init__.py +0 -114
  805. endoreg_db/models/quiz/__init__.py +0 -2
  806. endoreg_db/models/quiz/quiz_answer.py +0 -41
  807. endoreg_db/models/quiz/quiz_question.py +0 -54
  808. endoreg_db/models/report_reader/__init__.py +0 -2
  809. endoreg_db/models/report_reader/report_reader_config.py +0 -53
  810. endoreg_db/models/rules/__init__.py +0 -5
  811. endoreg_db/queries/get/__init__.py +0 -6
  812. endoreg_db/queries/get/center.py +0 -42
  813. endoreg_db/queries/get/model.py +0 -13
  814. endoreg_db/queries/get/patient.py +0 -14
  815. endoreg_db/queries/get/patient_examination.py +0 -20
  816. endoreg_db/queries/get/report_file.py +0 -33
  817. endoreg_db/queries/get/video.py +0 -31
  818. endoreg_db/serializers/ai_model.py +0 -19
  819. endoreg_db/serializers/annotation.py +0 -17
  820. endoreg_db/serializers/center.py +0 -11
  821. endoreg_db/serializers/examination.py +0 -33
  822. endoreg_db/serializers/frame.py +0 -13
  823. endoreg_db/serializers/hardware.py +0 -21
  824. endoreg_db/serializers/label.py +0 -22
  825. endoreg_db/serializers/patient.py +0 -10
  826. endoreg_db/serializers/prediction.py +0 -15
  827. endoreg_db/serializers/report_file.py +0 -7
  828. endoreg_db/serializers/video.py +0 -27
  829. endoreg_db/tests.py +0 -3
  830. endoreg_db/utils/legacy_ocr.py +0 -201
  831. endoreg_db/utils/video_metadata.py +0 -87
  832. endoreg_db-0.4.5.dist-info/METADATA +0 -34
  833. endoreg_db-0.4.5.dist-info/RECORD +0 -316
  834. /endoreg_db/{data/distribution/numeric/.init → api/serializers/finding_descriptions.py} +0 -0
  835. /endoreg_db/{models/persons/patient/case/__init__.py → api/views/finding_descriptions.py} +0 -0
  836. /endoreg_db/{queries/get/annotation.py → config/__init__.py} +0 -0
  837. /endoreg_db/data/{label → ai_model_label}/label-type/data.yaml +0 -0
  838. /endoreg_db/data/{model_type → ai_model_type}/data.yaml +0 -0
  839. /endoreg_db/{queries/get/prediction.py → data/shift/m2.yaml} +0 -0
  840. /endoreg_db/{queries/get/video_import_meta.py → factories/__init__.py} +0 -0
  841. /endoreg_db/{queries/get/video_prediction_meta.py → helpers/__init__.py} +0 -0
  842. /endoreg_db/management/commands/{load_report_reader_flag.py → load_report_reader_flag_data.py} +0 -0
  843. /endoreg_db/models/{persons → administration/person}/person.py +0 -0
  844. /endoreg_db/models/{report_reader → media/pdf/report_reader}/report_reader_flag.py +0 -0
  845. /endoreg_db/models/{rules → rule}/rule_applicator.py +0 -0
  846. {endoreg_db-0.4.5.dist-info → endoreg_db-0.8.6.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,1259 @@
1
+ """
2
+ Video import service module.
3
+
4
+ Provides high-level functions for importing and anonymizing video files,
5
+ combining VideoFile creation with frame-level anonymization.
6
+
7
+ Changelog:
8
+ October 14, 2025: Added file locking mechanism to prevent race conditions
9
+ during concurrent video imports (matches PDF import pattern)
10
+ """
11
+
12
+ import logging
13
+ import os
14
+ import random
15
+ import shutil
16
+ import sys
17
+ import time
18
+ from contextlib import contextmanager
19
+ from datetime import date
20
+ from pathlib import Path
21
+ from typing import Any, Dict, List, Optional, Tuple, Union
22
+
23
+ from django.db import transaction
24
+ from django.db.models.fields.files import FieldFile
25
+ from lx_anonymizer import FrameCleaner
26
+ from moviepy import video
27
+
28
+ from endoreg_db.models import EndoscopyProcessor, SensitiveMeta, VideoFile
29
+ from endoreg_db.models.media.video.video_file_anonymize import _cleanup_raw_assets
30
+ from endoreg_db.utils.hashs import get_video_hash
31
+ from endoreg_db.utils.paths import ANONYM_VIDEO_DIR, STORAGE_DIR, VIDEO_DIR
32
+
33
+ # File lock configuration (matches PDF import)
34
+ 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
+ )
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class VideoImportService:
43
+ """
44
+ Service for importing and anonymizing video files.
45
+ Uses a central video instance pattern for cleaner state management.
46
+
47
+ Features (October 14, 2025):
48
+ - File locking to prevent concurrent processing of the same video
49
+ - Stale lock detection and reclamation (600s timeout)
50
+ - Hash-based duplicate detection
51
+ - Graceful fallback processing without lx_anonymizer
52
+ """
53
+
54
+ def __init__(self, project_root: Optional[Path] = None):
55
+ # Set up project root path
56
+ if project_root:
57
+ self.project_root = Path(project_root)
58
+ else:
59
+ self.project_root = Path(__file__).parent.parent.parent.parent
60
+
61
+ # Track processed files to prevent duplicates
62
+ try:
63
+ # Ensure anonym_video directory exists before listing files
64
+ anonym_video_dir = Path(ANONYM_VIDEO_DIR)
65
+ 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
+ )
70
+ else:
71
+ logger.info(f"Creating anonym_videos directory: {anonym_video_dir}")
72
+ anonym_video_dir.mkdir(parents=True, exist_ok=True)
73
+ self.processed_files = set()
74
+ except Exception as e:
75
+ logger.warning(f"Failed to initialize processed files tracking: {e}")
76
+ self.processed_files = set()
77
+
78
+ # Central video instance and processing context
79
+ self.current_video: Optional[VideoFile] = None
80
+ self.processing_context: Dict[str, Any] = {}
81
+
82
+ self.delete_source = True
83
+
84
+ self.logger = logging.getLogger(__name__)
85
+
86
+ self.cleaner = (
87
+ None # This gets instantiated in the perform_frame_cleaning method
88
+ )
89
+
90
+ def _require_current_video(self) -> VideoFile:
91
+ """Return the current VideoFile or raise if it has not been initialized."""
92
+ if self.current_video is None:
93
+ raise RuntimeError("Current video instance is not set")
94
+ return self.current_video
95
+
96
+ @contextmanager
97
+ def _file_lock(self, path: Path):
98
+ """
99
+ Create a file lock to prevent duplicate processing of the same video.
100
+
101
+ This context manager creates a .lock file alongside the video file.
102
+ If the lock file already exists, it checks if it's stale (older than
103
+ STALE_LOCK_SECONDS) and reclaims it if necessary. If it's not stale,
104
+ we now WAIT (up to MAX_LOCK_WAIT_SECONDS) instead of failing immediately.
105
+ """
106
+ lock_path = Path(str(path) + ".lock")
107
+ fd = None
108
+ try:
109
+ deadline = time.time() + MAX_LOCK_WAIT_SECONDS
110
+ while True:
111
+ try:
112
+ # Atomic create; fail if exists
113
+ fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
114
+ break # acquired
115
+ except FileExistsError:
116
+ # Check for stale lock
117
+ age = None
118
+ try:
119
+ st = os.stat(lock_path)
120
+ age = time.time() - st.st_mtime
121
+ except FileNotFoundError:
122
+ # Race: lock removed between exists and stat; retry acquire in next loop
123
+ age = None
124
+
125
+ if age is not None and age > STALE_LOCK_SECONDS:
126
+ try:
127
+ logger.warning(
128
+ "Stale lock detected for %s (age %.0fs). Reclaiming lock...",
129
+ path,
130
+ age,
131
+ )
132
+ lock_path.unlink()
133
+ except Exception as e:
134
+ logger.warning(
135
+ "Failed to remove stale lock %s: %s", lock_path, e
136
+ )
137
+ # Loop continues and retries acquire immediately
138
+ continue
139
+
140
+ # Not stale: wait until deadline, then give up gracefully
141
+ if time.time() >= deadline:
142
+ raise ValueError(f"File already being processed: {path}")
143
+ time.sleep(1.0)
144
+
145
+ os.write(fd, b"lock")
146
+ os.close(fd)
147
+ fd = None
148
+ yield
149
+ finally:
150
+ try:
151
+ if fd is not None:
152
+ os.close(fd)
153
+ if lock_path.exists():
154
+ lock_path.unlink()
155
+ except OSError:
156
+ pass
157
+
158
+ def processed(self) -> bool:
159
+ """Indicates if the current file has already been processed."""
160
+ return getattr(self, "_processed", False)
161
+
162
+ def import_and_anonymize(
163
+ self,
164
+ file_path: Union[Path, str],
165
+ center_name: str,
166
+ processor_name: str,
167
+ save_video: bool = True,
168
+ delete_source: bool = True,
169
+ ) -> "VideoFile|None":
170
+ """
171
+ High-level helper that orchestrates the complete video import and anonymization process.
172
+ Uses the central video instance pattern for improved state management.
173
+ """
174
+ # DEFENSIVE: Initialize processing_context immediately to prevent KeyError crashes
175
+ self.processing_context = {"file_path": Path(file_path)}
176
+
177
+ try:
178
+ # Initialize processing context
179
+ self._initialize_processing_context(
180
+ file_path, center_name, processor_name, save_video, delete_source
181
+ )
182
+
183
+ # Validate and prepare file (may raise ValueError if another worker holds a non-stale lock)
184
+ try:
185
+ self._validate_and_prepare_file()
186
+ except ValueError as ve:
187
+ # Relaxed behavior: if another process is working on this file, skip cleanly
188
+ if "already being processed" in str(ve):
189
+ self.logger.info(f"Skipping {file_path}: {ve}")
190
+ return None
191
+ raise
192
+
193
+ # Create or retrieve video instance
194
+ self._create_or_retrieve_video_instance()
195
+
196
+ # Create sensitive meta file, ensure raw is moved out of processing folder watched by file watcher.
197
+ self._create_sensitive_file()
198
+
199
+ # Setup processing environment
200
+ self._setup_processing_environment()
201
+
202
+ # Process frames and metadata
203
+ self._process_frames_and_metadata()
204
+
205
+ # Finalize processing
206
+ self._finalize_processing()
207
+
208
+ # Move files and cleanup
209
+ self._cleanup_and_archive()
210
+
211
+ return self.current_video
212
+
213
+ except Exception as e:
214
+ # 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
+ )
218
+ # Debug: Log context state for troubleshooting
219
+ context_keys = list(getattr(self, "processing_context", {}).keys())
220
+ 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
+ )
224
+ self._cleanup_on_error()
225
+ raise
226
+ finally:
227
+ self._cleanup_processing_context()
228
+
229
+ def _initialize_processing_context(
230
+ self,
231
+ file_path: Union[Path, str],
232
+ center_name: str,
233
+ processor_name: str,
234
+ save_video: bool,
235
+ delete_source: bool,
236
+ ):
237
+ """Initialize the processing context for the current video import."""
238
+ self.processing_context = {
239
+ "file_path": Path(file_path),
240
+ "center_name": center_name,
241
+ "processor_name": processor_name,
242
+ "save_video": save_video,
243
+ "delete_source": delete_source,
244
+ "processing_started": False,
245
+ "frames_extracted": False,
246
+ "anonymization_completed": False,
247
+ "error_reason": None,
248
+ }
249
+
250
+ self.logger.info(f"Initialized processing context for: {file_path}")
251
+
252
+ def _validate_and_prepare_file(self):
253
+ """
254
+ Validate the video file and prepare for processing.
255
+
256
+ Uses file locking to prevent concurrent processing of the same video file.
257
+ This prevents race conditions where multiple workers might try to process
258
+ the same video simultaneously.
259
+
260
+ The lock is acquired here and held for the entire import process.
261
+ See _file_lock() for lock reclamation logic.
262
+ """
263
+ file_path = self.processing_context["file_path"]
264
+
265
+ # Acquire file lock to prevent concurrent processing
266
+ # Lock will be held until finally block in import_and_anonymize()
267
+ try:
268
+ self.processing_context["_lock_context"] = self._file_lock(file_path)
269
+ self.processing_context["_lock_context"].__enter__()
270
+ except Exception:
271
+ self._cleanup_processing_context()
272
+ raise
273
+
274
+ self.logger.info("Acquired file lock for: %s", file_path)
275
+
276
+ # Check if already processed (memory-based check)
277
+ if str(file_path) in self.processed_files:
278
+ self.logger.info("File %s already processed, skipping", file_path)
279
+ self._processed = True
280
+ raise ValueError(f"File already processed: {file_path}")
281
+
282
+ # Check file exists
283
+ if not file_path.exists():
284
+ raise FileNotFoundError(f"Video file not found: {file_path}")
285
+
286
+ self.logger.info("File validation completed for: %s", file_path)
287
+
288
+ def _create_or_retrieve_video_instance(self):
289
+ """Create or retrieve the VideoFile instance and move to final storage."""
290
+
291
+ self.logger.info("Creating VideoFile instance...")
292
+
293
+ self.current_video = VideoFile.create_from_file_initialized(
294
+ file_path=self.processing_context["file_path"],
295
+ center_name=self.processing_context["center_name"],
296
+ processor_name=self.processing_context["processor_name"],
297
+ delete_source=self.processing_context["delete_source"],
298
+ save_video_file=self.processing_context["save_video"],
299
+ )
300
+
301
+ if not self.current_video:
302
+ raise RuntimeError("Failed to create VideoFile instance")
303
+
304
+ # Immediately move to final storage locations
305
+ self._move_to_final_storage()
306
+
307
+ self.logger.info("Created VideoFile with UUID: %s", self.current_video.uuid)
308
+
309
+ # Get and mark processing state
310
+ state = VideoFile.get_or_create_state(self.current_video)
311
+ if not state:
312
+ raise RuntimeError("Failed to create VideoFile state")
313
+
314
+ state.mark_processing_started(save=True)
315
+ self.processing_context["processing_started"] = True
316
+
317
+ def _move_to_final_storage(self):
318
+ """
319
+ Move video from raw_videos to final storage locations.
320
+ - Raw video → /data/videos (raw_file_path)
321
+ - Processed video will later → /data/anonym_videos (file_path)
322
+ """
323
+ from endoreg_db.utils import data_paths
324
+
325
+ source_path = Path(self.processing_context["file_path"])
326
+ _current_video = self._require_current_video()
327
+ videos_dir = Path(data_paths["video"])
328
+ storage_root = Path(data_paths["storage"])
329
+
330
+ videos_dir.mkdir(parents=True, exist_ok=True)
331
+
332
+ # --- Derive stored_raw_path safely ---
333
+ stored_raw_path = None
334
+ try:
335
+ if hasattr(_current_video, "get_raw_file_path"):
336
+ candidate = _current_video.get_raw_file_path()
337
+ if candidate:
338
+ candidate_path = Path(candidate)
339
+ # Accept only if under storage_root
340
+ try:
341
+ candidate_path.relative_to(storage_root)
342
+ stored_raw_path = candidate_path
343
+ except ValueError:
344
+ # outside storage_root, reset
345
+ stored_raw_path = None
346
+ except Exception:
347
+ stored_raw_path = None
348
+
349
+ # Fallback: derive from UUID + suffix - ALWAYS use UUID for consistency
350
+ if not stored_raw_path:
351
+ suffix = source_path.suffix or ".mp4"
352
+ uuid_str = getattr(_current_video, "uuid", None)
353
+ if uuid_str:
354
+ filename = f"{uuid_str}{suffix}"
355
+ else:
356
+ # Emergency fallback with timestamp to avoid conflicts
357
+ import time
358
+
359
+ timestamp = int(time.time())
360
+ filename = f"video_{timestamp}{suffix}"
361
+ self.logger.warning(
362
+ "No UUID available, using timestamp-based filename: %s", filename
363
+ )
364
+ stored_raw_path = videos_dir / filename
365
+ self.logger.debug("Using UUID-based raw filename: %s", filename)
366
+
367
+ delete_source = bool(self.processing_context.get("delete_source", True))
368
+ stored_raw_path.parent.mkdir(parents=True, exist_ok=True)
369
+
370
+ # --- Move or copy raw video ---
371
+ try:
372
+ if delete_source:
373
+ # Try atomic move first, fallback to copy+unlink
374
+ try:
375
+ os.replace(source_path, stored_raw_path)
376
+ self.logger.info("Moved raw video to: %s", stored_raw_path)
377
+ except Exception:
378
+ shutil.copy2(source_path, stored_raw_path)
379
+ os.remove(source_path)
380
+ self.logger.info(
381
+ "Copied & removed raw video to: %s", stored_raw_path
382
+ )
383
+ else:
384
+ shutil.copy2(source_path, stored_raw_path)
385
+ self.logger.info("Copied raw video to: %s", stored_raw_path)
386
+ except Exception as e:
387
+ self.logger.error("Failed to move/copy video to final storage: %s", e)
388
+ raise
389
+
390
+ # --- Ensure DB raw_file is relative to storage root ---
391
+ try:
392
+ rel_path = stored_raw_path.relative_to(storage_root)
393
+ except Exception:
394
+ rel_path = Path("videos") / stored_raw_path.name
395
+
396
+ if _current_video.raw_file.name != rel_path.as_posix():
397
+ _current_video.raw_file.name = rel_path.as_posix()
398
+ _current_video.save(update_fields=["raw_file"])
399
+ self.logger.info("Updated raw_file path to: %s", rel_path.as_posix())
400
+
401
+ # --- Store for later stages ---
402
+ self.processing_context["raw_video_path"] = stored_raw_path
403
+ self.processing_context["video_filename"] = stored_raw_path.name
404
+
405
+ def _setup_processing_environment(self):
406
+ """Setup the processing environment without file movement."""
407
+ video = self._require_current_video()
408
+
409
+ # Initialize video specifications
410
+ video.initialize_video_specs()
411
+
412
+
413
+
414
+ # Extract frames BEFORE processing to prevent pipeline 1 conflicts
415
+ self.logger.info("Pre-extracting frames to avoid pipeline conflicts...")
416
+ try:
417
+ frames_extracted = video.extract_frames(overwrite=False)
418
+ if frames_extracted:
419
+ self.processing_context["frames_extracted"] = True
420
+ self.logger.info("Frame extraction completed successfully")
421
+ # Initialize frame objects in database
422
+ video.initialize_frames(video.get_frame_paths())
423
+
424
+ # CRITICAL: Immediately save the frames_extracted state to database
425
+ # to prevent refresh_from_db() in pipeline 1 from overriding it
426
+ state = video.get_or_create_state()
427
+ if not state.frames_extracted:
428
+ state.frames_extracted = True
429
+ state.save(update_fields=["frames_extracted"])
430
+ self.logger.info("Persisted frames_extracted=True to database")
431
+ else:
432
+ self.logger.warning("Frame extraction failed, but continuing...")
433
+ self.processing_context["frames_extracted"] = False
434
+ except Exception as e:
435
+ self.logger.warning(
436
+ f"Frame extraction failed during setup: {e}, but continuing..."
437
+ )
438
+ self.processing_context["frames_extracted"] = False
439
+
440
+ # Ensure default patient data
441
+ self._ensure_default_patient_data(video_instance=video)
442
+
443
+ self.logger.info("Processing environment setup completed")
444
+
445
+ def _process_frames_and_metadata(self):
446
+ """Process frames and extract metadata with anonymization."""
447
+ # Check frame cleaning availability
448
+ frame_cleaning_available, frame_cleaner = (
449
+ self._ensure_frame_cleaning_available()
450
+ )
451
+ video = self._require_current_video()
452
+
453
+ raw_file_field = video.raw_file
454
+ has_raw_file = isinstance(raw_file_field, FieldFile) and bool(
455
+ raw_file_field.name
456
+ )
457
+
458
+ 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
+ )
462
+ self._fallback_anonymize_video()
463
+ return
464
+
465
+ try:
466
+ self.logger.info(
467
+ "Starting frame-level anonymization with processor ROI masking..."
468
+ )
469
+
470
+ # Get processor ROI information
471
+ endoscope_data_roi_nested, endoscope_image_roi = (
472
+ self._get_processor_roi_info()
473
+ )
474
+
475
+ # Perform frame cleaning with timeout to prevent blocking
476
+ from concurrent.futures import ThreadPoolExecutor
477
+ from concurrent.futures import TimeoutError as FutureTimeoutError
478
+
479
+ with ThreadPoolExecutor(max_workers=1) as executor:
480
+ future = executor.submit(
481
+ self._perform_frame_cleaning,
482
+ endoscope_data_roi_nested,
483
+ endoscope_image_roi,
484
+ )
485
+ try:
486
+ # Increased timeout to better accommodate ffmpeg + OCR
487
+ future.result(timeout=50000)
488
+ self.processing_context["anonymization_completed"] = True
489
+ self.logger.info(
490
+ "Frame cleaning completed successfully within timeout"
491
+ )
492
+ except FutureTimeoutError:
493
+ self.logger.warning(
494
+ "Frame cleaning timed out; entering grace period check for cleaned output"
495
+ )
496
+ # 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
+ grace_seconds = 60
503
+ expected_cleaned_path: Optional[Path] = None
504
+ processed_field = video.processed_file
505
+ if isinstance(processed_field, FieldFile) and processed_field.name:
506
+ try:
507
+ expected_cleaned_path = Path(processed_field.path)
508
+ except (NotImplementedError, TypeError, ValueError):
509
+ expected_cleaned_path = None
510
+ found = False
511
+ if expected_cleaned_path is not None:
512
+ for _ in range(grace_seconds):
513
+ 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
+ )
520
+ self.logger.info(
521
+ "Detected cleaned video during grace period: %s",
522
+ expected_cleaned_path,
523
+ )
524
+ found = True
525
+ break
526
+ time.sleep(1)
527
+ else:
528
+ self._fallback_anonymize_video()
529
+ if not found:
530
+ raise TimeoutError(
531
+ "Frame cleaning operation timed out - likely Ollama connection issue"
532
+ )
533
+
534
+ except Exception as e:
535
+ self.logger.warning(
536
+ "Frame cleaning failed (reason: %s), falling back to simple copy", e
537
+ )
538
+ # Try fallback anonymization when frame cleaning fails
539
+ try:
540
+ self._fallback_anonymize_video()
541
+ except Exception as fallback_error:
542
+ self.logger.error(
543
+ "Fallback anonymization also failed: %s", fallback_error
544
+ )
545
+ # If even fallback fails, mark as not anonymized but continue import
546
+ self.processing_context["anonymization_completed"] = False
547
+ self.processing_context["error_reason"] = (
548
+ f"Frame cleaning failed: {e}, Fallback failed: {fallback_error}"
549
+ )
550
+
551
+ def _save_anonymized_video(self):
552
+ original_raw_file_path_to_delete = None
553
+ original_raw_frame_dir_to_delete = None
554
+ video = self._require_current_video()
555
+ anonymized_video_path = video.get_target_anonymized_video_path()
556
+
557
+ 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
+ )
561
+
562
+ 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
+ )
571
+
572
+ video.processed_video_hash = new_processed_hash
573
+ video.processed_file.name = anonymized_video_path.relative_to(
574
+ STORAGE_DIR
575
+ ).as_posix()
576
+
577
+ update_fields = [
578
+ "processed_video_hash",
579
+ "processed_file",
580
+ "frame_dir",
581
+ ]
582
+
583
+ if self.delete_source:
584
+ original_raw_file_path_to_delete = video.get_raw_file_path()
585
+ original_raw_frame_dir_to_delete = video.get_frame_dir_path()
586
+
587
+ video.raw_file.name = None # type: ignore[assignment]
588
+
589
+ update_fields.extend(["raw_file", "video_hash"])
590
+
591
+ transaction.on_commit(
592
+ lambda: _cleanup_raw_assets(
593
+ video_uuid=video.uuid,
594
+ raw_file_path=original_raw_file_path_to_delete,
595
+ raw_frame_dir=original_raw_frame_dir_to_delete,
596
+ )
597
+ )
598
+
599
+ 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
604
+
605
+ def _fallback_anonymize_video(self):
606
+ """
607
+ Fallback to create anonymized video if lx_anonymizer is not available.
608
+ """
609
+ try:
610
+ self.logger.info("Attempting fallback video anonymization...")
611
+ video = self.current_video
612
+ if video is None:
613
+ self.logger.warning(
614
+ "No VideoFile instance available for fallback anonymization"
615
+ )
616
+
617
+ # 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
+ )
621
+ self.processing_context["anonymization_completed"] = False
622
+ self.processing_context["use_raw_as_processed"] = True
623
+ self.logger.warning(
624
+ "Fallback: Video will be imported without anonymization (raw copy used)"
625
+ )
626
+ except Exception as e:
627
+ self.logger.error(
628
+ f"Error during fallback anonymization: {e}", exc_info=True
629
+ )
630
+ self.processing_context["anonymization_completed"] = False
631
+ self.processing_context["error_reason"] = str(e)
632
+
633
+ def _finalize_processing(self):
634
+ """Finalize processing and update video state."""
635
+ self.logger.info("Updating video processing state...")
636
+
637
+ with transaction.atomic():
638
+ video = self._require_current_video()
639
+ try:
640
+ video.refresh_from_db()
641
+ except Exception as refresh_error:
642
+ self.logger.warning(
643
+ "Could not refresh VideoFile %s from DB: %s",
644
+ video.uuid,
645
+ refresh_error,
646
+ )
647
+
648
+ state = video.get_or_create_state()
649
+
650
+ # Only mark frames as extracted if they were successfully extracted
651
+ if self.processing_context.get("frames_extracted", False):
652
+ state.frames_extracted = True
653
+ self.logger.info("Marked frames as extracted in state")
654
+ else:
655
+ self.logger.warning("Frames were not extracted, not updating state")
656
+
657
+ # Always mark these as true (metadata extraction attempts were made)
658
+ state.frames_initialized = True
659
+ state.video_meta_extracted = True
660
+ state.text_meta_extracted = True
661
+
662
+ # ✅ FIX: Only mark as processed if anonymization actually completed
663
+ anonymization_completed = self.processing_context.get(
664
+ "anonymization_completed", False
665
+ )
666
+ if anonymization_completed:
667
+ state.mark_sensitive_meta_processed(save=False)
668
+ self.logger.info(
669
+ "Anonymization completed - marking sensitive meta as processed"
670
+ )
671
+ else:
672
+ self.logger.warning(
673
+ f"Anonymization NOT completed - NOT marking as processed. Reason: {self.processing_context.get('error_reason', 'Unknown')}"
674
+ )
675
+ # Explicitly mark as NOT processed
676
+ state.sensitive_meta_processed = False
677
+
678
+ # Save all state changes
679
+ state.save()
680
+ self.logger.info("Video processing state updated")
681
+
682
+ # Signal completion
683
+ self._signal_completion()
684
+
685
+ def _cleanup_and_archive(self):
686
+ """Move processed video to anonym_videos and cleanup."""
687
+ from endoreg_db.utils import data_paths
688
+
689
+ anonym_videos_dir = data_paths["anonym_video"] # /data/anonym_videos
690
+ anonym_videos_dir.mkdir(parents=True, exist_ok=True)
691
+
692
+ video = self._require_current_video()
693
+
694
+ processed_video_path = None
695
+ if "cleaned_video_path" in self.processing_context:
696
+ processed_video_path = self.processing_context["cleaned_video_path"]
697
+ else:
698
+ raw_video_path = self.processing_context.get("raw_video_path")
699
+ if raw_video_path and Path(raw_video_path).exists():
700
+ # Use UUID-based naming to avoid conflicts
701
+ suffix = Path(raw_video_path).suffix or ".mp4"
702
+ processed_filename = f"processed_{video.uuid}{suffix}"
703
+ processed_video_path = Path(raw_video_path).parent / processed_filename
704
+ try:
705
+ 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
+ )
709
+ except Exception as exc:
710
+ self.logger.error("Failed to copy raw video: %s", exc)
711
+ processed_video_path = None
712
+
713
+ if processed_video_path and Path(processed_video_path).exists():
714
+ try:
715
+ ext = Path(processed_video_path).suffix or ".mp4"
716
+ anonym_video_filename = f"anonym_{video.uuid}{ext}"
717
+ anonym_target_path = anonym_videos_dir / anonym_video_filename
718
+
719
+ shutil.move(str(processed_video_path), str(anonym_target_path))
720
+ self.logger.info("Moved processed video to: %s", anonym_target_path)
721
+
722
+ if anonym_target_path.exists():
723
+ try:
724
+ storage_root = data_paths["storage"]
725
+ relative_path = anonym_target_path.relative_to(storage_root)
726
+ video.processed_file.name = str(relative_path)
727
+ video.save(update_fields=["processed_file"])
728
+ self.logger.info(
729
+ "Updated processed_file path to: %s", relative_path
730
+ )
731
+ 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
+ )
738
+ video.save(update_fields=["processed_file"])
739
+ self.logger.info(
740
+ "Updated processed_file path using fallback: %s",
741
+ f"anonym_videos/{anonym_video_filename}",
742
+ )
743
+
744
+ self.processing_context["anonymization_completed"] = True
745
+ else:
746
+ self.logger.warning(
747
+ "Processed video file not found after move: %s",
748
+ anonym_target_path,
749
+ )
750
+ except Exception as exc:
751
+ self.logger.error(
752
+ "Failed to move processed video to anonym_videos: %s", exc
753
+ )
754
+ else:
755
+ self.logger.warning(
756
+ "No processed video available - processed_file will remain empty"
757
+ )
758
+
759
+ try:
760
+ from endoreg_db.utils.paths import RAW_FRAME_DIR
761
+
762
+ shutil.rmtree(RAW_FRAME_DIR, ignore_errors=True)
763
+ self.logger.debug(
764
+ "Cleaned up temporary frames directory: %s", RAW_FRAME_DIR
765
+ )
766
+ except Exception as exc:
767
+ self.logger.warning("Failed to remove directory %s: %s", RAW_FRAME_DIR, exc)
768
+
769
+ source_path = self.processing_context["file_path"]
770
+ if self.processing_context["delete_source"] and Path(source_path).exists():
771
+ try:
772
+ os.remove(source_path)
773
+ self.logger.info("Removed remaining source file: %s", source_path)
774
+ except Exception as exc:
775
+ self.logger.warning(
776
+ "Failed to remove source file %s: %s", source_path, exc
777
+ )
778
+
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
+ )
783
+ try:
784
+ video.anonymize(delete_original_raw=self.delete_source)
785
+ video.save(update_fields=["processed_file"])
786
+ self.logger.info("Late-stage anonymization succeeded")
787
+ except Exception as e:
788
+ self.logger.error("Late-stage anonymization failed: %s", e)
789
+ self.processing_context["anonymization_completed"] = False
790
+
791
+ self.logger.info("Cleanup and archiving completed")
792
+
793
+ self.processed_files.add(str(self.processing_context["file_path"]))
794
+
795
+ with transaction.atomic():
796
+ video.refresh_from_db()
797
+ if hasattr(video, "state") and self.processing_context.get(
798
+ "anonymization_completed"
799
+ ):
800
+ video.state.mark_sensitive_meta_processed(save=True)
801
+
802
+ self.logger.info(
803
+ "Import and anonymization completed for VideoFile UUID: %s", video.uuid
804
+ )
805
+ self.logger.info("Raw video stored in: /data/videos")
806
+ self.logger.info("Processed video stored in: /data/anonym_videos")
807
+
808
+ def _create_sensitive_file(
809
+ self,
810
+ video_instance: VideoFile | None = None,
811
+ file_path: Path | str | None = None,
812
+ ) -> Path:
813
+ """Create or move a sensitive copy of the raw video file inside storage."""
814
+
815
+ video = video_instance or self._require_current_video()
816
+
817
+ 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
+
825
+ if source_path is None and file_path is not None:
826
+ source_path = Path(file_path)
827
+
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
+ )
834
+
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)
839
+
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))
851
+ try:
852
+ os.remove(source_path)
853
+ except FileNotFoundError:
854
+ pass
855
+
856
+ try:
857
+ from endoreg_db.utils import data_paths
858
+
859
+ storage_root = data_paths["storage"]
860
+ relative_path = target_file_path.relative_to(storage_root)
861
+ video.raw_file.name = str(relative_path)
862
+ video.save(update_fields=["raw_file"])
863
+ self.logger.info(
864
+ "Updated video.raw_file to point to sensitive location: %s",
865
+ relative_path,
866
+ )
867
+ except Exception as exc:
868
+ self.logger.warning("Failed to set relative path, using fallback: %s", exc)
869
+ video.raw_file.name = f"videos/sensitive/{target_file_path.name}"
870
+ video.save(update_fields=["raw_file"])
871
+ self.logger.info(
872
+ "Updated video.raw_file using fallback method: videos/sensitive/%s",
873
+ target_file_path.name,
874
+ )
875
+
876
+ self.processing_context["raw_video_path"] = target_file_path
877
+ self.processing_context["video_filename"] = target_file_path.name
878
+
879
+ self.logger.info(
880
+ "Created sensitive file for %s at %s", video.uuid, target_file_path
881
+ )
882
+ return target_file_path
883
+
884
+ def _get_processor_roi_info(
885
+ self,
886
+ ) -> Tuple[Optional[List[List[Dict[str, Any]]]], Optional[Dict[str, Any]]]:
887
+ """Get processor ROI information for masking."""
888
+ endoscope_data_roi_nested = None
889
+ endoscope_image_roi = None
890
+
891
+ video = self._require_current_video()
892
+
893
+ try:
894
+ video_meta = getattr(video, "video_meta", None)
895
+ processor = getattr(video_meta, "processor", None) if video_meta else None
896
+ if processor:
897
+ assert isinstance(processor, EndoscopyProcessor), (
898
+ "Processor is not of type EndoscopyProcessor"
899
+ )
900
+ endoscope_image_roi = processor.get_roi_endoscope_image()
901
+ endoscope_data_roi_nested = processor.get_sensitive_rois()
902
+ self.logger.info(
903
+ "Retrieved processor ROI information: endoscope_image_roi=%s",
904
+ endoscope_image_roi,
905
+ )
906
+ else:
907
+ self.logger.warning(
908
+ "No processor found for video %s, proceeding without ROI masking",
909
+ video.uuid,
910
+ )
911
+ except Exception as exc:
912
+ self.logger.error("Failed to retrieve processor ROI information: %s", exc)
913
+
914
+ # Convert dict to nested list if necessary to match return type
915
+ if isinstance(endoscope_data_roi_nested, dict):
916
+ # Convert dict[str, dict[str, int | None] | None] to List[List[Dict[str, Any]]]
917
+ converted_roi = []
918
+ for key, value in endoscope_data_roi_nested.items():
919
+ if isinstance(value, dict):
920
+ converted_roi.append([value])
921
+ elif value is None:
922
+ converted_roi.append([])
923
+ endoscope_data_roi_nested = converted_roi
924
+
925
+ return endoscope_data_roi_nested, endoscope_image_roi
926
+
927
+ def _ensure_default_patient_data(
928
+ self, video_instance: VideoFile | None = None
929
+ ) -> None:
930
+ """Ensure minimum patient data is present on the video's SensitiveMeta."""
931
+
932
+ video = video_instance or self._require_current_video()
933
+
934
+ sensitive_meta = getattr(video, "sensitive_meta", None)
935
+ if not sensitive_meta:
936
+ self.logger.info(
937
+ "No SensitiveMeta found for video %s, creating default", video.uuid
938
+ )
939
+ default_data = {
940
+ "patient_first_name": "Patient",
941
+ "patient_last_name": "Unknown",
942
+ "patient_dob": date(1990, 1, 1),
943
+ "examination_date": date.today(),
944
+ "center_name": video.center.name
945
+ if video.center
946
+ else "university_hospital_wuerzburg",
947
+ }
948
+ try:
949
+ sensitive_meta = SensitiveMeta.create_from_dict(default_data)
950
+ video.sensitive_meta = sensitive_meta
951
+ video.save(update_fields=["sensitive_meta"])
952
+ self.logger.info(
953
+ "Created default SensitiveMeta for video %s", video.uuid
954
+ )
955
+ except Exception as exc:
956
+ self.logger.error(
957
+ "Failed to create default SensitiveMeta for video %s: %s",
958
+ video.uuid,
959
+ exc,
960
+ )
961
+ return
962
+ 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
+ )
989
+
990
+ def _ensure_frame_cleaning_available(self):
991
+ """
992
+ Ensure frame cleaning modules are available by adding lx-anonymizer to path.
993
+
994
+ Returns:
995
+ Tuple of (availability_flag, FrameCleaner_class, ReportReader_class)
996
+ """
997
+ 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
+
1004
+ except Exception as e:
1005
+ self.logger.warning(
1006
+ f"Frame cleaning not available: {e} Please install or update lx_anonymizer."
1007
+ )
1008
+
1009
+ return False, None
1010
+
1011
+ def _perform_frame_cleaning(self, endoscope_data_roi_nested, endoscope_image_roi):
1012
+ """Perform frame cleaning and anonymization."""
1013
+ # Instantiate frame cleaner
1014
+ is_available, frame_cleaner = self._ensure_frame_cleaning_available()
1015
+
1016
+ if not is_available:
1017
+ raise RuntimeError("Frame cleaning not available")
1018
+
1019
+ # Prepare parameters for frame cleaning
1020
+ raw_video_path = self.processing_context.get("raw_video_path")
1021
+
1022
+ if not raw_video_path or not Path(raw_video_path).exists():
1023
+ try:
1024
+ self.current_video = self._require_current_video()
1025
+ raw_video_path = self.current_video.get_raw_file_path()
1026
+ except Exception:
1027
+ raise RuntimeError(f"Raw video path not found: {raw_video_path}")
1028
+
1029
+ # Create temporary output path for cleaned video using UUID to avoid naming conflicts
1030
+ video = self._require_current_video()
1031
+ # Ensure raw_video_path is not None
1032
+ if not raw_video_path:
1033
+ raise RuntimeError(
1034
+ "raw_video_path is None, cannot construct cleaned_video_path"
1035
+ )
1036
+ suffix = Path(raw_video_path).suffix or ".mp4"
1037
+ cleaned_filename = f"cleaned_{video.uuid}{suffix}"
1038
+ cleaned_video_path = Path(raw_video_path).parent / cleaned_filename
1039
+ self.logger.debug("Using UUID-based cleaned filename: %s", cleaned_filename)
1040
+
1041
+ # Clean video with ROI masking (heavy I/O operation)
1042
+ actual_cleaned_path, extracted_metadata = frame_cleaner.clean_video(
1043
+ video_path=Path(raw_video_path),
1044
+ endoscope_image_roi=endoscope_image_roi,
1045
+ endoscope_data_roi_nested=endoscope_data_roi_nested,
1046
+ output_path=cleaned_video_path,
1047
+ technique="mask_overlay",
1048
+ )
1049
+
1050
+ # Store cleaned video path for later use in _cleanup_and_archive
1051
+ self.processing_context["cleaned_video_path"] = actual_cleaned_path
1052
+ self.processing_context["extracted_metadata"] = extracted_metadata
1053
+
1054
+ # Update sensitive metadata with extracted information
1055
+ self._update_sensitive_metadata(extracted_metadata)
1056
+ self.logger.info(
1057
+ f"Extracted metadata from frame cleaning: {extracted_metadata}"
1058
+ )
1059
+
1060
+ self.logger.info(
1061
+ f"Frame cleaning with ROI masking completed: {actual_cleaned_path}"
1062
+ )
1063
+ self.logger.info("Cleaned video will be moved to anonym_videos during cleanup")
1064
+
1065
+ def _update_sensitive_metadata(self, extracted_metadata: Dict[str, Any]):
1066
+ """
1067
+ Update sensitive metadata with extracted information.
1068
+ Args:
1069
+ extracted_metadata (Dict[str, Any]): Extracted metadata to update.
1070
+ """
1071
+ video = self._require_current_video()
1072
+ sensitive_meta = getattr(video, "sensitive_meta", None)
1073
+
1074
+ if not (sensitive_meta and extracted_metadata):
1075
+ return
1076
+
1077
+ sm = sensitive_meta
1078
+ updated_fields = []
1079
+
1080
+ # Ensure center is set from video.center if not in extracted_metadata
1081
+ metadata_to_update = extracted_metadata.copy()
1082
+
1083
+ # FIX: Set center object instead of center_name string
1084
+ if not hasattr(sm, "center") or not sm.center:
1085
+ if video.center:
1086
+ metadata_to_update["center"] = video.center
1087
+ self.logger.debug(
1088
+ "Added center object '%s' to metadata for SensitiveMeta update",
1089
+ video.center.name,
1090
+ )
1091
+ else:
1092
+ center_name = metadata_to_update.get("center_name")
1093
+ if center_name:
1094
+ try:
1095
+ from ..models.administration import Center
1096
+
1097
+ center_obj = Center.objects.get(name=center_name)
1098
+ metadata_to_update["center"] = center_obj
1099
+ self.logger.debug(
1100
+ "Loaded center object '%s' from center_name", center_name
1101
+ )
1102
+ metadata_to_update.pop("center_name", None)
1103
+ except Center.DoesNotExist:
1104
+ self.logger.error(
1105
+ "Center '%s' not found in database", center_name
1106
+ )
1107
+ return
1108
+
1109
+ try:
1110
+ sm.update_from_dict(metadata_to_update)
1111
+ updated_fields = list(
1112
+ extracted_metadata.keys()
1113
+ ) # Only log originally extracted fields
1114
+ except KeyError as e:
1115
+ self.logger.warning(f"Failed to update SensitiveMeta field {e}")
1116
+ return
1117
+
1118
+ if updated_fields:
1119
+ try:
1120
+ sm.save() # Remove update_fields to allow all necessary fields to be saved
1121
+ self.logger.info(
1122
+ "Updated SensitiveMeta fields for video %s: %s",
1123
+ video.uuid,
1124
+ updated_fields,
1125
+ )
1126
+
1127
+ state = video.get_or_create_state()
1128
+ state.mark_sensitive_meta_processed(save=True)
1129
+ self.logger.info(
1130
+ "Marked sensitive metadata as processed for video %s", video.uuid
1131
+ )
1132
+ except Exception as e:
1133
+ self.logger.error(f"Failed to save SensitiveMeta: {e}")
1134
+ raise # Re-raise to trigger fallback in calling method
1135
+ else:
1136
+ self.logger.info(
1137
+ "No SensitiveMeta fields updated for video %s - all existing values preserved",
1138
+ video.uuid,
1139
+ )
1140
+
1141
+ def _signal_completion(self):
1142
+ """Signal completion to the tracking system."""
1143
+ try:
1144
+ video = self._require_current_video()
1145
+
1146
+ 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
+ )
1159
+
1160
+ if video_processing_complete:
1161
+ self.logger.info(
1162
+ "Video %s processing completed successfully - ready for validation",
1163
+ video.uuid,
1164
+ )
1165
+
1166
+ # Update completion flags if they exist
1167
+ completion_fields = []
1168
+ for field_name in [
1169
+ "import_completed",
1170
+ "processing_complete",
1171
+ "ready_for_validation",
1172
+ ]:
1173
+ if hasattr(video, field_name):
1174
+ setattr(video, field_name, True)
1175
+ completion_fields.append(field_name)
1176
+
1177
+ if completion_fields:
1178
+ video.save(update_fields=completion_fields)
1179
+ self.logger.info("Updated completion flags: %s", completion_fields)
1180
+ else:
1181
+ self.logger.warning(
1182
+ "Video %s processing incomplete - missing required components",
1183
+ video.uuid,
1184
+ )
1185
+
1186
+ except Exception as e:
1187
+ self.logger.warning(f"Failed to signal completion status: {e}")
1188
+
1189
+ def _cleanup_on_error(self):
1190
+ """Cleanup processing context on error."""
1191
+ if self.current_video and hasattr(self.current_video, "state"):
1192
+ try:
1193
+ if self.processing_context.get("processing_started"):
1194
+ self.current_video.state.frames_extracted = False
1195
+ self.current_video.state.frames_initialized = False
1196
+ self.current_video.state.video_meta_extracted = False
1197
+ self.current_video.state.text_meta_extracted = False
1198
+ self.current_video.state.save()
1199
+ except Exception as e:
1200
+ self.logger.warning(f"Error during cleanup: {e}")
1201
+
1202
+ def _cleanup_processing_context(self):
1203
+ """
1204
+ Cleanup processing context and release file lock.
1205
+
1206
+ This method is always called in the finally block of import_and_anonymize()
1207
+ to ensure the file lock is released even if processing fails.
1208
+ """
1209
+ # DEFENSIVE: Ensure processing_context exists before accessing it
1210
+ if not hasattr(self, "processing_context"):
1211
+ self.processing_context = {}
1212
+
1213
+ try:
1214
+ # Release file lock if it was acquired
1215
+ lock_context = self.processing_context.get("_lock_context")
1216
+ if lock_context is not None:
1217
+ try:
1218
+ lock_context.__exit__(None, None, None)
1219
+ self.logger.info("Released file lock")
1220
+ except Exception as e:
1221
+ self.logger.warning(f"Error releasing file lock: {e}")
1222
+
1223
+ # Remove file from processed set if processing failed
1224
+ file_path = self.processing_context.get("file_path")
1225
+ if file_path and not self.processing_context.get("anonymization_completed"):
1226
+ file_path_str = str(file_path)
1227
+ if file_path_str in self.processed_files:
1228
+ self.processed_files.remove(file_path_str)
1229
+ self.logger.info(
1230
+ f"Removed {file_path_str} from processed files (failed processing)"
1231
+ )
1232
+
1233
+ except Exception as e:
1234
+ self.logger.warning(f"Error during context cleanup: {e}")
1235
+ finally:
1236
+ # Reset context
1237
+ self.current_video = None
1238
+ self.processing_context = {}
1239
+
1240
+
1241
+ # Convenience function for callers/tests that expect a module-level import_and_anonymize
1242
+ def import_and_anonymize(
1243
+ file_path,
1244
+ center_name: str,
1245
+ processor_name: str,
1246
+ save_video: bool = True,
1247
+ delete_source: bool = True,
1248
+ ) -> VideoFile | None:
1249
+ """Module-level helper that instantiates VideoImportService and runs import_and_anonymize.
1250
+ Kept for backward compatibility with callers that import this function directly.
1251
+ """
1252
+ service = VideoImportService()
1253
+ return service.import_and_anonymize(
1254
+ file_path=file_path,
1255
+ center_name=center_name,
1256
+ processor_name=processor_name,
1257
+ save_video=save_video,
1258
+ delete_source=delete_source,
1259
+ )