endoreg-db 0.8.8.0__py3-none-any.whl → 0.8.9.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 (402) hide show
  1. endoreg_db/data/__init__.py +22 -8
  2. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +0 -1
  3. endoreg_db/data/examination/examinations/data.yaml +114 -14
  4. endoreg_db/data/examination/time-type/data.yaml +0 -3
  5. endoreg_db/data/examination_indication/endoscopy.yaml +108 -173
  6. endoreg_db/data/examination_indication_classification/endoscopy.yaml +0 -70
  7. endoreg_db/data/examination_indication_classification_choice/endoscopy.yaml +33 -37
  8. endoreg_db/data/finding/00_generic.yaml +35 -0
  9. endoreg_db/data/finding/00_generic_complication.yaml +9 -0
  10. endoreg_db/data/finding/01_gastroscopy_baseline.yaml +88 -0
  11. endoreg_db/data/finding/01_gastroscopy_observation.yaml +113 -0
  12. endoreg_db/data/finding/02_colonoscopy_baseline.yaml +53 -0
  13. endoreg_db/data/finding/02_colonoscopy_hidden.yaml +119 -0
  14. endoreg_db/data/finding/02_colonoscopy_observation.yaml +152 -0
  15. endoreg_db/data/finding_classification/00_generic.yaml +44 -0
  16. endoreg_db/data/finding_classification/00_generic_histology.yaml +28 -0
  17. endoreg_db/data/finding_classification/00_generic_lesion.yaml +52 -0
  18. endoreg_db/data/finding_classification/{colonoscopy_bowel_preparation.yaml → 02_colonoscopy_baseline.yaml} +35 -20
  19. endoreg_db/data/finding_classification/02_colonoscopy_histology.yaml +13 -0
  20. endoreg_db/data/finding_classification/02_colonoscopy_other.yaml +12 -0
  21. endoreg_db/data/finding_classification/02_colonoscopy_polyp.yaml +101 -0
  22. endoreg_db/data/finding_classification_choice/{yes_no_na.yaml → 00_generic.yaml} +5 -1
  23. endoreg_db/data/finding_classification_choice/{examination_setting_generic_types.yaml → 00_generic_baseline.yaml} +10 -2
  24. endoreg_db/data/finding_classification_choice/{complication_generic_types.yaml → 00_generic_complication.yaml} +1 -1
  25. endoreg_db/data/finding_classification_choice/{histology.yaml → 00_generic_histology.yaml} +1 -4
  26. endoreg_db/data/finding_classification_choice/00_generic_lesion.yaml +158 -0
  27. endoreg_db/data/finding_classification_choice/{bowel_preparation.yaml → 02_colonoscopy_bowel_preparation.yaml} +1 -30
  28. endoreg_db/data/{_examples/finding_classification_choice/colonoscopy_not_complete_reason.yaml → finding_classification_choice/02_colonoscopy_generic.yaml} +1 -1
  29. endoreg_db/data/finding_classification_choice/{histology_polyp.yaml → 02_colonoscopy_histology.yaml} +1 -1
  30. endoreg_db/data/{_examples/finding_classification_choice/colonoscopy_location.yaml → finding_classification_choice/02_colonoscopy_location.yaml} +23 -4
  31. endoreg_db/data/finding_classification_choice/02_colonoscopy_other.yaml +34 -0
  32. endoreg_db/data/finding_classification_choice/02_colonoscopy_polyp_advanced_imaging.yaml +76 -0
  33. endoreg_db/data/{_examples/finding_classification_choice/colon_lesion_paris.yaml → finding_classification_choice/02_colonoscopy_polyp_morphology.yaml} +26 -8
  34. endoreg_db/data/finding_classification_choice/02_colonoscopy_size.yaml +27 -0
  35. endoreg_db/data/finding_classification_type/{colonoscopy_basic.yaml → 00_generic.yaml} +18 -13
  36. endoreg_db/data/finding_classification_type/02_colonoscopy.yaml +9 -0
  37. endoreg_db/data/finding_intervention/00_generic_endoscopy.yaml +59 -0
  38. endoreg_db/data/finding_intervention/00_generic_endoscopy_ablation.yaml +44 -0
  39. endoreg_db/data/finding_intervention/00_generic_endoscopy_bleeding.yaml +55 -0
  40. endoreg_db/data/finding_intervention/00_generic_endoscopy_resection.yaml +85 -0
  41. endoreg_db/data/finding_intervention/00_generic_endoscopy_stenosis.yaml +17 -0
  42. endoreg_db/data/finding_intervention/00_generic_endoscopy_stent.yaml +9 -0
  43. endoreg_db/data/finding_intervention/01_gastroscopy.yaml +19 -0
  44. endoreg_db/data/finding_intervention/04_eus.yaml +39 -0
  45. endoreg_db/data/finding_intervention/05_ercp.yaml +3 -0
  46. endoreg_db/data/finding_type/data.yaml +8 -12
  47. endoreg_db/data/requirement/01_patient_data.yaml +93 -0
  48. endoreg_db/data/requirement_operator/new_operators.yaml +36 -0
  49. endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +0 -2
  50. endoreg_db/data/requirement_set/90_coloreg.yaml +20 -8
  51. endoreg_db/exceptions.py +0 -1
  52. endoreg_db/forms/examination_form.py +1 -1
  53. endoreg_db/helpers/data_loader.py +124 -52
  54. endoreg_db/helpers/default_objects.py +116 -81
  55. endoreg_db/import_files/__init__.py +27 -0
  56. endoreg_db/import_files/context/__init__.py +7 -0
  57. endoreg_db/import_files/context/default_sensitive_meta.py +81 -0
  58. endoreg_db/import_files/context/ensure_center.py +17 -0
  59. endoreg_db/import_files/context/file_lock.py +66 -0
  60. endoreg_db/import_files/context/import_context.py +43 -0
  61. endoreg_db/import_files/context/validate_directories.py +56 -0
  62. endoreg_db/import_files/file_storage/__init__.py +15 -0
  63. endoreg_db/import_files/file_storage/create_report_file.py +76 -0
  64. endoreg_db/import_files/file_storage/create_video_file.py +75 -0
  65. endoreg_db/import_files/file_storage/sensitive_meta_storage.py +39 -0
  66. endoreg_db/import_files/file_storage/state_management.py +496 -0
  67. endoreg_db/import_files/file_storage/storage.py +36 -0
  68. endoreg_db/import_files/import_service.md +26 -0
  69. endoreg_db/import_files/processing/__init__.py +11 -0
  70. endoreg_db/import_files/processing/report_processing/report_anonymization.py +94 -0
  71. endoreg_db/import_files/processing/sensitive_meta_adapter.py +51 -0
  72. endoreg_db/import_files/processing/video_processing/video_anonymization.py +107 -0
  73. endoreg_db/import_files/pseudonymization/fake.py +52 -0
  74. endoreg_db/import_files/pseudonymization/k_anonymity.py +182 -0
  75. endoreg_db/import_files/pseudonymization/k_pseudonymity.py +128 -0
  76. endoreg_db/import_files/pseudonymization/pseudonymize.py +0 -0
  77. endoreg_db/import_files/report_import_service.py +141 -0
  78. endoreg_db/import_files/video_import_service.py +150 -0
  79. endoreg_db/management/commands/import_report.py +130 -65
  80. endoreg_db/management/commands/import_video_with_classification.py +1 -1
  81. endoreg_db/management/commands/load_ai_model_data.py +5 -5
  82. endoreg_db/management/commands/load_ai_model_label_data.py +9 -7
  83. endoreg_db/management/commands/load_base_db_data.py +5 -134
  84. endoreg_db/management/commands/load_contraindication_data.py +14 -16
  85. endoreg_db/management/commands/load_disease_classification_choices_data.py +15 -18
  86. endoreg_db/management/commands/load_disease_classification_data.py +15 -18
  87. endoreg_db/management/commands/load_disease_data.py +25 -28
  88. endoreg_db/management/commands/load_endoscope_data.py +20 -27
  89. endoreg_db/management/commands/load_event_data.py +14 -16
  90. endoreg_db/management/commands/load_examination_data.py +31 -44
  91. endoreg_db/management/commands/load_examination_indication_data.py +20 -21
  92. endoreg_db/management/commands/load_finding_data.py +52 -80
  93. endoreg_db/management/commands/load_information_source.py +21 -23
  94. endoreg_db/management/commands/load_lab_value_data.py +17 -26
  95. endoreg_db/management/commands/load_medication_data.py +13 -12
  96. endoreg_db/management/commands/load_organ_data.py +15 -19
  97. endoreg_db/management/commands/load_pdf_type_data.py +19 -18
  98. endoreg_db/management/commands/load_profession_data.py +14 -17
  99. endoreg_db/management/commands/load_qualification_data.py +20 -23
  100. endoreg_db/management/commands/load_report_reader_flag_data.py +17 -19
  101. endoreg_db/management/commands/load_requirement_data.py +14 -20
  102. endoreg_db/management/commands/load_risk_data.py +7 -6
  103. endoreg_db/management/commands/load_shift_data.py +20 -23
  104. endoreg_db/management/commands/load_tag_data.py +8 -11
  105. endoreg_db/management/commands/load_unit_data.py +17 -19
  106. endoreg_db/management/commands/start_filewatcher.py +46 -37
  107. endoreg_db/management/commands/validate_video_files.py +1 -5
  108. endoreg_db/migrations/0001_initial.py +1360 -1812
  109. endoreg_db/models/administration/person/patient/patient.py +72 -46
  110. endoreg_db/models/label/__init__.py +2 -2
  111. endoreg_db/models/label/annotation/video_segmentation_annotation.py +18 -26
  112. endoreg_db/models/label/label_video_segment/label_video_segment.py +23 -1
  113. endoreg_db/models/media/pdf/raw_pdf.py +136 -64
  114. endoreg_db/models/media/pdf/report_reader/report_reader_config.py +34 -10
  115. endoreg_db/models/media/processing_history/__init__.py +5 -0
  116. endoreg_db/models/media/processing_history/processing_history.py +96 -0
  117. endoreg_db/models/media/video/create_from_file.py +101 -31
  118. endoreg_db/models/media/video/video_file.py +125 -105
  119. endoreg_db/models/media/video/video_file_io.py +31 -26
  120. endoreg_db/models/medical/contraindication/README.md +1 -0
  121. endoreg_db/models/medical/examination/examination.py +28 -8
  122. endoreg_db/models/medical/examination/examination_indication.py +13 -79
  123. endoreg_db/models/medical/examination/examination_time.py +8 -3
  124. endoreg_db/models/medical/finding/finding.py +5 -12
  125. endoreg_db/models/medical/finding/finding_classification.py +18 -37
  126. endoreg_db/models/medical/finding/finding_intervention.py +7 -9
  127. endoreg_db/models/medical/hardware/endoscope.py +6 -0
  128. endoreg_db/models/medical/patient/medication_examples.py +5 -1
  129. endoreg_db/models/medical/patient/patient_finding.py +1 -1
  130. endoreg_db/models/metadata/pdf_meta.py +22 -10
  131. endoreg_db/models/metadata/sensitive_meta.py +3 -0
  132. endoreg_db/models/metadata/sensitive_meta_logic.py +200 -124
  133. endoreg_db/models/other/information_source.py +27 -6
  134. endoreg_db/models/report/__init__.py +0 -0
  135. endoreg_db/models/report/images.py +0 -0
  136. endoreg_db/models/report/report.py +6 -0
  137. endoreg_db/models/requirement/requirement.py +59 -399
  138. endoreg_db/models/requirement/requirement_operator.py +86 -98
  139. endoreg_db/models/state/audit_ledger.py +4 -5
  140. endoreg_db/models/state/raw_pdf.py +69 -30
  141. endoreg_db/models/state/video.py +65 -49
  142. endoreg_db/models/upload_job.py +33 -9
  143. endoreg_db/models/utils.py +27 -23
  144. endoreg_db/queries/__init__.py +3 -1
  145. endoreg_db/schemas/examination_evaluation.py +1 -1
  146. endoreg_db/serializers/__init__.py +2 -8
  147. endoreg_db/serializers/label_video_segment/label_video_segment.py +2 -29
  148. endoreg_db/serializers/meta/__init__.py +1 -6
  149. endoreg_db/serializers/misc/sensitive_patient_data.py +50 -26
  150. endoreg_db/serializers/patient_examination/patient_examination.py +3 -3
  151. endoreg_db/serializers/pdf/anony_text_validation.py +39 -23
  152. endoreg_db/serializers/video/video_file_list.py +65 -34
  153. endoreg_db/services/__old/pdf_import.py +1487 -0
  154. endoreg_db/services/__old/video_import.py +1306 -0
  155. endoreg_db/services/anonymization.py +63 -26
  156. endoreg_db/services/lookup_service.py +28 -28
  157. endoreg_db/services/lookup_store.py +2 -2
  158. endoreg_db/services/pdf_import.py +0 -1480
  159. endoreg_db/services/report_import.py +10 -0
  160. endoreg_db/services/video_import.py +6 -1165
  161. endoreg_db/tasks/upload_tasks.py +79 -70
  162. endoreg_db/tasks/video_ingest.py +8 -4
  163. endoreg_db/urls/__init__.py +0 -14
  164. endoreg_db/urls/ai.py +32 -0
  165. endoreg_db/urls/media.py +21 -24
  166. endoreg_db/utils/dataloader.py +87 -57
  167. endoreg_db/utils/paths.py +110 -46
  168. endoreg_db/utils/pipelines/Readme.md +1 -1
  169. endoreg_db/utils/requirement_operator_logic/new_operator_logic.py +97 -0
  170. endoreg_db/utils/video/ffmpeg_wrapper.py +217 -52
  171. endoreg_db/views/__init__.py +85 -173
  172. endoreg_db/views/ai/__init__.py +8 -0
  173. endoreg_db/views/ai/label.py +155 -0
  174. endoreg_db/views/anonymization/media_management.py +8 -7
  175. endoreg_db/views/anonymization/overview.py +97 -68
  176. endoreg_db/views/anonymization/validate.py +25 -21
  177. endoreg_db/views/media/__init__.py +5 -20
  178. endoreg_db/views/media/pdf_media.py +109 -65
  179. endoreg_db/views/media/sensitive_metadata.py +163 -148
  180. endoreg_db/views/meta/__init__.py +0 -8
  181. endoreg_db/views/misc/__init__.py +1 -7
  182. endoreg_db/views/misc/upload_views.py +94 -93
  183. endoreg_db/views/report/__init__.py +7 -0
  184. endoreg_db/views/{pdf → report}/reimport.py +45 -24
  185. endoreg_db/views/{pdf/pdf_stream.py → report/report_stream.py} +40 -32
  186. endoreg_db/views/requirement/lookup_store.py +22 -90
  187. endoreg_db/views/video/__init__.py +23 -22
  188. endoreg_db/views/video/correction.py +201 -172
  189. endoreg_db/views/video/reimport.py +1 -1
  190. endoreg_db/views/{media/video_segments.py → video/segments_crud.py} +75 -37
  191. endoreg_db/views/video/{video_meta.py → video_meta_stats.py} +2 -2
  192. endoreg_db/views/video/video_stream.py +7 -8
  193. {endoreg_db-0.8.8.0.dist-info → endoreg_db-0.8.9.2.dist-info}/METADATA +2 -2
  194. {endoreg_db-0.8.8.0.dist-info → endoreg_db-0.8.9.2.dist-info}/RECORD +217 -335
  195. {endoreg_db-0.8.8.0.dist-info → endoreg_db-0.8.9.2.dist-info}/WHEEL +1 -1
  196. endoreg_db/data/_examples/disease.yaml +0 -55
  197. endoreg_db/data/_examples/disease_classification.yaml +0 -13
  198. endoreg_db/data/_examples/disease_classification_choice.yaml +0 -62
  199. endoreg_db/data/_examples/event.yaml +0 -64
  200. endoreg_db/data/_examples/examination.yaml +0 -72
  201. endoreg_db/data/_examples/finding/anatomy_colon.yaml +0 -128
  202. endoreg_db/data/_examples/finding/colonoscopy.yaml +0 -40
  203. endoreg_db/data/_examples/finding/colonoscopy_bowel_prep.yaml +0 -56
  204. endoreg_db/data/_examples/finding/complication.yaml +0 -16
  205. endoreg_db/data/_examples/finding/data.yaml +0 -105
  206. endoreg_db/data/_examples/finding/examination_setting.yaml +0 -16
  207. endoreg_db/data/_examples/finding/medication_related.yaml +0 -18
  208. endoreg_db/data/_examples/finding/outcome.yaml +0 -12
  209. endoreg_db/data/_examples/finding_classification/colonoscopy_bowel_preparation.yaml +0 -68
  210. endoreg_db/data/_examples/finding_classification/colonoscopy_jnet.yaml +0 -22
  211. endoreg_db/data/_examples/finding_classification/colonoscopy_kudo.yaml +0 -25
  212. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_circularity.yaml +0 -20
  213. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_planarity.yaml +0 -24
  214. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_size.yaml +0 -68
  215. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_surface.yaml +0 -20
  216. endoreg_db/data/_examples/finding_classification/colonoscopy_location.yaml +0 -80
  217. endoreg_db/data/_examples/finding_classification/colonoscopy_lst.yaml +0 -21
  218. endoreg_db/data/_examples/finding_classification/colonoscopy_nice.yaml +0 -20
  219. endoreg_db/data/_examples/finding_classification/colonoscopy_paris.yaml +0 -26
  220. endoreg_db/data/_examples/finding_classification/colonoscopy_sano.yaml +0 -22
  221. endoreg_db/data/_examples/finding_classification/colonoscopy_summary.yaml +0 -53
  222. endoreg_db/data/_examples/finding_classification/complication_generic.yaml +0 -25
  223. endoreg_db/data/_examples/finding_classification/examination_setting_generic.yaml +0 -40
  224. endoreg_db/data/_examples/finding_classification/histology_colo.yaml +0 -51
  225. endoreg_db/data/_examples/finding_classification/intervention_required.yaml +0 -26
  226. endoreg_db/data/_examples/finding_classification/medication_related.yaml +0 -23
  227. endoreg_db/data/_examples/finding_classification/visualized.yaml +0 -33
  228. endoreg_db/data/_examples/finding_classification_choice/bowel_preparation.yaml +0 -78
  229. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_circularity_default.yaml +0 -32
  230. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_jnet.yaml +0 -15
  231. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_kudo.yaml +0 -23
  232. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_lst.yaml +0 -15
  233. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_nice.yaml +0 -17
  234. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_planarity_default.yaml +0 -49
  235. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_sano.yaml +0 -14
  236. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_surface_intact_default.yaml +0 -36
  237. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_size.yaml +0 -82
  238. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +0 -15
  239. endoreg_db/data/_examples/finding_classification_choice/complication_generic_types.yaml +0 -15
  240. endoreg_db/data/_examples/finding_classification_choice/examination_setting_generic_types.yaml +0 -15
  241. endoreg_db/data/_examples/finding_classification_choice/histology.yaml +0 -24
  242. endoreg_db/data/_examples/finding_classification_choice/histology_polyp.yaml +0 -20
  243. endoreg_db/data/_examples/finding_classification_choice/outcome.yaml +0 -19
  244. endoreg_db/data/_examples/finding_classification_choice/yes_no_na.yaml +0 -11
  245. endoreg_db/data/_examples/finding_classification_type/colonoscopy_basic.yaml +0 -48
  246. endoreg_db/data/_examples/finding_intervention/endoscopy.yaml +0 -43
  247. endoreg_db/data/_examples/finding_intervention/endoscopy_colonoscopy.yaml +0 -168
  248. endoreg_db/data/_examples/finding_intervention/endoscopy_egd.yaml +0 -128
  249. endoreg_db/data/_examples/finding_intervention/endoscopy_ercp.yaml +0 -32
  250. endoreg_db/data/_examples/finding_intervention/endoscopy_eus_lower.yaml +0 -9
  251. endoreg_db/data/_examples/finding_intervention/endoscopy_eus_upper.yaml +0 -36
  252. endoreg_db/data/_examples/finding_intervention_type/endoscopy.yaml +0 -15
  253. endoreg_db/data/_examples/finding_type/data.yaml +0 -43
  254. endoreg_db/data/_examples/requirement/age.yaml +0 -26
  255. endoreg_db/data/_examples/requirement/gender.yaml +0 -25
  256. endoreg_db/data/_examples/requirement_set/01_endoscopy_generic.yaml +0 -48
  257. endoreg_db/data/_examples/requirement_set/colonoscopy_austria_screening.yaml +0 -57
  258. endoreg_db/data/_examples/requirement_set/endoscopy_bleeding_risk.yaml +0 -52
  259. endoreg_db/data/_examples/yaml_examples.xlsx +0 -0
  260. endoreg_db/data/finding/anatomy_colon.yaml +0 -128
  261. endoreg_db/data/finding/colonoscopy.yaml +0 -40
  262. endoreg_db/data/finding/colonoscopy_bowel_prep.yaml +0 -56
  263. endoreg_db/data/finding/complication.yaml +0 -16
  264. endoreg_db/data/finding/data.yaml +0 -105
  265. endoreg_db/data/finding/examination_setting.yaml +0 -16
  266. endoreg_db/data/finding/medication_related.yaml +0 -18
  267. endoreg_db/data/finding/outcome.yaml +0 -12
  268. endoreg_db/data/finding_classification/colonoscopy_jnet.yaml +0 -22
  269. endoreg_db/data/finding_classification/colonoscopy_kudo.yaml +0 -25
  270. endoreg_db/data/finding_classification/colonoscopy_lesion_circularity.yaml +0 -20
  271. endoreg_db/data/finding_classification/colonoscopy_lesion_planarity.yaml +0 -24
  272. endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +0 -38
  273. endoreg_db/data/finding_classification/colonoscopy_lesion_surface.yaml +0 -20
  274. endoreg_db/data/finding_classification/colonoscopy_location.yaml +0 -49
  275. endoreg_db/data/finding_classification/colonoscopy_lst.yaml +0 -21
  276. endoreg_db/data/finding_classification/colonoscopy_nice.yaml +0 -20
  277. endoreg_db/data/finding_classification/colonoscopy_paris.yaml +0 -26
  278. endoreg_db/data/finding_classification/colonoscopy_sano.yaml +0 -22
  279. endoreg_db/data/finding_classification/colonoscopy_summary.yaml +0 -53
  280. endoreg_db/data/finding_classification/complication_generic.yaml +0 -25
  281. endoreg_db/data/finding_classification/examination_setting_generic.yaml +0 -40
  282. endoreg_db/data/finding_classification/histology_colo.yaml +0 -43
  283. endoreg_db/data/finding_classification/intervention_required.yaml +0 -26
  284. endoreg_db/data/finding_classification/medication_related.yaml +0 -23
  285. endoreg_db/data/finding_classification/visualized.yaml +0 -33
  286. endoreg_db/data/finding_classification_choice/colon_lesion_circularity_default.yaml +0 -32
  287. endoreg_db/data/finding_classification_choice/colon_lesion_jnet.yaml +0 -15
  288. endoreg_db/data/finding_classification_choice/colon_lesion_kudo.yaml +0 -23
  289. endoreg_db/data/finding_classification_choice/colon_lesion_lst.yaml +0 -15
  290. endoreg_db/data/finding_classification_choice/colon_lesion_nice.yaml +0 -17
  291. endoreg_db/data/finding_classification_choice/colon_lesion_paris.yaml +0 -57
  292. endoreg_db/data/finding_classification_choice/colon_lesion_planarity_default.yaml +0 -49
  293. endoreg_db/data/finding_classification_choice/colon_lesion_sano.yaml +0 -14
  294. endoreg_db/data/finding_classification_choice/colon_lesion_surface_intact_default.yaml +0 -36
  295. endoreg_db/data/finding_classification_choice/colonoscopy_location.yaml +0 -229
  296. endoreg_db/data/finding_classification_choice/colonoscopy_not_complete_reason.yaml +0 -19
  297. endoreg_db/data/finding_classification_choice/colonoscopy_size.yaml +0 -82
  298. endoreg_db/data/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +0 -15
  299. endoreg_db/data/finding_classification_choice/outcome.yaml +0 -19
  300. endoreg_db/data/finding_intervention/endoscopy.yaml +0 -43
  301. endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +0 -168
  302. endoreg_db/data/finding_intervention/endoscopy_egd.yaml +0 -128
  303. endoreg_db/data/finding_intervention/endoscopy_ercp.yaml +0 -32
  304. endoreg_db/data/finding_intervention/endoscopy_eus_lower.yaml +0 -9
  305. endoreg_db/data/finding_intervention/endoscopy_eus_upper.yaml +0 -36
  306. endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +0 -79
  307. endoreg_db/data/requirement/age.yaml +0 -26
  308. endoreg_db/data/requirement/colonoscopy_baseline_austria.yaml +0 -45
  309. endoreg_db/data/requirement/disease_cardiovascular.yaml +0 -79
  310. endoreg_db/data/requirement/disease_classification_choice_cardiovascular.yaml +0 -41
  311. endoreg_db/data/requirement/disease_hepatology.yaml +0 -12
  312. endoreg_db/data/requirement/disease_misc.yaml +0 -12
  313. endoreg_db/data/requirement/disease_renal.yaml +0 -96
  314. endoreg_db/data/requirement/endoscopy_bleeding_risk.yaml +0 -59
  315. endoreg_db/data/requirement/event_cardiology.yaml +0 -251
  316. endoreg_db/data/requirement/event_requirements.yaml +0 -145
  317. endoreg_db/data/requirement/finding_colon_polyp.yaml +0 -50
  318. endoreg_db/data/requirement/gender.yaml +0 -25
  319. endoreg_db/data/requirement/lab_value.yaml +0 -441
  320. endoreg_db/data/requirement/medication.yaml +0 -93
  321. endoreg_db/data/requirement_operator/age.yaml +0 -13
  322. endoreg_db/data/requirement_operator/lab_operators.yaml +0 -129
  323. endoreg_db/data/requirement_operator/model_operators.yaml +0 -96
  324. endoreg_db/management/commands/init_default_ai_model.py +0 -112
  325. endoreg_db/management/commands/reset_celery_schedule.py +0 -9
  326. endoreg_db/management/commands/validate_video.py +0 -204
  327. endoreg_db/migrations/0002_requirementset_depends_on.py +0 -18
  328. endoreg_db/migrations/_old/0001_initial.py +0 -1857
  329. endoreg_db/migrations/_old/0002_add_video_correction_models.py +0 -52
  330. endoreg_db/migrations/_old/0003_add_center_display_name.py +0 -30
  331. endoreg_db/migrations/_old/0004_employee_city_employee_post_code_employee_street_and_more.py +0 -68
  332. endoreg_db/migrations/_old/0004_remove_casetemplate_rules_and_more.py +0 -77
  333. endoreg_db/migrations/_old/0005_merge_20251111_1003.py +0 -14
  334. endoreg_db/migrations/_old/0006_sensitivemeta_anonymized_text_and_more.py +0 -68
  335. endoreg_db/migrations/_old/0007_remove_rule_attribute_dtype_remove_rule_rule_type_and_more.py +0 -89
  336. endoreg_db/migrations/_old/0008_remove_event_event_classification_and_more.py +0 -27
  337. endoreg_db/migrations/_old/0009_alter_modelmeta_options_and_more.py +0 -21
  338. endoreg_db/renames.yml +0 -8
  339. endoreg_db/serializers/_old/raw_pdf_meta_validation.py +0 -223
  340. endoreg_db/serializers/_old/raw_video_meta_validation.py +0 -179
  341. endoreg_db/serializers/_old/video.py +0 -71
  342. endoreg_db/serializers/meta/pdf_file_meta_extraction.py +0 -115
  343. endoreg_db/serializers/meta/report_meta.py +0 -53
  344. endoreg_db/serializers/report/__init__.py +0 -9
  345. endoreg_db/serializers/report/mixins.py +0 -45
  346. endoreg_db/serializers/report/report.py +0 -105
  347. endoreg_db/serializers/report/report_list.py +0 -22
  348. endoreg_db/serializers/report/secure_file_url.py +0 -26
  349. endoreg_db/services/requirements_object.py +0 -147
  350. endoreg_db/services/storage_aware_video_processor.py +0 -370
  351. endoreg_db/urls/files.py +0 -6
  352. endoreg_db/urls/label_video_segment_validate.py +0 -33
  353. endoreg_db/urls/label_video_segments.py +0 -46
  354. endoreg_db/views/label/__init__.py +0 -5
  355. endoreg_db/views/label/label.py +0 -15
  356. endoreg_db/views/label_video_segment/__init__.py +0 -16
  357. endoreg_db/views/label_video_segment/create_lvs_from_annotation.py +0 -44
  358. endoreg_db/views/label_video_segment/get_lvs_by_name_and_video.py +0 -50
  359. endoreg_db/views/label_video_segment/label_video_segment.py +0 -77
  360. endoreg_db/views/label_video_segment/label_video_segment_by_label.py +0 -174
  361. endoreg_db/views/label_video_segment/label_video_segment_detail.py +0 -73
  362. endoreg_db/views/label_video_segment/update_lvs_from_annotation.py +0 -46
  363. endoreg_db/views/label_video_segment/validate.py +0 -226
  364. endoreg_db/views/media/segments.py +0 -71
  365. endoreg_db/views/meta/available_files_list.py +0 -146
  366. endoreg_db/views/meta/report_meta.py +0 -53
  367. endoreg_db/views/meta/sensitive_meta_detail.py +0 -85
  368. endoreg_db/views/misc/secure_file_serving_view.py +0 -80
  369. endoreg_db/views/misc/secure_file_url_view.py +0 -84
  370. endoreg_db/views/misc/secure_url_validate.py +0 -79
  371. endoreg_db/views/patient_examination/DEPRECATED_video_backup.py +0 -164
  372. endoreg_db/views/patient_finding_location/__init__.py +0 -5
  373. endoreg_db/views/patient_finding_location/pfl_create.py +0 -70
  374. endoreg_db/views/patient_finding_morphology/__init__.py +0 -5
  375. endoreg_db/views/patient_finding_morphology/pfm_create.py +0 -70
  376. endoreg_db/views/pdf/__init__.py +0 -8
  377. endoreg_db/views/video/segmentation.py +0 -274
  378. endoreg_db/views/video/task_status.py +0 -49
  379. endoreg_db/views/video/timeline.py +0 -46
  380. endoreg_db/views/video/video_analyze.py +0 -52
  381. /endoreg_db/data/requirement/{colon_polyp_intervention.yaml → old/colon_polyp_intervention.yaml} +0 -0
  382. /endoreg_db/data/{_examples/requirement → requirement/old}/colonoscopy_baseline_austria.yaml +0 -0
  383. /endoreg_db/data/requirement/{coloreg_colon_polyp.yaml → old/coloreg_colon_polyp.yaml} +0 -0
  384. /endoreg_db/data/{_examples/requirement → requirement/old}/disease_cardiovascular.yaml +0 -0
  385. /endoreg_db/data/{_examples/requirement → requirement/old}/disease_classification_choice_cardiovascular.yaml +0 -0
  386. /endoreg_db/data/{_examples/requirement → requirement/old}/disease_hepatology.yaml +0 -0
  387. /endoreg_db/data/{_examples/requirement → requirement/old}/disease_misc.yaml +0 -0
  388. /endoreg_db/data/{_examples/requirement → requirement/old}/disease_renal.yaml +0 -0
  389. /endoreg_db/data/{_examples/requirement → requirement/old}/endoscopy_bleeding_risk.yaml +0 -0
  390. /endoreg_db/data/{_examples/requirement → requirement/old}/event_cardiology.yaml +0 -0
  391. /endoreg_db/data/{_examples/requirement → requirement/old}/event_requirements.yaml +0 -0
  392. /endoreg_db/data/{_examples/requirement → requirement/old}/finding_colon_polyp.yaml +0 -0
  393. /endoreg_db/{urls/sensitive_meta.py → data/requirement/old/gender.yaml} +0 -0
  394. /endoreg_db/data/{_examples/requirement → requirement/old}/lab_value.yaml +0 -0
  395. /endoreg_db/data/{_examples/requirement → requirement/old}/medication.yaml +0 -0
  396. /endoreg_db/data/{_examples/requirement_operator → requirement_operator/_old}/age.yaml +0 -0
  397. /endoreg_db/data/{_examples/requirement_operator → requirement_operator/_old}/lab_operators.yaml +0 -0
  398. /endoreg_db/data/{_examples/requirement_operator → requirement_operator/_old}/model_operators.yaml +0 -0
  399. /endoreg_db/{views/pdf/pdf_stream_views.py → import_files/pseudonymization/__init__.py} +0 -0
  400. /endoreg_db/utils/requirement_operator_logic/{lab_value_operators.py → _old/lab_value_operators.py} +0 -0
  401. /endoreg_db/utils/requirement_operator_logic/{model_evaluators.py → _old/model_evaluators.py} +0 -0
  402. {endoreg_db-0.8.8.0.dist-info → endoreg_db-0.8.9.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,1169 +1,10 @@
1
- """
2
- Video import service module.
3
-
4
- Provides high-level functions for importing and anonymizing video files,
5
- combining VideoFile creation with frame-level anonymization.
6
-
7
- Changelog:
8
- October 14, 2025: Added file locking mechanism to prevent race conditions
9
- during concurrent video imports (matches PDF import pattern)
10
- """
11
-
12
- import logging
13
- import os
14
- import shutil
15
- import time
16
- from contextlib import contextmanager
17
- from datetime import date
1
+ from endoreg_db.import_files.video_import_service import VideoImportService as vis
2
+ from endoreg_db.models import VideoFile
18
3
  from pathlib import Path
19
- from typing import Any, Dict, List, Optional, Tuple, Union
20
- import subprocess
21
- from django.db import transaction
22
- from django.db.models.fields.files import FieldFile
23
-
24
- from endoreg_db.models import EndoscopyProcessor, SensitiveMeta, VideoFile
25
- from endoreg_db.models.media.video.video_file_anonymize import _cleanup_raw_assets
26
- from endoreg_db.utils import ensure_local_file, storage_file_exists
27
- from endoreg_db.utils.hashs import get_video_hash
28
- from endoreg_db.utils.paths import ANONYM_VIDEO_DIR, STORAGE_DIR, VIDEO_DIR
29
- from endoreg_db.models.state import VideoState
30
-
31
- # File lock configuration (matches PDF import)
32
- STALE_LOCK_SECONDS = 6000 # 100 minutes - reclaim locks older than this
33
- MAX_LOCK_WAIT_SECONDS = 90 # New: wait up to 90s for a non-stale lock to clear before skipping
34
-
35
- logger = logging.getLogger(__name__)
36
4
 
37
-
38
- class VideoImportService:
39
- """
40
- Service for importing and anonymizing video files.
41
- Uses a central video instance pattern for cleaner state management.
42
-
43
- Features (October 14, 2025):
44
- - File locking to prevent concurrent processing of the same video
45
- - Stale lock detection and reclamation (600s timeout)
46
- - Hash-based duplicate detection
47
- - Graceful fallback processing without lx_anonymizer
5
+ class VideoImportService(vis):
48
6
  """
49
-
50
- def __init__(self, project_root: Optional[Path] = None):
51
- # Set up project root path
52
- if project_root:
53
- self.project_root = Path(project_root)
54
- else:
55
- self.project_root = Path(__file__).parent.parent.parent.parent
56
-
57
- # Track processed files to prevent duplicates
58
- try:
59
- # Ensure anonym_video directory exists before listing files
60
- anonym_video_dir = Path(ANONYM_VIDEO_DIR)
61
- if anonym_video_dir.exists():
62
- self.processed_files = set(str(anonym_video_dir / file) for file in os.listdir(ANONYM_VIDEO_DIR))
63
- else:
64
- logger.info(f"Creating anonym_videos directory: {anonym_video_dir}")
65
- anonym_video_dir.mkdir(parents=True, exist_ok=True)
66
- self.processed_files = set()
67
- except Exception as e:
68
- logger.warning(f"Failed to initialize processed files tracking: {e}")
69
- self.processed_files = set()
70
-
71
- # Central video instance and processing context
72
- self.current_video: Optional[VideoFile] = None
73
- self.processing_context: Dict[str, Any] = {}
74
-
75
- self.delete_source = True
76
- self.original_file_path = None
77
-
78
- self.logger = logging.getLogger(__name__)
79
-
80
- self.current_video_id = Optional[int]
81
-
82
- self.cleaner = None # This gets instantiated in the perform_frame_cleaning method
83
-
84
- def _require_current_video(self) -> VideoFile:
85
- """Return the current VideoFile or raise if it has not been initialized."""
86
- if self.current_video is None:
87
- raise RuntimeError("Current video instance is not set")
88
- return self.current_video
89
-
90
- @contextmanager
91
- def _file_lock(self, path: Path):
92
- """
93
- Create a file lock to prevent duplicate processing of the same video.
94
-
95
- This context manager creates a .lock file alongside the video file.
96
- If the lock file already exists, it checks if it's stale (older than
97
- STALE_LOCK_SECONDS) and reclaims it if necessary. If it's not stale,
98
- we now WAIT (up to MAX_LOCK_WAIT_SECONDS) instead of failing immediately.
99
- """
100
- lock_path = Path(str(path) + ".lock")
101
- fd = None
102
- try:
103
- deadline = time.time() + MAX_LOCK_WAIT_SECONDS
104
- while True:
105
- try:
106
- # Atomic create; fail if exists
107
- fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
108
- break # acquired
109
- except FileExistsError:
110
- # Check for stale lock
111
- age = None
112
- try:
113
- st = os.stat(lock_path)
114
- age = time.time() - st.st_mtime
115
- except FileNotFoundError:
116
- # Race: lock removed between exists and stat; retry acquire in next loop
117
- age = None
118
-
119
- if age is not None and age > STALE_LOCK_SECONDS:
120
- try:
121
- logger.warning(
122
- "Stale lock detected for %s (age %.0fs). Reclaiming lock...",
123
- path,
124
- age,
125
- )
126
- lock_path.unlink()
127
- except Exception as e:
128
- logger.warning("Failed to remove stale lock %s: %s", lock_path, e)
129
- # Loop continues and retries acquire immediately
130
- continue
131
-
132
- # Not stale: wait until deadline, then give up gracefully
133
- if time.time() >= deadline:
134
- raise ValueError(f"File already being processed: {path}")
135
- time.sleep(1.0)
136
-
137
- os.write(fd, b"lock")
138
- os.close(fd)
139
- fd = None
140
- yield
141
- finally:
142
- try:
143
- if fd is not None:
144
- os.close(fd)
145
- if lock_path.exists():
146
- lock_path.unlink()
147
- except OSError:
148
- pass
149
-
150
- def processed(self) -> bool:
151
- """Indicates if the current file has already been processed."""
152
- return getattr(self, "_processed", False)
153
-
154
- def import_and_anonymize(
155
- self,
156
- file_path: Union[Path, str],
157
- center_name: str,
158
- processor_name: str,
159
- save_video: bool = True,
160
- delete_source: bool = True,
161
- ) -> "VideoFile|None":
162
- """
163
- High-level helper that orchestrates the complete video import and anonymization process.
164
- Uses the central video instance pattern for improved state management.
165
- """
166
- # DEFENSIVE: Initialize processing_context immediately to prevent KeyError crashes
167
- self.processing_context = {"file_path": Path(file_path)}
168
-
169
- try:
170
- # Initialize processing context
171
- self._initialize_processing_context(file_path, center_name, processor_name, save_video, delete_source)
172
-
173
- # Validate and prepare file (may raise ValueError if another worker holds a non-stale lock)
174
- try:
175
- self._validate_and_prepare_file()
176
- except ValueError as ve:
177
- # Relaxed behavior: if another process is working on this file, skip cleanly
178
- if "already being processed" in str(ve):
179
- self.logger.info(f"Skipping {file_path}: {ve}")
180
- return None
181
- raise
182
-
183
- # Create or retrieve video instance
184
- self._create_or_retrieve_video_instance()
185
-
186
- # Create sensitive meta file, ensure raw is moved out of processing folder watched by file watcher.
187
- self._create_sensitive_file()
188
-
189
- # Setup processing environment
190
- self._setup_processing_environment()
191
-
192
- # Process frames and metadata
193
- self._process_frames_and_metadata()
194
-
195
- # Finalize processing
196
- self._finalize_processing()
197
-
198
- # Move files and cleanup
199
- self._cleanup_and_archive()
200
-
201
- return self.current_video
202
-
203
- except Exception as e:
204
- # Safe file path access - handles cases where processing_context wasn't initialized
205
- safe_file_path = getattr(self, "processing_context", {}).get("file_path", file_path)
206
- # Debug: Log context state for troubleshooting
207
- context_keys = list(getattr(self, "processing_context", {}).keys())
208
- self.logger.debug(f"Context keys during error: {context_keys}")
209
- self.logger.error(f"Video import and anonymization failed for {safe_file_path}: {e}")
210
- self._cleanup_on_error()
211
- raise
212
- finally:
213
- self._cleanup_processing_context()
214
-
215
- def _initialize_processing_context(
216
- self,
217
- file_path: Union[Path, str],
218
- center_name: str,
219
- processor_name: str,
220
- save_video: bool,
221
- delete_source: bool,
222
- ):
223
- """Initialize the processing context for the current video import."""
224
- self.processing_context = {
225
- "file_path": Path(file_path),
226
- "center_name": center_name,
227
- "processor_name": processor_name,
228
- "save_video": save_video,
229
- "delete_source": delete_source,
230
- "processing_started": False,
231
- "frames_extracted": False,
232
- "anonymization_completed": False,
233
- "error_reason": None,
234
- }
235
- self.original_file_path = str(file_path)
236
-
237
- self.logger.info(f"Initialized processing context for: {file_path}")
238
-
239
- def _validate_and_prepare_file(self):
240
- """
241
- Validate the video file and prepare for processing.
242
-
243
- Uses file locking to prevent concurrent processing of the same video file.
244
- This prevents race conditions where multiple workers might try to process
245
- the same video simultaneously.
246
-
247
- The lock is acquired here and held for the entire import process.
248
- See _file_lock() for lock reclamation logic.
249
- """
250
- file_path = self.processing_context["file_path"]
251
-
252
- # Acquire file lock to prevent concurrent processing
253
- # Lock will be held until finally block in import_and_anonymize()
254
- try:
255
- self.processing_context["_lock_context"] = self._file_lock(file_path)
256
- self.processing_context["_lock_context"].__enter__()
257
- except Exception:
258
- self._cleanup_processing_context()
259
- raise
260
-
261
- self.logger.info("Acquired file lock for: %s", file_path)
262
-
263
- # Check if already processed (memory-based check)
264
- if str(file_path) in self.processed_files:
265
- self.logger.info("File %s already processed, skipping", file_path)
266
- self._processed = True
267
- raise ValueError(f"File already processed: {file_path}")
268
-
269
- # Check file exists
270
- if not file_path.exists():
271
- raise FileNotFoundError(f"Video file not found: {file_path}")
272
-
273
- self.logger.info("File validation completed for: %s", file_path)
274
-
275
- def _create_or_retrieve_video_instance(self):
276
- """Create or retrieve the VideoFile instance and move to final storage."""
277
-
278
- self.logger.info("Creating VideoFile instance...")
279
-
280
- self.current_video = VideoFile.create_from_file_initialized(
281
- file_path=self.processing_context["file_path"],
282
- center_name=self.processing_context["center_name"],
283
- processor_name=self.processing_context["processor_name"],
284
- delete_source=self.processing_context["delete_source"],
285
- save_video_file=self.processing_context["save_video"],
286
- )
287
- self.current_video_id = self.current_video.pk
288
-
289
- if not self.current_video:
290
- raise RuntimeError("Failed to create VideoFile instance")
291
-
292
- # Immediately move to final storage locations
293
- self._move_to_final_storage()
294
-
295
- self.logger.info("Created VideoFile with UUID: %s", self.current_video.uuid)
296
-
297
- # Get and mark processing state
298
- state = VideoFile.get_or_create_state(self.current_video)
299
- if not state:
300
- raise RuntimeError("Failed to create VideoFile state")
301
-
302
- state.mark_processing_started(save=True)
303
- self.processing_context["processing_started"] = True
304
-
305
- def _move_to_final_storage(self):
306
- """
307
- Move video from raw_videos to final storage locations.
308
- - Raw video → /data/videos (raw_file_path)
309
- - Processed video will later → /data/anonym_videos (file_path)
310
- """
311
- from endoreg_db.utils import data_paths
312
-
313
- source_path = Path(self.processing_context["file_path"])
314
- _current_video = self._require_current_video()
315
- videos_dir = Path(data_paths["video"])
316
- storage_root = Path(data_paths["storage"])
317
-
318
- videos_dir.mkdir(parents=True, exist_ok=True)
319
-
320
- # --- Derive stored_raw_path safely ---
321
- stored_raw_path = None
322
- try:
323
- if hasattr(_current_video, "get_raw_file_path"):
324
- candidate = _current_video.get_raw_file_path()
325
- if candidate:
326
- candidate_path = Path(candidate)
327
- # Accept only if under storage_root
328
- try:
329
- candidate_path.relative_to(storage_root)
330
- stored_raw_path = candidate_path
331
- except ValueError:
332
- # outside storage_root, reset
333
- stored_raw_path = None
334
- except Exception:
335
- stored_raw_path = None
336
-
337
- # Fallback: derive from UUID + suffix - ALWAYS use UUID for consistency
338
- if not stored_raw_path:
339
- suffix = source_path.suffix or ".mp4"
340
- uuid_str = getattr(_current_video, "uuid", None)
341
- if uuid_str:
342
- filename = f"{uuid_str}{suffix}"
343
- else:
344
- # Emergency fallback with timestamp to avoid conflicts
345
- import time
346
-
347
- timestamp = int(time.time())
348
- filename = f"video_{timestamp}{suffix}"
349
- self.logger.warning("No UUID available, using timestamp-based filename: %s", filename)
350
- stored_raw_path = videos_dir / filename
351
- self.logger.debug("Using UUID-based raw filename: %s", filename)
352
-
353
- delete_source = bool(self.processing_context.get("delete_source", True))
354
- stored_raw_path.parent.mkdir(parents=True, exist_ok=True)
355
-
356
- # --- Move or copy raw video ---
357
- try:
358
- if delete_source:
359
- # Try atomic move first, fallback to copy+unlink
360
- try:
361
- os.replace(source_path, stored_raw_path)
362
- self.logger.info("Moved raw video to: %s", stored_raw_path)
363
- except Exception:
364
- shutil.copy2(source_path, stored_raw_path)
365
- os.remove(source_path)
366
- self.logger.info("Copied & removed raw video to: %s", stored_raw_path)
367
- else:
368
- shutil.copy2(source_path, stored_raw_path)
369
- self.logger.info("Copied raw video to: %s", stored_raw_path)
370
- except Exception as e:
371
- self.logger.error("Failed to move/copy video to final storage: %s", e)
372
- raise
373
-
374
- # --- Ensure DB raw_file is relative to storage root ---
375
- try:
376
- rel_path = stored_raw_path.relative_to(storage_root)
377
- except Exception:
378
- rel_path = Path("videos") / stored_raw_path.name
379
-
380
- if _current_video.raw_file.name != rel_path.as_posix():
381
- _current_video.raw_file.name = rel_path.as_posix()
382
- _current_video.save(update_fields=["raw_file"])
383
- self.logger.info("Updated raw_file path to: %s", rel_path.as_posix())
384
-
385
- # --- Store for later stages ---
386
- self.processing_context["raw_video_path"] = stored_raw_path
387
- self.processing_context["video_filename"] = stored_raw_path.name
388
-
389
- def _setup_processing_environment(self):
390
- """Setup the processing environment without file movement."""
391
- video = self._require_current_video()
392
-
393
- # Initialize video specifications
394
- video.initialize_video_specs()
395
-
396
- # Extract frames BEFORE processing to prevent pipeline 1 conflicts
397
- self.logger.info("Pre-extracting frames to avoid pipeline conflicts...")
398
- try:
399
- frames_extracted = video.extract_frames(overwrite=False)
400
- if frames_extracted:
401
- self.processing_context["frames_extracted"] = True
402
- self.logger.info("Frame extraction completed successfully")
403
- # Initialize frame objects in database
404
- video.initialize_frames(video.get_frame_paths())
405
-
406
- # CRITICAL: Immediately save the frames_extracted state to database
407
- # to prevent refresh_from_db() in pipeline 1 from overriding it
408
- state = video.get_or_create_state()
409
- if not state.frames_extracted:
410
- state.frames_extracted = True
411
- state.save(update_fields=["frames_extracted"])
412
- self.logger.info("Persisted frames_extracted=True to database")
413
- else:
414
- self.logger.warning("Frame extraction failed, but continuing...")
415
- self.processing_context["frames_extracted"] = False
416
- except Exception as e:
417
- self.logger.warning(f"Frame extraction failed during setup: {e}, but continuing...")
418
- self.processing_context["frames_extracted"] = False
419
-
420
- # Ensure default patient data
421
- self._ensure_default_patient_data(video_instance=video)
422
-
423
- self.logger.info("Processing environment setup completed")
424
-
425
- def _process_frames_and_metadata(self):
426
- """Process frames and extract metadata with anonymization."""
427
- # Check frame cleaning availability
428
- frame_cleaning_available, frame_cleaner = self._ensure_frame_cleaning_available()
429
- video = self._require_current_video()
430
-
431
- raw_file_field = video.raw_file
432
- has_raw_file = isinstance(raw_file_field, FieldFile) and bool(raw_file_field.name)
433
-
434
- if not (frame_cleaning_available and has_raw_file):
435
- self.logger.warning("Frame cleaning not available or conditions not met, using fallback anonymization.")
436
- self._fallback_anonymize_video()
437
- return
438
-
439
- try:
440
- self.logger.info("Starting frame-level anonymization with processor ROI masking...")
441
-
442
- # Get processor ROI information
443
- endoscope_data_roi_nested, endoscope_image_roi = self._get_processor_roi_info()
444
-
445
- # Perform frame cleaning with timeout to prevent blocking
446
- from concurrent.futures import ThreadPoolExecutor
447
- from concurrent.futures import TimeoutError as FutureTimeoutError
448
-
449
- with ThreadPoolExecutor(max_workers=1) as executor:
450
- future = executor.submit(
451
- self._perform_frame_cleaning,
452
- endoscope_data_roi_nested,
453
- endoscope_image_roi,
454
- )
455
- try:
456
- # Increased timeout to better accommodate ffmpeg + OCR
457
- future.result(timeout=5000)
458
- self.processing_context["anonymization_completed"] = True
459
- self.logger.info("Frame cleaning completed successfully within timeout")
460
- except FutureTimeoutError:
461
- self.logger.warning("Frame cleaning timed out; entering grace period check for cleaned output")
462
- # Grace period: detect if cleaned file appears shortly after timeout
463
- grace_seconds = 60
464
- expected_cleaned_path: Optional[Path] = None
465
- processed_field = video.processed_file
466
- if isinstance(processed_field, FieldFile) and processed_field.name:
467
- try:
468
- expected_cleaned_path = Path(processed_field.path)
469
- except (NotImplementedError, TypeError, ValueError):
470
- expected_cleaned_path = None
471
- found = False
472
- if expected_cleaned_path is not None:
473
- for _ in range(grace_seconds):
474
- if expected_cleaned_path.exists():
475
- self.processing_context["cleaned_video_path"] = expected_cleaned_path
476
- self.processing_context["anonymization_completed"] = True
477
- self.logger.info(
478
- "Detected cleaned video during grace period: %s",
479
- expected_cleaned_path,
480
- )
481
- found = True
482
- break
483
- time.sleep(1)
484
- else:
485
- self._fallback_anonymize_video()
486
- if not found:
487
- raise TimeoutError("Frame cleaning operation timed out - likely Ollama connection issue")
488
-
489
- except Exception as e:
490
- self.logger.warning("Frame cleaning failed (reason: %s), falling back to simple copy", e)
491
- # Try fallback anonymization when frame cleaning fails
492
- try:
493
- self._fallback_anonymize_video()
494
- except Exception as fallback_error:
495
- self.logger.error("Fallback anonymization also failed: %s", fallback_error)
496
- # If even fallback fails, mark as not anonymized but continue import
497
- self.processing_context["anonymization_completed"] = False
498
- self.processing_context["error_reason"] = f"Frame cleaning failed: {e}, Fallback failed: {fallback_error}"
499
-
500
- def _save_anonymized_video(self):
501
- original_raw_file_path_to_delete = None
502
- original_raw_frame_dir_to_delete = None
503
- video = self._require_current_video()
504
- anonymized_video_path = video.get_target_anonymized_video_path()
505
-
506
- if not anonymized_video_path.exists():
507
- raise RuntimeError(f"Processed video file not found after assembly for {video.uuid}: {anonymized_video_path}")
508
-
509
- new_processed_hash = get_video_hash(anonymized_video_path)
510
- if video.__class__.objects.filter(processed_video_hash=new_processed_hash).exclude(pk=video.pk).exists():
511
- raise ValueError(f"Processed video hash {new_processed_hash} already exists for another video (Video: {video.uuid}).")
512
-
513
- video.processed_video_hash = new_processed_hash
514
- video.processed_file.name = anonymized_video_path.relative_to(STORAGE_DIR).as_posix()
515
-
516
- update_fields = [
517
- "processed_video_hash",
518
- "processed_file",
519
- "frame_dir",
520
- ]
521
-
522
- if self.delete_source:
523
- original_raw_file_path_to_delete = video.get_raw_file_path()
524
- original_raw_frame_dir_to_delete = video.get_frame_dir_path()
525
-
526
- video.raw_file.name = ""
527
-
528
- update_fields.extend(["raw_file", "video_hash"])
529
-
530
- transaction.on_commit(
531
- lambda: _cleanup_raw_assets(
532
- video_uuid=video.uuid,
533
- raw_file_path=original_raw_file_path_to_delete,
534
- raw_frame_dir=original_raw_frame_dir_to_delete,
535
- )
536
- )
537
-
538
- video.save(update_fields=update_fields)
539
- if not isinstance(video.state, VideoState):
540
- try:
541
- video.get_or_create_state()
542
- except ValueError as e:
543
- raise RuntimeError(f"Video state not found for video {video.uuid}. Error {e}")
544
-
545
- else:
546
- video.state.mark_anonymized(save=True)
547
- video.refresh_from_db()
548
- self.current_video = video
549
-
550
- return True
551
-
552
- def _fallback_anonymize_video(self):
553
- """
554
- Fallback to create anonymized video if lx_anonymizer is not available.
555
- """
556
- try:
557
- self.logger.info("Attempting fallback video anonymization...")
558
- video = self.current_video
559
- if video is None:
560
- self.logger.warning("No VideoFile instance available for fallback anonymization")
561
-
562
- # Strategy 2: Simple copy (no processing, just copy raw to processed)
563
- self.logger.info("Using simple copy fallback (raw video will be used as 'processed' video)")
564
- self.processing_context["anonymization_completed"] = False
565
- self.processing_context["use_raw_as_processed"] = True
566
- self.logger.warning("Fallback: Video will be imported without anonymization (raw copy used)")
567
- except Exception as e:
568
- self.logger.error(f"Error during fallback anonymization: {e}", exc_info=True)
569
- self.processing_context["anonymization_completed"] = False
570
- self.processing_context["error_reason"] = str(e)
571
-
572
- def _finalize_processing(self):
573
- """Finalize processing and update video state."""
574
- self.logger.info("Updating video processing state...")
575
-
576
- with transaction.atomic():
577
- video = self._require_current_video()
578
- try:
579
- video.refresh_from_db()
580
- except Exception as refresh_error:
581
- self.logger.warning(
582
- "Could not refresh VideoFile %s from DB: %s",
583
- video.uuid,
584
- refresh_error,
585
- )
586
-
587
- state = video.get_or_create_state()
588
-
589
- # Only mark frames as extracted if they were successfully extracted
590
- if self.processing_context.get("frames_extracted", False):
591
- state.frames_extracted = True
592
- self.logger.info("Marked frames as extracted in state")
593
- else:
594
- self.logger.warning("Frames were not extracted, not updating state")
595
-
596
- # Always mark these as true (metadata extraction attempts were made)
597
- state.frames_initialized = True
598
- state.video_meta_extracted = True
599
- state.text_meta_extracted = True
600
-
601
- # ✅ FIX: Only mark as processed if anonymization actually completed
602
- anonymization_completed = self.processing_context.get("anonymization_completed", False)
603
- if anonymization_completed:
604
- state.mark_sensitive_meta_processed(save=False)
605
- self.logger.info("Anonymization completed - marking sensitive meta as processed")
606
- else:
607
- self.logger.warning(f"Anonymization NOT completed - NOT marking as processed. Reason: {self.processing_context.get('error_reason', 'Unknown')}")
608
- # Explicitly mark as NOT processed
609
- state.sensitive_meta_processed = False
610
-
611
- # Save all state changes
612
- state.save()
613
- self.logger.info("Video processing state updated")
614
-
615
- # Signal completion
616
- self._signal_completion()
617
-
618
- def _cleanup_and_archive(self):
619
- """Move processed video to anonym_videos and cleanup."""
620
- from endoreg_db.utils import data_paths
621
-
622
- anonym_videos_dir = data_paths["anonym_video"] # /data/anonym_videos
623
- anonym_videos_dir.mkdir(parents=True, exist_ok=True)
624
-
625
- video = self._require_current_video()
626
-
627
- processed_video_path = None
628
- if "cleaned_video_path" in self.processing_context:
629
- processed_video_path = self.processing_context["cleaned_video_path"]
630
- else:
631
- raw_video_path = self.processing_context.get("raw_video_path")
632
- if raw_video_path and Path(raw_video_path).exists():
633
- # Use UUID-based naming to avoid conflicts
634
- suffix = Path(raw_video_path).suffix or ".mp4"
635
- processed_filename = f"processed_{video.uuid}{suffix}"
636
- processed_video_path = Path(raw_video_path).parent / processed_filename
637
- try:
638
- shutil.copy2(str(raw_video_path), str(processed_video_path))
639
- self.logger.info("Copied raw video for processing: %s", processed_video_path)
640
- except Exception as exc:
641
- self.logger.error("Failed to copy raw video: %s", exc)
642
- processed_video_path = None
643
-
644
- if processed_video_path and Path(processed_video_path).exists():
645
- try:
646
- ext = Path(processed_video_path).suffix or ".mp4"
647
- anonym_video_filename = f"anonym_{video.uuid}{ext}"
648
- anonym_target_path = anonym_videos_dir / anonym_video_filename
649
-
650
- shutil.move(str(processed_video_path), str(anonym_target_path))
651
- self.logger.info("Moved processed video to: %s", anonym_target_path)
652
-
653
- if anonym_target_path.exists():
654
- try:
655
- storage_root = data_paths["storage"]
656
- relative_path = anonym_target_path.relative_to(storage_root)
657
- video.processed_file.name = str(relative_path)
658
- video.save(update_fields=["processed_file"])
659
- self.logger.info("Updated processed_file path to: %s", relative_path)
660
- except Exception as exc:
661
- self.logger.error("Failed to update processed_file path: %s", exc)
662
- video.processed_file.name = f"anonym_videos/{anonym_video_filename}"
663
- video.save(update_fields=["processed_file"])
664
- self.logger.info(
665
- "Updated processed_file path using fallback: %s",
666
- f"anonym_videos/{anonym_video_filename}",
667
- )
668
-
669
- self.processing_context["anonymization_completed"] = True
670
- else:
671
- self.logger.warning(
672
- "Processed video file not found after move: %s",
673
- anonym_target_path,
674
- )
675
- except Exception as exc:
676
- self.logger.error("Failed to move processed video to anonym_videos: %s", exc)
677
- else:
678
- self.logger.warning("No processed video available - processed_file will remain empty")
679
-
680
- try:
681
- from endoreg_db.utils.paths import RAW_FRAME_DIR
682
-
683
- shutil.rmtree(RAW_FRAME_DIR, ignore_errors=True)
684
- self.logger.debug("Cleaned up temporary frames directory: %s", RAW_FRAME_DIR)
685
- except Exception as exc:
686
- self.logger.warning("Failed to remove directory %s: %s", RAW_FRAME_DIR, exc)
687
-
688
- source_path = self.processing_context["file_path"]
689
- if self.processing_context["delete_source"] and Path(source_path).exists():
690
- try:
691
- os.remove(source_path)
692
- self.logger.info("Removed remaining source file: %s", source_path)
693
- except Exception as exc:
694
- self.logger.warning("Failed to remove source file %s: %s", source_path, exc)
695
-
696
- if not video.processed_file or not storage_file_exists(video.processed_file):
697
- self.logger.warning("No processed_file found after cleanup - video will be unprocessed")
698
- try:
699
- video.anonymize(delete_original_raw=self.delete_source)
700
- video.save(update_fields=["processed_file"])
701
- self.logger.info("Late-stage anonymization succeeded")
702
- except Exception as e:
703
- self.logger.error("Late-stage anonymization failed: %s", e)
704
- self.processing_context["anonymization_completed"] = False
705
-
706
- self.logger.info("Cleanup and archiving completed")
707
-
708
- self.processed_files.add(str(self.processing_context["file_path"]))
709
-
710
- with transaction.atomic():
711
- video.refresh_from_db()
712
- if hasattr(video, "state") and self.processing_context.get("anonymization_completed"):
713
- if not isinstance(video.state, VideoState):
714
- try:
715
- video.get_or_create_state()
716
- except:
717
- raise RuntimeError(f"Video state not found for video {video.uuid}")
718
-
719
- video.state.mark_sensitive_meta_processed(save=True)
720
-
721
- self.logger.info("Import and anonymization completed for VideoFile UUID: %s", video.uuid)
722
- self.logger.info("Raw video stored in: /data/videos")
723
- self.logger.info("Processed video stored in: /data/anonym_videos")
724
-
725
- def _create_sensitive_file(
726
- self,
727
- video_instance: VideoFile | None = None,
728
- file_path: Path | str | None = None,
729
- ) -> Path:
730
- """Create or move a sensitive copy of the raw video file inside storage."""
731
-
732
- video = video_instance or self._require_current_video()
733
- raw_field: FieldFile | None = getattr(video, "raw_file", None)
734
-
735
- def copy_into_sensitive(source: Path) -> Path:
736
- target_dir = VIDEO_DIR / "sensitive"
737
- if not target_dir.exists():
738
- self.logger.info("Creating sensitive file directory: %s", target_dir)
739
- os.makedirs(target_dir, exist_ok=True)
740
-
741
- target_name = source.name or "raw_video"
742
- target_file_path = target_dir / target_name
743
-
744
- if source != target_file_path:
745
- try:
746
- shutil.copy2(source, target_file_path)
747
- self.logger.info("Copied raw file to sensitive directory: %s", target_file_path)
748
- except Exception as exc:
749
- self.logger.warning("Failed to copy raw file to sensitive dir: %s", exc)
750
- shutil.copy(source, target_file_path)
751
- self.logger.info(
752
- "Fallback copy succeeded for sensitive directory: %s",
753
- target_file_path,
754
- )
755
- else:
756
- self.logger.debug(
757
- "Source path already in sensitive directory: %s",
758
- target_file_path,
759
- )
760
-
761
- return target_file_path
762
-
763
- target_file_path: Path | None = None
764
-
765
- # Prefer an on-disk path from the FieldFile when available
766
- if raw_field:
767
- try:
768
- local_candidate = Path(raw_field.path)
769
- if local_candidate.exists():
770
- target_file_path = copy_into_sensitive(local_candidate)
771
- except Exception:
772
- target_file_path = None
773
-
774
- if target_file_path is None and storage_file_exists(raw_field):
775
- try:
776
- with ensure_local_file(raw_field) as temp_source:
777
- target_file_path = copy_into_sensitive(Path(temp_source))
778
- except Exception as exc:
779
- self.logger.warning("Failed to download raw_field for sensitive copy: %s", exc)
780
-
781
- if target_file_path is None and file_path is not None:
782
- file_candidate = Path(file_path)
783
- if file_candidate.exists():
784
- target_file_path = copy_into_sensitive(file_candidate)
785
-
786
- if target_file_path is None:
787
- context_path = self.processing_context.get("raw_video_path")
788
- if context_path:
789
- context_candidate = Path(context_path)
790
- if context_candidate.exists():
791
- target_file_path = copy_into_sensitive(context_candidate)
792
-
793
- if target_file_path is None:
794
- raise ValueError("No file path available for creating sensitive file")
795
- if not raw_field:
796
- raise ValueError("VideoFile must have a raw_file to create a sensitive file")
797
-
798
- try:
799
- from endoreg_db.utils import data_paths
800
-
801
- storage_root = data_paths["storage"]
802
- relative_path = target_file_path.relative_to(storage_root)
803
- video.raw_file.name = relative_path.as_posix()
804
- video.save(update_fields=["raw_file"])
805
- self.logger.info(
806
- "Updated video.raw_file to point to sensitive location: %s",
807
- relative_path,
808
- )
809
- except Exception as exc:
810
- self.logger.warning("Failed to set relative path, using fallback: %s", exc)
811
- video.raw_file.name = f"videos/sensitive/{target_file_path.name}"
812
- video.save(update_fields=["raw_file"])
813
- self.logger.info(
814
- "Updated video.raw_file using fallback method: videos/sensitive/%s",
815
- target_file_path.name,
816
- )
817
-
818
- self.processing_context["raw_video_path"] = target_file_path
819
- self.processing_context["video_filename"] = target_file_path.name
820
-
821
- self.logger.info("Created sensitive file for %s at %s", video.uuid, target_file_path)
822
- return target_file_path
823
-
824
- def _get_processor_roi_info(
825
- self,
826
- ) -> Tuple[Optional[List[List[Dict[str, Any]]]], Optional[Dict[str, Any]]]:
827
- """Get processor ROI information for masking."""
828
- endoscope_data_roi_nested = None
829
- endoscope_image_roi = None
830
-
831
- video = self._require_current_video()
832
-
833
- try:
834
- video_meta = getattr(video, "video_meta", None)
835
- processor = getattr(video_meta, "processor", None) if video_meta else None
836
- if processor:
837
- assert isinstance(processor, EndoscopyProcessor), "Processor is not of type EndoscopyProcessor"
838
- endoscope_image_roi = processor.get_roi_endoscope_image()
839
- endoscope_data_roi_nested = processor.get_sensitive_rois()
840
- self.logger.info(
841
- "Retrieved processor ROI information: endoscope_image_roi=%s",
842
- endoscope_image_roi,
843
- )
844
- else:
845
- self.logger.warning(
846
- "No processor found for video %s, proceeding without ROI masking",
847
- video.uuid,
848
- )
849
- except Exception as exc:
850
- self.logger.error("Failed to retrieve processor ROI information: %s", exc)
851
-
852
- # Convert dict to nested list if necessary to match return type
853
- if isinstance(endoscope_data_roi_nested, dict):
854
- # Convert dict[str, dict[str, int | None] | None] to List[List[Dict[str, Any]]]
855
- converted_roi = []
856
- for key, value in endoscope_data_roi_nested.items():
857
- if isinstance(value, dict):
858
- converted_roi.append([value])
859
- elif value is None:
860
- converted_roi.append([])
861
- endoscope_data_roi_nested = converted_roi
862
-
863
- return endoscope_data_roi_nested, endoscope_image_roi
864
-
865
- def _ensure_default_patient_data(self, video_instance: VideoFile | None = None) -> None:
866
- """Ensure minimum patient data is present on the video's SensitiveMeta."""
867
-
868
- video = video_instance or self._require_current_video()
869
-
870
- sensitive_meta = getattr(video, "sensitive_meta", None)
871
- if not sensitive_meta:
872
- self.logger.info("No SensitiveMeta found for video %s, creating default", video.uuid)
873
- default_data = {
874
- "patient_first_name": "Patient",
875
- "patient_last_name": "Unknown",
876
- "patient_dob": date(1990, 1, 1),
877
- "examination_date": date.today(),
878
- "center_name": video.center.name if video.center else "university_hospital_wuerzburg",
879
- }
880
- try:
881
- sensitive_meta = SensitiveMeta.create_from_dict(default_data)
882
- video.sensitive_meta = sensitive_meta
883
- video.save(update_fields=["sensitive_meta"])
884
- self.logger.info("Created default SensitiveMeta for video %s", video.uuid)
885
- except Exception as exc:
886
- self.logger.error(
887
- "Failed to create default SensitiveMeta for video %s: %s",
888
- video.uuid,
889
- exc,
890
- )
891
- return
892
- else:
893
- state = video.get_or_create_state()
894
- state.mark_sensitive_meta_processed(save=True)
895
-
896
-
897
- def _ensure_frame_cleaning_available(self):
898
- """
899
- Ensure frame cleaning modules are available by adding lx-anonymizer to path.
900
-
901
- Returns:
902
- Tuple of (availability_flag, FrameCleaner_class, ReportReader_class)
903
- """
904
- try:
905
- from lx_anonymizer import FrameCleaner
906
- except Exception as e:
907
- self.logger.warning(f"Frame cleaning not available: {e} Please install or update lx_anonymizer.")
908
- _available = False
909
- FrameCleaner = None
910
-
911
- assert FrameCleaner is not None
912
- frame_cleaner = FrameCleaner()
913
- _available = True
914
-
915
- return _available, frame_cleaner
916
-
917
- def _perform_frame_cleaning(self, endoscope_data_roi_nested, endoscope_image_roi):
918
- """Perform frame cleaning and anonymization."""
919
- # Instantiate frame cleaner
920
- is_available, frame_cleaner = self._ensure_frame_cleaning_available()
921
-
922
- if not is_available or frame_cleaner is None:
923
- raise RuntimeError("Frame cleaning not available")
924
-
925
- # Prepare parameters for frame cleaning
926
- raw_video_path = self.processing_context.get("raw_video_path")
927
-
928
- if not raw_video_path or not Path(raw_video_path).exists():
929
- try:
930
- self.current_video = self._require_current_video()
931
- raw_video_path = self.current_video.get_raw_file_path()
932
- except Exception:
933
- raise RuntimeError(f"Raw video path not found: {raw_video_path}")
934
-
935
- # Create temporary output path for cleaned video using UUID to avoid naming conflicts
936
- video = self._require_current_video()
937
- # Ensure raw_video_path is not None
938
- if not raw_video_path:
939
- raise RuntimeError("raw_video_path is None, cannot construct cleaned_video_path")
940
- suffix = Path(raw_video_path).suffix or ".mp4"
941
- cleaned_filename = f"cleaned_{video.uuid}{suffix}"
942
- cleaned_video_path = Path(raw_video_path).parent / cleaned_filename
943
- self.logger.debug("Using UUID-based cleaned filename: %s", cleaned_filename)
944
-
945
- # Clean video with ROI masking (heavy I/O operation)
946
- actual_cleaned_path, extracted_metadata = frame_cleaner.clean_video(
947
- video_path=Path(raw_video_path),
948
- endoscope_image_roi=endoscope_image_roi,
949
- endoscope_data_roi_nested=endoscope_data_roi_nested,
950
- output_path=cleaned_video_path,
951
- technique="mask_overlay",
952
- )
953
-
954
- # Store cleaned video path for later use in _cleanup_and_archive
955
- self.processing_context["cleaned_video_path"] = actual_cleaned_path
956
- self.processing_context["extracted_metadata"] = extracted_metadata
957
-
958
- # Update sensitive metadata with extracted information
959
- self._update_sensitive_metadata(extracted_metadata)
960
- self.logger.info(f"Extracted metadata from frame cleaning: {extracted_metadata}")
961
-
962
- self.logger.info(f"Frame cleaning with ROI masking completed: {actual_cleaned_path}")
963
- self.logger.info("Cleaned video will be moved to anonym_videos during cleanup")
964
-
965
- def _update_sensitive_metadata(self, extracted_metadata: Dict[str, Any]):
966
- """
967
- Update sensitive metadata with extracted information.
968
- Args:
969
- extracted_metadata (Dict[str, Any]): Extracted metadata to update.
970
- """
971
- video = self._require_current_video()
972
- sensitive_meta = getattr(video, "sensitive_meta", None)
973
-
974
- if not (sensitive_meta and extracted_metadata):
975
- return
976
-
977
- sm = sensitive_meta
978
- updated_fields = []
979
-
980
- # Ensure center is set from video.center if not in extracted_metadata
981
- metadata_to_update = extracted_metadata.copy()
982
-
983
- # FIX: Set center object instead of center_name string
984
- if not hasattr(sm, "center") or not sm.center:
985
- if video.center:
986
- metadata_to_update["center"] = video.center
987
- self.logger.debug(
988
- "Added center object '%s' to metadata for SensitiveMeta update",
989
- video.center.name,
990
- )
991
- else:
992
- center_name = metadata_to_update.get("center_name")
993
- if center_name:
994
- try:
995
- from ..models.administration import Center
996
-
997
- center_obj = Center.objects.get(name=center_name)
998
- metadata_to_update["center"] = center_obj
999
- self.logger.debug("Loaded center object '%s' from center_name", center_name)
1000
- metadata_to_update.pop("center_name", None)
1001
- except Center.DoesNotExist:
1002
- self.logger.error("Center '%s' not found in database", center_name)
1003
- return
1004
-
1005
- try:
1006
- sm.update_from_dict(metadata_to_update)
1007
- updated_fields = list(extracted_metadata.keys()) # Only log originally extracted fields
1008
- except KeyError as e:
1009
- self.logger.warning(f"Failed to update SensitiveMeta field {e}")
1010
- return
1011
-
1012
- if updated_fields:
1013
- try:
1014
- sm.save() # Remove update_fields to allow all necessary fields to be saved
1015
- self.logger.info(
1016
- "Updated SensitiveMeta fields for video %s: %s",
1017
- video.uuid,
1018
- updated_fields,
1019
- )
1020
-
1021
- state = video.get_or_create_state()
1022
- state.mark_sensitive_meta_processed(save=True)
1023
- self.logger.info("Marked sensitive metadata as processed for video %s", video.uuid)
1024
- except Exception as e:
1025
- self.logger.error(f"Failed to save SensitiveMeta: {e}")
1026
- raise # Re-raise to trigger fallback in calling method
1027
- else:
1028
- self.logger.info(
1029
- "No SensitiveMeta fields updated for video %s - all existing values preserved",
1030
- video.uuid,
1031
- )
1032
-
1033
- def _signal_completion(self):
1034
- """Signal completion to the tracking system."""
1035
- try:
1036
- video = self._require_current_video()
1037
-
1038
- raw_field: FieldFile | None = getattr(video, "raw_file", None)
1039
- raw_exists = storage_file_exists(raw_field)
1040
-
1041
- video_processing_complete = video.sensitive_meta is not None and video.video_meta is not None and raw_exists
1042
-
1043
- if video_processing_complete:
1044
- self.logger.info(
1045
- "Video %s processing completed successfully - ready for validation",
1046
- video.uuid,
1047
- )
1048
-
1049
- # Update completion flags if they exist
1050
- completion_fields = []
1051
- for field_name in [
1052
- "import_completed",
1053
- "processing_complete",
1054
- "ready_for_validation",
1055
- ]:
1056
- if hasattr(video, field_name):
1057
- setattr(video, field_name, True)
1058
- completion_fields.append(field_name)
1059
-
1060
- if completion_fields:
1061
- video.save(update_fields=completion_fields)
1062
- self.logger.info("Updated completion flags: %s", completion_fields)
1063
- else:
1064
- self.logger.warning(
1065
- "Video %s processing incomplete - missing required components",
1066
- video.uuid,
1067
- )
1068
-
1069
- except Exception as e:
1070
- self.logger.warning(f"Failed to signal completion status: {e}")
1071
-
1072
- def _cleanup_on_error(self):
1073
- """Cleanup processing context on error."""
1074
- if self.current_video and hasattr(self.current_video, "state"):
1075
- if self.current_video.state is None:
1076
- try:
1077
- self.current_video.get_or_create_state()
1078
- except Exception as e:
1079
- self.logger.warning(f"Video state not found for video {self.current_video.uuid} during error cleanup {e}")
1080
- return
1081
- self.current_video.state = self.current_video.get_or_create_state()
1082
- try:
1083
- if self.original_file_path is not None:
1084
- assert Path(self.original_file_path).exists()
1085
- else:
1086
- self.logger.warning("Original file path is None")
1087
- self.logger.info("Marked video import as failed in state")
1088
- raw_file_path = getattr(self.current_video.raw_file, "path", None)
1089
- original_file_path = self.original_file_path
1090
- if raw_file_path and original_file_path:
1091
- shutil.copy2(str(raw_file_path), str(original_file_path))
1092
- else:
1093
- self.logger.warning("Cannot restore original raw file: path is None")
1094
- except AssertionError:
1095
- self.logger.warning("Original file path does not exist")
1096
- try:
1097
-
1098
- if not isinstance(self.current_video.state, VideoState):
1099
- logger.error("Current video is none after Assertion for Video File")
1100
- raise AssertionError
1101
-
1102
-
1103
- if self.processing_context.get("processing_started"):
1104
- self.current_video.state.frames_extracted = False
1105
- self.current_video.state.frames_initialized = False
1106
- self.current_video.state.video_meta_extracted = False
1107
- self.current_video.state.text_meta_extracted = False
1108
- self.current_video.state.save()
1109
-
1110
- except Exception as e:
1111
- self.logger.warning(f"Error during cleanup: {e}")
1112
-
1113
-
1114
- def _cleanup_processing_context(self):
1115
- """
1116
- Cleanup processing context and release file lock.
1117
-
1118
- This method is always called in the finally block of import_and_anonymize()
1119
- to ensure the file lock is released even if processing fails.
1120
- """
1121
- # DEFENSIVE: Ensure processing_context exists before accessing it
1122
- if not hasattr(self, "processing_context"):
1123
- self.processing_context = {}
1124
-
1125
- try:
1126
- # Release file lock if it was acquired
1127
- lock_context = self.processing_context.get("_lock_context")
1128
- if lock_context is not None:
1129
- try:
1130
- lock_context.__exit__(None, None, None)
1131
- self.logger.info("Released file lock")
1132
- except Exception as e:
1133
- self.logger.warning(f"Error releasing file lock: {e}")
1134
-
1135
- # Remove file from processed set if processing failed
1136
- file_path = self.processing_context.get("file_path")
1137
- if file_path and not self.processing_context.get("anonymization_completed"):
1138
- file_path_str = str(file_path)
1139
- if file_path_str in self.processed_files:
1140
- self.processed_files.remove(file_path_str)
1141
- self.logger.info(f"Removed {file_path_str} from processed files (failed processing)")
1142
-
1143
- except Exception as e:
1144
- self.logger.warning(f"Error during context cleanup: {e}")
1145
- finally:
1146
- # Reset context
1147
- self.current_video = None
1148
- self.processing_context = {}
1149
-
1150
-
1151
- # Convenience function for callers/tests that expect a module-level import_and_anonymize
1152
- def import_and_anonymize(
1153
- file_path,
1154
- center_name: str,
1155
- processor_name: str,
1156
- save_video: bool = True,
1157
- delete_source: bool = True,
1158
- ) -> VideoFile | None:
1159
- """Module-level helper that instantiates VideoImportService and runs import_and_anonymize.
1160
- Kept for backward compatibility with callers that import this function directly.
7
+ Wrapper for service import of concurrency safe VideoImportService,
8
+ here celery will be implemented in the future.
9
+ 12.02.2025
1161
10
  """
1162
- service = VideoImportService()
1163
- return service.import_and_anonymize(
1164
- file_path=file_path,
1165
- center_name=center_name,
1166
- processor_name=processor_name,
1167
- save_video=save_video,
1168
- delete_source=delete_source,
1169
- )