lightly-studio 0.4.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. lightly_studio/__init__.py +12 -0
  2. lightly_studio/api/__init__.py +0 -0
  3. lightly_studio/api/app.py +131 -0
  4. lightly_studio/api/cache.py +77 -0
  5. lightly_studio/api/db_tables.py +35 -0
  6. lightly_studio/api/features.py +5 -0
  7. lightly_studio/api/routes/api/annotation.py +305 -0
  8. lightly_studio/api/routes/api/annotation_label.py +87 -0
  9. lightly_studio/api/routes/api/annotations/__init__.py +7 -0
  10. lightly_studio/api/routes/api/annotations/create_annotation.py +52 -0
  11. lightly_studio/api/routes/api/caption.py +100 -0
  12. lightly_studio/api/routes/api/classifier.py +384 -0
  13. lightly_studio/api/routes/api/dataset.py +191 -0
  14. lightly_studio/api/routes/api/dataset_tag.py +266 -0
  15. lightly_studio/api/routes/api/embeddings2d.py +90 -0
  16. lightly_studio/api/routes/api/exceptions.py +114 -0
  17. lightly_studio/api/routes/api/export.py +114 -0
  18. lightly_studio/api/routes/api/features.py +17 -0
  19. lightly_studio/api/routes/api/frame.py +241 -0
  20. lightly_studio/api/routes/api/image.py +155 -0
  21. lightly_studio/api/routes/api/metadata.py +161 -0
  22. lightly_studio/api/routes/api/operator.py +75 -0
  23. lightly_studio/api/routes/api/sample.py +103 -0
  24. lightly_studio/api/routes/api/selection.py +87 -0
  25. lightly_studio/api/routes/api/settings.py +41 -0
  26. lightly_studio/api/routes/api/status.py +19 -0
  27. lightly_studio/api/routes/api/text_embedding.py +50 -0
  28. lightly_studio/api/routes/api/validators.py +17 -0
  29. lightly_studio/api/routes/api/video.py +133 -0
  30. lightly_studio/api/routes/healthz.py +13 -0
  31. lightly_studio/api/routes/images.py +104 -0
  32. lightly_studio/api/routes/video_frames_media.py +116 -0
  33. lightly_studio/api/routes/video_media.py +223 -0
  34. lightly_studio/api/routes/webapp.py +51 -0
  35. lightly_studio/api/server.py +94 -0
  36. lightly_studio/core/__init__.py +0 -0
  37. lightly_studio/core/add_samples.py +533 -0
  38. lightly_studio/core/add_videos.py +294 -0
  39. lightly_studio/core/dataset.py +780 -0
  40. lightly_studio/core/dataset_query/__init__.py +14 -0
  41. lightly_studio/core/dataset_query/boolean_expression.py +67 -0
  42. lightly_studio/core/dataset_query/dataset_query.py +317 -0
  43. lightly_studio/core/dataset_query/field.py +113 -0
  44. lightly_studio/core/dataset_query/field_expression.py +79 -0
  45. lightly_studio/core/dataset_query/match_expression.py +23 -0
  46. lightly_studio/core/dataset_query/order_by.py +79 -0
  47. lightly_studio/core/dataset_query/sample_field.py +37 -0
  48. lightly_studio/core/dataset_query/tags_expression.py +46 -0
  49. lightly_studio/core/image_sample.py +36 -0
  50. lightly_studio/core/loading_log.py +56 -0
  51. lightly_studio/core/sample.py +291 -0
  52. lightly_studio/core/start_gui.py +54 -0
  53. lightly_studio/core/video_sample.py +38 -0
  54. lightly_studio/dataset/__init__.py +0 -0
  55. lightly_studio/dataset/edge_embedding_generator.py +155 -0
  56. lightly_studio/dataset/embedding_generator.py +129 -0
  57. lightly_studio/dataset/embedding_manager.py +349 -0
  58. lightly_studio/dataset/env.py +20 -0
  59. lightly_studio/dataset/file_utils.py +49 -0
  60. lightly_studio/dataset/fsspec_lister.py +275 -0
  61. lightly_studio/dataset/mobileclip_embedding_generator.py +158 -0
  62. lightly_studio/dataset/perception_encoder_embedding_generator.py +260 -0
  63. lightly_studio/db_manager.py +166 -0
  64. lightly_studio/dist_lightly_studio_view_app/_app/env.js +1 -0
  65. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.GcXvs2l7.css +1 -0
  66. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/12.Dx6SXgAb.css +1 -0
  67. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/17.9X9_k6TP.css +1 -0
  68. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/18.BxiimdIO.css +1 -0
  69. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/2.CkOblLn7.css +1 -0
  70. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/ClassifierSamplesGrid.BJbCDlvs.css +1 -0
  71. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/LightlyLogo.BNjCIww-.png +0 -0
  72. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Bold.DGvYQtcs.ttf +0 -0
  73. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Italic-VariableFont_wdth_wght.B4AZ-wl6.ttf +0 -0
  74. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Medium.DVUZMR_6.ttf +0 -0
  75. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Regular.DxJTClRG.ttf +0 -0
  76. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-SemiBold.D3TTYgdB.ttf +0 -0
  77. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-VariableFont_wdth_wght.BZBpG5Iz.ttf +0 -0
  78. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.CefECEWA.css +1 -0
  79. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.D5tDcjY-.css +1 -0
  80. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_page.9X9_k6TP.css +1 -0
  81. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_page.BxiimdIO.css +1 -0
  82. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_page.Dx6SXgAb.css +1 -0
  83. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/transform._-1mPSEI.css +1 -0
  84. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/0dDyq72A.js +20 -0
  85. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/69_IOA4Y.js +1 -0
  86. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BK4An2kI.js +1 -0
  87. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BRmB-kJ9.js +1 -0
  88. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B_1cpokE.js +1 -0
  89. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BiqpDEr0.js +1 -0
  90. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BpLiSKgx.js +1 -0
  91. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BscxbINH.js +39 -0
  92. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C1FmrZbK.js +1 -0
  93. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C80h3dJx.js +1 -0
  94. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C8mfFM-u.js +2 -0
  95. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CGY1p9L4.js +517 -0
  96. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/COfLknXM.js +1 -0
  97. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWj6FrbW.js +1 -0
  98. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CYgJF_JY.js +1 -0
  99. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CmLg0ys7.js +1 -0
  100. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvGjimpO.js +1 -0
  101. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D3RDXHoj.js +39 -0
  102. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D4y7iiT3.js +1 -0
  103. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D9SC3jBb.js +1 -0
  104. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DCuAdx1Q.js +20 -0
  105. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DDBy-_jD.js +1 -0
  106. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIeogL5L.js +1 -0
  107. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DL9a7v5o.js +1 -0
  108. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DSKECuqX.js +39 -0
  109. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D_FFv0Oe.js +1 -0
  110. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DiZ5o5vz.js +1 -0
  111. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DkbXUtyG.js +1 -0
  112. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DmK2hulV.js +1 -0
  113. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DqnHaLTj.js +1 -0
  114. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DtWZc_tl.js +1 -0
  115. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DuUalyFS.js +1 -0
  116. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DwIonDAZ.js +1 -0
  117. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Il-mSPmK.js +1 -0
  118. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/KNLP4aJU.js +1 -0
  119. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/KjYeVjkE.js +1 -0
  120. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/MErlcOXj.js +1 -0
  121. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/VRI4prUD.js +1 -0
  122. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/VYb2dkNs.js +1 -0
  123. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/VqWvU2yF.js +1 -0
  124. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/dHC3otuL.js +1 -0
  125. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/da7Oy_lO.js +1 -0
  126. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/eAy8rZzC.js +2 -0
  127. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/erjNR5MX.js +1 -0
  128. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/f1oG3eFE.js +1 -0
  129. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/rsLi1iKv.js +20 -0
  130. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/rwuuBP9f.js +1 -0
  131. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/xGHZQ1pe.js +3 -0
  132. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.DrTRUgT3.js +2 -0
  133. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.BK5EOJl2.js +1 -0
  134. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.CIvTuljF.js +4 -0
  135. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/1.UBvSzxdA.js +1 -0
  136. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.CQ_tiLJa.js +1 -0
  137. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/11.KqkAcaxW.js +1 -0
  138. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.DoYsmxQc.js +1 -0
  139. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/13.571n2LZA.js +1 -0
  140. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/14.DGs689M-.js +1 -0
  141. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/15.CWG1ehzT.js +1 -0
  142. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/16.Dpq6jbSh.js +1 -0
  143. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/17.B5AZbHUU.js +1 -0
  144. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/18.CBga8cnq.js +1 -0
  145. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.D2HXgz-8.js +1090 -0
  146. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/3.f4HAg-y3.js +1 -0
  147. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/4.BKF4xuKQ.js +1 -0
  148. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.BAE0Pm_f.js +39 -0
  149. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.CouWWpzA.js +1 -0
  150. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.UBHT0ktp.js +1 -0
  151. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.FiYNElcc.js +1 -0
  152. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/9.B3-UaT23.js +1 -0
  153. lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/clustering.worker-DKqeLtG0.js +2 -0
  154. lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/search.worker-vNSty3B0.js +1 -0
  155. lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -0
  156. lightly_studio/dist_lightly_studio_view_app/apple-touch-icon-precomposed.png +0 -0
  157. lightly_studio/dist_lightly_studio_view_app/apple-touch-icon.png +0 -0
  158. lightly_studio/dist_lightly_studio_view_app/favicon.png +0 -0
  159. lightly_studio/dist_lightly_studio_view_app/index.html +45 -0
  160. lightly_studio/errors.py +5 -0
  161. lightly_studio/examples/example.py +25 -0
  162. lightly_studio/examples/example_coco.py +27 -0
  163. lightly_studio/examples/example_coco_caption.py +29 -0
  164. lightly_studio/examples/example_metadata.py +369 -0
  165. lightly_studio/examples/example_operators.py +111 -0
  166. lightly_studio/examples/example_selection.py +28 -0
  167. lightly_studio/examples/example_split_work.py +48 -0
  168. lightly_studio/examples/example_video.py +22 -0
  169. lightly_studio/examples/example_video_annotations.py +157 -0
  170. lightly_studio/examples/example_yolo.py +22 -0
  171. lightly_studio/export/coco_captions.py +69 -0
  172. lightly_studio/export/export_dataset.py +104 -0
  173. lightly_studio/export/lightly_studio_label_input.py +120 -0
  174. lightly_studio/export_schema.py +18 -0
  175. lightly_studio/export_version.py +57 -0
  176. lightly_studio/few_shot_classifier/__init__.py +0 -0
  177. lightly_studio/few_shot_classifier/classifier.py +80 -0
  178. lightly_studio/few_shot_classifier/classifier_manager.py +644 -0
  179. lightly_studio/few_shot_classifier/random_forest_classifier.py +495 -0
  180. lightly_studio/metadata/complex_metadata.py +47 -0
  181. lightly_studio/metadata/compute_similarity.py +84 -0
  182. lightly_studio/metadata/compute_typicality.py +67 -0
  183. lightly_studio/metadata/gps_coordinate.py +41 -0
  184. lightly_studio/metadata/metadata_protocol.py +17 -0
  185. lightly_studio/models/__init__.py +1 -0
  186. lightly_studio/models/annotation/__init__.py +0 -0
  187. lightly_studio/models/annotation/annotation_base.py +303 -0
  188. lightly_studio/models/annotation/instance_segmentation.py +56 -0
  189. lightly_studio/models/annotation/links.py +17 -0
  190. lightly_studio/models/annotation/object_detection.py +47 -0
  191. lightly_studio/models/annotation/semantic_segmentation.py +44 -0
  192. lightly_studio/models/annotation_label.py +47 -0
  193. lightly_studio/models/caption.py +49 -0
  194. lightly_studio/models/classifier.py +20 -0
  195. lightly_studio/models/dataset.py +70 -0
  196. lightly_studio/models/embedding_model.py +30 -0
  197. lightly_studio/models/image.py +96 -0
  198. lightly_studio/models/metadata.py +208 -0
  199. lightly_studio/models/range.py +17 -0
  200. lightly_studio/models/sample.py +154 -0
  201. lightly_studio/models/sample_embedding.py +36 -0
  202. lightly_studio/models/settings.py +69 -0
  203. lightly_studio/models/tag.py +96 -0
  204. lightly_studio/models/two_dim_embedding.py +16 -0
  205. lightly_studio/models/video.py +161 -0
  206. lightly_studio/plugins/__init__.py +0 -0
  207. lightly_studio/plugins/base_operator.py +60 -0
  208. lightly_studio/plugins/operator_registry.py +47 -0
  209. lightly_studio/plugins/parameter.py +70 -0
  210. lightly_studio/py.typed +0 -0
  211. lightly_studio/resolvers/__init__.py +0 -0
  212. lightly_studio/resolvers/annotation_label_resolver/__init__.py +22 -0
  213. lightly_studio/resolvers/annotation_label_resolver/create.py +27 -0
  214. lightly_studio/resolvers/annotation_label_resolver/delete.py +28 -0
  215. lightly_studio/resolvers/annotation_label_resolver/get_all.py +37 -0
  216. lightly_studio/resolvers/annotation_label_resolver/get_by_id.py +24 -0
  217. lightly_studio/resolvers/annotation_label_resolver/get_by_ids.py +25 -0
  218. lightly_studio/resolvers/annotation_label_resolver/get_by_label_name.py +24 -0
  219. lightly_studio/resolvers/annotation_label_resolver/names_by_ids.py +25 -0
  220. lightly_studio/resolvers/annotation_label_resolver/update.py +38 -0
  221. lightly_studio/resolvers/annotation_resolver/__init__.py +40 -0
  222. lightly_studio/resolvers/annotation_resolver/count_annotations_by_dataset.py +129 -0
  223. lightly_studio/resolvers/annotation_resolver/create_many.py +124 -0
  224. lightly_studio/resolvers/annotation_resolver/delete_annotation.py +87 -0
  225. lightly_studio/resolvers/annotation_resolver/delete_annotations.py +60 -0
  226. lightly_studio/resolvers/annotation_resolver/get_all.py +85 -0
  227. lightly_studio/resolvers/annotation_resolver/get_all_with_payload.py +179 -0
  228. lightly_studio/resolvers/annotation_resolver/get_by_id.py +34 -0
  229. lightly_studio/resolvers/annotation_resolver/get_by_id_with_payload.py +130 -0
  230. lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +142 -0
  231. lightly_studio/resolvers/annotation_resolver/update_bounding_box.py +68 -0
  232. lightly_studio/resolvers/annotations/__init__.py +1 -0
  233. lightly_studio/resolvers/annotations/annotations_filter.py +88 -0
  234. lightly_studio/resolvers/caption_resolver.py +129 -0
  235. lightly_studio/resolvers/dataset_resolver/__init__.py +55 -0
  236. lightly_studio/resolvers/dataset_resolver/check_dataset_type.py +29 -0
  237. lightly_studio/resolvers/dataset_resolver/create.py +20 -0
  238. lightly_studio/resolvers/dataset_resolver/delete.py +20 -0
  239. lightly_studio/resolvers/dataset_resolver/export.py +267 -0
  240. lightly_studio/resolvers/dataset_resolver/get_all.py +19 -0
  241. lightly_studio/resolvers/dataset_resolver/get_by_id.py +16 -0
  242. lightly_studio/resolvers/dataset_resolver/get_by_name.py +12 -0
  243. lightly_studio/resolvers/dataset_resolver/get_dataset_details.py +27 -0
  244. lightly_studio/resolvers/dataset_resolver/get_hierarchy.py +31 -0
  245. lightly_studio/resolvers/dataset_resolver/get_or_create_child_dataset.py +58 -0
  246. lightly_studio/resolvers/dataset_resolver/get_parent_dataset_by_sample_id.py +27 -0
  247. lightly_studio/resolvers/dataset_resolver/get_parent_dataset_id.py +22 -0
  248. lightly_studio/resolvers/dataset_resolver/get_root_dataset.py +61 -0
  249. lightly_studio/resolvers/dataset_resolver/get_root_datasets_overview.py +41 -0
  250. lightly_studio/resolvers/dataset_resolver/update.py +25 -0
  251. lightly_studio/resolvers/embedding_model_resolver.py +120 -0
  252. lightly_studio/resolvers/image_filter.py +50 -0
  253. lightly_studio/resolvers/image_resolver/__init__.py +21 -0
  254. lightly_studio/resolvers/image_resolver/create_many.py +52 -0
  255. lightly_studio/resolvers/image_resolver/delete.py +20 -0
  256. lightly_studio/resolvers/image_resolver/filter_new_paths.py +23 -0
  257. lightly_studio/resolvers/image_resolver/get_all_by_dataset_id.py +117 -0
  258. lightly_studio/resolvers/image_resolver/get_by_id.py +14 -0
  259. lightly_studio/resolvers/image_resolver/get_dimension_bounds.py +75 -0
  260. lightly_studio/resolvers/image_resolver/get_many_by_id.py +22 -0
  261. lightly_studio/resolvers/image_resolver/get_samples_excluding.py +43 -0
  262. lightly_studio/resolvers/metadata_resolver/__init__.py +15 -0
  263. lightly_studio/resolvers/metadata_resolver/metadata_filter.py +163 -0
  264. lightly_studio/resolvers/metadata_resolver/sample/__init__.py +21 -0
  265. lightly_studio/resolvers/metadata_resolver/sample/bulk_update_metadata.py +46 -0
  266. lightly_studio/resolvers/metadata_resolver/sample/get_by_sample_id.py +24 -0
  267. lightly_studio/resolvers/metadata_resolver/sample/get_metadata_info.py +104 -0
  268. lightly_studio/resolvers/metadata_resolver/sample/get_value_for_sample.py +27 -0
  269. lightly_studio/resolvers/metadata_resolver/sample/set_value_for_sample.py +53 -0
  270. lightly_studio/resolvers/sample_embedding_resolver.py +132 -0
  271. lightly_studio/resolvers/sample_resolver/__init__.py +17 -0
  272. lightly_studio/resolvers/sample_resolver/count_by_dataset_id.py +16 -0
  273. lightly_studio/resolvers/sample_resolver/create.py +16 -0
  274. lightly_studio/resolvers/sample_resolver/create_many.py +25 -0
  275. lightly_studio/resolvers/sample_resolver/get_by_id.py +14 -0
  276. lightly_studio/resolvers/sample_resolver/get_filtered_samples.py +56 -0
  277. lightly_studio/resolvers/sample_resolver/get_many_by_id.py +22 -0
  278. lightly_studio/resolvers/sample_resolver/sample_filter.py +74 -0
  279. lightly_studio/resolvers/settings_resolver.py +62 -0
  280. lightly_studio/resolvers/tag_resolver.py +299 -0
  281. lightly_studio/resolvers/twodim_embedding_resolver.py +119 -0
  282. lightly_studio/resolvers/video_frame_resolver/__init__.py +23 -0
  283. lightly_studio/resolvers/video_frame_resolver/count_video_frames_annotations.py +83 -0
  284. lightly_studio/resolvers/video_frame_resolver/create_many.py +57 -0
  285. lightly_studio/resolvers/video_frame_resolver/get_all_by_dataset_id.py +63 -0
  286. lightly_studio/resolvers/video_frame_resolver/get_by_id.py +13 -0
  287. lightly_studio/resolvers/video_frame_resolver/get_table_fields_bounds.py +44 -0
  288. lightly_studio/resolvers/video_frame_resolver/video_frame_annotations_counter_filter.py +47 -0
  289. lightly_studio/resolvers/video_frame_resolver/video_frame_filter.py +57 -0
  290. lightly_studio/resolvers/video_resolver/__init__.py +27 -0
  291. lightly_studio/resolvers/video_resolver/count_video_frame_annotations_by_video_dataset.py +86 -0
  292. lightly_studio/resolvers/video_resolver/create_many.py +58 -0
  293. lightly_studio/resolvers/video_resolver/filter_new_paths.py +33 -0
  294. lightly_studio/resolvers/video_resolver/get_all_by_dataset_id.py +181 -0
  295. lightly_studio/resolvers/video_resolver/get_by_id.py +22 -0
  296. lightly_studio/resolvers/video_resolver/get_table_fields_bounds.py +72 -0
  297. lightly_studio/resolvers/video_resolver/get_view_by_id.py +52 -0
  298. lightly_studio/resolvers/video_resolver/video_count_annotations_filter.py +50 -0
  299. lightly_studio/resolvers/video_resolver/video_filter.py +98 -0
  300. lightly_studio/selection/__init__.py +1 -0
  301. lightly_studio/selection/mundig.py +143 -0
  302. lightly_studio/selection/select.py +203 -0
  303. lightly_studio/selection/select_via_db.py +273 -0
  304. lightly_studio/selection/selection_config.py +49 -0
  305. lightly_studio/services/annotations_service/__init__.py +33 -0
  306. lightly_studio/services/annotations_service/create_annotation.py +64 -0
  307. lightly_studio/services/annotations_service/delete_annotation.py +22 -0
  308. lightly_studio/services/annotations_service/get_annotation_by_id.py +31 -0
  309. lightly_studio/services/annotations_service/update_annotation.py +54 -0
  310. lightly_studio/services/annotations_service/update_annotation_bounding_box.py +36 -0
  311. lightly_studio/services/annotations_service/update_annotation_label.py +48 -0
  312. lightly_studio/services/annotations_service/update_annotations.py +29 -0
  313. lightly_studio/setup_logging.py +59 -0
  314. lightly_studio/type_definitions.py +31 -0
  315. lightly_studio/utils/__init__.py +3 -0
  316. lightly_studio/utils/download.py +94 -0
  317. lightly_studio/vendor/__init__.py +1 -0
  318. lightly_studio/vendor/mobileclip/ACKNOWLEDGEMENTS +422 -0
  319. lightly_studio/vendor/mobileclip/LICENSE +31 -0
  320. lightly_studio/vendor/mobileclip/LICENSE_weights_data +50 -0
  321. lightly_studio/vendor/mobileclip/README.md +5 -0
  322. lightly_studio/vendor/mobileclip/__init__.py +96 -0
  323. lightly_studio/vendor/mobileclip/clip.py +77 -0
  324. lightly_studio/vendor/mobileclip/configs/mobileclip_b.json +18 -0
  325. lightly_studio/vendor/mobileclip/configs/mobileclip_s0.json +18 -0
  326. lightly_studio/vendor/mobileclip/configs/mobileclip_s1.json +18 -0
  327. lightly_studio/vendor/mobileclip/configs/mobileclip_s2.json +18 -0
  328. lightly_studio/vendor/mobileclip/image_encoder.py +67 -0
  329. lightly_studio/vendor/mobileclip/logger.py +154 -0
  330. lightly_studio/vendor/mobileclip/models/__init__.py +10 -0
  331. lightly_studio/vendor/mobileclip/models/mci.py +933 -0
  332. lightly_studio/vendor/mobileclip/models/vit.py +433 -0
  333. lightly_studio/vendor/mobileclip/modules/__init__.py +4 -0
  334. lightly_studio/vendor/mobileclip/modules/common/__init__.py +4 -0
  335. lightly_studio/vendor/mobileclip/modules/common/mobileone.py +341 -0
  336. lightly_studio/vendor/mobileclip/modules/common/transformer.py +451 -0
  337. lightly_studio/vendor/mobileclip/modules/image/__init__.py +4 -0
  338. lightly_studio/vendor/mobileclip/modules/image/image_projection.py +113 -0
  339. lightly_studio/vendor/mobileclip/modules/image/replknet.py +188 -0
  340. lightly_studio/vendor/mobileclip/modules/text/__init__.py +4 -0
  341. lightly_studio/vendor/mobileclip/modules/text/repmixer.py +281 -0
  342. lightly_studio/vendor/mobileclip/modules/text/tokenizer.py +38 -0
  343. lightly_studio/vendor/mobileclip/text_encoder.py +245 -0
  344. lightly_studio/vendor/perception_encoder/LICENSE.PE +201 -0
  345. lightly_studio/vendor/perception_encoder/README.md +11 -0
  346. lightly_studio/vendor/perception_encoder/vision_encoder/__init__.py +0 -0
  347. lightly_studio/vendor/perception_encoder/vision_encoder/bpe_simple_vocab_16e6.txt.gz +0 -0
  348. lightly_studio/vendor/perception_encoder/vision_encoder/config.py +205 -0
  349. lightly_studio/vendor/perception_encoder/vision_encoder/config_src.py +264 -0
  350. lightly_studio/vendor/perception_encoder/vision_encoder/pe.py +766 -0
  351. lightly_studio/vendor/perception_encoder/vision_encoder/rope.py +352 -0
  352. lightly_studio/vendor/perception_encoder/vision_encoder/tokenizer.py +347 -0
  353. lightly_studio/vendor/perception_encoder/vision_encoder/transforms.py +36 -0
  354. lightly_studio-0.4.6.dist-info/METADATA +88 -0
  355. lightly_studio-0.4.6.dist-info/RECORD +356 -0
  356. lightly_studio-0.4.6.dist-info/WHEEL +4 -0
@@ -0,0 +1,191 @@
1
+ """This module contains the API routes for managing datasets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import List
7
+ from uuid import UUID
8
+
9
+ from fastapi import APIRouter, Depends, HTTPException, Path, Query
10
+ from fastapi.responses import PlainTextResponse
11
+ from pydantic import BaseModel
12
+ from sqlmodel import Field
13
+ from typing_extensions import Annotated
14
+
15
+ from lightly_studio.api.routes.api.status import HTTP_STATUS_NOT_FOUND
16
+ from lightly_studio.api.routes.api.validators import Paginated
17
+ from lightly_studio.db_manager import SessionDep
18
+ from lightly_studio.models.dataset import (
19
+ DatasetCreate,
20
+ DatasetOverviewView,
21
+ DatasetTable,
22
+ DatasetView,
23
+ DatasetViewWithCount,
24
+ )
25
+ from lightly_studio.resolvers import dataset_resolver
26
+ from lightly_studio.resolvers.dataset_resolver.export import ExportFilter
27
+
28
+ dataset_router = APIRouter()
29
+
30
+
31
+ def get_and_validate_dataset_id(
32
+ session: SessionDep,
33
+ dataset_id: UUID,
34
+ ) -> DatasetTable:
35
+ """Get and validate the existence of a dataset on a route."""
36
+ dataset = dataset_resolver.get_by_id(session=session, dataset_id=dataset_id)
37
+ if not dataset:
38
+ raise HTTPException(
39
+ status_code=HTTP_STATUS_NOT_FOUND,
40
+ detail=f""" Dataset with {dataset_id} not found.""",
41
+ )
42
+ return dataset
43
+
44
+
45
+ @dataset_router.get("/datasets", response_model=List[DatasetView])
46
+ def read_datasets(
47
+ session: SessionDep,
48
+ paginated: Annotated[Paginated, Query()],
49
+ ) -> list[DatasetTable]:
50
+ """Retrieve a list of datasets from the database."""
51
+ return dataset_resolver.get_all(session=session, offset=paginated.offset, limit=paginated.limit)
52
+
53
+
54
+ @dataset_router.get("/datasets/{dataset_id}/root_dataset", response_model=DatasetView)
55
+ def read_root_dataset(
56
+ session: SessionDep,
57
+ dataset_id: Annotated[UUID, Path(title="Dataset Id")],
58
+ ) -> DatasetTable:
59
+ """Retrieve the root dataset for a given dataset."""
60
+ return dataset_resolver.get_root_dataset(session=session, dataset_id=dataset_id)
61
+
62
+
63
+ @dataset_router.get("/datasets/{dataset_id}/hierarchy", response_model=List[DatasetView])
64
+ def read_dataset_hierarchy(
65
+ session: SessionDep,
66
+ dataset_id: Annotated[UUID, Path(title="Root Dataset Id")],
67
+ ) -> list[DatasetTable]:
68
+ """Retrieve the dataset hierarchy from the database, starting with the root node."""
69
+ return dataset_resolver.get_hierarchy(session=session, root_dataset_id=dataset_id)
70
+
71
+
72
+ @dataset_router.get("/datasets/overview", response_model=List[DatasetOverviewView])
73
+ def read_datasets_overview(session: SessionDep) -> list[DatasetOverviewView]:
74
+ """Retrieve root datasets with metadata for dashboard display."""
75
+ return dataset_resolver.get_root_datasets_overview(session=session)
76
+
77
+
78
+ @dataset_router.get("/datasets/{dataset_id}", response_model=DatasetViewWithCount)
79
+ def read_dataset(
80
+ session: SessionDep,
81
+ dataset: Annotated[
82
+ DatasetTable,
83
+ Path(title="Dataset Id"),
84
+ Depends(get_and_validate_dataset_id),
85
+ ],
86
+ ) -> DatasetViewWithCount:
87
+ """Retrieve a single dataset from the database."""
88
+ return dataset_resolver.get_dataset_details(session=session, dataset=dataset)
89
+
90
+
91
+ @dataset_router.put("/datasets/{dataset_id}")
92
+ def update_dataset(
93
+ session: SessionDep,
94
+ dataset: Annotated[
95
+ DatasetTable,
96
+ Path(title="Dataset Id"),
97
+ Depends(get_and_validate_dataset_id),
98
+ ],
99
+ dataset_input: DatasetCreate,
100
+ ) -> DatasetTable:
101
+ """Update an existing dataset in the database."""
102
+ return dataset_resolver.update(
103
+ session=session,
104
+ dataset_id=dataset.dataset_id,
105
+ dataset_data=dataset_input,
106
+ )
107
+
108
+
109
+ @dataset_router.delete("/datasets/{dataset_id}")
110
+ def delete_dataset(
111
+ session: SessionDep,
112
+ dataset: Annotated[
113
+ DatasetTable,
114
+ Path(title="Dataset Id"),
115
+ Depends(get_and_validate_dataset_id),
116
+ ],
117
+ ) -> dict[str, str]:
118
+ """Delete a dataset from the database."""
119
+ dataset_resolver.delete(session=session, dataset_id=dataset.dataset_id)
120
+ return {"status": "deleted"}
121
+
122
+
123
+ # TODO(Michal, 09/2025): Move to export.py
124
+ class ExportBody(BaseModel):
125
+ """body parameters for including or excluding tag_ids or sample_ids."""
126
+
127
+ include: ExportFilter | None = Field(
128
+ None, description="include filter for sample_ids or tag_ids"
129
+ )
130
+ exclude: ExportFilter | None = Field(
131
+ None, description="exclude filter for sample_ids or tag_ids"
132
+ )
133
+
134
+
135
+ # This endpoint should be a GET, however due to the potential huge size
136
+ # of sample_ids, it is a POST request to avoid URL length limitations.
137
+ # A body with a GET request is supported by fastAPI however it has undefined
138
+ # behavior: https://fastapi.tiangolo.com/tutorial/body/
139
+ # TODO(Michal, 09/2025): Move to export.py
140
+ @dataset_router.post(
141
+ "/datasets/{dataset_id}/export",
142
+ )
143
+ def export_dataset_to_absolute_paths(
144
+ session: SessionDep,
145
+ dataset: Annotated[
146
+ DatasetTable,
147
+ Path(title="Dataset Id"),
148
+ Depends(get_and_validate_dataset_id),
149
+ ],
150
+ body: ExportBody,
151
+ ) -> PlainTextResponse:
152
+ """Export dataset from the database."""
153
+ # export dataset to absolute paths
154
+ exported = dataset_resolver.export(
155
+ session=session,
156
+ dataset_id=dataset.dataset_id,
157
+ include=body.include,
158
+ exclude=body.exclude,
159
+ )
160
+
161
+ # Create a response with the exported data
162
+ response = PlainTextResponse("\n".join(exported))
163
+
164
+ # Add the Content-Disposition header to force download
165
+ filename = f"{dataset.name}_exported_{datetime.now(timezone.utc)}.txt"
166
+ response.headers["Access-Control-Expose-Headers"] = "Content-Disposition"
167
+ response.headers["Content-Disposition"] = f"attachment; filename={filename}"
168
+
169
+ return response
170
+
171
+
172
+ # TODO(Michal, 09/2025): Move to export.py
173
+ @dataset_router.post(
174
+ "/datasets/{dataset_id}/export/stats",
175
+ )
176
+ def export_dataset_stats(
177
+ session: SessionDep,
178
+ dataset: Annotated[
179
+ DatasetTable,
180
+ Path(title="Dataset Id"),
181
+ Depends(get_and_validate_dataset_id),
182
+ ],
183
+ body: ExportBody,
184
+ ) -> int:
185
+ """Get statistics about the export query."""
186
+ return dataset_resolver.get_filtered_samples_count(
187
+ session=session,
188
+ dataset_id=dataset.dataset_id,
189
+ include=body.include,
190
+ exclude=body.exclude,
191
+ )
@@ -0,0 +1,266 @@
1
+ """This module contains the API routes for managing tags."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import List
6
+ from uuid import UUID
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException, Path, Query
9
+ from pydantic import BaseModel
10
+ from sqlalchemy.exc import IntegrityError
11
+ from sqlmodel import Field
12
+ from typing_extensions import Annotated
13
+
14
+ from lightly_studio.api.routes.api.dataset import get_and_validate_dataset_id
15
+ from lightly_studio.api.routes.api.status import (
16
+ HTTP_STATUS_CONFLICT,
17
+ HTTP_STATUS_CREATED,
18
+ HTTP_STATUS_NOT_FOUND,
19
+ )
20
+ from lightly_studio.api.routes.api.validators import Paginated
21
+ from lightly_studio.db_manager import SessionDep
22
+ from lightly_studio.models.dataset import DatasetTable
23
+ from lightly_studio.models.tag import (
24
+ TagCreate,
25
+ TagCreateBody,
26
+ TagTable,
27
+ TagUpdate,
28
+ TagUpdateBody,
29
+ TagView,
30
+ )
31
+ from lightly_studio.resolvers import tag_resolver
32
+
33
+ tag_router = APIRouter()
34
+
35
+
36
+ @tag_router.post(
37
+ "/datasets/{dataset_id}/tags",
38
+ response_model=TagView,
39
+ status_code=HTTP_STATUS_CREATED,
40
+ )
41
+ def create_tag(
42
+ session: SessionDep,
43
+ dataset: Annotated[
44
+ DatasetTable,
45
+ Path(title="Dataset Id"),
46
+ Depends(get_and_validate_dataset_id),
47
+ ],
48
+ body: TagCreateBody,
49
+ ) -> TagTable:
50
+ """Create a new tag in the database."""
51
+ dataset_id = dataset.dataset_id
52
+ try:
53
+ return tag_resolver.create(
54
+ session=session,
55
+ tag=TagCreate(**body.model_dump(exclude_unset=True), dataset_id=dataset_id),
56
+ )
57
+ except IntegrityError as e:
58
+ raise HTTPException(
59
+ status_code=HTTP_STATUS_CONFLICT,
60
+ detail=f"""
61
+ Tag with name {body.name} already exists
62
+ in the dataset {dataset_id}.
63
+ """,
64
+ ) from e
65
+
66
+
67
+ @tag_router.get("/datasets/{dataset_id}/tags", response_model=List[TagView])
68
+ def read_tags(
69
+ session: SessionDep,
70
+ dataset: Annotated[
71
+ DatasetTable,
72
+ Path(title="Dataset Id"),
73
+ Depends(get_and_validate_dataset_id),
74
+ ],
75
+ paginated: Annotated[Paginated, Query()],
76
+ ) -> list[TagTable]:
77
+ """Retrieve a list of tags from the database."""
78
+ return tag_resolver.get_all_by_dataset_id(
79
+ session=session,
80
+ dataset_id=dataset.dataset_id,
81
+ offset=paginated.offset,
82
+ limit=paginated.limit,
83
+ )
84
+
85
+
86
+ @tag_router.get("/datasets/{dataset_id}/tags/{tag_id}")
87
+ def read_tag(
88
+ session: SessionDep,
89
+ dataset: Annotated[
90
+ DatasetTable,
91
+ Path(title="Dataset Id"),
92
+ Depends(get_and_validate_dataset_id),
93
+ ],
94
+ tag_id: Annotated[UUID, Path(title="Tag Id")],
95
+ ) -> TagTable:
96
+ """Retrieve a single tag from the database."""
97
+ tag = tag_resolver.get_by_id(session=session, tag_id=tag_id)
98
+ if not tag:
99
+ raise HTTPException(
100
+ status_code=HTTP_STATUS_NOT_FOUND,
101
+ detail=f"""
102
+ Tag with id {tag_id} for dataset {dataset.dataset_id} not found.
103
+ """,
104
+ )
105
+ return tag
106
+
107
+
108
+ @tag_router.put("/datasets/{dataset_id}/tags/{tag_id}")
109
+ def update_tag(
110
+ session: SessionDep,
111
+ dataset: Annotated[
112
+ DatasetTable,
113
+ Path(title="Dataset Id"),
114
+ Depends(get_and_validate_dataset_id),
115
+ ],
116
+ tag_id: Annotated[UUID, Path(title="Tag Id")],
117
+ body: TagUpdateBody,
118
+ ) -> TagTable:
119
+ """Update an existing tag in the database."""
120
+ try:
121
+ tag = tag_resolver.update(
122
+ session=session,
123
+ tag_id=tag_id,
124
+ tag_data=TagUpdate(
125
+ **body.model_dump(exclude_unset=True),
126
+ ),
127
+ )
128
+ if not tag:
129
+ raise HTTPException(
130
+ status_code=HTTP_STATUS_NOT_FOUND,
131
+ detail=f"Tag with id {tag_id} not found.",
132
+ )
133
+ except IntegrityError as e:
134
+ raise HTTPException(
135
+ status_code=HTTP_STATUS_CONFLICT,
136
+ detail=f"""
137
+ Cannot update tag. Tag with name {body.name}
138
+ already exists in the dataset {dataset.dataset_id}.
139
+ """,
140
+ ) from e
141
+ return tag
142
+
143
+
144
+ @tag_router.delete("/datasets/{dataset_id}/tags/{tag_id}")
145
+ def delete_tag(
146
+ session: SessionDep,
147
+ tag_id: Annotated[UUID, Path(title="Tag Id")],
148
+ ) -> dict[str, str]:
149
+ """Delete a tag from the database."""
150
+ if not tag_resolver.delete(session=session, tag_id=tag_id):
151
+ raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail="tag not found")
152
+ return {"status": "deleted"}
153
+
154
+
155
+ class SampleIdsBody(BaseModel):
156
+ """body parameters for adding or removing thing_ids."""
157
+
158
+ sample_ids: list[UUID] | None = Field(None, description="sample ids to add/remove")
159
+
160
+
161
+ @tag_router.post(
162
+ "/datasets/{dataset_id}/tags/{tag_id}/add/samples",
163
+ status_code=HTTP_STATUS_CREATED,
164
+ )
165
+ def add_sample_ids_to_tag_id(
166
+ session: SessionDep,
167
+ # dataset_id is needed for the generator
168
+ dataset_id: Annotated[ # noqa: ARG001
169
+ UUID,
170
+ Path(title="Dataset Id", description="The ID of the dataset"),
171
+ ],
172
+ tag_id: UUID,
173
+ body: SampleIdsBody,
174
+ ) -> bool:
175
+ """Add sample_ids to a tag_id."""
176
+ tag = tag_resolver.get_by_id(session=session, tag_id=tag_id)
177
+ if not tag:
178
+ raise HTTPException(
179
+ status_code=HTTP_STATUS_NOT_FOUND,
180
+ detail=f"Tag {tag_id} not found, can't add sample_ids.",
181
+ )
182
+
183
+ sample_ids = body.sample_ids if body.sample_ids else []
184
+ tag_resolver.add_sample_ids_to_tag_id(session=session, tag_id=tag_id, sample_ids=sample_ids)
185
+ return True
186
+
187
+
188
+ @tag_router.delete(
189
+ "/datasets/{dataset_id}/tags/{tag_id}/remove/samples",
190
+ )
191
+ def remove_thing_ids_to_tag_id(
192
+ session: SessionDep,
193
+ tag_id: UUID,
194
+ body: SampleIdsBody,
195
+ ) -> bool:
196
+ """Add thing_ids to a tag_id."""
197
+ tag = tag_resolver.get_by_id(session=session, tag_id=tag_id)
198
+ if not tag:
199
+ raise HTTPException(
200
+ status_code=HTTP_STATUS_NOT_FOUND,
201
+ detail=f"Tag {tag_id} not found, can't remove samples.",
202
+ )
203
+
204
+ sample_ids = body.sample_ids if body.sample_ids else []
205
+ tag_resolver.remove_sample_ids_from_tag_id(
206
+ session=session, tag_id=tag_id, sample_ids=sample_ids
207
+ )
208
+ return True
209
+
210
+
211
+ class AnnotationIdsBody(BaseModel):
212
+ """body parameters for adding or removing annotation_ids."""
213
+
214
+ annotation_ids: list[UUID] | None = Field(None, description="annotation ids to add/remove")
215
+
216
+
217
+ @tag_router.post(
218
+ "/datasets/{dataset_id}/tags/{tag_id}/add/annotations",
219
+ status_code=HTTP_STATUS_CREATED,
220
+ )
221
+ def add_annotation_ids_to_tag_id(
222
+ session: SessionDep,
223
+ # dataset_id is needed for the generator
224
+ dataset_id: Annotated[ # noqa: ARG001
225
+ UUID,
226
+ Path(title="Dataset Id", description="The ID of the dataset"),
227
+ ],
228
+ tag_id: UUID,
229
+ body: AnnotationIdsBody,
230
+ ) -> bool:
231
+ """Add thing_ids to a tag_id."""
232
+ tag = tag_resolver.get_by_id(session=session, tag_id=tag_id)
233
+ if not tag:
234
+ raise HTTPException(
235
+ status_code=HTTP_STATUS_NOT_FOUND,
236
+ detail=f"Tag {tag_id} not found, can't add annotations.",
237
+ )
238
+
239
+ annotation_ids = body.annotation_ids if body.annotation_ids else []
240
+ tag_resolver.add_annotation_ids_to_tag_id(
241
+ session=session, tag_id=tag_id, annotation_ids=annotation_ids
242
+ )
243
+ return True
244
+
245
+
246
+ @tag_router.delete(
247
+ "/datasets/{dataset_id}/tags/{tag_id}/remove/annotations",
248
+ )
249
+ def remove_annotation_ids_to_tag_id(
250
+ session: SessionDep,
251
+ tag_id: UUID,
252
+ body: AnnotationIdsBody,
253
+ ) -> bool:
254
+ """Add thing_ids to a tag_id."""
255
+ tag = tag_resolver.get_by_id(session=session, tag_id=tag_id)
256
+ if not tag:
257
+ raise HTTPException(
258
+ status_code=HTTP_STATUS_NOT_FOUND,
259
+ detail=f"Tag {tag_id} not found, can't remove annotations.",
260
+ )
261
+
262
+ annotation_ids = body.annotation_ids if body.annotation_ids else []
263
+ tag_resolver.remove_annotation_ids_from_tag_id(
264
+ session=session, tag_id=tag_id, annotation_ids=annotation_ids
265
+ )
266
+ return True
@@ -0,0 +1,90 @@
1
+ """Routes delivering 2D embeddings for visualization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ from uuid import UUID
7
+
8
+ import pyarrow as pa
9
+ from fastapi import APIRouter, Response
10
+ from pyarrow import ipc
11
+ from pydantic import BaseModel, Field
12
+ from sqlmodel import select
13
+
14
+ from lightly_studio.db_manager import SessionDep
15
+ from lightly_studio.models.embedding_model import EmbeddingModelTable
16
+ from lightly_studio.resolvers import image_resolver, twodim_embedding_resolver
17
+ from lightly_studio.resolvers.image_filter import ImageFilter
18
+
19
+ embeddings2d_router = APIRouter()
20
+
21
+
22
+ class GetEmbeddings2DRequest(BaseModel):
23
+ """Request body for retrieving 2D embeddings."""
24
+
25
+ filters: ImageFilter = Field(description="Filter parameters identifying matching samples")
26
+
27
+
28
+ @embeddings2d_router.post("/embeddings2d/default")
29
+ def get_2d_embeddings(
30
+ session: SessionDep,
31
+ body: GetEmbeddings2DRequest,
32
+ ) -> Response:
33
+ """Return 2D embeddings serialized as an Arrow stream."""
34
+ dataset_id = (
35
+ body.filters.sample_filter.dataset_id if body.filters.sample_filter is not None else None
36
+ )
37
+ if dataset_id is None:
38
+ raise ValueError("Dataset ID must be provided in filters.")
39
+
40
+ # TODO(Malte, 09/2025): Support choosing the embedding model via API parameter.
41
+ embedding_model = session.exec(
42
+ select(EmbeddingModelTable).where(EmbeddingModelTable.dataset_id == dataset_id).limit(1)
43
+ ).first()
44
+ if embedding_model is None:
45
+ raise ValueError("No embedding model configured.")
46
+
47
+ x_array, y_array, sample_ids = twodim_embedding_resolver.get_twodim_embeddings(
48
+ session=session,
49
+ dataset_id=dataset_id,
50
+ embedding_model_id=embedding_model.embedding_model_id,
51
+ )
52
+
53
+ matching_sample_ids: set[UUID] | None = None
54
+ filters = body.filters if body else None
55
+ if filters:
56
+ matching_samples_result = image_resolver.get_all_by_dataset_id(
57
+ session=session,
58
+ dataset_id=dataset_id,
59
+ filters=filters,
60
+ )
61
+ matching_sample_ids = {sample.sample_id for sample in matching_samples_result.samples}
62
+
63
+ if matching_sample_ids is None:
64
+ fulfils_filter = [1] * len(sample_ids)
65
+ else:
66
+ fulfils_filter = [1 if sample_id in matching_sample_ids else 0 for sample_id in sample_ids]
67
+
68
+ table = pa.table(
69
+ {
70
+ "x": pa.array(x_array, type=pa.float32()),
71
+ "y": pa.array(y_array, type=pa.float32()),
72
+ "fulfils_filter": pa.array(fulfils_filter, type=pa.uint8()),
73
+ "sample_id": pa.array([str(sample_id) for sample_id in sample_ids], type=pa.string()),
74
+ }
75
+ )
76
+
77
+ buffer = io.BytesIO()
78
+ with ipc.new_stream(buffer, table.schema) as writer:
79
+ writer.write_table(table)
80
+ buffer.seek(0)
81
+
82
+ return Response(
83
+ content=buffer.getvalue(),
84
+ media_type="application/vnd.apache.arrow.stream",
85
+ headers={
86
+ "Content-Disposition": "inline; filename=embeddings2d.arrow",
87
+ "Content-Type": "application/vnd.apache.arrow.stream",
88
+ "X-Content-Type-Options": "nosniff",
89
+ },
90
+ )
@@ -0,0 +1,114 @@
1
+ """This module contains the FastAPI app configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from fastapi import FastAPI, Request
8
+ from fastapi.exceptions import RequestValidationError, ResponseValidationError
9
+ from fastapi.responses import JSONResponse
10
+ from sqlalchemy.exc import DataError, IntegrityError, OperationalError
11
+
12
+ from lightly_studio.api.routes.api.status import (
13
+ HTTP_STATUS_BAD_REQUEST,
14
+ HTTP_STATUS_CONFLICT,
15
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
16
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
17
+ )
18
+
19
+ # Set up logger for error handling
20
+ logger = logging.getLogger("lightly_studio.api.exceptions")
21
+
22
+
23
+ def _log_error_details(
24
+ exc: Exception,
25
+ status_code: int,
26
+ ) -> None:
27
+ """Log detailed error information with request context."""
28
+ # Log the error with different levels based on status code
29
+ logger.error(f"Server Error {status_code}: {exc}")
30
+
31
+
32
+ def register_exception_handlers(app: FastAPI) -> None:
33
+ """Register exception handlers for the FastAPI app."""
34
+
35
+ @app.exception_handler(IntegrityError)
36
+ async def _integrity_error_handler(_request: Request, _exc: IntegrityError) -> JSONResponse:
37
+ """Handle database integrity errors."""
38
+ _log_error_details(
39
+ exc=_exc,
40
+ status_code=HTTP_STATUS_CONFLICT,
41
+ )
42
+
43
+ return JSONResponse(
44
+ status_code=HTTP_STATUS_CONFLICT,
45
+ content={"error": _exc.statement or "Database constraint violated."},
46
+ )
47
+
48
+ @app.exception_handler(OperationalError)
49
+ async def _operational_error_handler(_request: Request, _exc: OperationalError) -> JSONResponse:
50
+ """Handle database operational errors."""
51
+ _log_error_details(
52
+ exc=_exc,
53
+ status_code=HTTP_STATUS_INTERNAL_SERVER_ERROR,
54
+ )
55
+ return JSONResponse(
56
+ status_code=HTTP_STATUS_INTERNAL_SERVER_ERROR,
57
+ content={"error": _exc.statement or "Database operation failed."},
58
+ )
59
+
60
+ @app.exception_handler(DataError)
61
+ async def _data_error_handler(_request: Request, _exc: DataError) -> JSONResponse:
62
+ """Handle database data errors."""
63
+ _log_error_details(
64
+ exc=_exc,
65
+ status_code=HTTP_STATUS_BAD_REQUEST,
66
+ )
67
+ return JSONResponse(
68
+ status_code=HTTP_STATUS_BAD_REQUEST,
69
+ content={"error": _exc.statement or "Invalid response."},
70
+ )
71
+
72
+ @app.exception_handler(ResponseValidationError)
73
+ async def _data_validation_error_handler(
74
+ _request: Request, _exc: ResponseValidationError
75
+ ) -> JSONResponse:
76
+ """Handle database data errors."""
77
+ error_details = _exc.errors()
78
+ if error_details:
79
+ detail = error_details[0].get("msg", "Invalid data provided.")
80
+ else:
81
+ detail = "Invalid data provided."
82
+
83
+ _log_error_details(
84
+ exc=_exc,
85
+ status_code=HTTP_STATUS_BAD_REQUEST,
86
+ )
87
+
88
+ return JSONResponse(status_code=HTTP_STATUS_BAD_REQUEST, content={"error": detail})
89
+
90
+ @app.exception_handler(ValueError)
91
+ async def _value_error_handler(_request: Request, _exc: ValueError) -> JSONResponse:
92
+ """Handle value errors."""
93
+ _log_error_details(
94
+ exc=_exc,
95
+ status_code=HTTP_STATUS_INTERNAL_SERVER_ERROR,
96
+ )
97
+ return JSONResponse(status_code=HTTP_STATUS_BAD_REQUEST, content={"error": str(_exc)})
98
+
99
+ @app.exception_handler(RequestValidationError)
100
+ async def _request_validation_error_handler(
101
+ request: Request, exc: RequestValidationError
102
+ ) -> JSONResponse:
103
+ body = (await request.body()).decode("utf-8", errors="replace")
104
+ logger.warning(
105
+ "Request validation error on %s?%s | errors=%s | body=%s",
106
+ request.url.path,
107
+ request.url.query,
108
+ exc.errors(),
109
+ body[:500], # don't log huge bodies
110
+ )
111
+ return JSONResponse(
112
+ status_code=HTTP_STATUS_UNPROCESSABLE_ENTITY,
113
+ content={"detail": exc.errors()},
114
+ )