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.
- lightly_studio/__init__.py +12 -0
- lightly_studio/api/__init__.py +0 -0
- lightly_studio/api/app.py +131 -0
- lightly_studio/api/cache.py +77 -0
- lightly_studio/api/db_tables.py +35 -0
- lightly_studio/api/features.py +5 -0
- lightly_studio/api/routes/api/annotation.py +305 -0
- lightly_studio/api/routes/api/annotation_label.py +87 -0
- lightly_studio/api/routes/api/annotations/__init__.py +7 -0
- lightly_studio/api/routes/api/annotations/create_annotation.py +52 -0
- lightly_studio/api/routes/api/caption.py +100 -0
- lightly_studio/api/routes/api/classifier.py +384 -0
- lightly_studio/api/routes/api/dataset.py +191 -0
- lightly_studio/api/routes/api/dataset_tag.py +266 -0
- lightly_studio/api/routes/api/embeddings2d.py +90 -0
- lightly_studio/api/routes/api/exceptions.py +114 -0
- lightly_studio/api/routes/api/export.py +114 -0
- lightly_studio/api/routes/api/features.py +17 -0
- lightly_studio/api/routes/api/frame.py +241 -0
- lightly_studio/api/routes/api/image.py +155 -0
- lightly_studio/api/routes/api/metadata.py +161 -0
- lightly_studio/api/routes/api/operator.py +75 -0
- lightly_studio/api/routes/api/sample.py +103 -0
- lightly_studio/api/routes/api/selection.py +87 -0
- lightly_studio/api/routes/api/settings.py +41 -0
- lightly_studio/api/routes/api/status.py +19 -0
- lightly_studio/api/routes/api/text_embedding.py +50 -0
- lightly_studio/api/routes/api/validators.py +17 -0
- lightly_studio/api/routes/api/video.py +133 -0
- lightly_studio/api/routes/healthz.py +13 -0
- lightly_studio/api/routes/images.py +104 -0
- lightly_studio/api/routes/video_frames_media.py +116 -0
- lightly_studio/api/routes/video_media.py +223 -0
- lightly_studio/api/routes/webapp.py +51 -0
- lightly_studio/api/server.py +94 -0
- lightly_studio/core/__init__.py +0 -0
- lightly_studio/core/add_samples.py +533 -0
- lightly_studio/core/add_videos.py +294 -0
- lightly_studio/core/dataset.py +780 -0
- lightly_studio/core/dataset_query/__init__.py +14 -0
- lightly_studio/core/dataset_query/boolean_expression.py +67 -0
- lightly_studio/core/dataset_query/dataset_query.py +317 -0
- lightly_studio/core/dataset_query/field.py +113 -0
- lightly_studio/core/dataset_query/field_expression.py +79 -0
- lightly_studio/core/dataset_query/match_expression.py +23 -0
- lightly_studio/core/dataset_query/order_by.py +79 -0
- lightly_studio/core/dataset_query/sample_field.py +37 -0
- lightly_studio/core/dataset_query/tags_expression.py +46 -0
- lightly_studio/core/image_sample.py +36 -0
- lightly_studio/core/loading_log.py +56 -0
- lightly_studio/core/sample.py +291 -0
- lightly_studio/core/start_gui.py +54 -0
- lightly_studio/core/video_sample.py +38 -0
- lightly_studio/dataset/__init__.py +0 -0
- lightly_studio/dataset/edge_embedding_generator.py +155 -0
- lightly_studio/dataset/embedding_generator.py +129 -0
- lightly_studio/dataset/embedding_manager.py +349 -0
- lightly_studio/dataset/env.py +20 -0
- lightly_studio/dataset/file_utils.py +49 -0
- lightly_studio/dataset/fsspec_lister.py +275 -0
- lightly_studio/dataset/mobileclip_embedding_generator.py +158 -0
- lightly_studio/dataset/perception_encoder_embedding_generator.py +260 -0
- lightly_studio/db_manager.py +166 -0
- lightly_studio/dist_lightly_studio_view_app/_app/env.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.GcXvs2l7.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/12.Dx6SXgAb.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/17.9X9_k6TP.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/18.BxiimdIO.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/2.CkOblLn7.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/ClassifierSamplesGrid.BJbCDlvs.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/LightlyLogo.BNjCIww-.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Bold.DGvYQtcs.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Italic-VariableFont_wdth_wght.B4AZ-wl6.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Medium.DVUZMR_6.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Regular.DxJTClRG.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-SemiBold.D3TTYgdB.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-VariableFont_wdth_wght.BZBpG5Iz.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.CefECEWA.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.D5tDcjY-.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_page.9X9_k6TP.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_page.BxiimdIO.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_page.Dx6SXgAb.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/transform._-1mPSEI.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/0dDyq72A.js +20 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/69_IOA4Y.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BK4An2kI.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BRmB-kJ9.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B_1cpokE.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BiqpDEr0.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BpLiSKgx.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BscxbINH.js +39 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C1FmrZbK.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C80h3dJx.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C8mfFM-u.js +2 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CGY1p9L4.js +517 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/COfLknXM.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWj6FrbW.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CYgJF_JY.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CmLg0ys7.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvGjimpO.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D3RDXHoj.js +39 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D4y7iiT3.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D9SC3jBb.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DCuAdx1Q.js +20 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DDBy-_jD.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIeogL5L.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DL9a7v5o.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DSKECuqX.js +39 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D_FFv0Oe.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DiZ5o5vz.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DkbXUtyG.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DmK2hulV.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DqnHaLTj.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DtWZc_tl.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DuUalyFS.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DwIonDAZ.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Il-mSPmK.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/KNLP4aJU.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/KjYeVjkE.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/MErlcOXj.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/VRI4prUD.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/VYb2dkNs.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/VqWvU2yF.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/dHC3otuL.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/da7Oy_lO.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/eAy8rZzC.js +2 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/erjNR5MX.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/f1oG3eFE.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/rsLi1iKv.js +20 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/rwuuBP9f.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/xGHZQ1pe.js +3 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.DrTRUgT3.js +2 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.BK5EOJl2.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.CIvTuljF.js +4 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/1.UBvSzxdA.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.CQ_tiLJa.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/11.KqkAcaxW.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.DoYsmxQc.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/13.571n2LZA.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/14.DGs689M-.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/15.CWG1ehzT.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/16.Dpq6jbSh.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/17.B5AZbHUU.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/18.CBga8cnq.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.D2HXgz-8.js +1090 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/3.f4HAg-y3.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/4.BKF4xuKQ.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.BAE0Pm_f.js +39 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.CouWWpzA.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.UBHT0ktp.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.FiYNElcc.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/9.B3-UaT23.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/clustering.worker-DKqeLtG0.js +2 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/search.worker-vNSty3B0.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -0
- lightly_studio/dist_lightly_studio_view_app/apple-touch-icon-precomposed.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/apple-touch-icon.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/favicon.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/index.html +45 -0
- lightly_studio/errors.py +5 -0
- lightly_studio/examples/example.py +25 -0
- lightly_studio/examples/example_coco.py +27 -0
- lightly_studio/examples/example_coco_caption.py +29 -0
- lightly_studio/examples/example_metadata.py +369 -0
- lightly_studio/examples/example_operators.py +111 -0
- lightly_studio/examples/example_selection.py +28 -0
- lightly_studio/examples/example_split_work.py +48 -0
- lightly_studio/examples/example_video.py +22 -0
- lightly_studio/examples/example_video_annotations.py +157 -0
- lightly_studio/examples/example_yolo.py +22 -0
- lightly_studio/export/coco_captions.py +69 -0
- lightly_studio/export/export_dataset.py +104 -0
- lightly_studio/export/lightly_studio_label_input.py +120 -0
- lightly_studio/export_schema.py +18 -0
- lightly_studio/export_version.py +57 -0
- lightly_studio/few_shot_classifier/__init__.py +0 -0
- lightly_studio/few_shot_classifier/classifier.py +80 -0
- lightly_studio/few_shot_classifier/classifier_manager.py +644 -0
- lightly_studio/few_shot_classifier/random_forest_classifier.py +495 -0
- lightly_studio/metadata/complex_metadata.py +47 -0
- lightly_studio/metadata/compute_similarity.py +84 -0
- lightly_studio/metadata/compute_typicality.py +67 -0
- lightly_studio/metadata/gps_coordinate.py +41 -0
- lightly_studio/metadata/metadata_protocol.py +17 -0
- lightly_studio/models/__init__.py +1 -0
- lightly_studio/models/annotation/__init__.py +0 -0
- lightly_studio/models/annotation/annotation_base.py +303 -0
- lightly_studio/models/annotation/instance_segmentation.py +56 -0
- lightly_studio/models/annotation/links.py +17 -0
- lightly_studio/models/annotation/object_detection.py +47 -0
- lightly_studio/models/annotation/semantic_segmentation.py +44 -0
- lightly_studio/models/annotation_label.py +47 -0
- lightly_studio/models/caption.py +49 -0
- lightly_studio/models/classifier.py +20 -0
- lightly_studio/models/dataset.py +70 -0
- lightly_studio/models/embedding_model.py +30 -0
- lightly_studio/models/image.py +96 -0
- lightly_studio/models/metadata.py +208 -0
- lightly_studio/models/range.py +17 -0
- lightly_studio/models/sample.py +154 -0
- lightly_studio/models/sample_embedding.py +36 -0
- lightly_studio/models/settings.py +69 -0
- lightly_studio/models/tag.py +96 -0
- lightly_studio/models/two_dim_embedding.py +16 -0
- lightly_studio/models/video.py +161 -0
- lightly_studio/plugins/__init__.py +0 -0
- lightly_studio/plugins/base_operator.py +60 -0
- lightly_studio/plugins/operator_registry.py +47 -0
- lightly_studio/plugins/parameter.py +70 -0
- lightly_studio/py.typed +0 -0
- lightly_studio/resolvers/__init__.py +0 -0
- lightly_studio/resolvers/annotation_label_resolver/__init__.py +22 -0
- lightly_studio/resolvers/annotation_label_resolver/create.py +27 -0
- lightly_studio/resolvers/annotation_label_resolver/delete.py +28 -0
- lightly_studio/resolvers/annotation_label_resolver/get_all.py +37 -0
- lightly_studio/resolvers/annotation_label_resolver/get_by_id.py +24 -0
- lightly_studio/resolvers/annotation_label_resolver/get_by_ids.py +25 -0
- lightly_studio/resolvers/annotation_label_resolver/get_by_label_name.py +24 -0
- lightly_studio/resolvers/annotation_label_resolver/names_by_ids.py +25 -0
- lightly_studio/resolvers/annotation_label_resolver/update.py +38 -0
- lightly_studio/resolvers/annotation_resolver/__init__.py +40 -0
- lightly_studio/resolvers/annotation_resolver/count_annotations_by_dataset.py +129 -0
- lightly_studio/resolvers/annotation_resolver/create_many.py +124 -0
- lightly_studio/resolvers/annotation_resolver/delete_annotation.py +87 -0
- lightly_studio/resolvers/annotation_resolver/delete_annotations.py +60 -0
- lightly_studio/resolvers/annotation_resolver/get_all.py +85 -0
- lightly_studio/resolvers/annotation_resolver/get_all_with_payload.py +179 -0
- lightly_studio/resolvers/annotation_resolver/get_by_id.py +34 -0
- lightly_studio/resolvers/annotation_resolver/get_by_id_with_payload.py +130 -0
- lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +142 -0
- lightly_studio/resolvers/annotation_resolver/update_bounding_box.py +68 -0
- lightly_studio/resolvers/annotations/__init__.py +1 -0
- lightly_studio/resolvers/annotations/annotations_filter.py +88 -0
- lightly_studio/resolvers/caption_resolver.py +129 -0
- lightly_studio/resolvers/dataset_resolver/__init__.py +55 -0
- lightly_studio/resolvers/dataset_resolver/check_dataset_type.py +29 -0
- lightly_studio/resolvers/dataset_resolver/create.py +20 -0
- lightly_studio/resolvers/dataset_resolver/delete.py +20 -0
- lightly_studio/resolvers/dataset_resolver/export.py +267 -0
- lightly_studio/resolvers/dataset_resolver/get_all.py +19 -0
- lightly_studio/resolvers/dataset_resolver/get_by_id.py +16 -0
- lightly_studio/resolvers/dataset_resolver/get_by_name.py +12 -0
- lightly_studio/resolvers/dataset_resolver/get_dataset_details.py +27 -0
- lightly_studio/resolvers/dataset_resolver/get_hierarchy.py +31 -0
- lightly_studio/resolvers/dataset_resolver/get_or_create_child_dataset.py +58 -0
- lightly_studio/resolvers/dataset_resolver/get_parent_dataset_by_sample_id.py +27 -0
- lightly_studio/resolvers/dataset_resolver/get_parent_dataset_id.py +22 -0
- lightly_studio/resolvers/dataset_resolver/get_root_dataset.py +61 -0
- lightly_studio/resolvers/dataset_resolver/get_root_datasets_overview.py +41 -0
- lightly_studio/resolvers/dataset_resolver/update.py +25 -0
- lightly_studio/resolvers/embedding_model_resolver.py +120 -0
- lightly_studio/resolvers/image_filter.py +50 -0
- lightly_studio/resolvers/image_resolver/__init__.py +21 -0
- lightly_studio/resolvers/image_resolver/create_many.py +52 -0
- lightly_studio/resolvers/image_resolver/delete.py +20 -0
- lightly_studio/resolvers/image_resolver/filter_new_paths.py +23 -0
- lightly_studio/resolvers/image_resolver/get_all_by_dataset_id.py +117 -0
- lightly_studio/resolvers/image_resolver/get_by_id.py +14 -0
- lightly_studio/resolvers/image_resolver/get_dimension_bounds.py +75 -0
- lightly_studio/resolvers/image_resolver/get_many_by_id.py +22 -0
- lightly_studio/resolvers/image_resolver/get_samples_excluding.py +43 -0
- lightly_studio/resolvers/metadata_resolver/__init__.py +15 -0
- lightly_studio/resolvers/metadata_resolver/metadata_filter.py +163 -0
- lightly_studio/resolvers/metadata_resolver/sample/__init__.py +21 -0
- lightly_studio/resolvers/metadata_resolver/sample/bulk_update_metadata.py +46 -0
- lightly_studio/resolvers/metadata_resolver/sample/get_by_sample_id.py +24 -0
- lightly_studio/resolvers/metadata_resolver/sample/get_metadata_info.py +104 -0
- lightly_studio/resolvers/metadata_resolver/sample/get_value_for_sample.py +27 -0
- lightly_studio/resolvers/metadata_resolver/sample/set_value_for_sample.py +53 -0
- lightly_studio/resolvers/sample_embedding_resolver.py +132 -0
- lightly_studio/resolvers/sample_resolver/__init__.py +17 -0
- lightly_studio/resolvers/sample_resolver/count_by_dataset_id.py +16 -0
- lightly_studio/resolvers/sample_resolver/create.py +16 -0
- lightly_studio/resolvers/sample_resolver/create_many.py +25 -0
- lightly_studio/resolvers/sample_resolver/get_by_id.py +14 -0
- lightly_studio/resolvers/sample_resolver/get_filtered_samples.py +56 -0
- lightly_studio/resolvers/sample_resolver/get_many_by_id.py +22 -0
- lightly_studio/resolvers/sample_resolver/sample_filter.py +74 -0
- lightly_studio/resolvers/settings_resolver.py +62 -0
- lightly_studio/resolvers/tag_resolver.py +299 -0
- lightly_studio/resolvers/twodim_embedding_resolver.py +119 -0
- lightly_studio/resolvers/video_frame_resolver/__init__.py +23 -0
- lightly_studio/resolvers/video_frame_resolver/count_video_frames_annotations.py +83 -0
- lightly_studio/resolvers/video_frame_resolver/create_many.py +57 -0
- lightly_studio/resolvers/video_frame_resolver/get_all_by_dataset_id.py +63 -0
- lightly_studio/resolvers/video_frame_resolver/get_by_id.py +13 -0
- lightly_studio/resolvers/video_frame_resolver/get_table_fields_bounds.py +44 -0
- lightly_studio/resolvers/video_frame_resolver/video_frame_annotations_counter_filter.py +47 -0
- lightly_studio/resolvers/video_frame_resolver/video_frame_filter.py +57 -0
- lightly_studio/resolvers/video_resolver/__init__.py +27 -0
- lightly_studio/resolvers/video_resolver/count_video_frame_annotations_by_video_dataset.py +86 -0
- lightly_studio/resolvers/video_resolver/create_many.py +58 -0
- lightly_studio/resolvers/video_resolver/filter_new_paths.py +33 -0
- lightly_studio/resolvers/video_resolver/get_all_by_dataset_id.py +181 -0
- lightly_studio/resolvers/video_resolver/get_by_id.py +22 -0
- lightly_studio/resolvers/video_resolver/get_table_fields_bounds.py +72 -0
- lightly_studio/resolvers/video_resolver/get_view_by_id.py +52 -0
- lightly_studio/resolvers/video_resolver/video_count_annotations_filter.py +50 -0
- lightly_studio/resolvers/video_resolver/video_filter.py +98 -0
- lightly_studio/selection/__init__.py +1 -0
- lightly_studio/selection/mundig.py +143 -0
- lightly_studio/selection/select.py +203 -0
- lightly_studio/selection/select_via_db.py +273 -0
- lightly_studio/selection/selection_config.py +49 -0
- lightly_studio/services/annotations_service/__init__.py +33 -0
- lightly_studio/services/annotations_service/create_annotation.py +64 -0
- lightly_studio/services/annotations_service/delete_annotation.py +22 -0
- lightly_studio/services/annotations_service/get_annotation_by_id.py +31 -0
- lightly_studio/services/annotations_service/update_annotation.py +54 -0
- lightly_studio/services/annotations_service/update_annotation_bounding_box.py +36 -0
- lightly_studio/services/annotations_service/update_annotation_label.py +48 -0
- lightly_studio/services/annotations_service/update_annotations.py +29 -0
- lightly_studio/setup_logging.py +59 -0
- lightly_studio/type_definitions.py +31 -0
- lightly_studio/utils/__init__.py +3 -0
- lightly_studio/utils/download.py +94 -0
- lightly_studio/vendor/__init__.py +1 -0
- lightly_studio/vendor/mobileclip/ACKNOWLEDGEMENTS +422 -0
- lightly_studio/vendor/mobileclip/LICENSE +31 -0
- lightly_studio/vendor/mobileclip/LICENSE_weights_data +50 -0
- lightly_studio/vendor/mobileclip/README.md +5 -0
- lightly_studio/vendor/mobileclip/__init__.py +96 -0
- lightly_studio/vendor/mobileclip/clip.py +77 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_b.json +18 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_s0.json +18 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_s1.json +18 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_s2.json +18 -0
- lightly_studio/vendor/mobileclip/image_encoder.py +67 -0
- lightly_studio/vendor/mobileclip/logger.py +154 -0
- lightly_studio/vendor/mobileclip/models/__init__.py +10 -0
- lightly_studio/vendor/mobileclip/models/mci.py +933 -0
- lightly_studio/vendor/mobileclip/models/vit.py +433 -0
- lightly_studio/vendor/mobileclip/modules/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/common/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/common/mobileone.py +341 -0
- lightly_studio/vendor/mobileclip/modules/common/transformer.py +451 -0
- lightly_studio/vendor/mobileclip/modules/image/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/image/image_projection.py +113 -0
- lightly_studio/vendor/mobileclip/modules/image/replknet.py +188 -0
- lightly_studio/vendor/mobileclip/modules/text/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/text/repmixer.py +281 -0
- lightly_studio/vendor/mobileclip/modules/text/tokenizer.py +38 -0
- lightly_studio/vendor/mobileclip/text_encoder.py +245 -0
- lightly_studio/vendor/perception_encoder/LICENSE.PE +201 -0
- lightly_studio/vendor/perception_encoder/README.md +11 -0
- lightly_studio/vendor/perception_encoder/vision_encoder/__init__.py +0 -0
- lightly_studio/vendor/perception_encoder/vision_encoder/bpe_simple_vocab_16e6.txt.gz +0 -0
- lightly_studio/vendor/perception_encoder/vision_encoder/config.py +205 -0
- lightly_studio/vendor/perception_encoder/vision_encoder/config_src.py +264 -0
- lightly_studio/vendor/perception_encoder/vision_encoder/pe.py +766 -0
- lightly_studio/vendor/perception_encoder/vision_encoder/rope.py +352 -0
- lightly_studio/vendor/perception_encoder/vision_encoder/tokenizer.py +347 -0
- lightly_studio/vendor/perception_encoder/vision_encoder/transforms.py +36 -0
- lightly_studio-0.4.6.dist-info/METADATA +88 -0
- lightly_studio-0.4.6.dist-info/RECORD +356 -0
- lightly_studio-0.4.6.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""API routes for streaming video frames."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
from collections.abc import Generator
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
import cv2
|
|
11
|
+
import fsspec
|
|
12
|
+
from fastapi import APIRouter, HTTPException
|
|
13
|
+
from fastapi.responses import StreamingResponse
|
|
14
|
+
|
|
15
|
+
from lightly_studio.db_manager import SessionDep
|
|
16
|
+
from lightly_studio.resolvers import video_frame_resolver
|
|
17
|
+
|
|
18
|
+
frames_router = APIRouter(prefix="/frames/media", tags=["frames streaming"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ROTATION_MAP: dict[int, Any] = {
|
|
22
|
+
0: None,
|
|
23
|
+
90: cv2.ROTATE_90_COUNTERCLOCKWISE,
|
|
24
|
+
180: cv2.ROTATE_180,
|
|
25
|
+
270: cv2.ROTATE_90_CLOCKWISE,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FSSpecStreamReader(io.BufferedIOBase):
|
|
30
|
+
"""Wrapper to make fsspec file objects compatible with cv2.VideoCapture's interface."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, path: str) -> None:
|
|
33
|
+
"""Initialize the stream reader.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
path: Path to the video file (local path or cloud URL).
|
|
37
|
+
"""
|
|
38
|
+
self.fs, self.fs_path = fsspec.core.url_to_fs(url=path)
|
|
39
|
+
self.file = self.fs.open(path=self.fs_path, mode="rb")
|
|
40
|
+
# Get file size for size() method
|
|
41
|
+
try:
|
|
42
|
+
self.file_size = self.file.size
|
|
43
|
+
except AttributeError:
|
|
44
|
+
# Fallback: seek to end to get size
|
|
45
|
+
current_pos = self.file.tell()
|
|
46
|
+
self.file.seek(0, 2)
|
|
47
|
+
self.file_size = self.file.tell()
|
|
48
|
+
self.file.seek(current_pos)
|
|
49
|
+
|
|
50
|
+
def read(self, n: int | None = -1) -> bytes:
|
|
51
|
+
"""Read n bytes from the stream."""
|
|
52
|
+
return cast(bytes, self.file.read(n))
|
|
53
|
+
|
|
54
|
+
def read1(self, n: int = -1) -> bytes:
|
|
55
|
+
"""Read up to n bytes from the stream (implementation for BufferedIOBase)."""
|
|
56
|
+
return cast(bytes, self.file.read(n))
|
|
57
|
+
|
|
58
|
+
def seek(self, offset: int, whence: int = 0) -> int:
|
|
59
|
+
"""Seek to the given offset in the stream."""
|
|
60
|
+
return cast(int, self.file.seek(offset, whence))
|
|
61
|
+
|
|
62
|
+
def tell(self) -> int:
|
|
63
|
+
"""Return the current position in the stream."""
|
|
64
|
+
return cast(int, self.file.tell())
|
|
65
|
+
|
|
66
|
+
def size(self) -> int:
|
|
67
|
+
"""Return the total size of the stream."""
|
|
68
|
+
return cast(int, self.file_size)
|
|
69
|
+
|
|
70
|
+
def close(self) -> None:
|
|
71
|
+
"""Close the stream."""
|
|
72
|
+
if not self.closed:
|
|
73
|
+
self.file.close()
|
|
74
|
+
super().close()
|
|
75
|
+
|
|
76
|
+
def __enter__(self) -> FSSpecStreamReader:
|
|
77
|
+
"""Enter the context manager."""
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
81
|
+
"""Exit the context manager and close the stream."""
|
|
82
|
+
self.close()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@frames_router.get("/{sample_id}")
|
|
86
|
+
async def stream_frame(sample_id: UUID, session: SessionDep) -> StreamingResponse:
|
|
87
|
+
"""Serve a single video frame as PNG using StreamingResponse."""
|
|
88
|
+
video_frame = video_frame_resolver.get_by_id(session=session, sample_id=sample_id)
|
|
89
|
+
video_path = video_frame.video.file_path_abs
|
|
90
|
+
|
|
91
|
+
# Open video with cv2.VideoCapture using fsspec stream.
|
|
92
|
+
with FSSpecStreamReader(video_path) as stream:
|
|
93
|
+
cap = cv2.VideoCapture(cast(Any, stream), apiPreference=cv2.CAP_FFMPEG, params=())
|
|
94
|
+
if not cap.isOpened():
|
|
95
|
+
raise HTTPException(400, f"Could not open video: {video_path}")
|
|
96
|
+
# Seek to the correct frame and read it
|
|
97
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, video_frame.frame_number)
|
|
98
|
+
ret, frame = cap.read()
|
|
99
|
+
cap.release()
|
|
100
|
+
if not ret:
|
|
101
|
+
raise HTTPException(400, f"No frame at index {video_frame.frame_number}")
|
|
102
|
+
|
|
103
|
+
# Apply counter-rotation if needed.
|
|
104
|
+
rotate_code = ROTATION_MAP[video_frame.rotation_deg]
|
|
105
|
+
if rotate_code is not None:
|
|
106
|
+
frame = cv2.rotate(src=frame, rotateCode=rotate_code)
|
|
107
|
+
|
|
108
|
+
# Encode frame as PNG
|
|
109
|
+
success, buffer = cv2.imencode(".png", frame)
|
|
110
|
+
if not success:
|
|
111
|
+
raise HTTPException(400, f"Could not encode frame: {sample_id}")
|
|
112
|
+
|
|
113
|
+
def frame_stream() -> Generator[bytes, None, None]:
|
|
114
|
+
yield buffer.tobytes()
|
|
115
|
+
|
|
116
|
+
return StreamingResponse(frame_stream(), media_type="image/png")
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Video serving endpoint that supports multiple formats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import AsyncGenerator
|
|
7
|
+
|
|
8
|
+
import fsspec
|
|
9
|
+
from fastapi import APIRouter, Header, HTTPException, Request
|
|
10
|
+
from fastapi.responses import StreamingResponse
|
|
11
|
+
|
|
12
|
+
from lightly_studio.api.routes.api import status
|
|
13
|
+
from lightly_studio.db_manager import SessionDep
|
|
14
|
+
from lightly_studio.models import video
|
|
15
|
+
|
|
16
|
+
app_router = APIRouter(prefix="/videos/media")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_range_header(range_header: str | None, file_size: int) -> tuple[int, int] | None:
|
|
20
|
+
"""Parse the Range header and return (start, end) byte positions.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
range_header: The Range header value (e.g., "bytes=0-1023")
|
|
24
|
+
file_size: The total size of the file in bytes.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Tuple of (start, end) byte positions, or None if range is invalid.
|
|
28
|
+
"""
|
|
29
|
+
if not range_header or not range_header.startswith("bytes="):
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
range_spec = range_header[6:] # Remove "bytes=" prefix
|
|
34
|
+
if "-" not in range_spec:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
start_str, end_str = range_spec.split("-", 1)
|
|
38
|
+
start = int(start_str) if start_str else 0
|
|
39
|
+
end = int(end_str) if end_str else file_size - 1
|
|
40
|
+
|
|
41
|
+
# Validate range
|
|
42
|
+
if start < 0 or end >= file_size or start > end:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
return (start, end)
|
|
46
|
+
except (ValueError, AttributeError):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _stream_file_range(
|
|
51
|
+
fs: fsspec.AbstractFileSystem,
|
|
52
|
+
fs_path: str,
|
|
53
|
+
start: int,
|
|
54
|
+
end: int,
|
|
55
|
+
request: Request,
|
|
56
|
+
) -> AsyncGenerator[bytes, None]:
|
|
57
|
+
"""Stream a specific byte range from a file.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
fs: The filesystem instance.
|
|
61
|
+
fs_path: The path to the file.
|
|
62
|
+
start: Start byte position.
|
|
63
|
+
end: End byte position.
|
|
64
|
+
request: FastAPI request object for disconnect detection.
|
|
65
|
+
"""
|
|
66
|
+
content_length = end - start + 1
|
|
67
|
+
chunk_size = 1024 * 1024 # 1MB chunks
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
with fs.open(fs_path, "rb") as f:
|
|
71
|
+
f.seek(start)
|
|
72
|
+
remaining = content_length
|
|
73
|
+
|
|
74
|
+
while remaining > 0:
|
|
75
|
+
# Check if client disconnected
|
|
76
|
+
if await request.is_disconnected():
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
read_size = min(chunk_size, remaining)
|
|
80
|
+
chunk = f.read(read_size)
|
|
81
|
+
if not chunk:
|
|
82
|
+
break
|
|
83
|
+
yield chunk
|
|
84
|
+
remaining -= len(chunk)
|
|
85
|
+
except Exception:
|
|
86
|
+
# Handle file read errors gracefully
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def _stream_full_file(
|
|
91
|
+
fs: fsspec.AbstractFileSystem,
|
|
92
|
+
fs_path: str,
|
|
93
|
+
request: Request,
|
|
94
|
+
) -> AsyncGenerator[bytes, None]:
|
|
95
|
+
"""Stream the entire file.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
fs: The filesystem instance.
|
|
99
|
+
fs_path: The path to the file.
|
|
100
|
+
request: FastAPI request object for disconnect detection.
|
|
101
|
+
"""
|
|
102
|
+
chunk_size = 1024 * 1024 # 1MB chunks
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with fs.open(fs_path, "rb") as f:
|
|
106
|
+
while True:
|
|
107
|
+
# Check if client disconnected
|
|
108
|
+
if await request.is_disconnected():
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
chunk = f.read(chunk_size)
|
|
112
|
+
if not chunk:
|
|
113
|
+
break
|
|
114
|
+
yield chunk
|
|
115
|
+
except Exception:
|
|
116
|
+
# Handle file read errors gracefully
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app_router.get("/{sample_id}")
|
|
121
|
+
async def serve_video_by_sample_id(
|
|
122
|
+
sample_id: str,
|
|
123
|
+
session: SessionDep,
|
|
124
|
+
request: Request,
|
|
125
|
+
range_header: str | None = Header(None, alias="range"),
|
|
126
|
+
) -> StreamingResponse:
|
|
127
|
+
"""Serve a video by sample ID with HTTP Range request support.
|
|
128
|
+
|
|
129
|
+
This endpoint supports HTTP Range requests, which are essential for
|
|
130
|
+
efficient video streaming. Browsers use Range requests to:
|
|
131
|
+
- Load only the necessary byte ranges
|
|
132
|
+
- Enable seeking without downloading the entire file
|
|
133
|
+
- Support multiple concurrent requests
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
sample_id: The ID of the video sample.
|
|
137
|
+
session: Database session dependency (closed when function returns, before streaming).
|
|
138
|
+
request: FastAPI request object.
|
|
139
|
+
range_header: The HTTP Range header value.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
StreamingResponse with the video data, supporting partial content.
|
|
143
|
+
"""
|
|
144
|
+
# Get sample record and close session early to avoid blocking
|
|
145
|
+
sample_record = session.get(video.VideoTable, sample_id)
|
|
146
|
+
if not sample_record:
|
|
147
|
+
raise HTTPException(
|
|
148
|
+
status_code=status.HTTP_STATUS_NOT_FOUND,
|
|
149
|
+
detail=f"Video sample not found: {sample_id}",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
file_path = sample_record.file_path_abs
|
|
153
|
+
content_type = _get_content_type(file_path)
|
|
154
|
+
|
|
155
|
+
# Extract file_path (a string) before returning StreamingResponse.
|
|
156
|
+
# FastAPI's dependency system will close the session when this function returns,
|
|
157
|
+
# which happens immediately after creating the StreamingResponse (before streaming starts).
|
|
158
|
+
# This ensures the DB connection isn't held during the async streaming operation.
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
fs, fs_path = fsspec.core.url_to_fs(file_path)
|
|
162
|
+
file_size = fs.size(fs_path)
|
|
163
|
+
|
|
164
|
+
# Parse range header if present
|
|
165
|
+
range_tuple = _parse_range_header(range_header, file_size)
|
|
166
|
+
|
|
167
|
+
if range_tuple:
|
|
168
|
+
# Partial content request
|
|
169
|
+
start, end = range_tuple
|
|
170
|
+
content_length = end - start + 1
|
|
171
|
+
|
|
172
|
+
return StreamingResponse(
|
|
173
|
+
_stream_file_range(fs, fs_path, start, end, request),
|
|
174
|
+
status_code=206, # Partial Content
|
|
175
|
+
media_type=content_type,
|
|
176
|
+
headers={
|
|
177
|
+
"Accept-Ranges": "bytes",
|
|
178
|
+
"Content-Range": f"bytes {start}-{end}/{file_size}",
|
|
179
|
+
"Content-Length": str(content_length),
|
|
180
|
+
"Cache-Control": "public, max-age=3600",
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Full file request
|
|
185
|
+
return StreamingResponse(
|
|
186
|
+
_stream_full_file(fs, fs_path, request),
|
|
187
|
+
media_type=content_type,
|
|
188
|
+
headers={
|
|
189
|
+
"Accept-Ranges": "bytes",
|
|
190
|
+
"Content-Length": str(file_size),
|
|
191
|
+
"Cache-Control": "public, max-age=3600",
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
except FileNotFoundError as exc:
|
|
195
|
+
raise HTTPException(
|
|
196
|
+
status_code=status.HTTP_STATUS_NOT_FOUND,
|
|
197
|
+
detail=f"File not found: {file_path}",
|
|
198
|
+
) from exc
|
|
199
|
+
except OSError as exc:
|
|
200
|
+
raise HTTPException(
|
|
201
|
+
status_code=status.HTTP_STATUS_NOT_FOUND,
|
|
202
|
+
detail=f"Error accessing file {file_path}: {exc.strerror}",
|
|
203
|
+
) from exc
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _get_content_type(file_path: str) -> str:
|
|
207
|
+
"""Get the appropriate content type for a video file based on its extension."""
|
|
208
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
209
|
+
content_types = {
|
|
210
|
+
".mp4": "video/mp4",
|
|
211
|
+
".mov": "video/quicktime",
|
|
212
|
+
".avi": "video/x-msvideo",
|
|
213
|
+
".mkv": "video/x-matroska",
|
|
214
|
+
".webm": "video/webm",
|
|
215
|
+
".flv": "video/x-flv",
|
|
216
|
+
".wmv": "video/x-ms-wmv",
|
|
217
|
+
".mpeg": "video/mpeg",
|
|
218
|
+
".mpg": "video/mpeg",
|
|
219
|
+
".3gp": "video/3gpp",
|
|
220
|
+
".ts": "video/mp2t",
|
|
221
|
+
".m4v": "video/x-m4v",
|
|
222
|
+
}
|
|
223
|
+
return content_types.get(ext, "application/octet-stream")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""This module contains the API routes for managing datasets."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
from fastapi.responses import FileResponse
|
|
7
|
+
|
|
8
|
+
from .api.status import HTTP_STATUS_NOT_FOUND
|
|
9
|
+
|
|
10
|
+
app_router = APIRouter()
|
|
11
|
+
|
|
12
|
+
# Get the current project root directory
|
|
13
|
+
project_root = Path(__file__).resolve().parent.parent.parent
|
|
14
|
+
|
|
15
|
+
webapp_dir = project_root / "dist_lightly_studio_view_app"
|
|
16
|
+
|
|
17
|
+
# Check if the webapp directory exists and raise an error if it doesn't
|
|
18
|
+
if not webapp_dir.exists():
|
|
19
|
+
raise RuntimeError(f"Directory '{webapp_dir}' does not exist in '{project_root}'")
|
|
20
|
+
|
|
21
|
+
# Ensure the path is absolute
|
|
22
|
+
webapp_dir = webapp_dir.resolve()
|
|
23
|
+
|
|
24
|
+
# ensure the webapp index.html file exists
|
|
25
|
+
index_file = webapp_dir / "index.html"
|
|
26
|
+
if not index_file.exists():
|
|
27
|
+
raise RuntimeError("No index file. Did you forget to build the webapp?")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app_router.get("/{path:path}", include_in_schema=False)
|
|
31
|
+
async def serve_static_webapp_files_or_default_index_file(
|
|
32
|
+
path: str,
|
|
33
|
+
) -> FileResponse:
|
|
34
|
+
"""Serve static files of webapp or serve the index.html file.
|
|
35
|
+
|
|
36
|
+
Try to serve static files with file extensions or return 404.
|
|
37
|
+
If no file extension, return the main webapp index.html.
|
|
38
|
+
"""
|
|
39
|
+
file_path = webapp_dir / path
|
|
40
|
+
|
|
41
|
+
# if file has an extension, try to return the file
|
|
42
|
+
if file_path.suffix:
|
|
43
|
+
if not file_path.exists() or not file_path.is_file():
|
|
44
|
+
raise HTTPException(
|
|
45
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
46
|
+
detail=f"File '{path}' not found",
|
|
47
|
+
)
|
|
48
|
+
return FileResponse(file_path)
|
|
49
|
+
|
|
50
|
+
# if file has no extension, return the index.html file regardless of path
|
|
51
|
+
return FileResponse(index_file)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""This module contains the Server class for running the API using Uvicorn."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
import uvicorn
|
|
7
|
+
|
|
8
|
+
from lightly_studio.api.app import app
|
|
9
|
+
from lightly_studio.dataset import env
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Server:
|
|
13
|
+
"""This class represents a server for running the API using Uvicorn."""
|
|
14
|
+
|
|
15
|
+
port: int
|
|
16
|
+
host: str
|
|
17
|
+
|
|
18
|
+
def __init__(self, host: str, port: int) -> None:
|
|
19
|
+
"""Initialize the Server with host and port.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
host (str): The hostname to bind the server to.
|
|
23
|
+
port (int): The port number to run the server on.
|
|
24
|
+
"""
|
|
25
|
+
self.host = host
|
|
26
|
+
self.port = _get_available_port(host=host, preferred_port=port)
|
|
27
|
+
if port != self.port:
|
|
28
|
+
env.LIGHTLY_STUDIO_PORT = self.port
|
|
29
|
+
env.APP_URL = f"{env.LIGHTLY_STUDIO_PROTOCOL}://{env.LIGHTLY_STUDIO_HOST}:{env.LIGHTLY_STUDIO_PORT}"
|
|
30
|
+
|
|
31
|
+
def start(self) -> None:
|
|
32
|
+
"""Start the API server using Uvicorn."""
|
|
33
|
+
# start the app with connection limits and timeouts
|
|
34
|
+
uvicorn.run(
|
|
35
|
+
app,
|
|
36
|
+
host=self.host,
|
|
37
|
+
port=self.port,
|
|
38
|
+
http="h11",
|
|
39
|
+
# https://uvicorn.dev/settings/#resource-limits
|
|
40
|
+
limit_concurrency=100, # Max concurrent connections
|
|
41
|
+
limit_max_requests=10000, # Max requests before worker restart
|
|
42
|
+
# https://uvicorn.dev/settings/#timeouts
|
|
43
|
+
timeout_keep_alive=5, # Keep-alive timeout in seconds
|
|
44
|
+
timeout_graceful_shutdown=30, # Graceful shutdown timeout
|
|
45
|
+
access_log=env.LIGHTLY_STUDIO_DEBUG,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_available_port(host: str, preferred_port: int, max_tries: int = 50) -> int:
|
|
50
|
+
"""Get an available port, if possible, otherwise a random one.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
host: The hostname or IP address to bind to.
|
|
54
|
+
preferred_port: The port to try first.
|
|
55
|
+
max_tries: Maximum number of random ports to try.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
RuntimeError if it cannot find an available port.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
An available port number.
|
|
62
|
+
"""
|
|
63
|
+
if _is_port_available(host=host, port=preferred_port):
|
|
64
|
+
return preferred_port
|
|
65
|
+
|
|
66
|
+
# Try random ports in the range 1024-65535
|
|
67
|
+
for _ in range(max_tries):
|
|
68
|
+
port = random.randint(1024, 65535)
|
|
69
|
+
if _is_port_available(host=host, port=port):
|
|
70
|
+
return port
|
|
71
|
+
|
|
72
|
+
raise RuntimeError("Could not find an available port.")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_port_available(host: str, port: int) -> bool:
|
|
76
|
+
# Determine address family based on host.
|
|
77
|
+
try:
|
|
78
|
+
socket.inet_pton(socket.AF_INET, host)
|
|
79
|
+
families = [socket.AF_INET]
|
|
80
|
+
except OSError:
|
|
81
|
+
try:
|
|
82
|
+
socket.inet_pton(socket.AF_INET6, host)
|
|
83
|
+
families = [socket.AF_INET6]
|
|
84
|
+
except OSError:
|
|
85
|
+
# Fallback for hostnames like 'localhost'
|
|
86
|
+
families = [socket.AF_INET, socket.AF_INET6]
|
|
87
|
+
|
|
88
|
+
for family in families:
|
|
89
|
+
with socket.socket(family, socket.SOCK_STREAM) as s:
|
|
90
|
+
try:
|
|
91
|
+
s.bind((host, port))
|
|
92
|
+
except OSError:
|
|
93
|
+
return False
|
|
94
|
+
return True
|
|
File without changes
|