endoreg-db 0.6.4__py3-none-any.whl → 0.8.2__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 (779) hide show
  1. endoreg_db/admin.py +26 -26
  2. endoreg_db/api_urls.py +4 -0
  3. endoreg_db/apps.py +12 -0
  4. endoreg_db/assets/dummy_model.ckpt +1 -0
  5. endoreg_db/codemods/readme.md +88 -0
  6. endoreg_db/codemods/rename_datetime_fields.py +92 -0
  7. endoreg_db/config/env.py +101 -0
  8. endoreg_db/data/__init__.py +12 -0
  9. endoreg_db/data/ai_model/data.yaml +1 -1
  10. endoreg_db/data/ai_model_label/label/polyp_classification.yaml +52 -0
  11. endoreg_db/data/ai_model_label/label-set/data.yaml +20 -1
  12. endoreg_db/data/ai_model_label/label-set/polyp_classifications.yaml +25 -0
  13. endoreg_db/data/center/data.yaml +13 -12
  14. endoreg_db/data/center_shift/ukw.yaml +9 -0
  15. endoreg_db/data/db_summary.csv +58 -0
  16. endoreg_db/data/db_summary.xlsx +0 -0
  17. endoreg_db/data/disease/misc.yaml +1 -2
  18. endoreg_db/data/endoscopy_processor/data.yaml +3 -0
  19. endoreg_db/data/event/cardiology.yaml +0 -13
  20. endoreg_db/data/examination/examinations/data.yaml +14 -9
  21. endoreg_db/data/examination_indication/endoscopy.yaml +30 -30
  22. endoreg_db/data/examination_indication_classification/endoscopy.yaml +11 -11
  23. endoreg_db/data/examination_requirement_set/colonoscopy.yaml +15 -0
  24. endoreg_db/data/finding/anatomy_colon.yaml +128 -0
  25. endoreg_db/data/finding/colonoscopy.yaml +40 -0
  26. endoreg_db/data/finding/colonoscopy_bowel_prep.yaml +56 -0
  27. endoreg_db/data/finding/complication.yaml +16 -0
  28. endoreg_db/data/finding/data.yaml +3 -46
  29. endoreg_db/data/finding/examination_setting.yaml +16 -0
  30. endoreg_db/data/finding/medication_related.yaml +18 -0
  31. endoreg_db/data/finding/outcome.yaml +12 -0
  32. endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +95 -0
  33. endoreg_db/data/finding_classification/colonoscopy_jnet.yaml +22 -0
  34. endoreg_db/data/finding_classification/colonoscopy_kudo.yaml +25 -0
  35. endoreg_db/data/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
  36. endoreg_db/data/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
  37. endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +68 -0
  38. endoreg_db/data/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
  39. endoreg_db/data/finding_classification/colonoscopy_location.yaml +80 -0
  40. endoreg_db/data/finding_classification/colonoscopy_lst.yaml +21 -0
  41. endoreg_db/data/finding_classification/colonoscopy_nice.yaml +20 -0
  42. endoreg_db/data/finding_classification/colonoscopy_paris.yaml +26 -0
  43. endoreg_db/data/finding_classification/colonoscopy_sano.yaml +22 -0
  44. endoreg_db/data/finding_classification/colonoscopy_summary.yaml +53 -0
  45. endoreg_db/data/finding_classification/complication_generic.yaml +25 -0
  46. endoreg_db/data/finding_classification/examination_setting_generic.yaml +40 -0
  47. endoreg_db/data/finding_classification/histology_colo.yaml +51 -0
  48. endoreg_db/data/finding_classification/intervention_required.yaml +26 -0
  49. endoreg_db/data/finding_classification/medication_related.yaml +23 -0
  50. endoreg_db/data/finding_classification/visualized.yaml +33 -0
  51. endoreg_db/data/finding_classification_choice/bowel_preparation.yaml +78 -0
  52. endoreg_db/data/{finding_morphology_classification_choice → finding_classification_choice}/colon_lesion_circularity_default.yaml +0 -2
  53. endoreg_db/data/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
  54. endoreg_db/data/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
  55. endoreg_db/data/finding_classification_choice/colon_lesion_lst.yaml +15 -0
  56. endoreg_db/data/{finding_morphology_classification_choice → finding_classification_choice}/colon_lesion_nice.yaml +4 -7
  57. endoreg_db/data/{finding_morphology_classification_choice → finding_classification_choice}/colon_lesion_paris.yaml +0 -8
  58. endoreg_db/data/{finding_morphology_classification_choice → finding_classification_choice}/colon_lesion_planarity_default.yaml +6 -13
  59. endoreg_db/data/finding_classification_choice/colon_lesion_sano.yaml +14 -0
  60. endoreg_db/data/{finding_morphology_classification_choice → finding_classification_choice}/colon_lesion_surface_intact_default.yaml +3 -6
  61. endoreg_db/data/{finding_location_classification_choice/colonoscopy.yaml → finding_classification_choice/colonoscopy_location.yaml} +11 -22
  62. endoreg_db/data/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
  63. endoreg_db/data/finding_classification_choice/colonoscopy_size.yaml +82 -0
  64. endoreg_db/data/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
  65. endoreg_db/data/finding_classification_choice/complication_generic_types.yaml +15 -0
  66. endoreg_db/data/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
  67. endoreg_db/data/finding_classification_choice/histology.yaml +24 -0
  68. endoreg_db/data/finding_classification_choice/histology_polyp.yaml +20 -0
  69. endoreg_db/data/finding_classification_choice/outcome.yaml +19 -0
  70. endoreg_db/data/finding_classification_choice/yes_no_na.yaml +11 -0
  71. endoreg_db/data/finding_classification_type/colonoscopy_basic.yaml +48 -0
  72. endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +8 -3
  73. endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +6 -6
  74. endoreg_db/data/finding_type/data.yaml +23 -10
  75. endoreg_db/data/gender/data.yaml +8 -1
  76. endoreg_db/data/information_source/annotation.yaml +6 -0
  77. endoreg_db/data/information_source/prediction.yaml +7 -0
  78. endoreg_db/data/information_source_type/data.yaml +8 -0
  79. endoreg_db/data/lab_value/misc.yaml +43 -0
  80. endoreg_db/data/medication/anticoagulation.yaml +5 -5
  81. endoreg_db/data/medication/tah.yaml +5 -5
  82. endoreg_db/data/medication_intake_time/base.yaml +4 -4
  83. endoreg_db/data/names_first/first_names.yaml +3 -0
  84. endoreg_db/data/pdf_type/data.yaml +26 -2
  85. endoreg_db/data/qualification/endoscopy.yaml +36 -0
  86. endoreg_db/data/qualification/m2.yaml +39 -0
  87. endoreg_db/data/qualification/outpatient_clinic.yaml +35 -0
  88. endoreg_db/data/qualification/sonography.yaml +36 -0
  89. endoreg_db/data/qualification_type/base.yaml +29 -0
  90. endoreg_db/data/report_reader_flag/rkh-histology-generic.yaml +10 -0
  91. endoreg_db/data/report_reader_flag/ukw-histology-generic.yaml +5 -0
  92. endoreg_db/data/requirement/age.yaml +26 -0
  93. endoreg_db/data/requirement/colonoscopy_baseline_austria.yaml +45 -0
  94. endoreg_db/data/requirement/disease_cardiovascular.yaml +6 -6
  95. endoreg_db/data/requirement/disease_classification_choice_cardiovascular.yaml +9 -6
  96. endoreg_db/data/requirement/disease_hepatology.yaml +1 -1
  97. endoreg_db/data/requirement/disease_misc.yaml +3 -3
  98. endoreg_db/data/requirement/disease_renal.yaml +18 -2
  99. endoreg_db/data/requirement/{colonoscopy_indications.yaml → endoscopy_bleeding_risk.yaml} +6 -3
  100. endoreg_db/data/requirement/event_cardiology.yaml +17 -17
  101. endoreg_db/data/requirement/event_requirements.yaml +145 -0
  102. endoreg_db/data/requirement/finding_colon_polyp.yaml +50 -0
  103. endoreg_db/data/requirement/gender.yaml +25 -0
  104. endoreg_db/data/requirement/lab_value.yaml +352 -31
  105. endoreg_db/data/requirement/medication.yaml +93 -0
  106. endoreg_db/data/requirement_operator/age.yaml +13 -0
  107. endoreg_db/data/requirement_operator/lab_operators.yaml +36 -35
  108. endoreg_db/data/requirement_operator/model_operators.yaml +13 -7
  109. endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +48 -0
  110. endoreg_db/data/requirement_set/colonoscopy_austria_screening.yaml +57 -0
  111. endoreg_db/data/requirement_set/endoscopy_bleeding_risk.yaml +42 -2
  112. endoreg_db/data/requirement_type/requirement_types.yaml +82 -0
  113. endoreg_db/data/shift/endoscopy.yaml +21 -0
  114. endoreg_db/data/shift_type/base.yaml +35 -0
  115. endoreg_db/data/tag/requirement_set_tags.yaml +11 -0
  116. endoreg_db/data/unit/concentration.yaml +23 -0
  117. endoreg_db/exceptions.py +19 -0
  118. endoreg_db/forms/patient_finding_intervention_form.py +4 -5
  119. endoreg_db/forms/patient_form.py +7 -6
  120. endoreg_db/forms/questionnaires/__init__.py +1 -1
  121. endoreg_db/forms/questionnaires/tto_questionnaire.py +19 -19
  122. endoreg_db/helpers/count_db.py +45 -0
  123. endoreg_db/helpers/data_loader.py +208 -0
  124. endoreg_db/helpers/default_objects.py +359 -0
  125. endoreg_db/helpers/download_segmentation_model.py +31 -0
  126. endoreg_db/helpers/interact.py +6 -0
  127. endoreg_db/helpers/test_video_helper.py +119 -0
  128. endoreg_db/logger_conf.py +140 -0
  129. endoreg_db/management/__init__.py +1 -0
  130. endoreg_db/management/commands/__init__.py +1 -0
  131. endoreg_db/management/commands/anonymize_video.py +0 -0
  132. endoreg_db/management/commands/check_auth.py +125 -0
  133. endoreg_db/management/commands/create_multilabel_model_meta.py +214 -0
  134. endoreg_db/management/commands/fix_missing_patient_data.py +172 -0
  135. endoreg_db/management/commands/fix_video_paths.py +165 -0
  136. endoreg_db/management/commands/import_fallback_video.py +203 -0
  137. endoreg_db/management/commands/import_report.py +298 -0
  138. endoreg_db/management/commands/import_video.py +422 -0
  139. endoreg_db/management/commands/import_video_with_classification.py +367 -0
  140. endoreg_db/management/commands/init_default_ai_model.py +112 -0
  141. endoreg_db/management/commands/load_ai_model_data.py +2 -7
  142. endoreg_db/management/commands/load_base_db_data.py +1 -0
  143. endoreg_db/management/commands/load_endoscope_data.py +2 -2
  144. endoreg_db/management/commands/load_examination_indication_data.py +2 -3
  145. endoreg_db/management/commands/load_finding_data.py +49 -92
  146. endoreg_db/management/commands/load_green_endoscopy_wuerzburg_data.py +0 -1
  147. endoreg_db/management/commands/load_information_source.py +13 -7
  148. endoreg_db/management/commands/load_name_data.py +37 -0
  149. endoreg_db/management/commands/load_qualification_data.py +59 -0
  150. endoreg_db/management/commands/load_requirement_data.py +30 -6
  151. endoreg_db/management/commands/load_shift_data.py +60 -0
  152. endoreg_db/management/commands/load_tag_data.py +57 -0
  153. endoreg_db/management/commands/register_ai_model.py +1 -1
  154. endoreg_db/management/commands/start_filewatcher.py +106 -0
  155. endoreg_db/management/commands/storage_management.py +548 -0
  156. endoreg_db/management/commands/summarize_db_content.py +189 -0
  157. endoreg_db/management/commands/validate_video.py +204 -0
  158. endoreg_db/management/commands/validate_video_files.py +161 -0
  159. endoreg_db/management/commands/video_validation.py +22 -0
  160. endoreg_db/migrations/0001_initial.py +625 -813
  161. endoreg_db/migrations/0002_add_video_correction_models.py +52 -0
  162. endoreg_db/models/__init__.py +270 -307
  163. endoreg_db/models/administration/__init__.py +116 -0
  164. endoreg_db/models/{ai_model → administration/ai}/__init__.py +6 -1
  165. endoreg_db/models/administration/ai/active_model.py +35 -0
  166. endoreg_db/models/administration/ai/ai_model.py +156 -0
  167. endoreg_db/models/{ai_model → administration/ai}/model_type.py +6 -1
  168. endoreg_db/models/administration/case/__init__.py +19 -0
  169. endoreg_db/models/administration/case/case.py +114 -0
  170. endoreg_db/models/{case_template → administration/case/case_template}/case_template.py +3 -3
  171. endoreg_db/models/{case_template → administration/case/case_template}/case_template_rule.py +3 -10
  172. endoreg_db/models/{case_template → administration/case/case_template}/case_template_rule_value.py +2 -4
  173. endoreg_db/models/{case_template → administration/case/case_template}/case_template_type.py +1 -3
  174. endoreg_db/models/{center → administration/center}/__init__.py +3 -1
  175. endoreg_db/models/administration/center/center.py +61 -0
  176. endoreg_db/models/administration/center/center_product.py +64 -0
  177. endoreg_db/models/{center → administration/center}/center_resource.py +19 -3
  178. endoreg_db/models/administration/center/center_shift.py +88 -0
  179. endoreg_db/models/administration/center/center_waste.py +30 -0
  180. endoreg_db/models/administration/permissions/__init__.py +44 -0
  181. endoreg_db/models/administration/person/__init__.py +24 -0
  182. endoreg_db/models/administration/person/employee/__init__.py +3 -0
  183. endoreg_db/models/administration/person/employee/employee.py +35 -0
  184. endoreg_db/models/administration/person/employee/employee_qualification.py +39 -0
  185. endoreg_db/models/administration/person/employee/employee_type.py +42 -0
  186. endoreg_db/models/administration/person/examiner/__init__.py +4 -0
  187. endoreg_db/models/administration/person/examiner/examiner.py +54 -0
  188. endoreg_db/models/administration/person/names/__init__.py +0 -0
  189. endoreg_db/models/{persons → administration/person/names}/first_name.py +1 -1
  190. endoreg_db/models/{persons → administration/person/names}/last_name.py +2 -3
  191. endoreg_db/models/administration/person/patient/__init__.py +5 -0
  192. endoreg_db/models/administration/person/patient/patient.py +460 -0
  193. endoreg_db/models/administration/person/profession/__init__.py +24 -0
  194. endoreg_db/models/administration/person/user/__init__.py +5 -0
  195. endoreg_db/models/administration/person/user/portal_user_information.py +37 -0
  196. endoreg_db/models/administration/product/product.py +97 -0
  197. endoreg_db/models/administration/product/product_group.py +39 -0
  198. endoreg_db/models/administration/product/product_material.py +54 -0
  199. endoreg_db/models/{product → administration/product}/product_weight.py +9 -0
  200. endoreg_db/models/{product → administration/product}/reference_product.py +26 -11
  201. endoreg_db/models/administration/qualification/__init__.py +7 -0
  202. endoreg_db/models/administration/qualification/qualification.py +37 -0
  203. endoreg_db/models/administration/qualification/qualification_type.py +35 -0
  204. endoreg_db/models/administration/shift/__init__.py +9 -0
  205. endoreg_db/models/administration/shift/scheduled_days.py +69 -0
  206. endoreg_db/models/administration/shift/shift.py +51 -0
  207. endoreg_db/models/administration/shift/shift_type.py +108 -0
  208. endoreg_db/models/label/__init__.py +24 -1
  209. endoreg_db/models/label/annotation/__init__.py +12 -0
  210. endoreg_db/models/label/annotation/image_classification.py +84 -0
  211. endoreg_db/models/label/annotation/video_segmentation_annotation.py +66 -0
  212. endoreg_db/models/label/label.py +45 -74
  213. endoreg_db/models/label/label_set.py +53 -0
  214. endoreg_db/models/label/label_type.py +29 -0
  215. endoreg_db/models/label/label_video_segment/__init__.py +3 -0
  216. endoreg_db/models/label/label_video_segment/_create_from_video.py +41 -0
  217. endoreg_db/models/label/label_video_segment/label_video_segment.py +511 -0
  218. endoreg_db/models/label/video_segmentation_label.py +31 -0
  219. endoreg_db/models/{annotation → label}/video_segmentation_labelset.py +7 -0
  220. endoreg_db/models/media/__init__.py +14 -0
  221. endoreg_db/models/media/frame/__init__.py +3 -0
  222. endoreg_db/models/media/frame/frame.py +111 -0
  223. endoreg_db/models/media/pdf/__init__.py +11 -0
  224. endoreg_db/models/media/pdf/raw_pdf.py +608 -0
  225. endoreg_db/models/media/pdf/report_file.py +162 -0
  226. endoreg_db/models/media/pdf/report_reader/report_reader_config.py +77 -0
  227. endoreg_db/models/media/video/__init__.py +4 -0
  228. endoreg_db/models/media/video/create_from_file.py +336 -0
  229. endoreg_db/models/media/video/pipe_1.py +207 -0
  230. endoreg_db/models/media/video/pipe_2.py +105 -0
  231. endoreg_db/models/media/video/refactor_plan.md +0 -0
  232. endoreg_db/models/media/video/video_file.py +680 -0
  233. endoreg_db/models/media/video/video_file_ai.py +443 -0
  234. endoreg_db/models/media/video/video_file_anonymize.py +348 -0
  235. endoreg_db/models/media/video/video_file_frames/__init__.py +47 -0
  236. endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +22 -0
  237. endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +23 -0
  238. endoreg_db/models/media/video/video_file_frames/_delete_frames.py +104 -0
  239. endoreg_db/models/media/video/video_file_frames/_extract_frames.py +174 -0
  240. endoreg_db/models/media/video/video_file_frames/_get_frame.py +28 -0
  241. endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +27 -0
  242. endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +20 -0
  243. endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +27 -0
  244. endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +34 -0
  245. endoreg_db/models/media/video/video_file_frames/_get_frames.py +27 -0
  246. endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +129 -0
  247. endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +129 -0
  248. endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +65 -0
  249. endoreg_db/models/media/video/video_file_frames.py +0 -0
  250. endoreg_db/models/media/video/video_file_io.py +166 -0
  251. endoreg_db/models/media/video/video_file_meta/__init__.py +22 -0
  252. endoreg_db/models/media/video/video_file_meta/get_crop_template.py +45 -0
  253. endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +39 -0
  254. endoreg_db/models/media/video/video_file_meta/get_fps.py +147 -0
  255. endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +143 -0
  256. endoreg_db/models/media/video/video_file_meta/text_meta.py +134 -0
  257. endoreg_db/models/media/video/video_file_meta/video_meta.py +70 -0
  258. endoreg_db/models/media/video/video_file_meta.py +11 -0
  259. endoreg_db/models/media/video/video_file_segments.py +209 -0
  260. endoreg_db/models/medical/__init__.py +146 -0
  261. endoreg_db/models/{contraindication → medical/contraindication}/__init__.py +1 -5
  262. endoreg_db/models/{disease.py → medical/disease.py} +60 -52
  263. endoreg_db/models/{event.py → medical/event.py} +31 -54
  264. endoreg_db/models/{examination → medical/examination}/__init__.py +1 -1
  265. endoreg_db/models/medical/examination/examination.py +148 -0
  266. endoreg_db/models/{examination → medical/examination}/examination_indication.py +64 -35
  267. endoreg_db/models/{examination → medical/examination}/examination_time.py +0 -4
  268. endoreg_db/models/{examination → medical/examination}/examination_time_type.py +1 -8
  269. endoreg_db/models/{examination → medical/examination}/examination_type.py +1 -7
  270. endoreg_db/models/medical/finding/__init__.py +18 -0
  271. endoreg_db/models/medical/finding/finding.py +96 -0
  272. endoreg_db/models/medical/finding/finding_classification.py +142 -0
  273. endoreg_db/models/{finding → medical/finding}/finding_intervention.py +2 -10
  274. endoreg_db/models/medical/finding/finding_type.py +35 -0
  275. endoreg_db/models/medical/hardware/__init__.py +8 -0
  276. endoreg_db/models/{hardware → medical/hardware}/endoscope.py +28 -23
  277. endoreg_db/models/medical/laboratory/__init__.py +5 -0
  278. endoreg_db/models/medical/laboratory/lab_value.py +419 -0
  279. endoreg_db/models/{medication → medical/medication}/medication.py +1 -3
  280. endoreg_db/models/{medication → medical/medication}/medication_indication_type.py +8 -3
  281. endoreg_db/models/{medication → medical/medication}/medication_intake_time.py +21 -3
  282. endoreg_db/models/{medication → medical/medication}/medication_schedule.py +13 -5
  283. endoreg_db/models/{organ → medical/organ}/__init__.py +3 -6
  284. endoreg_db/models/medical/patient/__init__.py +56 -0
  285. endoreg_db/models/medical/patient/medication_examples.py +38 -0
  286. endoreg_db/models/medical/patient/patient_disease.py +63 -0
  287. endoreg_db/models/medical/patient/patient_event.py +75 -0
  288. endoreg_db/models/medical/patient/patient_examination.py +249 -0
  289. endoreg_db/models/{persons → medical}/patient/patient_examination_indication.py +21 -9
  290. endoreg_db/models/medical/patient/patient_finding.py +357 -0
  291. endoreg_db/models/medical/patient/patient_finding_classification.py +207 -0
  292. endoreg_db/models/{patient → medical/patient}/patient_finding_intervention.py +15 -1
  293. endoreg_db/models/medical/patient/patient_lab_sample.py +148 -0
  294. endoreg_db/models/{persons → medical}/patient/patient_lab_value.py +40 -15
  295. endoreg_db/models/medical/patient/patient_medication.py +104 -0
  296. endoreg_db/models/medical/patient/patient_medication_schedule.py +136 -0
  297. endoreg_db/models/{risk → medical/risk}/risk_type.py +0 -4
  298. endoreg_db/models/{data_file/metadata → metadata}/__init__.py +6 -0
  299. endoreg_db/models/metadata/frame_ocr_result.py +0 -0
  300. endoreg_db/models/metadata/model_meta.py +193 -0
  301. endoreg_db/models/metadata/model_meta_logic.py +236 -0
  302. endoreg_db/models/{data_file/metadata → metadata}/pdf_meta.py +28 -13
  303. endoreg_db/models/metadata/sensitive_meta.py +288 -0
  304. endoreg_db/models/metadata/sensitive_meta_logic.py +643 -0
  305. endoreg_db/models/metadata/video_meta.py +332 -0
  306. endoreg_db/models/metadata/video_prediction_logic.py +190 -0
  307. endoreg_db/models/metadata/video_prediction_meta.py +270 -0
  308. endoreg_db/models/other/__init__.py +17 -0
  309. endoreg_db/models/other/distribution/date_value_distribution.py +0 -2
  310. endoreg_db/models/other/distribution/numeric_value_distribution.py +30 -2
  311. endoreg_db/models/{emission → other/emission}/emission_factor.py +15 -6
  312. endoreg_db/models/{persons → other}/gender.py +8 -3
  313. endoreg_db/models/other/information_source.py +159 -0
  314. endoreg_db/models/other/material.py +10 -2
  315. endoreg_db/models/other/resource.py +6 -2
  316. endoreg_db/models/other/tag.py +27 -0
  317. endoreg_db/models/other/transport_route.py +13 -2
  318. endoreg_db/models/{unit.py → other/unit.py} +16 -6
  319. endoreg_db/models/other/waste.py +10 -3
  320. endoreg_db/models/requirement/requirement.py +556 -114
  321. endoreg_db/models/requirement/requirement_evaluation/__init__.py +4 -132
  322. endoreg_db/models/requirement/requirement_evaluation/get_values.py +40 -0
  323. endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +9 -0
  324. endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +80 -87
  325. endoreg_db/models/requirement/requirement_operator.py +132 -14
  326. endoreg_db/models/requirement/requirement_set.py +181 -21
  327. endoreg_db/models/rule/__init__.py +13 -0
  328. endoreg_db/models/{rules → rule}/rule.py +6 -3
  329. endoreg_db/models/{rules → rule}/rule_attribute_dtype.py +0 -2
  330. endoreg_db/models/{rules → rule}/rule_type.py +0 -2
  331. endoreg_db/models/{rules → rule}/ruleset.py +0 -2
  332. endoreg_db/models/state/__init__.py +12 -0
  333. endoreg_db/models/state/abstract.py +11 -0
  334. endoreg_db/models/state/audit_ledger.py +150 -0
  335. endoreg_db/models/state/label_video_segment.py +22 -0
  336. endoreg_db/models/state/raw_pdf.py +187 -0
  337. endoreg_db/models/state/sensitive_meta.py +46 -0
  338. endoreg_db/models/state/video.py +232 -0
  339. endoreg_db/models/upload_job.py +99 -0
  340. endoreg_db/models/utils.py +135 -0
  341. endoreg_db/models/video_metadata.py +66 -0
  342. endoreg_db/models/video_processing.py +153 -0
  343. endoreg_db/renames.yml +8 -0
  344. endoreg_db/root_urls.py +9 -0
  345. endoreg_db/schemas/__init__.py +0 -0
  346. endoreg_db/schemas/examination_evaluation.py +27 -0
  347. endoreg_db/serializers/Frames_NICE_and_PARIS_classifications.py +775 -0
  348. endoreg_db/serializers/__init__.py +147 -10
  349. endoreg_db/serializers/{raw_pdf_meta_validation.py → _old/raw_pdf_meta_validation.py} +3 -3
  350. endoreg_db/serializers/{raw_video_meta_validation.py → _old/raw_video_meta_validation.py} +18 -14
  351. endoreg_db/serializers/_old/video.py +71 -0
  352. endoreg_db/serializers/administration/__init__.py +14 -0
  353. endoreg_db/serializers/administration/ai/__init__.py +10 -0
  354. endoreg_db/serializers/administration/ai/active_model.py +10 -0
  355. endoreg_db/serializers/administration/ai/ai_model.py +18 -0
  356. endoreg_db/serializers/administration/ai/model_type.py +10 -0
  357. endoreg_db/serializers/administration/center.py +9 -0
  358. endoreg_db/serializers/administration/gender.py +9 -0
  359. endoreg_db/serializers/anonymization.py +69 -0
  360. endoreg_db/serializers/evaluation/examination_evaluation.py +1 -0
  361. endoreg_db/serializers/examination/__init__.py +10 -0
  362. endoreg_db/serializers/examination/base.py +46 -0
  363. endoreg_db/serializers/examination/dropdown.py +21 -0
  364. endoreg_db/serializers/examination_serializer.py +12 -0
  365. endoreg_db/serializers/finding/__init__.py +5 -0
  366. endoreg_db/serializers/finding/finding.py +54 -0
  367. endoreg_db/serializers/finding_classification/__init__.py +7 -0
  368. endoreg_db/serializers/finding_classification/choice.py +19 -0
  369. endoreg_db/serializers/finding_classification/classification.py +13 -0
  370. endoreg_db/serializers/label/__init__.py +7 -0
  371. endoreg_db/serializers/label/image_classification_annotation.py +62 -0
  372. endoreg_db/serializers/label/label.py +15 -0
  373. endoreg_db/serializers/label_video_segment/__init__.py +7 -0
  374. endoreg_db/serializers/label_video_segment/_lvs_create.py +149 -0
  375. endoreg_db/serializers/label_video_segment/_lvs_update.py +138 -0
  376. endoreg_db/serializers/label_video_segment/_lvs_validate.py +149 -0
  377. endoreg_db/serializers/label_video_segment/label_video_segment.py +344 -0
  378. endoreg_db/serializers/label_video_segment/label_video_segment_annotation.py +99 -0
  379. endoreg_db/serializers/label_video_segment/label_video_segment_update.py +163 -0
  380. endoreg_db/serializers/meta/__init__.py +19 -0
  381. endoreg_db/serializers/meta/pdf_file_meta_extraction.py +115 -0
  382. endoreg_db/serializers/meta/report_meta.py +53 -0
  383. endoreg_db/serializers/meta/sensitive_meta_detail.py +162 -0
  384. endoreg_db/serializers/meta/sensitive_meta_update.py +148 -0
  385. endoreg_db/serializers/meta/sensitive_meta_verification.py +59 -0
  386. endoreg_db/serializers/meta/video_meta.py +39 -0
  387. endoreg_db/serializers/misc/__init__.py +14 -0
  388. endoreg_db/serializers/misc/file_overview.py +152 -0
  389. endoreg_db/serializers/misc/stats.py +33 -0
  390. endoreg_db/serializers/misc/translatable_field_mix_in.py +44 -0
  391. endoreg_db/serializers/misc/upload_job.py +71 -0
  392. endoreg_db/serializers/misc/vop_patient_data.py +120 -0
  393. endoreg_db/serializers/patient/__init__.py +11 -0
  394. endoreg_db/serializers/patient/patient.py +86 -0
  395. endoreg_db/serializers/patient/patient_dropdown.py +27 -0
  396. endoreg_db/serializers/patient_examination/__init__.py +7 -0
  397. endoreg_db/serializers/patient_examination/patient_examination.py +141 -0
  398. endoreg_db/serializers/patient_finding/__init__.py +15 -0
  399. endoreg_db/serializers/patient_finding/patient_finding.py +31 -0
  400. endoreg_db/serializers/patient_finding/patient_finding_classification.py +39 -0
  401. endoreg_db/serializers/patient_finding/patient_finding_detail.py +53 -0
  402. endoreg_db/serializers/patient_finding/patient_finding_intervention.py +26 -0
  403. endoreg_db/serializers/patient_finding/patient_finding_list.py +41 -0
  404. endoreg_db/serializers/patient_finding/patient_finding_write.py +126 -0
  405. endoreg_db/serializers/pdf/__init__.py +5 -0
  406. endoreg_db/serializers/pdf/anony_text_validation.py +85 -0
  407. endoreg_db/serializers/report/__init__.py +9 -0
  408. endoreg_db/serializers/report/mixins.py +45 -0
  409. endoreg_db/serializers/report/report.py +105 -0
  410. endoreg_db/serializers/report/report_list.py +22 -0
  411. endoreg_db/serializers/report/secure_file_url.py +26 -0
  412. endoreg_db/serializers/requirements/requirement_schema.py +25 -0
  413. endoreg_db/serializers/requirements/requirement_sets.py +29 -0
  414. endoreg_db/serializers/sensitive_meta_serializer.py +282 -0
  415. endoreg_db/serializers/video/__init__.py +7 -0
  416. endoreg_db/serializers/video/segmentation.py +263 -0
  417. endoreg_db/serializers/video/video_file_brief.py +10 -0
  418. endoreg_db/serializers/video/video_file_detail.py +83 -0
  419. endoreg_db/serializers/video/video_file_list.py +67 -0
  420. endoreg_db/serializers/video/video_metadata.py +105 -0
  421. endoreg_db/serializers/video/video_processing_history.py +153 -0
  422. endoreg_db/services/__init__.py +5 -0
  423. endoreg_db/services/anonymization.py +223 -0
  424. endoreg_db/services/examination_evaluation.py +149 -0
  425. endoreg_db/services/finding_description_service.py +0 -0
  426. endoreg_db/services/lookup_service.py +241 -0
  427. endoreg_db/services/lookup_store.py +122 -0
  428. endoreg_db/services/ollama_api_docs.py +1528 -0
  429. endoreg_db/services/pdf_import.py +963 -0
  430. endoreg_db/services/polling_coordinator.py +288 -0
  431. endoreg_db/services/pseudonym_service.py +89 -0
  432. endoreg_db/services/requirements_object.py +147 -0
  433. endoreg_db/services/segment_sync.py +155 -0
  434. endoreg_db/services/storage_aware_video_processor.py +344 -0
  435. endoreg_db/services/video_import.py +1118 -0
  436. endoreg_db/tasks/upload_tasks.py +207 -0
  437. endoreg_db/tasks/video_ingest.py +157 -0
  438. endoreg_db/tasks/video_processing_tasks.py +327 -0
  439. endoreg_db/urls/__init__.py +70 -0
  440. endoreg_db/urls/anonymization.py +32 -0
  441. endoreg_db/urls/auth.py +16 -0
  442. endoreg_db/urls/classification.py +39 -0
  443. endoreg_db/urls/examination.py +54 -0
  444. endoreg_db/urls/files.py +6 -0
  445. endoreg_db/urls/label_video_segment_validate.py +33 -0
  446. endoreg_db/urls/label_video_segments.py +44 -0
  447. endoreg_db/urls/media.py +229 -0
  448. endoreg_db/urls/patient.py +19 -0
  449. endoreg_db/urls/report.py +48 -0
  450. endoreg_db/urls/requirements.py +13 -0
  451. endoreg_db/urls/stats.py +46 -0
  452. endoreg_db/urls/upload.py +20 -0
  453. endoreg_db/urls/video.py +61 -0
  454. endoreg_db/urls.py +6 -283
  455. endoreg_db/utils/__init__.py +66 -57
  456. endoreg_db/utils/ai/__init__.py +9 -0
  457. endoreg_db/{models/ai_model/utils.py → utils/ai/get.py} +1 -4
  458. endoreg_db/{models/ai_model/lightning → utils/ai}/inference_dataset.py +0 -1
  459. endoreg_db/{models/ai_model/lightning → utils/ai}/multilabel_classification_net.py +14 -10
  460. endoreg_db/{models/ai_model/lightning → utils/ai}/postprocess.py +15 -5
  461. endoreg_db/utils/ai/predict.py +291 -0
  462. endoreg_db/{models/ai_model/lightning → utils/ai}/preprocess.py +1 -1
  463. endoreg_db/utils/calc_duration_seconds.py +24 -0
  464. endoreg_db/utils/case_generator/__init__.py +0 -0
  465. endoreg_db/utils/check_video_files.py +148 -0
  466. endoreg_db/utils/dataloader.py +50 -12
  467. endoreg_db/utils/dates.py +21 -0
  468. endoreg_db/utils/env.py +33 -0
  469. endoreg_db/utils/extract_specific_frames.py +72 -0
  470. endoreg_db/utils/file_operations.py +29 -1
  471. endoreg_db/utils/fix_video_path_direct.py +141 -0
  472. endoreg_db/utils/frame_anonymization_utils.py +463 -0
  473. endoreg_db/utils/links/__init__.py +0 -0
  474. endoreg_db/utils/links/requirement_link.py +193 -0
  475. endoreg_db/utils/mime_types.py +0 -0
  476. endoreg_db/utils/names.py +2 -0
  477. endoreg_db/utils/paths.py +100 -82
  478. endoreg_db/utils/permissions.py +143 -0
  479. endoreg_db/utils/pipelines/Readme.md +235 -0
  480. endoreg_db/utils/pipelines/__init__.py +0 -0
  481. endoreg_db/utils/pipelines/process_video_dir.py +120 -0
  482. endoreg_db/utils/product/__init__.py +0 -0
  483. endoreg_db/utils/product/sum_emissions.py +20 -0
  484. endoreg_db/utils/product/sum_weights.py +18 -0
  485. endoreg_db/utils/pydantic_models/db_config.py +1 -1
  486. endoreg_db/utils/requirement_helpers.py +0 -0
  487. endoreg_db/utils/requirement_operator_logic/__init__.py +0 -0
  488. endoreg_db/utils/requirement_operator_logic/lab_value_operators.py +578 -0
  489. endoreg_db/utils/requirement_operator_logic/model_evaluators.py +368 -0
  490. endoreg_db/utils/translation.py +27 -0
  491. endoreg_db/utils/validate_video_detailed.py +357 -0
  492. endoreg_db/utils/video/__init__.py +19 -6
  493. endoreg_db/utils/video/extract_frames.py +37 -70
  494. endoreg_db/utils/video/ffmpeg_wrapper.py +772 -0
  495. endoreg_db/utils/video/names.py +42 -0
  496. endoreg_db/utils/video/streaming_processor.py +312 -0
  497. endoreg_db/utils/video/video_splitter.py +94 -0
  498. endoreg_db/views/Frames_NICE_and_PARIS_classifications_views.py +238 -0
  499. endoreg_db/views/__init__.py +282 -2
  500. endoreg_db/views/anonymization/__init__.py +27 -0
  501. endoreg_db/views/anonymization/media_management.py +454 -0
  502. endoreg_db/views/anonymization/overview.py +216 -0
  503. endoreg_db/views/anonymization/validate.py +66 -0
  504. endoreg_db/views/auth/__init__.py +13 -0
  505. endoreg_db/views/{views.py → auth/keycloak.py} +19 -13
  506. endoreg_db/views/examination/__init__.py +33 -0
  507. endoreg_db/views/examination/examination.py +37 -0
  508. endoreg_db/views/examination/examination_manifest_cache.py +26 -0
  509. endoreg_db/views/examination/get_finding_classification_choices.py +59 -0
  510. endoreg_db/views/examination/get_finding_classifications.py +36 -0
  511. endoreg_db/views/examination/get_findings.py +41 -0
  512. endoreg_db/views/examination/get_instruments.py +18 -0
  513. endoreg_db/views/examination/get_interventions.py +14 -0
  514. endoreg_db/views/finding/__init__.py +9 -0
  515. endoreg_db/views/finding/finding.py +112 -0
  516. endoreg_db/views/finding/get_classifications.py +14 -0
  517. endoreg_db/views/finding/get_interventions.py +17 -0
  518. endoreg_db/views/finding_classification/__init__.py +13 -0
  519. endoreg_db/views/finding_classification/base.py +0 -0
  520. endoreg_db/views/finding_classification/finding_classification.py +42 -0
  521. endoreg_db/views/finding_classification/get_classification_choices.py +55 -0
  522. endoreg_db/views/label/__init__.py +5 -0
  523. endoreg_db/views/label/label.py +15 -0
  524. endoreg_db/views/label_video_segment/__init__.py +16 -0
  525. endoreg_db/views/label_video_segment/create_lvs_from_annotation.py +44 -0
  526. endoreg_db/views/label_video_segment/get_lvs_by_name_and_video.py +50 -0
  527. endoreg_db/views/label_video_segment/label_video_segment.py +77 -0
  528. endoreg_db/views/label_video_segment/label_video_segment_by_label.py +174 -0
  529. endoreg_db/views/label_video_segment/label_video_segment_detail.py +73 -0
  530. endoreg_db/views/label_video_segment/update_lvs_from_annotation.py +46 -0
  531. endoreg_db/views/label_video_segment/validate.py +226 -0
  532. endoreg_db/views/media/__init__.py +45 -0
  533. endoreg_db/views/media/pdf_media.py +386 -0
  534. endoreg_db/views/media/segments.py +71 -0
  535. endoreg_db/views/media/sensitive_metadata.py +314 -0
  536. endoreg_db/views/media/video_media.py +272 -0
  537. endoreg_db/views/media/video_segments.py +596 -0
  538. endoreg_db/views/meta/__init__.py +15 -0
  539. endoreg_db/views/meta/available_files_list.py +146 -0
  540. endoreg_db/views/meta/report_meta.py +53 -0
  541. endoreg_db/views/meta/sensitive_meta_detail.py +148 -0
  542. endoreg_db/views/meta/sensitive_meta_list.py +104 -0
  543. endoreg_db/views/meta/sensitive_meta_verification.py +71 -0
  544. endoreg_db/views/misc/__init__.py +63 -0
  545. endoreg_db/views/misc/center.py +13 -0
  546. endoreg_db/views/misc/gender.py +14 -0
  547. endoreg_db/views/misc/secure_file_serving_view.py +80 -0
  548. endoreg_db/views/misc/secure_file_url_view.py +84 -0
  549. endoreg_db/views/misc/secure_url_validate.py +79 -0
  550. endoreg_db/views/misc/stats.py +220 -0
  551. endoreg_db/views/misc/translation.py +182 -0
  552. endoreg_db/views/misc/upload_views.py +240 -0
  553. endoreg_db/views/patient/__init__.py +5 -0
  554. endoreg_db/views/patient/patient.py +210 -0
  555. endoreg_db/views/patient_examination/DEPRECATED_video_backup.py +164 -0
  556. endoreg_db/views/patient_examination/__init__.py +11 -0
  557. endoreg_db/views/patient_examination/patient_examination.py +140 -0
  558. endoreg_db/views/patient_examination/patient_examination_create.py +63 -0
  559. endoreg_db/views/patient_examination/patient_examination_detail.py +66 -0
  560. endoreg_db/views/patient_examination/patient_examination_list.py +68 -0
  561. endoreg_db/views/patient_examination/video.py +194 -0
  562. endoreg_db/views/patient_finding/__init__.py +7 -0
  563. endoreg_db/views/patient_finding/base.py +0 -0
  564. endoreg_db/views/patient_finding/patient_finding.py +64 -0
  565. endoreg_db/views/patient_finding/patient_finding_optimized.py +259 -0
  566. endoreg_db/views/patient_finding_classification/__init__.py +5 -0
  567. endoreg_db/views/patient_finding_classification/pfc_create.py +67 -0
  568. endoreg_db/views/patient_finding_location/__init__.py +5 -0
  569. endoreg_db/views/patient_finding_location/pfl_create.py +70 -0
  570. endoreg_db/views/patient_finding_morphology/__init__.py +5 -0
  571. endoreg_db/views/patient_finding_morphology/pfm_create.py +70 -0
  572. endoreg_db/views/pdf/__init__.py +11 -0
  573. endoreg_db/views/pdf/pdf_media.py +239 -0
  574. endoreg_db/views/pdf/pdf_stream_views.py +127 -0
  575. endoreg_db/views/pdf/reimport.py +161 -0
  576. endoreg_db/views/report/__init__.py +9 -0
  577. endoreg_db/views/report/report_list.py +112 -0
  578. endoreg_db/views/report/report_with_secure_url.py +28 -0
  579. endoreg_db/views/report/start_examination.py +7 -0
  580. endoreg_db/views/requirement/__init__.py +10 -0
  581. endoreg_db/views/requirement/evaluate.py +279 -0
  582. endoreg_db/views/requirement/lookup.py +483 -0
  583. endoreg_db/views/requirement/lookup_store.py +252 -0
  584. endoreg_db/views/requirement_lookup/lookup.py +0 -0
  585. endoreg_db/views/requirement_lookup/lookup_store.py +0 -0
  586. endoreg_db/views/stats/__init__.py +13 -0
  587. endoreg_db/views/stats/stats_views.py +229 -0
  588. endoreg_db/views/video/__init__.py +64 -0
  589. endoreg_db/views/video/correction.py +672 -0
  590. endoreg_db/views/video/reimport.py +195 -0
  591. endoreg_db/views/video/segmentation.py +274 -0
  592. endoreg_db/views/video/task_status.py +49 -0
  593. endoreg_db/views/{views_for_timeline.py → video/timeline.py} +3 -3
  594. endoreg_db/views/video/video_analyze.py +52 -0
  595. endoreg_db/views/video/video_apply_mask.py +48 -0
  596. endoreg_db/views/video/video_correction.py +21 -0
  597. endoreg_db/views/video/video_download_processed.py +58 -0
  598. endoreg_db/views/video/video_examination_viewset.py +329 -0
  599. endoreg_db/views/video/video_media.py +158 -0
  600. endoreg_db/views/video/video_meta.py +29 -0
  601. endoreg_db/views/video/video_processing_history.py +24 -0
  602. endoreg_db/views/video/video_remove_frames.py +48 -0
  603. endoreg_db/views/video/video_reprocess.py +40 -0
  604. endoreg_db/views/video/video_stream.py +306 -0
  605. endoreg_db-0.8.2.dist-info/METADATA +384 -0
  606. endoreg_db-0.8.2.dist-info/RECORD +790 -0
  607. endoreg_db/data/agl_service/data.yaml +0 -19
  608. endoreg_db/data/finding_location_classification/colonoscopy.yaml +0 -46
  609. endoreg_db/data/finding_morphology_classification/colonoscopy.yaml +0 -48
  610. endoreg_db/data/finding_morphology_classification_choice/colonoscopy_size.yaml +0 -57
  611. endoreg_db/management/commands/_load_model_template.py +0 -41
  612. endoreg_db/management/commands/delete_all.py +0 -18
  613. endoreg_db/management/commands/fetch_legacy_image_dataset.py +0 -32
  614. endoreg_db/management/commands/fix_auth_permission.py +0 -20
  615. endoreg_db/management/commands/load_active_model_data.py +0 -45
  616. endoreg_db/management/commands/load_g_play_data.py +0 -113
  617. endoreg_db/management/commands/load_logging_data.py +0 -39
  618. endoreg_db/management/commands/load_lx_data.py +0 -64
  619. endoreg_db/management/commands/load_medication_indication_data.py +0 -63
  620. endoreg_db/management/commands/load_medication_indication_type_data.py +0 -41
  621. endoreg_db/management/commands/load_medication_intake_time_data.py +0 -41
  622. endoreg_db/management/commands/load_medication_schedule_data.py +0 -55
  623. endoreg_db/management/commands/load_network_data.py +0 -57
  624. endoreg_db/migrations/0002_alter_frame_image_alter_rawframe_image.py +0 -23
  625. endoreg_db/migrations/0003_alter_frame_image_alter_rawframe_image.py +0 -23
  626. endoreg_db/migrations/0004_alter_rawvideofile_file_alter_video_file.py +0 -25
  627. endoreg_db/migrations/0005_rawvideofile_frame_count_and_more.py +0 -33
  628. endoreg_db/migrations/0006_frame_extracted_rawframe_extracted.py +0 -23
  629. endoreg_db/migrations/0007_rename_pseudo_patient_video_patient_and_more.py +0 -24
  630. endoreg_db/migrations/0008_remove_reportfile_patient_examination_and_more.py +0 -48
  631. endoreg_db/migrations/0009_requirementoperator_requirementsettype_and_more.py +0 -154
  632. endoreg_db/models/ai_model/active_model.py +0 -9
  633. endoreg_db/models/ai_model/ai_model.py +0 -90
  634. endoreg_db/models/ai_model/lightning/__init__.py +0 -3
  635. endoreg_db/models/ai_model/lightning/predict.py +0 -172
  636. endoreg_db/models/ai_model/lightning/prediction_visualizer.py +0 -55
  637. endoreg_db/models/ai_model/lightning/run_visualizer.py +0 -21
  638. endoreg_db/models/ai_model/model_meta.py +0 -240
  639. endoreg_db/models/annotation/__init__.py +0 -32
  640. endoreg_db/models/annotation/anonymized_image_annotation.py +0 -115
  641. endoreg_db/models/annotation/binary_classification_annotation_task.py +0 -117
  642. endoreg_db/models/annotation/image_classification.py +0 -86
  643. endoreg_db/models/annotation/video_segmentation_annotation.py +0 -52
  644. endoreg_db/models/case/__init__.py +0 -1
  645. endoreg_db/models/case/case.py +0 -34
  646. endoreg_db/models/center/center.py +0 -51
  647. endoreg_db/models/center/center_product.py +0 -33
  648. endoreg_db/models/center/center_waste.py +0 -16
  649. endoreg_db/models/data_file/__init__.py +0 -39
  650. endoreg_db/models/data_file/base_classes/__init__.py +0 -7
  651. endoreg_db/models/data_file/base_classes/abstract_frame.py +0 -98
  652. endoreg_db/models/data_file/base_classes/abstract_pdf.py +0 -127
  653. endoreg_db/models/data_file/base_classes/abstract_video.py +0 -806
  654. endoreg_db/models/data_file/base_classes/frame_helpers.py +0 -17
  655. endoreg_db/models/data_file/base_classes/prepare_bulk_frames.py +0 -19
  656. endoreg_db/models/data_file/base_classes/utils.py +0 -58
  657. endoreg_db/models/data_file/frame.py +0 -29
  658. endoreg_db/models/data_file/import_classes/__init__.py +0 -18
  659. endoreg_db/models/data_file/import_classes/processing_functions/__init__.py +0 -35
  660. endoreg_db/models/data_file/import_classes/processing_functions/pdf.py +0 -28
  661. endoreg_db/models/data_file/import_classes/processing_functions/video.py +0 -260
  662. endoreg_db/models/data_file/import_classes/raw_pdf.py +0 -254
  663. endoreg_db/models/data_file/import_classes/raw_video.py +0 -290
  664. endoreg_db/models/data_file/metadata/sensitive_meta.py +0 -290
  665. endoreg_db/models/data_file/metadata/video_meta.py +0 -199
  666. endoreg_db/models/data_file/report_file.py +0 -56
  667. endoreg_db/models/data_file/video/__init__.py +0 -11
  668. endoreg_db/models/data_file/video/import_meta.py +0 -25
  669. endoreg_db/models/data_file/video/video.py +0 -196
  670. endoreg_db/models/data_file/video_segment.py +0 -214
  671. endoreg_db/models/examination/examination.py +0 -67
  672. endoreg_db/models/finding/__init__.py +0 -11
  673. endoreg_db/models/finding/finding.py +0 -75
  674. endoreg_db/models/finding/finding_location_classification.py +0 -94
  675. endoreg_db/models/finding/finding_morphology_classification.py +0 -89
  676. endoreg_db/models/finding/finding_type.py +0 -22
  677. endoreg_db/models/hardware/__init__.py +0 -2
  678. endoreg_db/models/information_source.py +0 -65
  679. endoreg_db/models/laboratory/__init__.py +0 -1
  680. endoreg_db/models/laboratory/lab_value.py +0 -162
  681. endoreg_db/models/logging/__init__.py +0 -11
  682. endoreg_db/models/logging/agl_service.py +0 -19
  683. endoreg_db/models/logging/base.py +0 -22
  684. endoreg_db/models/logging/log_type.py +0 -23
  685. endoreg_db/models/logging/network_device.py +0 -27
  686. endoreg_db/models/lx/__init__.py +0 -4
  687. endoreg_db/models/lx/client.py +0 -57
  688. endoreg_db/models/lx/identity.py +0 -34
  689. endoreg_db/models/lx/permission.py +0 -18
  690. endoreg_db/models/lx/user.py +0 -16
  691. endoreg_db/models/network/__init__.py +0 -9
  692. endoreg_db/models/network/agl_service.py +0 -38
  693. endoreg_db/models/network/network_device.py +0 -58
  694. endoreg_db/models/network/network_device_type.py +0 -23
  695. endoreg_db/models/other/distribution.py +0 -5
  696. endoreg_db/models/patient/__init__.py +0 -24
  697. endoreg_db/models/patient/patient_examination.py +0 -182
  698. endoreg_db/models/patient/patient_finding.py +0 -143
  699. endoreg_db/models/patient/patient_finding_location.py +0 -120
  700. endoreg_db/models/patient/patient_finding_morphology.py +0 -166
  701. endoreg_db/models/permissions/__init__.py +0 -44
  702. endoreg_db/models/persons/__init__.py +0 -34
  703. endoreg_db/models/persons/examiner/__init__.py +0 -2
  704. endoreg_db/models/persons/examiner/examiner.py +0 -60
  705. endoreg_db/models/persons/examiner/examiner_type.py +0 -2
  706. endoreg_db/models/persons/patient/__init__.py +0 -8
  707. endoreg_db/models/persons/patient/patient.py +0 -389
  708. endoreg_db/models/persons/patient/patient_disease.py +0 -22
  709. endoreg_db/models/persons/patient/patient_event.py +0 -52
  710. endoreg_db/models/persons/patient/patient_lab_sample.py +0 -108
  711. endoreg_db/models/persons/patient/patient_medication.py +0 -59
  712. endoreg_db/models/persons/patient/patient_medication_schedule.py +0 -88
  713. endoreg_db/models/persons/portal_user_information.py +0 -27
  714. endoreg_db/models/prediction/__init__.py +0 -8
  715. endoreg_db/models/prediction/image_classification.py +0 -51
  716. endoreg_db/models/prediction/video_prediction_meta.py +0 -306
  717. endoreg_db/models/product/product.py +0 -110
  718. endoreg_db/models/product/product_group.py +0 -27
  719. endoreg_db/models/product/product_material.py +0 -28
  720. endoreg_db/models/questionnaires/__init__.py +0 -114
  721. endoreg_db/models/quiz/__init__.py +0 -9
  722. endoreg_db/models/quiz/quiz_answer.py +0 -41
  723. endoreg_db/models/quiz/quiz_question.py +0 -54
  724. endoreg_db/models/report_reader/report_reader_config.py +0 -53
  725. endoreg_db/models/rules/__init__.py +0 -5
  726. endoreg_db/queries/get/__init__.py +0 -6
  727. endoreg_db/queries/get/center.py +0 -42
  728. endoreg_db/queries/get/model.py +0 -13
  729. endoreg_db/queries/get/patient.py +0 -14
  730. endoreg_db/queries/get/patient_examination.py +0 -20
  731. endoreg_db/queries/get/report_file.py +0 -33
  732. endoreg_db/queries/get/video.py +0 -31
  733. endoreg_db/serializers/ai_model.py +0 -19
  734. endoreg_db/serializers/annotation.py +0 -14
  735. endoreg_db/serializers/center.py +0 -11
  736. endoreg_db/serializers/examination.py +0 -33
  737. endoreg_db/serializers/frame.py +0 -9
  738. endoreg_db/serializers/hardware.py +0 -21
  739. endoreg_db/serializers/label.py +0 -22
  740. endoreg_db/serializers/patient.py +0 -33
  741. endoreg_db/serializers/prediction.py +0 -10
  742. endoreg_db/serializers/raw_pdf_anony_text_validation.py +0 -137
  743. endoreg_db/serializers/report_file.py +0 -7
  744. endoreg_db/serializers/video.py +0 -20
  745. endoreg_db/serializers/video_segmentation.py +0 -574
  746. endoreg_db/tests.py +0 -3
  747. endoreg_db/utils/legacy_ocr.py +0 -201
  748. endoreg_db/utils/video/transcode_videofile.py +0 -111
  749. endoreg_db/views/patient_views.py +0 -90
  750. endoreg_db/views/raw_pdf_anony_text_validation_views.py +0 -95
  751. endoreg_db/views/raw_pdf_meta_validation_views.py +0 -111
  752. endoreg_db/views/raw_video_meta_validation_views.py +0 -148
  753. endoreg_db/views/report_views.py +0 -96
  754. endoreg_db/views/video_segmentation_views.py +0 -166
  755. endoreg_db-0.6.4.dist-info/METADATA +0 -161
  756. endoreg_db-0.6.4.dist-info/RECORD +0 -470
  757. /endoreg_db/{case_generator/__init__.py → api/serializers/finding_descriptions.py} +0 -0
  758. /endoreg_db/{queries/get/annotation.py → api/views/finding_descriptions.py} +0 -0
  759. /endoreg_db/{queries/get/prediction.py → data/shift/m2.yaml} +0 -0
  760. /endoreg_db/{queries/get/video_import_meta.py → factories/__init__.py} +0 -0
  761. /endoreg_db/{queries/get/video_prediction_meta.py → helpers/__init__.py} +0 -0
  762. /endoreg_db/models/{case_template → administration/case/case_template}/__init__.py +0 -0
  763. /endoreg_db/models/{persons → administration/person}/person.py +0 -0
  764. /endoreg_db/models/{product → administration/product}/__init__.py +0 -0
  765. /endoreg_db/models/{report_reader → media/pdf/report_reader}/__init__.py +0 -0
  766. /endoreg_db/models/{report_reader → media/pdf/report_reader}/report_reader_flag.py +0 -0
  767. /endoreg_db/models/{hardware → medical/hardware}/endoscopy_processor.py +0 -0
  768. /endoreg_db/models/{medication → medical/medication}/__init__.py +0 -0
  769. /endoreg_db/models/{medication → medical/medication}/medication_indication.py +0 -0
  770. /endoreg_db/models/{risk → medical/risk}/__init__.py +0 -0
  771. /endoreg_db/models/{risk → medical/risk}/risk.py +0 -0
  772. /endoreg_db/models/{emission → other/emission}/__init__.py +0 -0
  773. /endoreg_db/models/{rules → rule}/rule_applicator.py +0 -0
  774. /endoreg_db/{case_generator → utils/case_generator}/case_generator.py +0 -0
  775. /endoreg_db/{case_generator → utils/case_generator}/lab_sample_factory.py +0 -0
  776. /endoreg_db/{case_generator → utils/case_generator}/utils.py +0 -0
  777. /endoreg_db/views/{csrf.py → misc/csrf.py} +0 -0
  778. {endoreg_db-0.6.4.dist-info → endoreg_db-0.8.2.dist-info}/WHEEL +0 -0
  779. {endoreg_db-0.6.4.dist-info → endoreg_db-0.8.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1118 @@
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
+ from datetime import date
12
+ import logging
13
+ import sys
14
+ import os
15
+ import shutil
16
+ import time
17
+ from contextlib import contextmanager
18
+ from pathlib import Path
19
+ from typing import Union, Dict, Any, Optional
20
+ from django.db import transaction
21
+ from endoreg_db.models import VideoFile, SensitiveMeta
22
+ from endoreg_db.utils.paths import STORAGE_DIR, RAW_FRAME_DIR, VIDEO_DIR, ANONYM_VIDEO_DIR
23
+ import random
24
+ from lx_anonymizer.ocr import trocr_full_image_ocr
25
+ from numpy import ma
26
+
27
+ # File lock configuration (matches PDF import)
28
+ STALE_LOCK_SECONDS = 600 # 10 minutes - reclaim locks older than this
29
+ MAX_LOCK_WAIT_SECONDS = 90 # New: wait up to 90s for a non-stale lock to clear before skipping
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class VideoImportService():
35
+ """
36
+ Service for importing and anonymizing video files.
37
+ Uses a central video instance pattern for cleaner state management.
38
+
39
+ Features (October 14, 2025):
40
+ - File locking to prevent concurrent processing of the same video
41
+ - Stale lock detection and reclamation (600s timeout)
42
+ - Hash-based duplicate detection
43
+ - Graceful fallback processing without lx_anonymizer
44
+ """
45
+
46
+ def __init__(self, project_root: Path = None):
47
+
48
+ # Set up project root path
49
+ if project_root:
50
+ self.project_root = Path(project_root)
51
+ else:
52
+ self.project_root = Path(__file__).parent.parent.parent.parent
53
+
54
+ # Track processed files to prevent duplicates
55
+ self.processed_files = set(str(file) for file in os.listdir(ANONYM_VIDEO_DIR))
56
+
57
+ self.STORAGE_DIR = STORAGE_DIR
58
+
59
+ # Central video instance and processing context
60
+ self.current_video = None
61
+ self.processing_context: Dict[str, Any] = {}
62
+
63
+ self.logger = logging.getLogger(__name__)
64
+
65
+ @contextmanager
66
+ def _file_lock(self, path: Path):
67
+ """
68
+ Create a file lock to prevent duplicate processing of the same video.
69
+
70
+ This context manager creates a .lock file alongside the video file.
71
+ If the lock file already exists, it checks if it's stale (older than
72
+ STALE_LOCK_SECONDS) and reclaims it if necessary. If it's not stale,
73
+ we now WAIT (up to MAX_LOCK_WAIT_SECONDS) instead of failing immediately.
74
+ """
75
+ lock_path = Path(str(path) + ".lock")
76
+ fd = None
77
+ try:
78
+ deadline = time.time() + MAX_LOCK_WAIT_SECONDS
79
+ while True:
80
+ try:
81
+ # Atomic create; fail if exists
82
+ fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
83
+ break # acquired
84
+ except FileExistsError:
85
+ # Check for stale lock
86
+ age = None
87
+ try:
88
+ st = os.stat(lock_path)
89
+ age = time.time() - st.st_mtime
90
+ except FileNotFoundError:
91
+ # Race: lock removed between exists and stat; retry acquire in next loop
92
+ age = None
93
+
94
+ if age is not None and age > STALE_LOCK_SECONDS:
95
+ try:
96
+ logger.warning(
97
+ "Stale lock detected for %s (age %.0fs). Reclaiming lock...",
98
+ path, age
99
+ )
100
+ lock_path.unlink()
101
+ except Exception as e:
102
+ logger.warning("Failed to remove stale lock %s: %s", lock_path, e)
103
+ # Loop continues and retries acquire immediately
104
+ continue
105
+
106
+ # Not stale: wait until deadline, then give up gracefully
107
+ if time.time() >= deadline:
108
+ raise ValueError(f"File already being processed: {path}")
109
+ time.sleep(1.0)
110
+
111
+ os.write(fd, b"lock")
112
+ os.close(fd)
113
+ fd = None
114
+ yield
115
+ finally:
116
+ try:
117
+ if fd is not None:
118
+ os.close(fd)
119
+ if lock_path.exists():
120
+ lock_path.unlink()
121
+ except OSError:
122
+ pass
123
+
124
+ def processed(self) -> bool:
125
+ """Indicates if the current file has already been processed."""
126
+ return getattr(self, '_processed', False)
127
+
128
+ def import_and_anonymize(
129
+ self,
130
+ file_path: Union[Path, str],
131
+ center_name: str,
132
+ processor_name: str,
133
+ save_video: bool = True,
134
+ delete_source: bool = True,
135
+ ) -> "VideoFile":
136
+ """
137
+ High-level helper that orchestrates the complete video import and anonymization process.
138
+ Uses the central video instance pattern for improved state management.
139
+ """
140
+ try:
141
+ # Initialize processing context
142
+ self._initialize_processing_context(file_path, center_name, processor_name,
143
+ save_video, delete_source)
144
+
145
+ # Validate and prepare file (may raise ValueError if another worker holds a non-stale lock)
146
+ try:
147
+ self._validate_and_prepare_file()
148
+ except ValueError as ve:
149
+ # Relaxed behavior: if another process is working on this file, skip cleanly
150
+ if "already being processed" in str(ve):
151
+ self.logger.info(f"Skipping {file_path}: {ve}")
152
+ return None
153
+ raise
154
+
155
+ # Create or retrieve video instance
156
+ self._create_or_retrieve_video_instance()
157
+
158
+ # Setup processing environment
159
+ self._setup_processing_environment()
160
+
161
+ # Process frames and metadata
162
+ self._process_frames_and_metadata()
163
+
164
+ # Finalize processing
165
+ self._finalize_processing()
166
+
167
+ # Move files and cleanup
168
+ self._cleanup_and_archive()
169
+
170
+ return self.current_video
171
+
172
+ except Exception as e:
173
+ self.logger.error(f"Video import and anonymization failed for {file_path}: {e}")
174
+ self._cleanup_on_error()
175
+ raise
176
+ finally:
177
+ self._cleanup_processing_context()
178
+
179
+ def _initialize_processing_context(self, file_path: Union[Path, str], center_name: str,
180
+ processor_name: str, save_video: bool, delete_source: bool):
181
+ """Initialize the processing context for the current video import."""
182
+ self.processing_context = {
183
+ 'file_path': Path(file_path),
184
+ 'center_name': center_name,
185
+ 'processor_name': processor_name,
186
+ 'save_video': save_video,
187
+ 'delete_source': delete_source,
188
+ 'processing_started': False,
189
+ 'frames_extracted': False,
190
+ 'anonymization_completed': False,
191
+ 'error_reason': None
192
+ }
193
+
194
+ self.logger.info(f"Initialized processing context for: {file_path}")
195
+
196
+ def _validate_and_prepare_file(self):
197
+ """
198
+ Validate the video file and prepare for processing.
199
+
200
+ Uses file locking to prevent concurrent processing of the same video file.
201
+ This prevents race conditions where multiple workers might try to process
202
+ the same video simultaneously.
203
+
204
+ The lock is acquired here and held for the entire import process.
205
+ See _file_lock() for lock reclamation logic.
206
+ """
207
+ file_path = self.processing_context['file_path']
208
+
209
+ # Acquire file lock to prevent concurrent processing
210
+ # Lock will be held until finally block in import_and_anonymize()
211
+ self.processing_context['_lock_context'] = self._file_lock(file_path)
212
+ self.processing_context['_lock_context'].__enter__()
213
+
214
+ self.logger.info("Acquired file lock for: %s", file_path)
215
+
216
+ # Check if already processed (memory-based check)
217
+ if str(file_path) in self.processed_files:
218
+ self.logger.info("File %s already processed, skipping", file_path)
219
+ self._processed = True
220
+ raise ValueError(f"File already processed: {file_path}")
221
+
222
+ # Check file exists
223
+ if not file_path.exists():
224
+ raise FileNotFoundError(f"Video file not found: {file_path}")
225
+
226
+ self.logger.info("File validation completed for: %s", file_path)
227
+
228
+ def _create_or_retrieve_video_instance(self):
229
+ """Create or retrieve the VideoFile instance and move to final storage."""
230
+ # Removed duplicate import of VideoFile (already imported at module level)
231
+
232
+ self.logger.info("Creating VideoFile instance...")
233
+
234
+ self.current_video = VideoFile.create_from_file_initialized(
235
+ file_path=self.processing_context['file_path'],
236
+ center_name=self.processing_context['center_name'],
237
+ processor_name=self.processing_context['processor_name'],
238
+ delete_source=self.processing_context['delete_source'],
239
+ save_video_file=self.processing_context['save_video'],
240
+ )
241
+
242
+ if not self.current_video:
243
+ raise RuntimeError("Failed to create VideoFile instance")
244
+
245
+ # Immediately move to final storage locations
246
+ self._move_to_final_storage()
247
+
248
+ self.logger.info("Created VideoFile with UUID: %s", self.current_video.uuid)
249
+
250
+ # Get and mark processing state
251
+ state = VideoFile.get_or_create_state(self.current_video)
252
+ if not state:
253
+ raise RuntimeError("Failed to create VideoFile state")
254
+
255
+ state.mark_processing_started(save=True)
256
+ self.processing_context['processing_started'] = True
257
+
258
+ def _move_to_final_storage(self):
259
+ """
260
+ Move video from raw_videos to final storage locations.
261
+ - Raw video → /data/videos (raw_file_path)
262
+ - Processed video will later → /data/anonym_videos (file_path)
263
+ """
264
+ from endoreg_db.utils import data_paths
265
+
266
+ source_path = self.processing_context['file_path']
267
+
268
+ # Define target directories
269
+ videos_dir = data_paths["video"] # /data/videos for raw files
270
+ videos_dir.mkdir(parents=True, exist_ok=True)
271
+
272
+ # Create target path for raw video in /data/videos
273
+ ext = Path(self.current_video.active_file_path).suffix or ".mp4"
274
+ video_filename = f"{self.current_video.uuid}{ext}"
275
+ raw_target_path = videos_dir / video_filename
276
+
277
+ # Move source file to raw video storage
278
+ try:
279
+ shutil.move(str(source_path), str(raw_target_path))
280
+ self.logger.info("Moved raw video to: %s", raw_target_path)
281
+ except Exception as e:
282
+ self.logger.error("Failed to move video to final storage: %s", e)
283
+ raise
284
+
285
+ # Update the raw_file path in database (relative to storage root)
286
+ try:
287
+ storage_root = data_paths["storage"]
288
+ relative_path = raw_target_path.relative_to(storage_root)
289
+ self.current_video.raw_file.name = str(relative_path)
290
+ self.current_video.save(update_fields=['raw_file'])
291
+ self.logger.info("Updated raw_file path to: %s", relative_path)
292
+ except Exception as e:
293
+ self.logger.error("Failed to update raw_file path: %s", e)
294
+ # Fallback to simple relative path
295
+ self.current_video.raw_file.name = f"videos/{video_filename}"
296
+ self.current_video.save(update_fields=['raw_file'])
297
+ self.logger.info("Updated raw_file path using fallback: %s", f"videos/{video_filename}")
298
+
299
+
300
+ # Store paths for later processing
301
+ self.processing_context['raw_video_path'] = raw_target_path
302
+ self.processing_context['video_filename'] = video_filename
303
+
304
+ def _setup_processing_environment(self):
305
+ """Setup the processing environment without file movement."""
306
+ # Ensure we have a valid video instance
307
+ if not self.current_video:
308
+ raise RuntimeError("No video instance available for processing environment setup")
309
+
310
+ # Initialize video specifications
311
+ self.current_video.initialize_video_specs()
312
+
313
+ # Initialize frame objects in database
314
+ self.current_video.initialize_frames()
315
+
316
+ # Extract frames BEFORE processing to prevent pipeline 1 conflicts
317
+ self.logger.info("Pre-extracting frames to avoid pipeline conflicts...")
318
+ try:
319
+ frames_extracted = self.current_video.extract_frames(overwrite=False)
320
+ if frames_extracted:
321
+ self.processing_context['frames_extracted'] = True
322
+ self.logger.info("Frame extraction completed successfully")
323
+
324
+ # CRITICAL: Immediately save the frames_extracted state to database
325
+ # to prevent refresh_from_db() in pipeline 1 from overriding it
326
+ state = self.current_video.get_or_create_state()
327
+ if not state.frames_extracted:
328
+ state.frames_extracted = True
329
+ state.save(update_fields=['frames_extracted'])
330
+ self.logger.info("Persisted frames_extracted=True to database")
331
+ else:
332
+ self.logger.warning("Frame extraction failed, but continuing...")
333
+ self.processing_context['frames_extracted'] = False
334
+ except Exception as e:
335
+ self.logger.warning(f"Frame extraction failed during setup: {e}, but continuing...")
336
+ self.processing_context['frames_extracted'] = False
337
+
338
+ # Ensure default patient data
339
+ self._ensure_default_patient_data()
340
+
341
+ self.logger.info("Processing environment setup completed")
342
+
343
+ def _process_frames_and_metadata(self):
344
+ """Process frames and extract metadata with anonymization."""
345
+ # Check frame cleaning availability
346
+ frame_cleaning_available, FrameCleaner, ReportReader = self._ensure_frame_cleaning_available()
347
+
348
+ if not (frame_cleaning_available and self.current_video.raw_file):
349
+ self.logger.warning("Frame cleaning not available or conditions not met, using fallback anonymization.")
350
+ self._fallback_anonymize_video()
351
+ return
352
+
353
+ try:
354
+ self.logger.info("Starting frame-level anonymization with processor ROI masking...")
355
+
356
+ # Get processor ROI information
357
+ processor_roi, endoscope_roi = self._get_processor_roi_info()
358
+
359
+ # Perform frame cleaning with timeout to prevent blocking
360
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
361
+
362
+ with ThreadPoolExecutor(max_workers=1) as executor:
363
+ future = executor.submit(self._perform_frame_cleaning, FrameCleaner, processor_roi, endoscope_roi)
364
+ try:
365
+ # Increased timeout to better accommodate ffmpeg + OCR
366
+ future.result(timeout=300)
367
+ self.processing_context['anonymization_completed'] = True
368
+ self.logger.info("Frame cleaning completed successfully within timeout")
369
+ except FutureTimeoutError:
370
+ self.logger.warning("Frame cleaning timed out; entering grace period check for cleaned output")
371
+ # Grace period: detect if cleaned file appears shortly after timeout
372
+ raw_video_path = self.processing_context.get('raw_video_path')
373
+ video_filename = self.processing_context.get('video_filename', Path(raw_video_path).name if raw_video_path else "video.mp4")
374
+ grace_seconds = 60
375
+ expected_cleaned = self.current_video.processed_file
376
+ found = False
377
+ if expected_cleaned is not None:
378
+ for _ in range(grace_seconds):
379
+ if expected_cleaned.exists():
380
+ self.processing_context['cleaned_video_path'] = expected_cleaned
381
+ self.processing_context['anonymization_completed'] = True
382
+ self.logger.info("Detected cleaned video during grace period: %s", expected_cleaned)
383
+ found = True
384
+ break
385
+ time.sleep(1)
386
+ else:
387
+ self._fallback_anonymize_video()
388
+ if not found:
389
+ raise TimeoutError("Frame cleaning operation timed out - likely Ollama connection issue")
390
+
391
+ except Exception as e:
392
+ self.logger.warning("Frame cleaning failed (reason: %s), falling back to simple copy", e)
393
+ # Try fallback anonymization when frame cleaning fails
394
+ try:
395
+ self._fallback_anonymize_video()
396
+ except Exception as fallback_error:
397
+ self.logger.error("Fallback anonymization also failed: %s", fallback_error)
398
+ # If even fallback fails, mark as not anonymized but continue import
399
+ self.processing_context['anonymization_completed'] = False
400
+ self.processing_context['error_reason'] = f"Frame cleaning failed: {e}, Fallback failed: {fallback_error}"
401
+
402
+
403
+ def _fallback_anonymize_video(self):
404
+ """
405
+ Fallback to create anonymized video if lx_anonymizer is not available.
406
+
407
+ This method tries multiple fallback strategies:
408
+ 1. Use VideoFile.anonymize_video() method if available
409
+ 2. Simple copy of raw video to anonym_videos (no processing)
410
+
411
+ The processed video will be marked in processing_context for _cleanup_and_archive().
412
+ """
413
+ try:
414
+ self.logger.info("Attempting fallback video anonymization...")
415
+
416
+ # Strategy 1: Try VideoFile.pipe_2() method
417
+ if hasattr(self.current_video, 'pipe_2'):
418
+ self.logger.info("Trying VideoFile.pipe_2() method...")
419
+
420
+ # Try to anonymize
421
+ if self.current_video.pipe_2:
422
+ self.logger.info("VideoFile.pipe_2() succeeded")
423
+ self.processing_context['anonymization_completed'] = True
424
+ return
425
+ else:
426
+ self.logger.warning("VideoFile.pipe_2() returned False, trying simple copy fallback")
427
+ else:
428
+ self.logger.warning("VideoFile.pipe_2() method not available")
429
+
430
+ # Strategy 2: Simple copy (no processing, just copy raw to processed)
431
+ self.logger.info("Using simple copy fallback (raw video will be used as 'processed' video)")
432
+
433
+ # The _cleanup_and_archive() method will handle the copy
434
+ # We just need to mark that no real anonymization happened
435
+ self.processing_context['anonymization_completed'] = False
436
+ self.processing_context['use_raw_as_processed'] = True # Signal for cleanup
437
+
438
+ self.logger.warning("Fallback: Video will be imported without anonymization (raw copy used)")
439
+
440
+ except Exception as e:
441
+ self.logger.error(f"Error during fallback anonymization: {e}", exc_info=True)
442
+ self.processing_context['anonymization_completed'] = False
443
+ self.processing_context['error_reason'] = f"Fallback anonymization failed: {e}"
444
+
445
+ def _finalize_processing(self):
446
+ """Finalize processing and update video state."""
447
+ self.logger.info("Updating video processing state...")
448
+
449
+ with transaction.atomic():
450
+ # Update basic processing states
451
+ # Ensure state exists before accessing it
452
+
453
+ if not self.current_video:
454
+ try:
455
+ self.current_video.refresh_from_db()
456
+ except Exception as e:
457
+ self.logger.error(f"Failed to refresh current_video from DB: {e}")
458
+ if not self.current_video:
459
+ raise RuntimeError("No current video instance available for finalization")
460
+
461
+ if not self.current_video.processed_file:
462
+ self.logger.warning("No processed file available for current video")
463
+ self.current_video.processed_file = None # Ensure field is not None
464
+ self.current_video.mark_sensitive_meta_processed = False
465
+ else:
466
+ self.current_video.mark_sensitive_meta_processed = True
467
+
468
+ state = self.current_video.get_or_create_state()
469
+ if not state:
470
+ raise RuntimeError("Failed to get or create video state")
471
+
472
+ # Only mark frames as extracted if they were successfully extracted
473
+ if self.processing_context.get('frames_extracted', False):
474
+ state.frames_extracted = True
475
+ self.logger.info("Marked frames as extracted in state")
476
+ else:
477
+ self.logger.warning("Frames were not extracted, not updating state")
478
+
479
+ # Always mark these as true (metadata extraction attempts were made)
480
+ state.frames_initialized = True
481
+ state.video_meta_extracted = True
482
+ state.text_meta_extracted = True
483
+
484
+ # ✅ FIX: Only mark as processed if anonymization actually completed
485
+ anonymization_completed = self.processing_context.get('anonymization_completed', False)
486
+ if anonymization_completed:
487
+ state.mark_sensitive_meta_processed(save=False)
488
+ self.logger.info("Anonymization completed - marking sensitive meta as processed")
489
+ else:
490
+ self.logger.warning(
491
+ "Anonymization NOT completed - NOT marking as processed. "
492
+ f"Reason: {self.processing_context.get('error_reason', 'Unknown')}"
493
+ )
494
+ # Explicitly mark as NOT processed
495
+ state.sensitive_meta_processed = False
496
+
497
+ # Save all state changes
498
+ state.save()
499
+ self.logger.info("Video processing state updated")
500
+ # Save all state changes
501
+ self.current_video.state.save()
502
+ self.current_video.save()
503
+
504
+ # Signal completion
505
+ self._signal_completion()
506
+
507
+ def _cleanup_and_archive(self):
508
+ """Move processed video to anonym_videos and cleanup."""
509
+ from endoreg_db.utils import data_paths
510
+
511
+ # Define target directory for processed videos
512
+ anonym_videos_dir = data_paths["anonym_video"] # /data/anonym_videos
513
+ anonym_videos_dir.mkdir(parents=True, exist_ok=True)
514
+
515
+ # Check if we have a processed/cleaned video
516
+ processed_video_path = None
517
+
518
+ # Look for cleaned video from frame cleaning process
519
+ if 'cleaned_video_path' in self.processing_context:
520
+ processed_video_path = self.processing_context['cleaned_video_path']
521
+ else:
522
+ # If no processing occurred, copy from raw video location
523
+ raw_video_path = self.processing_context.get('raw_video_path')
524
+ if raw_video_path and Path(raw_video_path).exists():
525
+ video_filename = self.processing_context.get('video_filename', Path(raw_video_path).name)
526
+ processed_filename = f"processed_{video_filename}"
527
+ processed_video_path = Path(raw_video_path).parent / processed_filename
528
+
529
+ # Copy raw to processed location (will be moved to anonym_videos)
530
+ try:
531
+ shutil.copy2(str(raw_video_path), str(processed_video_path))
532
+ self.logger.info("Copied raw video for processing: %s", processed_video_path)
533
+ except Exception as e:
534
+ self.logger.error("Failed to copy raw video: %s", e)
535
+ processed_video_path = None # FIXED: Don't use raw as fallback
536
+
537
+ # Move processed video to anonym_videos ONLY if it exists
538
+ if processed_video_path and Path(processed_video_path).exists():
539
+ try:
540
+ # ✅ Clean filename: no original filename leakage
541
+ ext = Path(processed_video_path).suffix or ".mp4"
542
+ anonym_video_filename = f"anonym_{self.current_video.uuid}{ext}"
543
+ anonym_target_path = anonym_videos_dir / anonym_video_filename
544
+
545
+ # Move processed video to anonym_videos/
546
+ shutil.move(str(processed_video_path), str(anonym_target_path))
547
+ self.logger.info("Moved processed video to: %s", anonym_target_path)
548
+
549
+ # Verify the file actually exists before updating database
550
+ if anonym_target_path.exists():
551
+ try:
552
+ storage_root = data_paths["storage"]
553
+ relative_path = anonym_target_path.relative_to(storage_root)
554
+ # Save relative path (e.g. anonym_videos/anonym_<uuid>.mp4)
555
+ self.current_video.processed_file.name = str(relative_path)
556
+ self.current_video.save(update_fields=["processed_file"])
557
+ self.logger.info("Updated processed_file path to: %s", relative_path)
558
+ except Exception as e:
559
+ self.logger.error("Failed to update processed_file path: %s", e)
560
+ # Fallback to simple relative path
561
+ self.current_video.processed_file.name = f"anonym_videos/{anonym_video_filename}"
562
+ self.current_video.save(update_fields=['processed_file'])
563
+ self.logger.info(
564
+ "Updated processed_file path using fallback: %s",
565
+ f"anonym_videos/{anonym_video_filename}",
566
+ )
567
+
568
+ self.processing_context['anonymization_completed'] = True
569
+ else:
570
+ self.logger.warning("Processed video file not found after move: %s", anonym_target_path)
571
+ except Exception as e:
572
+ self.logger.error("Failed to move processed video to anonym_videos: %s", e)
573
+ else:
574
+ self.logger.warning("No processed video available - processed_file will remain empty")
575
+ # Leave processed_file empty/null - frontend should fall back to raw_file
576
+
577
+ # Cleanup temporary directories
578
+ try:
579
+ from endoreg_db.utils.paths import RAW_FRAME_DIR
580
+ shutil.rmtree(RAW_FRAME_DIR, ignore_errors=True)
581
+ self.logger.debug("Cleaned up temporary frames directory: %s", RAW_FRAME_DIR)
582
+ except Exception as e:
583
+ self.logger.warning("Failed to remove directory %s: %s", RAW_FRAME_DIR, e)
584
+
585
+ # Handle source file deletion - this should already be moved, but check raw_videos
586
+ source_path = self.processing_context['file_path']
587
+ if self.processing_context['delete_source'] and Path(source_path).exists():
588
+ try:
589
+ os.remove(source_path)
590
+ self.logger.info("Removed remaining source file: %s", source_path)
591
+ except Exception as e:
592
+ self.logger.warning("Failed to remove source file %s: %s", source_path, e)
593
+
594
+ # Mark as processed (in-memory tracking)
595
+ self.processed_files.add(str(self.processing_context['file_path']))
596
+
597
+ # Refresh from database and finalize state
598
+ with transaction.atomic():
599
+ self.current_video.refresh_from_db()
600
+ if hasattr(self.current_video, 'state') and self.processing_context.get('anonymization_completed'):
601
+ self.current_video.state.mark_sensitive_meta_processed(save=True)
602
+
603
+ self.logger.info("Import and anonymization completed for VideoFile UUID: %s", self.current_video.uuid)
604
+ self.logger.info("Raw video stored in: /data/videos")
605
+ self.logger.info("Processed video stored in: /data/anonym_videos")
606
+
607
+ def _create_sensitive_file(self, video_instance: "VideoFile" = None, file_path: Union[Path, str] = None) -> Path:
608
+ """
609
+ Create a sensitive file for the given video file by copying the original file and updating the path.
610
+ Uses the central video instance and processing context if parameters not provided.
611
+
612
+ Args:
613
+ video_instance: Optional video instance, defaults to self.current_video
614
+ file_path: Optional file path, defaults to processing_context['file_path']
615
+
616
+ Returns:
617
+ Path: The path to the created sensitive file.
618
+ """
619
+ video_file = video_instance or self.current_video
620
+ # Always use the currently stored raw file path from the model to avoid deleting external source assets
621
+ source_path = None
622
+ try:
623
+ if video_file and hasattr(video_file, 'raw_file') and video_file.raw_file and hasattr(video_file.raw_file, 'path'):
624
+ source_path = Path(video_file.raw_file.path)
625
+ except Exception:
626
+ source_path = None
627
+ # Fallback only if explicitly provided (do NOT default to processing_context input file)
628
+ if source_path is None and file_path is not None:
629
+ source_path = Path(file_path)
630
+
631
+ if not video_file:
632
+ raise ValueError("No video instance available for creating sensitive file")
633
+ if not source_path:
634
+ raise ValueError("No file path available for creating sensitive file")
635
+
636
+ if not video_file.raw_file:
637
+ raise ValueError("VideoFile must have a raw_file to create a sensitive file")
638
+
639
+ # Ensure the target directory exists
640
+ target_dir = VIDEO_DIR / 'sensitive'
641
+ if not target_dir.exists():
642
+ self.logger.info(f"Creating sensitive file directory: {target_dir}")
643
+ os.makedirs(target_dir, exist_ok=True)
644
+
645
+ # Move the stored raw file into the sensitive directory within storage
646
+ target_file_path = target_dir / source_path.name
647
+ try:
648
+ # Prefer a move within the storage to avoid extra disk usage. This does not touch external input files.
649
+ shutil.move(str(source_path), str(target_file_path))
650
+ self.logger.info(f"Moved raw file to sensitive directory: {target_file_path}")
651
+ except Exception as e:
652
+ # Fallback to copy if move fails (e.g., cross-device or permissions), then remove only the original stored raw file
653
+ self.logger.warning(f"Failed to move raw file to sensitive dir, copying instead: {e}")
654
+ shutil.copy(str(source_path), str(target_file_path))
655
+ try:
656
+ # Remove only the stored raw file copy; never touch external input paths here
657
+ os.remove(source_path)
658
+ except FileNotFoundError:
659
+ pass
660
+
661
+ # Update the model to point to the sensitive file location
662
+ # Use relative path from storage root, like in create_from_file.py
663
+ try:
664
+ from endoreg_db.utils import data_paths
665
+ storage_root = data_paths["storage"]
666
+ relative_path = target_file_path.relative_to(storage_root)
667
+ video_file.raw_file.name = str(relative_path)
668
+ video_file.save(update_fields=['raw_file'])
669
+ self.logger.info(f"Updated video.raw_file to point to sensitive location: {relative_path}")
670
+ except Exception as e:
671
+ # Fallback to absolute path conversion if relative path fails
672
+ self.logger.warning(f"Failed to set relative path, using fallback: {e}")
673
+ video_file.raw_file.name = f"videos/sensitive/{target_file_path.name}"
674
+ video_file.save(update_fields=['raw_file'])
675
+ self.logger.info(f"Updated video.raw_file using fallback method: videos/sensitive/{target_file_path.name}")
676
+
677
+ # Important: Do NOT remove the original input asset passed to the service here.
678
+ # Source file cleanup for external inputs is handled by create_from_file via delete_source flag.
679
+
680
+ self.logger.info(f"Created sensitive file for {video_file.uuid} at {target_file_path}")
681
+ return target_file_path
682
+
683
+
684
+
685
+
686
+ def _ensure_frame_cleaning_available(self):
687
+ """
688
+ Ensure frame cleaning modules are available by adding lx-anonymizer to path.
689
+
690
+ Returns:
691
+ Tuple of (availability_flag, FrameCleaner_class, ReportReader_class)
692
+ """
693
+ try:
694
+ # Check if we can find the lx-anonymizer directory
695
+ from importlib import resources
696
+ lx_anonymizer_path = resources.files("lx_anonymizer")
697
+
698
+ if lx_anonymizer_path.exists():
699
+ # Add to Python path temporarily
700
+ if str(lx_anonymizer_path) not in sys.path:
701
+ sys.path.insert(0, str(lx_anonymizer_path))
702
+
703
+ # Try simple import
704
+ from lx_anonymizer import FrameCleaner, ReportReader
705
+
706
+ self.logger.info("Successfully imported lx_anonymizer modules")
707
+
708
+ # Remove from path to avoid conflicts
709
+ if str(lx_anonymizer_path) in sys.path:
710
+ sys.path.remove(str(lx_anonymizer_path))
711
+
712
+ return True, FrameCleaner, ReportReader
713
+
714
+ else:
715
+ self.logger.warning(f"lx-anonymizer path not found: {lx_anonymizer_path}")
716
+
717
+ except Exception as e:
718
+ self.logger.warning(f"Frame cleaning not available: {e}")
719
+
720
+ return False, None, None
721
+
722
+ def _get_processor_roi_info(self):
723
+ """Get processor ROI information for masking."""
724
+ processor_roi = None
725
+ endoscope_roi = None
726
+
727
+ try:
728
+ if self.current_video.video_meta and self.current_video.video_meta.processor:
729
+ processor = getattr(self.current_video.video_meta, "processor", None)
730
+
731
+ # Get the endoscope ROI for masking
732
+ endoscope_roi = processor.get_roi_endoscope_image()
733
+
734
+ # Get all processor ROIs for comprehensive masking
735
+ processor_roi = {
736
+ 'endoscope_image': endoscope_roi,
737
+ 'patient_first_name': processor.get_roi_patient_first_name(),
738
+ 'patient_last_name': processor.get_roi_patient_last_name(),
739
+ 'patient_dob': processor.get_roi_patient_dob(),
740
+ 'examination_date': processor.get_roi_examination_date(),
741
+ 'examination_time': processor.get_roi_examination_time(),
742
+ 'endoscope_type': processor.get_roi_endoscope_type(),
743
+ 'endoscopy_sn': processor.get_roi_endoscopy_sn(),
744
+ }
745
+
746
+ self.logger.info(f"Retrieved processor ROI information: endoscope_roi={endoscope_roi}")
747
+ else:
748
+ self.logger.warning(f"No processor found for video {self.current_video.uuid}, proceeding without ROI masking")
749
+
750
+ except Exception as e:
751
+ self.logger.error(f"Failed to retrieve processor ROI information: {e}")
752
+ # Continue without ROI - don't fail the entire import process
753
+
754
+ return processor_roi, endoscope_roi
755
+
756
+
757
+ def _ensure_default_patient_data(self, video_instance: "VideoFile" = None) -> None:
758
+ """
759
+ Ensure video has minimum required patient data in SensitiveMeta.
760
+ Creates default values if data is missing after OCR processing.
761
+ Uses the central video instance if parameter not provided.
762
+
763
+ Args:
764
+ video_instance: Optional video instance, defaults to self.current_video
765
+ """
766
+ video_file = video_instance or self.current_video
767
+
768
+ if not video_file:
769
+ raise ValueError("No video instance available for ensuring patient data")
770
+
771
+ if not video_file.sensitive_meta:
772
+ self.logger.info(f"No SensitiveMeta found for video {video_file.uuid}, creating default")
773
+
774
+ # Create default SensitiveMeta with placeholder data
775
+ default_data = {
776
+ "patient_first_name": "Patient",
777
+ "patient_last_name": "Unknown",
778
+ "patient_dob": date(1990, 1, 1), # Default DOB
779
+ "examination_date": date.today(),
780
+ "center_name": video_file.center.name if video_file.center else "university_hospital_wuerzburg"
781
+ }
782
+
783
+ try:
784
+ sensitive_meta = SensitiveMeta.create_from_dict(default_data)
785
+ video_file.sensitive_meta = sensitive_meta
786
+ video_file.save(update_fields=['sensitive_meta'])
787
+
788
+ # Mark sensitive meta as processed after creating default data
789
+ state = video_file.get_or_create_state()
790
+ state.mark_sensitive_meta_processed(save=True)
791
+
792
+ self.logger.info(f"Created default SensitiveMeta for video {video_file.uuid}")
793
+ except Exception as e:
794
+ self.logger.error(f"Failed to create default SensitiveMeta for video {video_file.uuid}: {e}")
795
+ return
796
+
797
+ else:
798
+ # Update existing SensitiveMeta with missing fields
799
+ update_needed = False
800
+ update_data = {}
801
+
802
+ if not video_file.sensitive_meta.patient_first_name:
803
+ update_data["patient_first_name"] = "Patient"
804
+ update_needed = True
805
+
806
+ if not video_file.sensitive_meta.patient_last_name:
807
+ update_data["patient_last_name"] = "Unknown"
808
+ update_needed = True
809
+
810
+ if not video_file.sensitive_meta.patient_dob:
811
+ update_data["patient_dob"] = date(1990, 1, 1)
812
+ update_needed = True
813
+
814
+ if not video_file.sensitive_meta.examination_date:
815
+ update_data["examination_date"] = date.today()
816
+ update_needed = True
817
+
818
+ if update_needed:
819
+ try:
820
+ video_file.sensitive_meta.update_from_dict(update_data)
821
+
822
+ # Mark sensitive meta as processed after updating missing fields
823
+ state = video_file.get_or_create_state()
824
+ state.mark_sensitive_meta_processed(save=True)
825
+
826
+ self.logger.info(f"Updated missing SensitiveMeta fields for video {video_file.uuid}: {list(update_data.keys())}")
827
+ except Exception as e:
828
+ self.logger.error(f"Failed to update SensitiveMeta for video {video_file.uuid}: {e}")
829
+
830
+
831
+ def _ensure_frame_cleaning_available(self):
832
+ """
833
+ Ensure frame cleaning modules are available by adding lx-anonymizer to path.
834
+
835
+ Returns:
836
+ Tuple of (availability_flag, FrameCleaner_class, ReportReader_class)
837
+ """
838
+ try:
839
+ # Check if we can find the lx-anonymizer directory
840
+ from importlib import resources
841
+ lx_anonymizer_path = resources.files("lx_anonymizer")
842
+
843
+ if lx_anonymizer_path.exists():
844
+ # Add to Python path temporarily
845
+ if str(lx_anonymizer_path) not in sys.path:
846
+ sys.path.insert(0, str(lx_anonymizer_path))
847
+
848
+ # Try simple import
849
+ from lx_anonymizer import FrameCleaner, ReportReader
850
+
851
+ self.logger.info("Successfully imported lx_anonymizer modules")
852
+
853
+ # Remove from path to avoid conflicts
854
+ if str(lx_anonymizer_path) in sys.path:
855
+ sys.path.remove(str(lx_anonymizer_path))
856
+
857
+ return True, FrameCleaner, ReportReader
858
+
859
+ else:
860
+ self.logger.warning(f"lx-anonymizer path not found: {lx_anonymizer_path}")
861
+
862
+ except Exception as e:
863
+ self.logger.warning(f"Frame cleaning not available: {e}")
864
+
865
+ return False, None, None
866
+
867
+ def _get_processor_roi_info(self):
868
+ """Get processor ROI information for masking."""
869
+ processor_roi = None
870
+ endoscope_roi = None
871
+
872
+ try:
873
+ if self.current_video.video_meta and self.current_video.video_meta.processor:
874
+ processor = getattr(self.current_video.video_meta, "processor", None)
875
+
876
+ # Get the endoscope ROI for masking
877
+ endoscope_roi = processor.get_roi_endoscope_image()
878
+
879
+ # Get all processor ROIs for comprehensive masking
880
+ processor_roi = {
881
+ 'endoscope_image': endoscope_roi,
882
+ 'patient_first_name': processor.get_roi_patient_first_name(),
883
+ 'patient_last_name': processor.get_roi_patient_last_name(),
884
+ 'patient_dob': processor.get_roi_patient_dob(),
885
+ 'examination_date': processor.get_roi_examination_date(),
886
+ 'examination_time': processor.get_roi_examination_time(),
887
+ 'endoscope_type': processor.get_roi_endoscope_type(),
888
+ 'endoscopy_sn': processor.get_roi_endoscopy_sn(),
889
+ }
890
+
891
+ self.logger.info(f"Retrieved processor ROI information: endoscope_roi={endoscope_roi}")
892
+ else:
893
+ self.logger.warning(f"No processor found for video {self.current_video.uuid}, proceeding without ROI masking")
894
+
895
+ except Exception as e:
896
+ self.logger.error(f"Failed to retrieve processor ROI information: {e}")
897
+ # Continue without ROI - don't fail the entire import process
898
+
899
+ return processor_roi, endoscope_roi
900
+
901
+ def _perform_frame_cleaning(self, FrameCleaner, processor_roi, endoscope_roi):
902
+ """Perform frame cleaning and anonymization."""
903
+ # Instantiate frame cleaner
904
+ frame_cleaner = FrameCleaner()
905
+
906
+ # Prepare parameters for frame cleaning
907
+ raw_video_path = self.processing_context.get('raw_video_path')
908
+
909
+ if not raw_video_path or not Path(raw_video_path).exists():
910
+ raise RuntimeError(f"Raw video path not found: {raw_video_path}")
911
+
912
+ # Get processor name safely
913
+ processor = getattr(self.current_video.video_meta, "processor", None) if self.current_video.video_meta else None
914
+ device_name = processor.name if processor else self.processing_context['processor_name']
915
+
916
+ tmp_dir = RAW_FRAME_DIR
917
+
918
+ # Create temporary output path for cleaned video
919
+ video_filename = self.processing_context.get('video_filename', Path(raw_video_path).name)
920
+ cleaned_filename = f"cleaned_{video_filename}"
921
+ cleaned_video_path = Path(raw_video_path).parent / cleaned_filename
922
+
923
+ # Clean video with ROI masking (heavy I/O operation)
924
+ actual_cleaned_path, extracted_metadata = frame_cleaner.clean_video(
925
+ Path(raw_video_path),
926
+ self.current_video,
927
+ tmp_dir,
928
+ device_name,
929
+ endoscope_roi,
930
+ processor_roi,
931
+ cleaned_video_path
932
+ )
933
+
934
+ # Optional: enrich metadata using TrOCR+LLM on one random extracted frame
935
+ try:
936
+ # Prefer frames belonging to this video (UUID in path), else pick any frame
937
+ frame_candidates = list(RAW_FRAME_DIR.rglob("*.jpg")) + list(RAW_FRAME_DIR.rglob("*.png"))
938
+ video_uuid = str(self.current_video.uuid)
939
+ filtered = [p for p in frame_candidates if video_uuid in str(p)] or frame_candidates
940
+ if filtered:
941
+ sample_frame = random.choice(filtered)
942
+ ocr_text = trocr_full_image_ocr(sample_frame)
943
+ if ocr_text:
944
+ llm_metadata = frame_cleaner.extract_metadata(ocr_text)
945
+ if llm_metadata:
946
+ # Merge with already extracted frame-level metadata
947
+ extracted_metadata = frame_cleaner.frame_metadata_extractor.merge_metadata(
948
+ extracted_metadata or {}, llm_metadata
949
+ )
950
+ self.logger.info("LLM metadata extraction (random frame) successful")
951
+ else:
952
+ self.logger.info("LLM metadata extraction (random frame) found no data")
953
+ else:
954
+ self.logger.info("No text extracted by TrOCR on random frame")
955
+ except Exception as e:
956
+ self.logger.error(f"LLM metadata enrichment step failed: {e}")
957
+
958
+ # Store cleaned video path for later use in _cleanup_and_archive
959
+ self.processing_context['cleaned_video_path'] = actual_cleaned_path
960
+ self.processing_context['extracted_metadata'] = extracted_metadata
961
+
962
+ # Update sensitive metadata with extracted information
963
+ self._update_sensitive_metadata(extracted_metadata)
964
+ self.logger.info(f"Extracted metadata from frame cleaning: {extracted_metadata}")
965
+
966
+ self.logger.info(f"Frame cleaning with ROI masking completed: {actual_cleaned_path}")
967
+ self.logger.info("Cleaned video will be moved to anonym_videos during cleanup")
968
+
969
+ def _update_sensitive_metadata(self, extracted_metadata):
970
+ """
971
+ Update sensitive metadata with extracted information.
972
+
973
+ SAFETY MECHANISM: Only updates fields that are empty, default values, or explicitly marked as safe to overwrite.
974
+ This prevents accidentally overwriting valuable manually entered or previously extracted data.
975
+ """
976
+ if not (self.current_video.sensitive_meta and extracted_metadata):
977
+ return
978
+
979
+ sm = self.current_video.sensitive_meta
980
+ updated_fields = []
981
+
982
+ # Map extracted metadata to SensitiveMeta fields
983
+ metadata_mapping = {
984
+ 'patient_first_name': 'patient_first_name',
985
+ 'patient_last_name': 'patient_last_name',
986
+ 'patient_dob': 'patient_dob',
987
+ 'examination_date': 'examination_date',
988
+ 'endoscope_type': 'endoscope_type'
989
+ }
990
+
991
+ # Define default/placeholder values that are safe to overwrite
992
+ SAFE_TO_OVERWRITE_VALUES = [
993
+ 'Vorname unbekannt', # Default first name
994
+ 'Nachname unbekannt', # Default last name
995
+ date(1990, 1, 1), # Default DOB
996
+ None, # Empty values
997
+ '', # Empty strings
998
+ 'N/A', # Placeholder values
999
+ 'Unbekanntes Gerät', # Default device name
1000
+ ]
1001
+
1002
+ for meta_key, sm_field in metadata_mapping.items():
1003
+ if extracted_metadata.get(meta_key) and hasattr(sm, sm_field):
1004
+ old_value = getattr(sm, sm_field)
1005
+ new_value = extracted_metadata[meta_key]
1006
+
1007
+ # Enhanced safety check: Only update if current value is safe to overwrite
1008
+ if new_value and (old_value in SAFE_TO_OVERWRITE_VALUES):
1009
+ self.logger.info(f"Updating {sm_field} from '{old_value}' to '{new_value}' for video {self.current_video.uuid}")
1010
+ setattr(sm, sm_field, new_value)
1011
+ updated_fields.append(sm_field)
1012
+ elif new_value and old_value and old_value not in SAFE_TO_OVERWRITE_VALUES:
1013
+ self.logger.info(f"Preserving existing {sm_field} value '{old_value}' (not overwriting with '{new_value}') for video {self.current_video.uuid}")
1014
+
1015
+ if updated_fields:
1016
+ sm.save(update_fields=updated_fields)
1017
+ self.logger.info(f"Updated SensitiveMeta fields for video {self.current_video.uuid}: {updated_fields}")
1018
+
1019
+ # Mark sensitive meta as processed after successful update
1020
+ self.current_video.state.mark_sensitive_meta_processed(save=True)
1021
+ self.logger.info(f"Marked sensitive metadata as processed for video {self.current_video.uuid}")
1022
+ else:
1023
+ self.logger.info(f"No SensitiveMeta fields updated for video {self.current_video.uuid} - all existing values preserved")
1024
+
1025
+ def _signal_completion(self):
1026
+ """Signal completion to the tracking system."""
1027
+ try:
1028
+ video_processing_complete = (
1029
+ self.current_video.sensitive_meta is not None and
1030
+ self.current_video.video_meta is not None and
1031
+ self.current_video.raw_file and
1032
+ hasattr(self.current_video.raw_file, 'path') and
1033
+ Path(self.current_video.raw_file.path).exists()
1034
+ )
1035
+
1036
+ if video_processing_complete:
1037
+ self.logger.info(f"Video {self.current_video.uuid} processing completed successfully - ready for validation")
1038
+
1039
+ # Update completion flags if they exist
1040
+ completion_fields = []
1041
+ for field_name in ['import_completed', 'processing_complete', 'ready_for_validation']:
1042
+ if hasattr(self.current_video, field_name):
1043
+ setattr(self.current_video, field_name, True)
1044
+ completion_fields.append(field_name)
1045
+
1046
+ if completion_fields:
1047
+ self.current_video.save(update_fields=completion_fields)
1048
+ self.logger.info(f"Updated completion flags: {completion_fields}")
1049
+ else:
1050
+ self.logger.warning(f"Video {self.current_video.uuid} processing incomplete - missing required components")
1051
+
1052
+ except Exception as e:
1053
+ self.logger.warning(f"Failed to signal completion status: {e}")
1054
+
1055
+ def _cleanup_on_error(self):
1056
+ """Cleanup processing context on error."""
1057
+ if self.current_video and hasattr(self.current_video, 'state'):
1058
+ try:
1059
+ if self.processing_context.get('processing_started'):
1060
+ self.current_video.state.frames_extracted = False
1061
+ self.current_video.state.frames_initialized = False
1062
+ self.current_video.state.video_meta_extracted = False
1063
+ self.current_video.state.text_meta_extracted = False
1064
+ self.current_video.state.save()
1065
+ except Exception as e:
1066
+ self.logger.warning(f"Error during cleanup: {e}")
1067
+
1068
+ def _cleanup_processing_context(self):
1069
+ """
1070
+ Cleanup processing context and release file lock.
1071
+
1072
+ This method is always called in the finally block of import_and_anonymize()
1073
+ to ensure the file lock is released even if processing fails.
1074
+ """
1075
+ try:
1076
+ # Release file lock if it was acquired
1077
+ lock_context = self.processing_context.get('_lock_context')
1078
+ if lock_context is not None:
1079
+ try:
1080
+ lock_context.__exit__(None, None, None)
1081
+ self.logger.info("Released file lock")
1082
+ except Exception as e:
1083
+ self.logger.warning(f"Error releasing file lock: {e}")
1084
+
1085
+ # Remove file from processed set if processing failed
1086
+ file_path = self.processing_context.get('file_path')
1087
+ if file_path and not self.processing_context.get('anonymization_completed'):
1088
+ file_path_str = str(file_path)
1089
+ if file_path_str in self.processed_files:
1090
+ self.processed_files.remove(file_path_str)
1091
+ self.logger.info(f"Removed {file_path_str} from processed files (failed processing)")
1092
+
1093
+ except Exception as e:
1094
+ self.logger.warning(f"Error during context cleanup: {e}")
1095
+ finally:
1096
+ # Reset context
1097
+ self.current_video = None
1098
+ self.processing_context = {}
1099
+
1100
+ # Convenience function for callers/tests that expect a module-level import_and_anonymize
1101
+ def import_and_anonymize(
1102
+ file_path,
1103
+ center_name: str,
1104
+ processor_name: str,
1105
+ save_video: bool = True,
1106
+ delete_source: bool = False,
1107
+ ) -> "VideoFile":
1108
+ """Module-level helper that instantiates VideoImportService and runs import_and_anonymize.
1109
+ Kept for backward compatibility with callers that import this function directly.
1110
+ """
1111
+ service = VideoImportService()
1112
+ return service.import_and_anonymize(
1113
+ file_path=file_path,
1114
+ center_name=center_name,
1115
+ processor_name=processor_name,
1116
+ save_video=save_video,
1117
+ delete_source=delete_source,
1118
+ )