vislearnlabpy 0.0.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.
File without changes
@@ -0,0 +1,17 @@
1
+ import pandas as pd
2
+
3
+ def rename_csv_column(file, cols, new_name):
4
+ for col in cols:
5
+ if col in file.columns:
6
+ file = file.rename(columns={col: new_name})
7
+ return file # Rename only the first match
8
+
9
+ def process_csv(input_csv):
10
+ images_df = pd.read_csv(input_csv)
11
+ text_cols = ['class_name', 'word', 'text']
12
+ image_cols = ['input_path', 'image_path']
13
+ if 'text1' not in images_df.columns:
14
+ images_df = rename_csv_column(images_df, text_cols, 'text1')
15
+ if 'image1' not in images_df.columns:
16
+ images_df = rename_csv_column(images_df, image_cols, 'image1')
17
+ return images_df
@@ -0,0 +1,40 @@
1
+ from vislearnlabpy.models.clip_model import CLIPGenerator
2
+ import torch
3
+ import os
4
+ from vislearnlabpy.embeddings.stimuli_loader import StimuliLoader
5
+ import pandas as pd
6
+
7
+ torch.set_num_threads(32)
8
+
9
+ class EmbeddingGenerator():
10
+ def __init__(self, device=None, model_type="clip"):
11
+ if device is None:
12
+ self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
13
+ else:
14
+ self.device = device
15
+ self.model_type = model_type
16
+
17
+ def generate_image_embeddings(self, output_path=os.getcwd(), overwrite=False,
18
+ normalize=True, output_type="csv", input_csv=None,
19
+ input_dir=None, batch_size=1):
20
+ if input_csv is None:
21
+ if input_dir is None:
22
+ raise Exception("Either input CSV or input image path needs to be provided")
23
+ images_dataloader = StimuliLoader(
24
+ image_folder=input_dir,
25
+ batch_size=batch_size,
26
+ stimuli_type="images"
27
+ ).dataloader()
28
+ else:
29
+ images_dataloader = StimuliLoader(
30
+ image_folder=input_dir,
31
+ dataset_file=input_csv,
32
+ batch_size=batch_size,
33
+ stimuli_type="images",
34
+ id_column="image1"
35
+ ).dataloader()
36
+ CLIPGenerator(device=self.device, dataloader=images_dataloader).save_image_embeddings(save_path=output_path,
37
+ output_type=output_type,
38
+ normalize_embeddings=normalize,
39
+ overwrite=overwrite,
40
+ )
@@ -0,0 +1,79 @@
1
+ import os
2
+ import pandas as pd
3
+ import re
4
+ from PIL import Image
5
+ from torch.utils.data import Dataset, DataLoader
6
+ from vislearnlabpy.embeddings.embedding_utils import process_csv
7
+ import random
8
+
9
+ random.seed(2)
10
+
11
+ class StimuliDataset(Dataset):
12
+ def __init__(self, manifest, images_folder=None, id_column=None):
13
+ self.manifest = manifest
14
+ self.images_folder = images_folder
15
+ self.num_text_cols = sum(1 for c in self.manifest.columns if re.compile("text[0-9]").match(c))
16
+ self.num_image_cols = len([c for c in self.manifest.columns if re.compile("image[0-9]").match(c)])
17
+ self.id_column = id_column
18
+
19
+ def __len__(self):
20
+ return len(self.manifest)
21
+
22
+ def __getitem__(self, idx):
23
+ row = self.manifest.iloc[idx]
24
+ texts = [str(row[f"text{i}"]) for i in range(1, self.num_text_cols + 1)]
25
+ images = []
26
+ image_paths = self._get_image_paths(row)
27
+ for image_path in image_paths:
28
+ if image_path is None or not os.path.exists(image_path):
29
+ images.append(None)
30
+ else:
31
+ with Image.open(image_path).convert('RGB') as img:
32
+ images.append(img.copy()) # Copy the image data to memory
33
+ return {"images": images, "text": texts, "id": [row[self.id_column] if self.id_column is not None else random.randint(0, 1000000)]}
34
+
35
+ def _get_image_paths(self, row):
36
+ # If this is a text only dataset
37
+ if self.images_folder is None and self.num_image_cols == 0:
38
+ return [None] * self.num_text_cols
39
+ # If images are not in the manifest
40
+ elif self.num_image_cols == 0:
41
+ return [os.path.join(self.images_folder, row[f"text{i}"] + ".jpg") for i in range(1, self.num_text_cols + 1)]
42
+ else:
43
+ # allowing for a secondary image path to be provided with different images in the same stimuli sets stored in different subpaths
44
+ return [
45
+ os.path.join(*(self.images_folder,) if self.images_folder is not None else (), *(row["image_path"],) if "image_path" in row else (), row[f"image{i}"])
46
+ for i in range(1, self.num_image_cols + 1)]
47
+
48
+ class StimuliLoader():
49
+ def __init__(self, dataset_file=None, batch_size=1, image_folder=None, id_column=None, stimuli_type='lookit', pairwise=False):
50
+ self.id_column = id_column
51
+ if dataset_file is None:
52
+ if image_folder is not None:
53
+ image_files = [
54
+ f for f in os.listdir(image_folder)
55
+ if (
56
+ os.path.isfile(os.path.join(image_folder, f)) and
57
+ not f.startswith("._") and
58
+ f.lower().endswith((".png", ".jpg", ".jpeg"))
59
+ )
60
+ ]
61
+ self.manifest = pd.DataFrame({'image1': image_files})
62
+ # assuming that the image path is a unique identifier if a specific ID column is not provided
63
+ self.id_column = "image1" if id_column is None else id_column
64
+ else:
65
+ raise ValueError("Either image folder or dataset file needs to be provided")
66
+ else:
67
+ self.manifest = process_csv(dataset_file)
68
+ self.stimuli_type = stimuli_type
69
+ self.image_folder = image_folder
70
+ self.batch_size = batch_size
71
+ self.pairwise = pairwise
72
+
73
+ def collator(self, batch):
74
+ return {key: [item for ex in batch for item in ex[key]] for key in batch[0]}
75
+
76
+ def dataloader(self):
77
+ dataset = StimuliDataset(self.manifest, self.image_folder, self.id_column)
78
+ return DataLoader(dataset, batch_size=self.batch_size, collate_fn=self.collator)
79
+ # TODO: allow for calculating all possible pairwise similarities in a space to do RSA etc.
@@ -0,0 +1,12 @@
1
+ import clip
2
+ from vislearnlabpy.models.multimodal_model import MultimodalModel
3
+
4
+ class CLIPGenerator(MultimodalModel):
5
+ def __init__(self, dataloader=None, device=None):
6
+ self.model, self.preprocess = clip.load("ViT-B/32")
7
+ super().__init__(self.model, self.preprocess, dataloader, device)
8
+ self.name = "clip"
9
+
10
+ def preprocess_text(self, text):
11
+ return clip.tokenize(f"a photo of a {text}").to(self.device)
12
+
@@ -0,0 +1,105 @@
1
+ from abc import ABC, abstractmethod
2
+ import itertools
3
+ import pandas as pd
4
+ import os
5
+ import torch
6
+ import random
7
+
8
+ from tqdm import tqdm
9
+ from torch.nn.functional import cosine_similarity
10
+ random.seed(4)
11
+
12
+ class FeatureGenerator(ABC):
13
+ """Abstract base class for generating similarity scores between pairs of concepts."""
14
+ def __init__(self, model, preprocess, dataloader, device=None, name="feature_generator"):
15
+ # working with tversky CPU capacity
16
+ torch.set_num_threads(1)
17
+ if device is None:
18
+ self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
19
+ else:
20
+ self.device = device
21
+ #model = torch.nn.DataParallel(model)
22
+ self.model = model.to(self.device)
23
+ self.model.eval()
24
+ self.preprocess = preprocess
25
+ self.name = name
26
+ self.dataloader = dataloader
27
+
28
+ def similarity(self, embeddings1, embeddings2):
29
+ # Compute cosine similarity
30
+ similarity_score = cosine_similarity(embeddings1, embeddings2)
31
+ return similarity_score.item()
32
+
33
+ # Normalize each embedding to have a unit L2 norm
34
+ def normalize_embeddings(self, embeddings):
35
+ return [embedding / embedding.norm(dim=-1, keepdim=True) for embedding in embeddings]
36
+
37
+ def save_similarities(self, sim_df: pd.DataFrame, save_path=None):
38
+ """Save similarity scores to CSV."""
39
+ self.save_df(sim_df, f'similarities-{self.name}_data.csv', save_path)
40
+
41
+ @abstractmethod
42
+ def similarities(self, stimulus1, stimulus2, dataloader_row):
43
+ """Compute similarity scores between two stimuli"""
44
+ pass
45
+
46
+ def format_similarity_row(self, word1, word2, similarity_score):
47
+ return {'target': word1, 'distractor': word2, **similarity_score}
48
+
49
+ def lookit_similarities(self, save_path=None):
50
+ """Calculate cosine similarities between all word pairs in the Lookit dataset"""
51
+ full_similarity_df = pd.DataFrame()
52
+ with torch.no_grad():
53
+ for d in tqdm(self.dataloader, desc=f"Calculating {self.name} similarities"):
54
+ similarity_data = []
55
+ # need to fix placing based on the stimuli set
56
+ text_set = set()
57
+ for count, (text1, text2) in enumerate(itertools.permutations(d['text'], 2)):
58
+ images = [d['images'][d['text'].index(text1)], d['images'][d['text'].index(text2)]]
59
+ pair = tuple(([text1, text2]))
60
+ if pair not in text_set:
61
+ curr_similarities = self.similarities(text1, text2, images)[0]
62
+ curr_similarities["stimuli_id"] = d['id'][0]
63
+ curr_similarities["row_id"] = f"{curr_similarities["stimuli_id"]}_{count}"
64
+ similarity_data.append(self.format_similarity_row(text1, text2, curr_similarities))
65
+ text_set.add(pair)
66
+ if len(similarity_data) > 0:
67
+ similarity_df = pd.DataFrame(similarity_data)
68
+ # Fix concatenation to handle empty DataFrames properly
69
+ if full_similarity_df.empty:
70
+ full_similarity_df = similarity_df
71
+ else:
72
+ full_similarity_df = pd.concat([full_similarity_df, similarity_df], ignore_index=True)
73
+ self.save_similarities(similarity_df, save_path)
74
+ return full_similarity_df
75
+
76
+ def image_word_alignment(self, images, words):
77
+ """Compute alignment between a set of images and a list of words"""
78
+ inputs = self.preprocess(images=images, text=words, return_tensors="pt", padding=True)
79
+ return self.image_word_alignment(**inputs)
80
+
81
+ def save_df(self, df, filename, save_path=None, overwrite=False):
82
+ """
83
+ Save dataframe to CSV, appending to existing file if it exists.
84
+ Avoids duplicate row_ids and handles new directory creation.
85
+ """
86
+ filepath = os.path.join(save_path, filename)
87
+ # create directory if it does not exist
88
+ if not os.path.exists(save_path):
89
+ os.makedirs(save_path)
90
+ if not os.path.exists(filepath):
91
+ df.to_csv(filepath, index=False)
92
+ return
93
+ try:
94
+ existing_df = pd.read_csv(filepath)
95
+ row_ids = df['row_id'].values
96
+ if overwrite:
97
+ # Remove rows from existing data that would be overwritten by new data
98
+ existing_df = existing_df[~existing_df['row_id'].isin(row_ids)]
99
+ existing_df.to_csv(filepath, index=False)
100
+ else:
101
+ df = df[~df['row_id'].isin(existing_df['row_id'].values)]
102
+ df.to_csv(filepath, mode='a', header=False, index=False)
103
+ except pd.errors.EmptyDataError:
104
+ # Handle case where existing file is empty
105
+ df.to_csv(filepath, index=False)
@@ -0,0 +1,190 @@
1
+ import itertools
2
+ from vislearnlabpy.models.feature_generator import FeatureGenerator
3
+ import torch
4
+ import numpy as np
5
+ import pandas as pd
6
+ from tqdm import tqdm
7
+ import os
8
+ from pathlib import Path
9
+
10
+ class MultimodalModel(FeatureGenerator):
11
+ """Abstract base class for multimodal models like CLIP and CVCL that extends FeatureGenerator"""
12
+
13
+ def __init__(self, model, preprocess, dataloader=None, device=None):
14
+ super().__init__(model, preprocess, dataloader, device)
15
+ print(self.dataloader)
16
+ self.image_word_alignment = lambda **x: self.model(**x).logits_per_image.softmax(dim=-1).detach().cpu().numpy()
17
+
18
+ # Load and preprocess images
19
+ def preprocess_image(self, image):
20
+ return self.preprocess(image).unsqueeze(0).to(self.device)
21
+
22
+ def preprocess_text(self, text):
23
+ return self.model.tokenize(text).to(self.device)
24
+
25
+ def encode_image(self, image):
26
+ return self.model.encode_image(image)
27
+
28
+ def encode_text(self, text):
29
+ return self.model.encode_text(text)
30
+
31
+ def image_embeddings(self, images, normalize_embeddings=True):
32
+ """Get image embeddings"""
33
+ images = [self.preprocess_image(image) for image in images]
34
+ with torch.no_grad():
35
+ embeddings = [self.encode_image(image) for image in images]
36
+ if normalize_embeddings:
37
+ return self.normalize_embeddings(embeddings)
38
+ else:
39
+ return embeddings
40
+
41
+ def text_embeddings(self, words, normalize_embeddings=True):
42
+ """Get text embeddings"""
43
+ all_text_features = [self.preprocess_text(word) for word in words]
44
+ with torch.no_grad():
45
+ embeddings = [self.encode_text(text_features) for text_features in all_text_features]
46
+ if normalize_embeddings:
47
+ return self.normalize_embeddings(embeddings)
48
+ else:
49
+ return embeddings
50
+
51
+ def multimodal_embeddings(self, image_embeddings, text_embeddings):
52
+ """Get multimodal embeddings: by default, averages image and text embeddings"""
53
+ return [(a + b) / 2 for a, b in zip(image_embeddings, text_embeddings)]
54
+
55
+ def text_to_images_logits(self, image_embeddings, text_embeddings, logit_scale=100):
56
+ """Get logits of text to image embedding dot products"""
57
+ return logit_scale * image_embeddings @ text_embeddings.t()
58
+
59
+ def text_to_images_similarity(self, image_embeddings, text_embedding, logit_scale=100):
60
+ # Convert image_embeddings list to tensor if needed
61
+ if isinstance(image_embeddings, list):
62
+ image_embeddings = torch.stack(image_embeddings)
63
+ logits = self.text_to_images_logits(image_embeddings, text_embedding, logit_scale).to(self.device)
64
+ softmaxes = torch.nn.functional.softmax(logits, dim=0)
65
+ return softmaxes[1][0].item()
66
+
67
+ def multimodal_luce(self, image_embeddings, text_embedding):
68
+ target_similarity = self.similarity(image_embeddings[0], text_embedding)
69
+ distractor_similarity = self.similarity(image_embeddings[1], text_embedding)
70
+ luce = distractor_similarity / (distractor_similarity + target_similarity)
71
+ return luce
72
+
73
+ def similarities(self, word1, word2, images):
74
+ valid_images = [img for img in images if img is not None]
75
+ similarity_scores = []
76
+ # TODO: this only returns the similarity scores for the first pair of images: need to separate out, indexing is weird
77
+ for image1, image2 in itertools.combinations(valid_images, 2):
78
+ curr_image_embeddings = self.image_embeddings([image1, image2])
79
+ curr_text_embeddings = self.text_embeddings([word1, word2])
80
+ # TODO: need to fix how each row is labeled in lookit_similarities,
81
+ similarity_scores.append({
82
+ 'image_similarity': self.similarity(curr_image_embeddings[0], curr_image_embeddings[1]),
83
+ 'text_similarity': self.similarity(curr_text_embeddings[0], curr_text_embeddings[1]),
84
+ # finding distractor image to target word similarity
85
+ 'multimodal_similarity': self.text_to_images_similarity(curr_image_embeddings, curr_text_embeddings[0], logit_scale=10),
86
+ })
87
+ if similarity_scores == []:
88
+ print(f"skipping {word1} and {word2} since they do not have valid images")
89
+ return [{
90
+ 'image_similarity': None,
91
+ 'text_similarity': None,
92
+ 'multimodal_similarity': None
93
+ }]
94
+ else:
95
+ return similarity_scores
96
+
97
+ # TODO: probably move this to the dataloader row level instead of to a pair of words within a dataloader row
98
+ # TODO: words or texts? what is my parameter
99
+ def embeddings(self, word1, word2, dataloader_row):
100
+ valid_images = [img for img in dataloader_row['images'] if img is not None]
101
+ output_embeddings = []
102
+ for image1, image2 in itertools.combinations(valid_images, 2):
103
+ curr_image_embeddings = self.image_embeddings([image1, image2])
104
+ curr_text_embeddings = self.text_embeddings([word1, word2])
105
+ output_embeddings.append({
106
+ 'image_embeddings': curr_image_embeddings,
107
+ 'text_embeddings': curr_text_embeddings,
108
+ 'multimodal_embeddings': self.multimodal_embeddings(curr_image_embeddings, curr_text_embeddings)
109
+ })
110
+ return output_embeddings
111
+
112
+ def save_embedding(self, embedding, curr_id, text, save_path):
113
+ # if curr_id represents the full path of the image, portion off only the last part
114
+ if os.path.exists(curr_id):
115
+ sub_save_path = f"{text}/{Path(curr_id).stem}"
116
+ else:
117
+ sub_save_path = curr_id
118
+ embedding_output_path = Path(f"{os.getcwd()}/{save_path}/{sub_save_path}").with_suffix('.npy')
119
+ os.makedirs(embedding_output_path.parent, exist_ok=True)
120
+ np_embedding = embedding.cpu().numpy()
121
+ np.save(str(embedding_output_path), np_embedding)
122
+ return str(embedding_output_path)
123
+
124
+ def save_text_embeddings(self, texts, save_path, normalize_embeddings=True, output_type="csv", overwrite=False):
125
+ filename = f"{self.name}_text_embeddings_{output_type}.csv"
126
+ text_save_path = os.path.join(str(save_path), "text_embeddings")
127
+ os.makedirs(text_save_path, exist_ok=True)
128
+ filepath = os.path.join(text_save_path, filename)
129
+ existing_row_ids = []
130
+ if os.path.exists(filepath):
131
+ existing_df = pd.read_csv(filepath)
132
+ existing_row_ids = existing_df['row_id'].values
133
+ # TODO: repeated code -- move embedding code to embeddings folder
134
+ with torch.no_grad():
135
+ row_data = []
136
+ for text in tqdm(texts, desc="Calculating text embeddings"):
137
+ if text not in existing_row_ids or overwrite:
138
+ curr_text_embeddings = self.text_embeddings([text], normalize_embeddings)[0]
139
+ curr_row_data = {'row_id': text}
140
+ if output_type == "csv":
141
+ # new row with a separate column for each number in the 512 dimensions and one for the image_path as the row_id
142
+ curr_image_embeddings = curr_image_embeddings.squeeze(0).tolist() if isinstance(curr_image_embeddings, torch.Tensor) else curr_image_embeddings
143
+ for i, value in enumerate(curr_image_embeddings):
144
+ curr_row_data[f"{i}"] = value.item() if isinstance(value, torch.Tensor) else value
145
+ elif output_type == "npy":
146
+ curr_row_data["embedding_path"] = self.save_embedding(curr_text_embeddings, text, text, text_save_path)
147
+ row_data.append(curr_row_data)
148
+ if len(row_data) > 0:
149
+ self.save_df(pd.DataFrame(row_data), filename, text_save_path, overwrite=overwrite)
150
+
151
+ def save_image_embeddings(self, save_path=None, normalize_embeddings=True, output_type="csv", overwrite=False):
152
+ filename = f"{self.name}_image_embeddings_{output_type}.csv"
153
+ save_path = str(save_path) or os.path.join(os.getcwd(), "output")
154
+ img_save_path = os.path.join(save_path, "image_embeddings")
155
+ os.makedirs(img_save_path, exist_ok=True)
156
+ filepath = os.path.join(img_save_path, filename)
157
+ existing_row_ids = []
158
+ if os.path.exists(filepath):
159
+ existing_df = pd.read_csv(filepath)
160
+ existing_row_ids = existing_df['row_id'].values
161
+ all_text = set()
162
+ with torch.no_grad():
163
+ for d in tqdm(self.dataloader, desc=f"Calculating {self.name} embeddings", position=tqdm._get_free_pos()):
164
+ print(d)
165
+ # TODO: only works for single images in a row
166
+ row_data = []
167
+ for count, (image, curr_id, text) in tqdm(enumerate(zip(d['images'], d['id'], d['text'] if d['text'] else itertools.repeat(None, len(d['images'])))), total=len(d['images']), desc="Image embedding progress in current batch", position=tqdm._get_free_pos()):
168
+ if curr_id not in existing_row_ids or overwrite:
169
+ curr_image_embeddings = self.image_embeddings([image], normalize_embeddings)[0]
170
+ curr_row_data = {'row_id': curr_id}
171
+ if text is not None:
172
+ curr_row_data['text'] = text
173
+ if output_type == "csv":
174
+ # new row with a separate column for each number in the 512 dimensions and one for the image_path as the row_id
175
+ curr_image_embeddings = curr_image_embeddings.squeeze(0).tolist() if isinstance(curr_image_embeddings, torch.Tensor) else curr_image_embeddings
176
+ for i, value in enumerate(curr_image_embeddings):
177
+ curr_row_data[f"{i}"] = value.item() if isinstance(value, torch.Tensor) else value
178
+ elif output_type == "npy":
179
+ curr_row_data["embedding_path"] = self.save_embedding(curr_image_embeddings, curr_id, text, img_save_path)
180
+ row_data.append(curr_row_data)
181
+ if text is not None:
182
+ all_text.add(text)
183
+ if len(row_data) > 0:
184
+ self.save_df(pd.DataFrame(row_data), filename, img_save_path, overwrite=overwrite)
185
+ self.save_text_embeddings(all_text, save_path, normalize_embeddings, output_type, overwrite)
186
+
187
+ def normalize_embeddings(self, embeddings):
188
+ """Normalize embeddings to unit L2 norm"""
189
+ return [embedding / embedding.norm(dim=-1, keepdim=True) for embedding in embeddings]
190
+
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: vislearnlabpy
3
+ Version: 0.0.1
4
+ Summary: Visual Learning Lab utility files and pipelines
5
+ Author-email: Tarun Sepuri <tarunsepuri@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/vislearnlab/vllpy
8
+ Project-URL: Issues, https://github.com/vislearnlab/vllpy/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pandas>=2.2.3
15
+ Requires-Dist: tqdm>=4.67.1
16
+ Requires-Dist: pillow>=11.0.0
17
+ Dynamic: license-file
18
+
19
+ # vllpy
20
+
21
+ This is a package with common utility functions, files and pipelines for the Visual Learning Lab. Creating a conda environment is recommended but optional. This package uses python=3.12.
22
+
23
+ ```
24
+ conda create -n vislearnlabpy python=3.12
25
+ conda activate vislearnlabpy
26
+ ```
27
+
28
+ Then, activate the environment and simply install vislearnlabpy via running the following pip command in your terminal. You will also have to install [PyTorch](https://pytorch.org/) and CLIP manually.
29
+
30
+ ```
31
+ pip install git+https://github.com/openai/CLIP.git
32
+ pip install --upgrade vislearnlabpy
33
+ ```
34
+
35
+ To install PyTorch on the Tversky server, run:
36
+ ```
37
+ pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
38
+ ```
@@ -0,0 +1,12 @@
1
+ vislearnlabpy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ vislearnlabpy/embeddings/embedding_utils.py,sha256=BotVbRg4AEQqxj9HMfcdiEDqzJzNw8dc58W_jdNt484,650
3
+ vislearnlabpy/embeddings/generate_embeddings.py,sha256=HVTTxZqVCEkVGXnKDtmX9AYIoyTfsW_TSFMRqs3Ozz0,1880
4
+ vislearnlabpy/embeddings/stimuli_loader.py,sha256=7vfcjmgdIRrWnDQlLlxEI-pP8IS6z0wp5SLHVaGCsxc,3429
5
+ vislearnlabpy/models/clip_model.py,sha256=c8LWkX1WSezzFliFOaO8ClBKtWCCV36YIdkvjqSqcew,447
6
+ vislearnlabpy/models/feature_generator.py,sha256=VOs_nEimuPJeoLHTLjPdM4MR7zGJtAwADtJENH55ves,4895
7
+ vislearnlabpy/models/multimodal_model.py,sha256=la-ASwoZNvh5BzxiII-1hlvXT7xd5J71fsOcgsBUtho,10718
8
+ vislearnlabpy-0.0.1.dist-info/licenses/LICENSE,sha256=iQFCLVcTzopcVqmTMbKI7LCN_IO0UYBCp7i7ocvwkuQ,1075
9
+ vislearnlabpy-0.0.1.dist-info/METADATA,sha256=gutq1VuFfykQjHJWZQS3p90fWEk0f5AOD8Krtp37Wq4,1327
10
+ vislearnlabpy-0.0.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
11
+ vislearnlabpy-0.0.1.dist-info/top_level.txt,sha256=3KYf9ks71988SFCQu0HnPY_Q2KRLkWuC09zJRYQOMxs,14
12
+ vislearnlabpy-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Visual Learning Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ vislearnlabpy