lightly-studio 0.3.1__py3-none-any.whl

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

Potentially problematic release.


This version of lightly-studio might be problematic. Click here for more details.

Files changed (219) hide show
  1. lightly_studio/__init__.py +11 -0
  2. lightly_studio/api/__init__.py +0 -0
  3. lightly_studio/api/app.py +110 -0
  4. lightly_studio/api/cache.py +77 -0
  5. lightly_studio/api/db.py +133 -0
  6. lightly_studio/api/db_tables.py +32 -0
  7. lightly_studio/api/features.py +7 -0
  8. lightly_studio/api/routes/api/annotation.py +233 -0
  9. lightly_studio/api/routes/api/annotation_label.py +90 -0
  10. lightly_studio/api/routes/api/annotation_task.py +38 -0
  11. lightly_studio/api/routes/api/classifier.py +387 -0
  12. lightly_studio/api/routes/api/dataset.py +182 -0
  13. lightly_studio/api/routes/api/dataset_tag.py +257 -0
  14. lightly_studio/api/routes/api/exceptions.py +96 -0
  15. lightly_studio/api/routes/api/features.py +17 -0
  16. lightly_studio/api/routes/api/metadata.py +37 -0
  17. lightly_studio/api/routes/api/metrics.py +80 -0
  18. lightly_studio/api/routes/api/sample.py +196 -0
  19. lightly_studio/api/routes/api/settings.py +45 -0
  20. lightly_studio/api/routes/api/status.py +19 -0
  21. lightly_studio/api/routes/api/text_embedding.py +48 -0
  22. lightly_studio/api/routes/api/validators.py +17 -0
  23. lightly_studio/api/routes/healthz.py +13 -0
  24. lightly_studio/api/routes/images.py +104 -0
  25. lightly_studio/api/routes/webapp.py +51 -0
  26. lightly_studio/api/server.py +82 -0
  27. lightly_studio/core/__init__.py +0 -0
  28. lightly_studio/core/dataset.py +523 -0
  29. lightly_studio/core/sample.py +77 -0
  30. lightly_studio/core/start_gui.py +15 -0
  31. lightly_studio/dataset/__init__.py +0 -0
  32. lightly_studio/dataset/edge_embedding_generator.py +144 -0
  33. lightly_studio/dataset/embedding_generator.py +91 -0
  34. lightly_studio/dataset/embedding_manager.py +163 -0
  35. lightly_studio/dataset/env.py +16 -0
  36. lightly_studio/dataset/file_utils.py +35 -0
  37. lightly_studio/dataset/loader.py +622 -0
  38. lightly_studio/dataset/mobileclip_embedding_generator.py +144 -0
  39. lightly_studio/dist_lightly_studio_view_app/_app/env.js +1 -0
  40. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.DenzbfeK.css +1 -0
  41. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/LightlyLogo.BNjCIww-.png +0 -0
  42. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans- +0 -0
  43. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Bold.DGvYQtcs.ttf +0 -0
  44. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Italic-VariableFont_wdth_wght.B4AZ-wl6.ttf +0 -0
  45. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Regular.DxJTClRG.ttf +0 -0
  46. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-SemiBold.D3TTYgdB.ttf +0 -0
  47. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-VariableFont_wdth_wght.BZBpG5Iz.ttf +0 -0
  48. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.OwPEPQZu.css +1 -0
  49. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.b653GmVf.css +1 -0
  50. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.T-zjSUd3.css +1 -0
  51. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/useFeatureFlags.CV-KWLNP.css +1 -0
  52. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/69_IOA4Y.js +1 -0
  53. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B2FVR0s0.js +1 -0
  54. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B90CZVMX.js +1 -0
  55. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B9zumHo5.js +1 -0
  56. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BJXwVxaE.js +1 -0
  57. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bsi3UGy5.js +1 -0
  58. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bu7uvVrG.js +1 -0
  59. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bx1xMsFy.js +1 -0
  60. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BylOuP6i.js +1 -0
  61. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C8I8rFJQ.js +1 -0
  62. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CDnpyLsT.js +1 -0
  63. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWj6FrbW.js +1 -0
  64. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CYgJF_JY.js +1 -0
  65. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CcaPhhk3.js +1 -0
  66. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvOmgdoc.js +93 -0
  67. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxtLVaYz.js +3 -0
  68. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D5-A_Ffd.js +4 -0
  69. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6RI2Zrd.js +1 -0
  70. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6su9Aln.js +1 -0
  71. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D98V7j6A.js +1 -0
  72. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIRAtgl0.js +1 -0
  73. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIeogL5L.js +1 -0
  74. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DOlTMNyt.js +1 -0
  75. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjUWrjOv.js +1 -0
  76. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjfY96ND.js +1 -0
  77. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/H7C68rOM.js +1 -0
  78. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/O-EABkf9.js +1 -0
  79. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/XO7A28GO.js +1 -0
  80. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/hQVEETDE.js +1 -0
  81. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/l7KrR96u.js +1 -0
  82. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/nAHhluT7.js +1 -0
  83. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/r64xT6ao.js +1 -0
  84. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/vC4nQVEB.js +1 -0
  85. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/x9G_hzyY.js +1 -0
  86. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.CjnvpsmS.js +2 -0
  87. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.0o1H7wM9.js +1 -0
  88. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.XRq_TUwu.js +1 -0
  89. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/1.B4rNYwVp.js +1 -0
  90. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.DfBwOEhN.js +1 -0
  91. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/11.CWG1ehzT.js +1 -0
  92. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CwF2_8mP.js +1 -0
  93. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.CS4muRY-.js +6 -0
  94. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/3.CWHpKonm.js +1 -0
  95. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/4.OUWOLQeV.js +1 -0
  96. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.Dm6t9F5W.js +1 -0
  97. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.Bw5ck4gK.js +1 -0
  98. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.CF0EDTR6.js +1 -0
  99. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cw30LEcV.js +1 -0
  100. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/9.CPu3CiBc.js +1 -0
  101. lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -0
  102. lightly_studio/dist_lightly_studio_view_app/apple-touch-icon-precomposed.png +0 -0
  103. lightly_studio/dist_lightly_studio_view_app/apple-touch-icon.png +0 -0
  104. lightly_studio/dist_lightly_studio_view_app/favicon.png +0 -0
  105. lightly_studio/dist_lightly_studio_view_app/index.html +44 -0
  106. lightly_studio/examples/example.py +23 -0
  107. lightly_studio/examples/example_metadata.py +338 -0
  108. lightly_studio/examples/example_selection.py +39 -0
  109. lightly_studio/examples/example_split_work.py +67 -0
  110. lightly_studio/examples/example_v2.py +21 -0
  111. lightly_studio/export_schema.py +18 -0
  112. lightly_studio/few_shot_classifier/__init__.py +0 -0
  113. lightly_studio/few_shot_classifier/classifier.py +80 -0
  114. lightly_studio/few_shot_classifier/classifier_manager.py +663 -0
  115. lightly_studio/few_shot_classifier/random_forest_classifier.py +489 -0
  116. lightly_studio/metadata/complex_metadata.py +47 -0
  117. lightly_studio/metadata/gps_coordinate.py +41 -0
  118. lightly_studio/metadata/metadata_protocol.py +17 -0
  119. lightly_studio/metrics/__init__.py +0 -0
  120. lightly_studio/metrics/detection/__init__.py +0 -0
  121. lightly_studio/metrics/detection/map.py +268 -0
  122. lightly_studio/models/__init__.py +1 -0
  123. lightly_studio/models/annotation/__init__.py +0 -0
  124. lightly_studio/models/annotation/annotation_base.py +171 -0
  125. lightly_studio/models/annotation/instance_segmentation.py +56 -0
  126. lightly_studio/models/annotation/links.py +17 -0
  127. lightly_studio/models/annotation/object_detection.py +47 -0
  128. lightly_studio/models/annotation/semantic_segmentation.py +44 -0
  129. lightly_studio/models/annotation_label.py +47 -0
  130. lightly_studio/models/annotation_task.py +28 -0
  131. lightly_studio/models/classifier.py +20 -0
  132. lightly_studio/models/dataset.py +84 -0
  133. lightly_studio/models/embedding_model.py +30 -0
  134. lightly_studio/models/metadata.py +208 -0
  135. lightly_studio/models/sample.py +180 -0
  136. lightly_studio/models/sample_embedding.py +37 -0
  137. lightly_studio/models/settings.py +60 -0
  138. lightly_studio/models/tag.py +96 -0
  139. lightly_studio/py.typed +0 -0
  140. lightly_studio/resolvers/__init__.py +7 -0
  141. lightly_studio/resolvers/annotation_label_resolver/__init__.py +21 -0
  142. lightly_studio/resolvers/annotation_label_resolver/create.py +27 -0
  143. lightly_studio/resolvers/annotation_label_resolver/delete.py +28 -0
  144. lightly_studio/resolvers/annotation_label_resolver/get_all.py +22 -0
  145. lightly_studio/resolvers/annotation_label_resolver/get_by_id.py +24 -0
  146. lightly_studio/resolvers/annotation_label_resolver/get_by_ids.py +25 -0
  147. lightly_studio/resolvers/annotation_label_resolver/get_by_label_name.py +24 -0
  148. lightly_studio/resolvers/annotation_label_resolver/names_by_ids.py +25 -0
  149. lightly_studio/resolvers/annotation_label_resolver/update.py +38 -0
  150. lightly_studio/resolvers/annotation_resolver/__init__.py +33 -0
  151. lightly_studio/resolvers/annotation_resolver/count_annotations_by_dataset.py +120 -0
  152. lightly_studio/resolvers/annotation_resolver/create.py +19 -0
  153. lightly_studio/resolvers/annotation_resolver/create_many.py +96 -0
  154. lightly_studio/resolvers/annotation_resolver/delete_annotation.py +45 -0
  155. lightly_studio/resolvers/annotation_resolver/delete_annotations.py +56 -0
  156. lightly_studio/resolvers/annotation_resolver/get_all.py +74 -0
  157. lightly_studio/resolvers/annotation_resolver/get_by_id.py +18 -0
  158. lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +144 -0
  159. lightly_studio/resolvers/annotation_resolver/update_bounding_box.py +68 -0
  160. lightly_studio/resolvers/annotation_task_resolver.py +31 -0
  161. lightly_studio/resolvers/annotations/__init__.py +1 -0
  162. lightly_studio/resolvers/annotations/annotations_filter.py +89 -0
  163. lightly_studio/resolvers/dataset_resolver.py +278 -0
  164. lightly_studio/resolvers/embedding_model_resolver.py +100 -0
  165. lightly_studio/resolvers/metadata_resolver/__init__.py +15 -0
  166. lightly_studio/resolvers/metadata_resolver/metadata_filter.py +163 -0
  167. lightly_studio/resolvers/metadata_resolver/sample/__init__.py +21 -0
  168. lightly_studio/resolvers/metadata_resolver/sample/bulk_set_metadata.py +48 -0
  169. lightly_studio/resolvers/metadata_resolver/sample/get_by_sample_id.py +24 -0
  170. lightly_studio/resolvers/metadata_resolver/sample/get_metadata_info.py +104 -0
  171. lightly_studio/resolvers/metadata_resolver/sample/get_value_for_sample.py +27 -0
  172. lightly_studio/resolvers/metadata_resolver/sample/set_value_for_sample.py +53 -0
  173. lightly_studio/resolvers/sample_embedding_resolver.py +86 -0
  174. lightly_studio/resolvers/sample_resolver.py +249 -0
  175. lightly_studio/resolvers/samples_filter.py +81 -0
  176. lightly_studio/resolvers/settings_resolver.py +58 -0
  177. lightly_studio/resolvers/tag_resolver.py +276 -0
  178. lightly_studio/selection/README.md +6 -0
  179. lightly_studio/selection/mundig.py +105 -0
  180. lightly_studio/selection/select.py +96 -0
  181. lightly_studio/selection/select_via_db.py +93 -0
  182. lightly_studio/selection/selection_config.py +31 -0
  183. lightly_studio/services/annotations_service/__init__.py +21 -0
  184. lightly_studio/services/annotations_service/get_annotation_by_id.py +31 -0
  185. lightly_studio/services/annotations_service/update_annotation.py +65 -0
  186. lightly_studio/services/annotations_service/update_annotation_label.py +48 -0
  187. lightly_studio/services/annotations_service/update_annotations.py +29 -0
  188. lightly_studio/setup_logging.py +19 -0
  189. lightly_studio/type_definitions.py +19 -0
  190. lightly_studio/vendor/ACKNOWLEDGEMENTS +422 -0
  191. lightly_studio/vendor/LICENSE +31 -0
  192. lightly_studio/vendor/LICENSE_weights_data +50 -0
  193. lightly_studio/vendor/README.md +5 -0
  194. lightly_studio/vendor/__init__.py +1 -0
  195. lightly_studio/vendor/mobileclip/__init__.py +96 -0
  196. lightly_studio/vendor/mobileclip/clip.py +77 -0
  197. lightly_studio/vendor/mobileclip/configs/mobileclip_b.json +18 -0
  198. lightly_studio/vendor/mobileclip/configs/mobileclip_s0.json +18 -0
  199. lightly_studio/vendor/mobileclip/configs/mobileclip_s1.json +18 -0
  200. lightly_studio/vendor/mobileclip/configs/mobileclip_s2.json +18 -0
  201. lightly_studio/vendor/mobileclip/image_encoder.py +67 -0
  202. lightly_studio/vendor/mobileclip/logger.py +154 -0
  203. lightly_studio/vendor/mobileclip/models/__init__.py +10 -0
  204. lightly_studio/vendor/mobileclip/models/mci.py +933 -0
  205. lightly_studio/vendor/mobileclip/models/vit.py +433 -0
  206. lightly_studio/vendor/mobileclip/modules/__init__.py +4 -0
  207. lightly_studio/vendor/mobileclip/modules/common/__init__.py +4 -0
  208. lightly_studio/vendor/mobileclip/modules/common/mobileone.py +341 -0
  209. lightly_studio/vendor/mobileclip/modules/common/transformer.py +451 -0
  210. lightly_studio/vendor/mobileclip/modules/image/__init__.py +4 -0
  211. lightly_studio/vendor/mobileclip/modules/image/image_projection.py +113 -0
  212. lightly_studio/vendor/mobileclip/modules/image/replknet.py +188 -0
  213. lightly_studio/vendor/mobileclip/modules/text/__init__.py +4 -0
  214. lightly_studio/vendor/mobileclip/modules/text/repmixer.py +281 -0
  215. lightly_studio/vendor/mobileclip/modules/text/tokenizer.py +38 -0
  216. lightly_studio/vendor/mobileclip/text_encoder.py +245 -0
  217. lightly_studio-0.3.1.dist-info/METADATA +520 -0
  218. lightly_studio-0.3.1.dist-info/RECORD +219 -0
  219. lightly_studio-0.3.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,20 @@
1
+ """This module defines the data model for the classifier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID
6
+
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class EmbeddingClassifier(BaseModel):
11
+ """Base class for the Classifier model."""
12
+
13
+ """The name of the classifier."""
14
+ classifier_name: str
15
+
16
+ """The ID of the classifier."""
17
+ classifier_id: UUID
18
+
19
+ """List of classes supported by the classifier."""
20
+ class_list: list[str]
@@ -0,0 +1,84 @@
1
+ """This module contains the Dataset model and related enumerations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from datetime import datetime, timezone
7
+ from typing import cast
8
+ from uuid import UUID, uuid4
9
+
10
+ from sqlalchemy.orm import Session as SQLAlchemySession
11
+ from sqlmodel import Field, Session, SQLModel
12
+
13
+ from lightly_studio.models.sample import SampleTable
14
+ from lightly_studio.resolvers import sample_resolver
15
+ from lightly_studio.resolvers.samples_filter import SampleFilter
16
+
17
+
18
+ class DatasetBase(SQLModel):
19
+ """Base class for the Dataset model."""
20
+
21
+ name: str
22
+ directory: str
23
+
24
+
25
+ class DatasetCreate(DatasetBase):
26
+ """Dataset class when inserting."""
27
+
28
+
29
+ class DatasetView(DatasetBase):
30
+ """Dataset class when retrieving."""
31
+
32
+ dataset_id: UUID
33
+ created_at: datetime
34
+ updated_at: datetime
35
+
36
+
37
+ class DatasetTable(DatasetBase, table=True):
38
+ """This class defines the Dataset model."""
39
+
40
+ __tablename__ = "datasets"
41
+ dataset_id: UUID = Field(default_factory=uuid4, primary_key=True)
42
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), index=True)
43
+ updated_at: datetime = Field(
44
+ default_factory=lambda: datetime.now(timezone.utc),
45
+ )
46
+
47
+ def get_samples(
48
+ self,
49
+ offset: int = 0,
50
+ limit: int | None = None,
51
+ filters: SampleFilter | None = None,
52
+ text_embedding: list[float] | None = None,
53
+ sample_ids: list[UUID] | None = None,
54
+ ) -> Sequence[SampleTable]:
55
+ """Retrieve samples for this dataset with optional filtering.
56
+
57
+ Just passes the parameters to the sample resolver.
58
+
59
+ Args:
60
+ offset: Offset for pagination.
61
+ limit: Limit for pagination.
62
+ filters: Optional filters to apply.
63
+ text_embedding: Optional text embedding for filtering.
64
+ sample_ids: Optional list of sample IDs to filter by.
65
+
66
+ Returns:
67
+ A sequence of SampleTable objects.
68
+ """
69
+ # Get the session from the instance.
70
+ # SQLAlchemy Session is compatible with SQLModel's Session at runtime,
71
+ # but we have to help mypy.
72
+ session = cast(Session, SQLAlchemySession.object_session(self))
73
+ if session is None:
74
+ raise RuntimeError("No database session found for this instance")
75
+
76
+ return sample_resolver.get_all_by_dataset_id(
77
+ session=session,
78
+ dataset_id=self.dataset_id,
79
+ offset=offset,
80
+ limit=limit,
81
+ filters=filters,
82
+ text_embedding=text_embedding,
83
+ sample_ids=sample_ids,
84
+ ).samples
@@ -0,0 +1,30 @@
1
+ """This module defines the Embedding_Model model for the application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from uuid import UUID, uuid4
7
+
8
+ from sqlmodel import CHAR, Column, Field, SQLModel
9
+
10
+
11
+ class EmbeddingModelBase(SQLModel):
12
+ """Base class for the EmbeddingModel."""
13
+
14
+ name: str
15
+ parameter_count_in_mb: int | None = None
16
+ embedding_model_hash: str = Field(default="", sa_column=Column(CHAR(128)))
17
+ embedding_dimension: int
18
+ dataset_id: UUID = Field(default=None, foreign_key="datasets.dataset_id")
19
+
20
+
21
+ class EmbeddingModelCreate(EmbeddingModelBase):
22
+ """Model used for creating an embedding model."""
23
+
24
+
25
+ class EmbeddingModelTable(EmbeddingModelBase, table=True):
26
+ """This class defines the EmbeddingModel model."""
27
+
28
+ __tablename__ = "embedding_models"
29
+ embedding_model_id: UUID = Field(default_factory=uuid4, primary_key=True)
30
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), index=True)
@@ -0,0 +1,208 @@
1
+ """Metadata models for storing custom metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import TYPE_CHECKING, Any
7
+ from uuid import UUID, uuid4
8
+
9
+ from pydantic import BaseModel
10
+ from sqlalchemy.orm.attributes import flag_modified
11
+ from sqlalchemy.types import JSON
12
+ from sqlmodel import Field, Relationship, SQLModel
13
+
14
+ from lightly_studio.metadata.complex_metadata import (
15
+ COMPLEX_METADATA_TYPES,
16
+ deserialize_complex_metadata,
17
+ serialize_complex_metadata,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from lightly_studio.models.sample import SampleTable
22
+ else:
23
+ SampleTable = object
24
+
25
+
26
+ TYPE_TO_NAME_MAP = {
27
+ bool: "boolean",
28
+ int: "integer",
29
+ float: "float",
30
+ str: "string",
31
+ list: "list",
32
+ dict: "dict",
33
+ }
34
+
35
+ NAME_TO_TYPE_MAP = {
36
+ "string": str,
37
+ "integer": int,
38
+ "float": float,
39
+ "boolean": bool,
40
+ "list": list,
41
+ "dict": dict,
42
+ }
43
+
44
+
45
+ def get_type_name(value: Any) -> str:
46
+ """Get the type name for a value.
47
+
48
+ Args:
49
+ value: The value to get the type name for.
50
+
51
+ Returns:
52
+ The type name as a string.
53
+ """
54
+ if value is None:
55
+ return "null"
56
+ # Check if it's a complex metadata type.
57
+ for name, cls in COMPLEX_METADATA_TYPES.items():
58
+ if isinstance(value, cls):
59
+ return name
60
+
61
+ # Return mapped type name or fallback to class name.
62
+ return TYPE_TO_NAME_MAP.get(type(value), type(value).__name__.lower())
63
+
64
+
65
+ def validate_type_compatibility(expected_type: str, value: Any) -> bool:
66
+ """Validate that a value is compatible with an expected type.
67
+
68
+ Args:
69
+ expected_type: The expected type name.
70
+ value: The value to validate.
71
+
72
+ Returns:
73
+ True if compatible, False otherwise.
74
+ """
75
+ if value is None:
76
+ return expected_type == "null"
77
+
78
+ # Check complex types.
79
+ if expected_type in COMPLEX_METADATA_TYPES:
80
+ expected_complex_cls = COMPLEX_METADATA_TYPES[expected_type]
81
+ assert expected_complex_cls is not None
82
+ return isinstance(value, expected_complex_cls)
83
+
84
+ # Check simple types
85
+ expected_cls = NAME_TO_TYPE_MAP.get(expected_type)
86
+ if expected_cls is None:
87
+ return False
88
+
89
+ return isinstance(value, expected_cls)
90
+
91
+
92
+ class MetadataBase(SQLModel):
93
+ """Base class for CustomMetadata models."""
94
+
95
+ custom_metadata_id: UUID = Field(default_factory=uuid4, primary_key=True)
96
+ created_at: datetime = Field(
97
+ default_factory=lambda: datetime.now(timezone.utc),
98
+ )
99
+ updated_at: datetime = Field(
100
+ default_factory=lambda: datetime.now(timezone.utc),
101
+ )
102
+ # Dictionary storing the actual metadata values as JSON.
103
+ data: dict[str, Any] = Field(
104
+ default_factory=dict,
105
+ sa_type=JSON,
106
+ description="Custom metadata stored as JSON",
107
+ )
108
+ # Dictionary storing the metadata schema.
109
+ metadata_schema: dict[str, str] = Field(
110
+ default_factory=dict,
111
+ sa_type=JSON,
112
+ description="Schema information for metadata keys",
113
+ )
114
+
115
+ def ensure_schema(self, key: str, value: Any) -> None:
116
+ """Ensure schema exists for a key and validate value type.
117
+
118
+ This method handles schema management for metadata keys:
119
+ If the key doesn't exist in the schema, it creates a new entry
120
+ with the inferred type from the provided value
121
+ If the key exists, it validates that the new value matches
122
+ the expected type from the schema
123
+
124
+ This ensures type consistency across metadata operations and prevents
125
+ accidental type mismatches that could cause issues in applications.
126
+
127
+ Args:
128
+ key: The metadata key to validate/update schema for
129
+ value: The value to validate/use for type inference
130
+
131
+ Raises:
132
+ ValueError: If the value type doesn't match existing schema
133
+ """
134
+ if key not in self.metadata_schema:
135
+ # New key - create schema with actual type name.
136
+ self.metadata_schema[key] = get_type_name(value)
137
+ else:
138
+ # Existing key - validate type.
139
+ existing_type = self.metadata_schema[key]
140
+ if not validate_type_compatibility(existing_type, value):
141
+ raise ValueError(
142
+ f"Value type mismatch for key '{key}'. "
143
+ f"Expected {existing_type}, got {get_type_name(value)}."
144
+ )
145
+
146
+ def set_value(self, key: str, value: Any) -> None:
147
+ """Set a metadata value with schema validation and database tracking.
148
+
149
+ Args:
150
+ key: The metadata key to set
151
+ value: The value to set
152
+
153
+ Raises:
154
+ ValueError: If the value type doesn't match the schema
155
+ """
156
+ self.ensure_schema(key, value)
157
+ # Serialize complex metadata for storage.
158
+ self.data[key] = serialize_complex_metadata(value)
159
+ self.updated_at = datetime.now(timezone.utc)
160
+ # Mark the object as modified so SQLAlchemy knows to update it.
161
+ flag_modified(self, "data")
162
+ flag_modified(self, "metadata_schema")
163
+
164
+ def get_value(self, key: str) -> Any:
165
+ """Get a metadata value with automatic deserialization.
166
+
167
+ Args:
168
+ key: The metadata key.
169
+
170
+ Returns:
171
+ The deserialized value (complex metadata object if applicable)
172
+ or None if the key doesn't exist.
173
+ """
174
+ value = self.data.get(key)
175
+ if value is not None:
176
+ # Get expected type from schema for deserialization.
177
+ expected_type = self.metadata_schema.get(key)
178
+ if expected_type:
179
+ return deserialize_complex_metadata(value, expected_type)
180
+ return value
181
+
182
+
183
+ class MetadataCreate(MetadataBase):
184
+ """Input class for Metadata model."""
185
+
186
+
187
+ class SampleMetadataTable(MetadataBase, table=True):
188
+ """This class defines the SampleMetadataTable model."""
189
+
190
+ __tablename__ = "metadata"
191
+ sample_id: UUID = Field(foreign_key="samples.sample_id")
192
+
193
+ sample: SampleTable = Relationship(back_populates="metadata_dict")
194
+
195
+
196
+ class SampleMetadataView(SQLModel):
197
+ """Sample metadata class when retrieving."""
198
+
199
+ data: dict[str, Any]
200
+
201
+
202
+ class MetadataInfoView(BaseModel):
203
+ """Metadata info response model for API endpoints."""
204
+
205
+ name: str = Field(description="The metadata key name")
206
+ type: str = Field(description="The metadata type (e.g., 'string', 'integer', 'float')")
207
+ min: int | float | None = Field(None, description="Minimum value for numerical metadata")
208
+ max: int | float | None = Field(None, description="Maximum value for numerical metadata")
@@ -0,0 +1,180 @@
1
+ """This module defines the User model for the application."""
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import TYPE_CHECKING, Any, List, Literal, Optional
5
+ from uuid import UUID, uuid4
6
+
7
+ from sqlalchemy.orm import Mapped, Session
8
+ from sqlmodel import Field, Relationship, SQLModel
9
+
10
+ from lightly_studio.models.annotation.annotation_base import AnnotationView
11
+ from lightly_studio.resolvers import metadata_resolver
12
+
13
+ if TYPE_CHECKING:
14
+ from lightly_studio.models.annotation.annotation_base import (
15
+ AnnotationBaseTable,
16
+ )
17
+ from lightly_studio.models.metadata import (
18
+ SampleMetadataTable,
19
+ SampleMetadataView,
20
+ )
21
+ from lightly_studio.models.sample_embedding import SampleEmbeddingTable
22
+ from lightly_studio.models.tag import TagTable
23
+ else:
24
+ AnnotationBaseTable = object
25
+ SampleEmbeddingTable = object
26
+ SampleMetadataTable = object
27
+ TagTable = object
28
+ SampleMetadataView = object
29
+
30
+
31
+ class SampleBase(SQLModel):
32
+ """Base class for the Sample model."""
33
+
34
+ """The name of the image file."""
35
+ file_name: str
36
+
37
+ """The width of the image in pixels."""
38
+ width: int
39
+
40
+ """The height of the image in pixels."""
41
+ height: int
42
+
43
+ """The dataset ID to which the sample belongs."""
44
+ dataset_id: UUID = Field(default=None, foreign_key="datasets.dataset_id")
45
+
46
+ """The dataset image path."""
47
+ file_path_abs: str
48
+
49
+
50
+ class SampleCreate(SampleBase):
51
+ """Sample class when inserting."""
52
+
53
+
54
+ class SampleViewForAnnotation(SQLModel):
55
+ """Sample class for annotation view."""
56
+
57
+ """The name of the image file."""
58
+ file_path_abs: str
59
+ sample_id: UUID
60
+
61
+ """The width of the image in pixels."""
62
+ width: int
63
+
64
+ """The height of the image in pixels."""
65
+ height: int
66
+
67
+ created_at: datetime
68
+ updated_at: datetime
69
+
70
+
71
+ class SampleTagLinkTable(SQLModel, table=True):
72
+ """Model to define links between Sample and Tag Many-to-Many."""
73
+
74
+ sample_id: Optional[UUID] = Field(
75
+ default=None, foreign_key="samples.sample_id", primary_key=True
76
+ )
77
+ tag_id: Optional[UUID] = Field(default=None, foreign_key="tags.tag_id", primary_key=True)
78
+
79
+
80
+ class SampleTable(SampleBase, table=True):
81
+ """This class defines the Sample model."""
82
+
83
+ __tablename__ = "samples"
84
+ sample_id: UUID = Field(default_factory=uuid4, primary_key=True)
85
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), index=True)
86
+ updated_at: datetime = Field(
87
+ default_factory=lambda: datetime.now(timezone.utc),
88
+ )
89
+ annotations: Mapped[List["AnnotationBaseTable"]] = Relationship(
90
+ back_populates="sample",
91
+ )
92
+
93
+ """The tag ids associated with the sample."""
94
+ tags: Mapped[List["TagTable"]] = Relationship(
95
+ back_populates="samples", link_model=SampleTagLinkTable
96
+ )
97
+ embeddings: Mapped[List["SampleEmbeddingTable"]] = Relationship(back_populates="sample")
98
+ metadata_dict: "SampleMetadataTable" = Relationship(back_populates="sample")
99
+
100
+ def __getitem__(self, key: str) -> Any:
101
+ """Provides dict-like access to sample metadata.
102
+
103
+ Args:
104
+ key: The metadata key to access.
105
+
106
+ Returns:
107
+ The metadata value for the given key, or None if the key doesn't
108
+ exist.
109
+ """
110
+ if self.metadata_dict is None:
111
+ return None
112
+ return self.metadata_dict.get_value(key)
113
+
114
+ def __setitem__(self, key: str, value: Any) -> None:
115
+ """Sets a metadata key-value pair for this sample.
116
+
117
+ Args:
118
+ key: The metadata key.
119
+ value: The metadata value.
120
+
121
+ Note:
122
+ If the sample has no metadata, a new Metadata Table instance
123
+ will be created. Changes are automatically committed to the
124
+ database.
125
+
126
+ Raises:
127
+ RuntimeError: If no database session is found.
128
+ """
129
+ # Get the session from the instance
130
+ session = Session.object_session(self)
131
+ if session is None:
132
+ raise RuntimeError("No database session found for this instance")
133
+
134
+ # Use metadata_resolver to handle the database operations.
135
+ # Added type: ignore to avoid type checking issues. SQLAlchemy and
136
+ # SQLModel sessions are compatible at runtime but have different type
137
+ # annotations.
138
+ metadata_resolver.set_value_for_sample(
139
+ session=session, # type: ignore[arg-type]
140
+ sample_id=self.sample_id,
141
+ key=key,
142
+ value=value,
143
+ )
144
+
145
+
146
+ TagKind = Literal[
147
+ "sample",
148
+ "annotation",
149
+ ]
150
+
151
+
152
+ class SampleView(SQLModel):
153
+ """Sample class when retrieving."""
154
+
155
+ class SampleViewTag(SQLModel):
156
+ """Tag view inside Sample view."""
157
+
158
+ tag_id: UUID
159
+ name: str
160
+ kind: TagKind
161
+ created_at: datetime
162
+ updated_at: datetime
163
+
164
+ """The name of the image file."""
165
+ file_name: str
166
+ file_path_abs: str
167
+ sample_id: UUID
168
+ dataset_id: UUID
169
+ annotations: List["AnnotationView"] = Field([])
170
+ tags: List[SampleViewTag] = Field([])
171
+ metadata_dict: Optional["SampleMetadataView"] = None
172
+ width: int
173
+ height: int
174
+
175
+
176
+ class SampleViewsWithCount(SQLModel):
177
+ """Response model for counted samples."""
178
+
179
+ data: List[SampleView]
180
+ total_count: int
@@ -0,0 +1,37 @@
1
+ """This module defines the SampleEmbedding model for the application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+ from uuid import UUID, uuid4
7
+
8
+ from sqlalchemy import ARRAY, Float
9
+ from sqlmodel import Column, Field, Relationship, SQLModel
10
+
11
+ if TYPE_CHECKING:
12
+ from lightly_studio.models.sample import SampleTable
13
+
14
+ else:
15
+ SampleTable = object
16
+
17
+
18
+ class SampleEmbeddingBase(SQLModel):
19
+ """Base class for the Embeddings used for Samples."""
20
+
21
+ sample_embedding_id: UUID = Field(default_factory=uuid4, primary_key=True)
22
+
23
+ sample_id: UUID = Field(foreign_key="samples.sample_id")
24
+
25
+ embedding_model_id: UUID = Field(foreign_key="embedding_models.embedding_model_id")
26
+ embedding: list[float] = Field(sa_column=Column(ARRAY(Float)))
27
+
28
+
29
+ class SampleEmbeddingCreate(SampleEmbeddingBase):
30
+ """Sample class when inserting."""
31
+
32
+
33
+ class SampleEmbeddingTable(SampleEmbeddingBase, table=True):
34
+ """This class defines the SampleEmbedding model."""
35
+
36
+ __tablename__ = "sample_embeddings"
37
+ sample: SampleTable = Relationship(back_populates="embeddings")
@@ -0,0 +1,60 @@
1
+ """This module contains settings model for user preferences."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from enum import Enum
7
+ from uuid import UUID, uuid4
8
+
9
+ from sqlmodel import Field, SQLModel
10
+
11
+
12
+ class GridViewSampleRenderingType(str, Enum):
13
+ """Defines how samples are rendered in the grid view."""
14
+
15
+ COVER = "cover"
16
+ CONTAIN = "contain"
17
+
18
+
19
+ class SettingBase(SQLModel):
20
+ """Base class for Settings model."""
21
+
22
+ grid_view_sample_rendering: GridViewSampleRenderingType = Field(
23
+ default=GridViewSampleRenderingType.CONTAIN,
24
+ description="Controls how samples are rendered in the grid view",
25
+ )
26
+
27
+ # Keyboard shortcuts.
28
+ key_hide_annotations: str = Field(
29
+ default="v",
30
+ description="Key to temporarily hide annotations while pressed",
31
+ )
32
+ key_go_back: str = Field(
33
+ default="Escape",
34
+ description="Key to navigate back from detail view to grid view",
35
+ )
36
+
37
+ # New setting for annotation text visibility.
38
+ show_annotation_text_labels: bool = Field(
39
+ default=True,
40
+ description="Controls whether to show text labels on annotations",
41
+ )
42
+
43
+
44
+ class SettingView(SettingBase):
45
+ """View class for Settings model."""
46
+
47
+ setting_id: UUID
48
+ created_at: datetime
49
+ updated_at: datetime
50
+
51
+
52
+ class SettingTable(SettingBase, table=True):
53
+ """This class defines the Setting model."""
54
+
55
+ __tablename__ = "settings"
56
+ setting_id: UUID = Field(default_factory=uuid4, primary_key=True)
57
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), index=True)
58
+ updated_at: datetime = Field(
59
+ default_factory=lambda: datetime.now(timezone.utc),
60
+ )