endoreg-db 0.8.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of endoreg-db might be problematic. Click here for more details.

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