simba-uw-tf-dev 4.7.3__py3-none-any.whl → 4.7.4__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.
@@ -170,6 +170,7 @@ class SimBAScaleBar(Frame):
170
170
  to: int = 100,
171
171
  tickinterval: Optional[int] = None,
172
172
  troughcolor: Optional[str] = None,
173
+ activebackground: Optional[str] = None,
173
174
  sliderrelief: Literal["raised", "sunken", "flat", "ridge", "solid", "groove"] = 'flat'):
174
175
 
175
176
  super().__init__(master=parent)
@@ -192,7 +193,8 @@ class SimBAScaleBar(Frame):
192
193
  troughcolor=troughcolor,
193
194
  tickinterval=tickinterval,
194
195
  resolution=resolution,
195
- showvalue=showvalue)
196
+ showvalue=showvalue,
197
+ activebackground=activebackground)
196
198
 
197
199
  if label is not None:
198
200
  self.lbl = SimBALabel(parent=self, txt=label, font=lbl_font, txt_clr=label_clr, width=label_width)
@@ -0,0 +1,332 @@
1
+ import os
2
+ from tkinter import *
3
+ from typing import Optional, Union
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from PIL import Image, ImageTk
8
+
9
+ from simba.mixins.image_mixin import ImageMixin
10
+ from simba.ui.tkinter_functions import SimbaButton, SimBALabel, SimBAScaleBar
11
+ from simba.utils.checks import (check_file_exist_and_readable, check_int,
12
+ check_valid_boolean)
13
+ from simba.utils.enums import Formats, TkBinds
14
+ from simba.utils.lookups import get_icons_paths, get_monitor_info
15
+ from simba.utils.read_write import (get_video_meta_data, read_frm_of_video,
16
+ seconds_to_timestamp)
17
+
18
+
19
+ class TimelapseSlider():
20
+
21
+ """
22
+ Interactive timelapse viewer with segment selection sliders.
23
+
24
+ Creates a Tkinter GUI window displaying a timelapse composite image generated from evenly-spaced frames
25
+ across a video. Includes interactive sliders to select start and end times for video segments, with
26
+ visual highlighting of the selected segment and frame previews.
27
+
28
+ .. image:: _static/img/TimelapseSlider.png
29
+ :width: 600
30
+ :align: center
31
+
32
+ :param Union[str, os.PathLike] video_path: Path to video file to create timelapse from.
33
+ :param int frame_cnt: Number of frames to include in timelapse composite. Default 25.
34
+ :param Optional[int] ruler_width: Width per frame in pixels. If None, calculated to match video width. Default None.
35
+ :param Optional[int] crop_ratio: Percentage of frame width to keep (0-100). Default 50.
36
+ :param int padding: Padding in pixels added to timelapse when ruler is shown. Default 60.
37
+ :param int ruler_divisions: Number of major divisions on time ruler. Default 6.
38
+ :param bool show_ruler: If True, display time ruler below timelapse. Default True.
39
+ :param int ruler_height: Height of ruler in pixels. Default 60.
40
+
41
+ :example:
42
+ >>> slider = TimelapseSlider(video_path='path/to/video.mp4', frame_cnt=25, crop_ratio=75)
43
+ >>> slider.run()
44
+ >>> # Use sliders to select segment, then access selected times and frames:
45
+ >>> start_time = slider.get_start_time() # seconds (float)
46
+ >>> end_time = slider.get_end_time() # seconds (float)
47
+ >>> start_time_str = slider.get_start_time_str() # "HH:MM:SS" string
48
+ >>> end_time_str = slider.get_end_time_str() # "HH:MM:SS" string
49
+ >>> start_frame = slider.get_start_frame() # frame number (int)
50
+ >>> end_frame = slider.get_end_frame() # frame number (int)
51
+ >>> slider.close()
52
+ """
53
+
54
+ def __init__(self,
55
+ video_path: Union[str, os.PathLike],
56
+ frame_cnt: int = 25,
57
+ crop_ratio: Optional[int] = 50,
58
+ padding: int = 60,
59
+ ruler_divisions: int = 6,
60
+ show_ruler: bool = True,
61
+ ruler_height: Optional[int] = None,
62
+ ruler_width: Optional[int] = None,
63
+ img_width: Optional[int] = None,
64
+ img_height: Optional[int] = None):
65
+
66
+ check_file_exist_and_readable(file_path=video_path)
67
+ check_int(name='frame_cnt', value=frame_cnt, min_value=1, raise_error=True)
68
+ _, (self.monitor_width, self.monitor_height) = get_monitor_info()
69
+ if ruler_width is not None: check_int(name='size', value=ruler_width, min_value=1, raise_error=True)
70
+ else: ruler_width = int(self.monitor_width * 0.5)
71
+ if ruler_height is not None: check_int(name='ruler_height', value=ruler_height, min_value=1, raise_error=True)
72
+ else: ruler_height = int(self.monitor_height * 0.05)
73
+ if img_width is not None: check_int(name='img_width', value=img_width, min_value=1, raise_error=True)
74
+ else: img_width = int(self.monitor_width * 0.5)
75
+ if img_height is not None: check_int(name='img_height', value=img_height, min_value=1, raise_error=True)
76
+ else: img_height = int(self.monitor_height * 0.5)
77
+
78
+
79
+ check_int(name='padding', value=padding, min_value=1, raise_error=True)
80
+ check_valid_boolean(value=show_ruler, source=f'{self.__class__.__name__} show_ruler', raise_error=True)
81
+ self.video_meta = get_video_meta_data(video_path=video_path, raise_error=True)
82
+ if show_ruler: check_int(name='ruler_divisions', value=ruler_divisions, min_value=1, raise_error=True)
83
+ self.size, self.padding, self.crop_ratio, self.frame_cnt = ruler_width, padding, crop_ratio, frame_cnt
84
+ self.ruler_height, self.video_path, self.show_ruler, self.ruler_divisions = ruler_height, video_path, show_ruler, ruler_divisions
85
+ self.img_width, self.img_height = img_width, img_height
86
+ self.frm_name = f'{self.video_meta["video_name"]} - TIMELAPSE VIEWER - hit "X" or ESC to close'
87
+ self.video_capture, self._pending_frame_update, self._frame_debounce_ms = None, None, 50
88
+
89
+ def _draw_img(self, img: np.ndarray, lbl: SimBALabel):
90
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
91
+ self.pil_image = Image.fromarray(img_rgb)
92
+ self.tk_image = ImageTk.PhotoImage(self.pil_image)
93
+ lbl.configure(image=self.tk_image)
94
+ lbl.image = self.tk_image
95
+
96
+ def _update_selection(self, slider_type: str):
97
+ start_sec = int(self.start_scale.get_value())
98
+ end_sec = int(self.end_scale.get_value())
99
+ max_sec = int(self.video_meta['video_length_s'])
100
+ if slider_type == 'start':
101
+ if start_sec >= end_sec:
102
+ end_sec = min(start_sec + 1, max_sec)
103
+ self.end_scale.set_value(end_sec)
104
+ else:
105
+ if end_sec <= start_sec:
106
+ start_sec = max(end_sec - 1, 0)
107
+ self.start_scale.set_value(start_sec)
108
+
109
+ self.selected_start[0] = start_sec
110
+ self.selected_end[0] = end_sec
111
+
112
+ start_frame = int(start_sec * self.video_meta['fps'])
113
+ end_frame = int(end_sec * self.video_meta['fps'])
114
+ if start_frame >= self.video_meta['frame_count']: start_frame = self.video_meta['frame_count'] - 1
115
+ if end_frame >= self.video_meta['frame_count']: end_frame = self.video_meta['frame_count'] - 1
116
+ if start_frame < 0: start_frame = 0
117
+ if end_frame < 0: end_frame = 0
118
+ self.selected_start_frame[0] = start_frame
119
+ self.selected_end_frame[0] = end_frame
120
+
121
+ self.start_time_label.config(text=seconds_to_timestamp(start_sec), fg='green')
122
+ self.end_time_label.config(text=seconds_to_timestamp(end_sec), fg='red')
123
+
124
+ if self.video_meta['video_length_s'] > 0:
125
+ self._highlight_segment(start_sec, end_sec)
126
+ self._schedule_frame_update(slider_type=slider_type)
127
+
128
+ def _move_start_frame(self, direction: int):
129
+ current_seconds = self.selected_start[0]
130
+ new_seconds = current_seconds + direction
131
+ new_seconds = max(0, min(new_seconds, int(self.video_meta['video_length_s'])))
132
+ self.start_scale.set_value(int(new_seconds))
133
+ self._update_selection(slider_type='start')
134
+ if self._pending_frame_update is not None:
135
+ if hasattr(self, 'img_window') and self.img_window.winfo_exists():
136
+ self.img_window.after_cancel(self._pending_frame_update)
137
+ self._update_frame_display(slider_type='start')
138
+
139
+ def _move_end_frame(self, direction: int):
140
+ current_seconds = self.selected_end[0]
141
+ new_seconds = current_seconds + direction
142
+ new_seconds = max(0, min(new_seconds, int(self.video_meta['video_length_s'])))
143
+ self.end_scale.set_value(int(new_seconds))
144
+ self._update_selection(slider_type='end')
145
+ if self._pending_frame_update is not None:
146
+ if hasattr(self, 'img_window') and self.img_window.winfo_exists():
147
+ self.img_window.after_cancel(self._pending_frame_update)
148
+ self._update_frame_display(slider_type='end')
149
+
150
+ def _schedule_frame_update(self, slider_type: str):
151
+ """Schedule frame preview update with debouncing.
152
+
153
+ Cancels any pending frame update and schedules a new one. If the slider
154
+ moves again before the delay expires, the update is cancelled and rescheduled.
155
+ This prevents expensive frame reads during fast slider dragging.
156
+ """
157
+ if not hasattr(self, 'img_window') or not self.img_window.winfo_exists():
158
+ return
159
+
160
+ if self._pending_frame_update is not None: self.img_window.after_cancel(self._pending_frame_update)
161
+
162
+ self._pending_frame_update = self.img_window.after(self._frame_debounce_ms, lambda: self._update_frame_display(slider_type=slider_type))
163
+
164
+ def _update_frame_display(self, slider_type: str):
165
+ if slider_type == 'start':
166
+ seconds = self.selected_start[0]
167
+ self.frame_label.config(text=f"Start Frame Preview ({seconds_to_timestamp(seconds)})", font=Formats.FONT_LARGE_BOLD.value, fg='green')
168
+ else:
169
+ seconds = self.selected_end[0]
170
+ self.frame_label.config(text=f"End Frame Preview ({seconds_to_timestamp(seconds)})", font=Formats.FONT_LARGE_BOLD.value, fg='red')
171
+
172
+ frame_index = int(seconds * self.video_meta['fps'])
173
+ if frame_index >= self.video_meta['frame_count']: frame_index = self.video_meta['frame_count'] - 1
174
+ if frame_index < 0: frame_index = 0
175
+
176
+ if self.video_capture is not None and self.video_capture.isOpened():
177
+ self.video_capture.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
178
+ ret, frame = self.video_capture.read()
179
+ if ret and frame is not None:
180
+ h, w = frame.shape[:2]
181
+ target_w, target_h = self.img_width, self.img_height
182
+ scale = min(target_w / w, target_h / h)
183
+ new_w, new_h = int(w * scale), int(h * scale)
184
+ frame = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
185
+ self._draw_img(img=frame, lbl=self.frame_display_lbl)
186
+
187
+ def _highlight_segment(self, start_sec: int, end_sec: int):
188
+ timelapse_width = self.original_timelapse.shape[1]
189
+ start_x = int((start_sec / self.video_meta['video_length_s']) * timelapse_width)
190
+ end_x = int((end_sec / self.video_meta['video_length_s']) * timelapse_width)
191
+ highlighted = self.original_timelapse.copy()
192
+ mask = np.ones(highlighted.shape[:2], dtype=np.uint8) * 128
193
+ mask[:, start_x:end_x] = 255
194
+ mask = cv2.merge([mask, mask, mask])
195
+ highlighted = cv2.multiply(highlighted, mask.astype(np.uint8), scale=1/255.0)
196
+ cv2.line(highlighted, (start_x, 0), (start_x, highlighted.shape[0]), (0, 255, 0), 2)
197
+ cv2.line(highlighted, (end_x, 0), (end_x, highlighted.shape[0]), (0, 255, 0), 2)
198
+ self._draw_img(img=highlighted, lbl=self.img_lbl)
199
+
200
+ def run(self):
201
+ self.video_capture = cv2.VideoCapture(self.video_path)
202
+ if not self.video_capture.isOpened():
203
+ raise ValueError(f"Failed to open video file: {self.video_path}")
204
+
205
+ self.timelapse_img = ImageMixin.get_timelapse_img(video_path=self.video_path, frame_cnt=self.frame_cnt, size=self.size, crop_ratio=self.crop_ratio)
206
+ if self.show_ruler:
207
+ timelapse_height, timelapse_width = self.timelapse_img.shape[0], self.timelapse_img.shape[1]
208
+ padded_timelapse = np.zeros((timelapse_height, timelapse_width + (2 * self.padding), 3), dtype=np.uint8)
209
+ padded_timelapse[:, self.padding:self.padding + timelapse_width] = self.timelapse_img
210
+ ruler = ImageMixin.create_time_ruler(video_path=self.video_path, width=timelapse_width, height=self.ruler_height, num_divisions=self.ruler_divisions)
211
+ self.timelapse_img = cv2.vconcat([padded_timelapse, ruler])
212
+
213
+ self.original_timelapse = self.timelapse_img.copy()
214
+ self.img_window = Toplevel()
215
+ self.img_window.resizable(True, True)
216
+ self.img_window.title(self.frm_name)
217
+ self.img_window.protocol("WM_DELETE_WINDOW", self.close)
218
+ # Bind Escape key to close window
219
+ self.img_window.bind(TkBinds.ESCAPE.value, lambda event: self.close())
220
+
221
+
222
+ self.img_lbl = SimBALabel(parent=self.img_window, txt='')
223
+ self.img_lbl.pack()
224
+ self._draw_img(img=self.timelapse_img, lbl=self.img_lbl)
225
+ self.frame_display_frame = Frame(self.img_window)
226
+ self.frame_display_frame.pack(pady=10, padx=10, fill=BOTH, expand=True)
227
+ self.frame_label = SimBALabel(parent=self.frame_display_frame, txt="Frame Preview", font=Formats.FONT_REGULAR_BOLD.value)
228
+ self.frame_label.pack()
229
+ self.frame_display_lbl = SimBALabel(parent=self.frame_display_frame, txt='', bg_clr='black')
230
+ self.frame_display_lbl.pack(pady=5)
231
+ self.slider_frame = Frame(self.img_window)
232
+ self.slider_frame.pack(pady=10, padx=10, fill=X)
233
+ self.slider_frame.columnconfigure(index=0, weight=1)
234
+ self.slider_frame.columnconfigure(index=1, weight=0)
235
+ self.slider_frame.columnconfigure(index=2, weight=0)
236
+ self.slider_frame.columnconfigure(index=3, weight=0)
237
+ self.slider_frame.columnconfigure(index=4, weight=0)
238
+ self.slider_frame.columnconfigure(index=5, weight=1)
239
+
240
+ self.start_scale = SimBAScaleBar(parent=self.slider_frame, label="START TIME:", from_=0, to=self.video_meta['video_length_s'], orient=HORIZONTAL, length=400, resolution=1, value=0, showvalue=False, label_width=15, sliderrelief='raised', troughcolor='white', activebackground='green', lbl_font=Formats.FONT_LARGE_BOLD.value)
241
+ self.start_scale.grid(row=0, column=1, padx=5)
242
+ self.start_scale.scale.config(command=lambda x: self._update_selection(slider_type='start'))
243
+
244
+ self.start_time_label = SimBALabel(parent=self.slider_frame, txt="00:00:00", font=Formats.FONT_LARGE_BOLD.value, width=10, txt_clr='green')
245
+ self.start_time_label.grid(row=0, column=2, padx=5)
246
+
247
+ self.start_frame_left_btn = SimbaButton(parent=self.slider_frame, txt="-1s", tooltip_txt="Previous second", cmd=self._move_start_frame, cmd_kwargs={'direction': -1}, font=Formats.FONT_REGULAR_BOLD.value, img='left_arrow_green')
248
+ self.start_frame_left_btn.grid(row=0, column=3, padx=2)
249
+ self.start_frame_right_btn = SimbaButton(parent=self.slider_frame, txt="+1s", tooltip_txt="Next second", cmd=self._move_start_frame, cmd_kwargs={'direction': 1}, font=Formats.FONT_REGULAR_BOLD.value, img='right_arrow_green')
250
+ self.start_frame_right_btn.grid(row=0, column=4, padx=2)
251
+
252
+ self.end_scale = SimBAScaleBar(parent=self.slider_frame, label="END TIME:", from_=0, to=int(self.video_meta['video_length_s']), orient=HORIZONTAL, length=400, resolution=1, value=int(self.video_meta['video_length_s']), showvalue=False, label_width=15, sliderrelief='raised', troughcolor='white', activebackground='red', lbl_font=Formats.FONT_LARGE_BOLD.value)
253
+ self.end_scale.grid(row=1, column=1, padx=5)
254
+ self.end_scale.scale.config(command=lambda x: self._update_selection(slider_type='end'))
255
+
256
+ self.end_time_label = SimBALabel(parent=self.slider_frame, txt=seconds_to_timestamp(int(self.video_meta['video_length_s'])), font=Formats.FONT_LARGE_BOLD.value, width=10, txt_clr='red')
257
+ self.end_time_label.grid(row=1, column=2, padx=5)
258
+
259
+ self.end_frame_left_btn = SimbaButton(parent=self.slider_frame, txt="-1s", tooltip_txt="Previous second", cmd=self._move_end_frame, cmd_kwargs={'direction': -1}, font=Formats.FONT_REGULAR_BOLD.value, img='left_arrow_red')
260
+ self.end_frame_left_btn.grid(row=1, column=3, padx=2)
261
+ self.end_frame_right_btn = SimbaButton(parent=self.slider_frame, txt="+1s", tooltip_txt="Next second", cmd=self._move_end_frame, cmd_kwargs={'direction': 1}, font=Formats.FONT_REGULAR_BOLD.value, img='right_arrow_red')
262
+ self.end_frame_right_btn.grid(row=1, column=4, padx=2)
263
+
264
+ self.selected_start = [0]
265
+ self.selected_end = [int(self.video_meta['video_length_s'])]
266
+ self.selected_start_frame = [0]
267
+ end_frame = int(self.video_meta['frame_count']) - 1
268
+ if end_frame < 0: end_frame = 0
269
+ self.selected_end_frame = [end_frame]
270
+
271
+ self.img_window.update_idletasks()
272
+ self.img_window.update()
273
+
274
+ req_width, req_height = self.img_window.winfo_reqwidth(), self.img_window.winfo_reqheight()
275
+ min_width = max(self.timelapse_img.shape[1] + 60, req_width + 20)
276
+ timelapse_height = self.timelapse_img.shape[0]
277
+ frame_preview_height = self.img_height
278
+ slider_height, padding_total = 150, 50
279
+ calculated_min_height = timelapse_height + frame_preview_height + slider_height + padding_total
280
+ min_height = max(calculated_min_height, req_height + 50, timelapse_height + 400)
281
+ max_height = int(self.monitor_height * 0.95)
282
+ if min_height > max_height: min_height = max_height
283
+
284
+ self.img_window.minsize(min_width, min_height)
285
+ self.img_window.geometry(f"{min_width}x{min_height}")
286
+ self._update_frame_display(slider_type='start')
287
+
288
+ def get_start_time(self) -> float:
289
+ return self.selected_start[0]
290
+
291
+ def get_end_time(self) -> float:
292
+ return self.selected_end[0]
293
+
294
+ def get_start_time_str(self) -> str:
295
+ return seconds_to_timestamp(self.selected_start[0])
296
+
297
+ def get_end_time_str(self) -> str:
298
+ return seconds_to_timestamp(self.selected_end[0])
299
+
300
+ def get_start_frame(self) -> int:
301
+ return self.selected_start_frame[0]
302
+
303
+ def get_end_frame(self) -> int:
304
+ return self.selected_end_frame[0]
305
+
306
+ def close(self):
307
+ if self._pending_frame_update is not None:
308
+ if hasattr(self, 'img_window') and self.img_window.winfo_exists():
309
+ self.img_window.after_cancel(self._pending_frame_update)
310
+ self._pending_frame_update = None
311
+
312
+ # Unbind Escape key if window still exists
313
+ if hasattr(self, 'img_window') and self.img_window.winfo_exists():
314
+ try:
315
+ self.img_window.unbind(TkBinds.ESCAPE.value)
316
+ except:
317
+ pass
318
+
319
+ if self.video_capture is not None:
320
+ self.video_capture.release()
321
+ self.video_capture = None
322
+
323
+ if hasattr(self, 'img_window') and self.img_window.winfo_exists():
324
+ self.img_window.destroy()
325
+
326
+
327
+
328
+ #
329
+ # x = TimelapseSlider(video_path=r"E:\troubleshooting\mitra_emergence\project_folder\clip_test\Box1_180mISOcontrol_Females_clipped_progress_bar.mp4",
330
+ # frame_cnt=25,
331
+ # crop_ratio=75)
332
+ # x.run()
simba/utils/read_write.py CHANGED
@@ -814,6 +814,7 @@ def read_frm_of_video(video_path: Union[str, os.PathLike, cv2.VideoCapture],
814
814
  frame_index: Optional[int] = 0,
815
815
  opacity: Optional[float] = None,
816
816
  size: Optional[Tuple[int, int]] = None,
817
+ keep_aspect_ratio: bool = False,
817
818
  greyscale: Optional[bool] = False,
818
819
  black_and_white: Optional[bool] = False,
819
820
  clahe: Optional[bool] = False,
@@ -831,7 +832,8 @@ def read_frm_of_video(video_path: Union[str, os.PathLike, cv2.VideoCapture],
831
832
  :param Union[str, os.PathLike, cv2.VideoCapture] video_path: Path to video file, or cv2.VideoCapture object.
832
833
  :param Optional[int] frame_index: The frame index to return (0-based). Default: 0. If -1 is passed, the last frame of the video is read.
833
834
  :param Optional[float] opacity: Value between 0 and 100 or None. If float value, returns image with opacity. 100 fully opaque. 0.0 fully transparent.
834
- :param Optional[Tuple[int, int]] size: If tuple, resizes the image to size. Else, returns original image size.
835
+ :param Optional[Tuple[int, int]] size: If tuple (width, height), resizes the image. If None, returns original image size. When used with keep_aspect_ratio=True, the image is resized to fit within the target size while maintaining aspect ratio.
836
+ :param bool keep_aspect_ratio: If True and size is provided, resizes the image to fit within the target size while maintaining aspect ratio. If False, resizes to exact size (may distort aspect ratio). Default False.
835
837
  :param Optional[bool] greyscale: If True, returns the greyscale image. Default False.
836
838
  :param Optional[bool] black_and_white: If True, returns black and white image at threshold 127. Default False.
837
839
  :param Optional[bool] clahe: If True, returns CLAHE enhanced image. Default False.
@@ -906,8 +908,14 @@ def read_frm_of_video(video_path: Union[str, os.PathLike, cv2.VideoCapture],
906
908
  h, w, clr = img.shape[:3]
907
909
  opacity_image = np.ones((h, w, clr), dtype=np.uint8) * int(255 * opacity)
908
910
  img = cv2.addWeighted( img.astype(np.uint8), 1 - opacity, opacity_image.astype(np.uint8), opacity, 0)
909
- if size:
911
+ if size is not None and not keep_aspect_ratio:
910
912
  img = cv2.resize(img, size, interpolation=cv2.INTER_LINEAR)
913
+ elif size is not None and keep_aspect_ratio:
914
+ target_w, target_h = size
915
+ h, w = img.shape[:2]
916
+ scale = min(target_w / w, target_h / h)
917
+ new_w, new_h = int(w * scale), int(h * scale)
918
+ img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
911
919
  if greyscale or black_and_white:
912
920
  if len(img.shape) > 2:
913
921
  img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
@@ -3761,7 +3769,6 @@ def find_closest_readable_frame(video_path: Union[str, os.PathLike],
3761
3769
  return None, None
3762
3770
 
3763
3771
 
3764
-
3765
3772
  #copy_multiple_videos_to_project(config_path=r"C:\troubleshooting\multi_animal_dlc_two_c57\project_folder\project_config.ini", source=r'E:\maplight_videos\video_test', file_type='mp4', recursive_search=False)
3766
3773
 
3767
3774
 
@@ -12,7 +12,6 @@ import shutil
12
12
 
13
13
  from simba.utils.checks import (check_ffmpeg_available,
14
14
  check_file_exist_and_readable, check_str)
15
- from simba.utils.enums import Formats
16
15
  from simba.utils.errors import CropError, FFMPEGNotFoundError, PermissionError
17
16
  from simba.utils.lookups import (get_current_time, get_ffmpeg_encoders,
18
17
  gpu_quality_to_cpu_quality_lk)
@@ -52,12 +52,12 @@ from simba.utils.errors import (CountError, DirectoryExistError,
52
52
  NoDataError, NoFilesFoundError,
53
53
  NotDirectoryError, ResolutionError,
54
54
  SimBAGPUError)
55
- from simba.utils.lookups import (get_current_time,
55
+ from simba.utils.lookups import (get_current_time, get_ffmpeg_codec,
56
56
  get_ffmpeg_crossfade_methods, get_fonts,
57
57
  get_named_colors, percent_to_crf_lookup,
58
58
  percent_to_qv_lk, quality_pct_to_crf,
59
- video_quality_to_preset_lookup, get_ffmpeg_codec)
60
- from simba.utils.printing import SimbaTimer, stdout_success, stdout_information
59
+ video_quality_to_preset_lookup)
60
+ from simba.utils.printing import SimbaTimer, stdout_information, stdout_success
61
61
  from simba.utils.read_write import (
62
62
  check_if_hhmmss_timestamp_is_valid_part_of_video,
63
63
  concatenate_videos_in_folder, create_directory,
@@ -1101,6 +1101,9 @@ def remove_beginning_of_video(file_path: Union[str, os.PathLike],
1101
1101
  """
1102
1102
  Remove N seconds from the beginning of a video file.
1103
1103
 
1104
+ .. seealso::
1105
+ To remove N seconds from the end of the video, see :func:`simba.video_processors.video_processing.remove_end_of_video`.
1106
+
1104
1107
  :param Union[str, os.PathLike] file_path: Path to video file
1105
1108
  :param int time: Number of seconds to remove from the beginning of the video.
1106
1109
  :param int quality: Video quality percentage (1-100). Higher values = higher quality. Default 60.
@@ -1144,6 +1147,66 @@ def remove_beginning_of_video(file_path: Union[str, os.PathLike],
1144
1147
  stdout_success(msg=f"SIMBA COMPLETE: Video converted! {save_name} generated!", elapsed_time=timer.elapsed_time_str, source=remove_beginning_of_video.__name__)
1145
1148
 
1146
1149
 
1150
+ def remove_end_of_video(file_path: Union[str, os.PathLike],
1151
+ time: int,
1152
+ quality: int = 60,
1153
+ save_path: Optional[Union[str, os.PathLike]] = None,
1154
+ gpu: Optional[bool] = False) -> None:
1155
+ """
1156
+ Remove N seconds from the end of a video file.
1157
+
1158
+ .. seealso::
1159
+ To remove N seconds from the beginning of the video, see :func:`simba.video_processors.video_processing.remove_beginning_of_video`
1160
+
1161
+ :param Union[str, os.PathLike] file_path: Path to video file
1162
+ :param int time: Number of seconds to remove from the end of the video.
1163
+ :param int quality: Video quality percentage (1-100). Higher values = higher quality. Default 60.
1164
+ :param Optional[Union[str, os.PathLike]] save_path: Optional save location for the shortened video. If None, then the new video is saved in the same directory as the input video with the ``_shortened`` suffix.
1165
+ :param Optional[bool] gpu: If True, use NVIDEA GPU codecs. Default False.
1166
+ :returns: None. If save_path is not passed, the result is stored in the same directory as the input file with the ``_shorten.mp4`` suffix.
1167
+
1168
+ .. note::
1169
+ Codec is automatically selected: libx264 for CPU encoding (ignored if gpu=True).
1170
+
1171
+ :example:
1172
+ >>> _ = remove_end_of_video(file_path='project_folder/videos/Video_1.avi', time=10)
1173
+ >>> remove_end_of_video(file_path=f'/Users/simon/Desktop/imgs_4/test/blahhhh.mp4', save_path='/Users/simon/Desktop/imgs_4/test/CUT.mp4', time=3)
1174
+ """
1175
+
1176
+ check_ffmpeg_available(raise_error=True)
1177
+ if gpu and not check_nvidea_gpu_available():
1178
+ raise FFMPEGCodecGPUError(msg="No GPU found (as evaluated by nvidea-smi returning None)",
1179
+ source=remove_end_of_video.__name__)
1180
+ timer = SimbaTimer(start=True)
1181
+ check_file_exist_and_readable(file_path=file_path)
1182
+ video_meta_data = get_video_meta_data(video_path=file_path)
1183
+ check_int(name="Cut time", value=time, min_value=1)
1184
+ check_int(name=f'{remove_end_of_video.__name__} quality', value=quality, min_value=1, max_value=100,
1185
+ raise_error=True)
1186
+ quality_crf = quality_pct_to_crf(pct=int(quality))
1187
+ time = int(time)
1188
+ dir, file_name, ext = get_fn_ext(filepath=file_path)
1189
+ if video_meta_data['video_length_s'] <= time:
1190
+ raise InvalidInputError(
1191
+ msg=f"The cut time {time}s is invalid for video {file_name} with length {video_meta_data['video_length_s']}s",
1192
+ source=remove_end_of_video.__name__)
1193
+ if save_path is None:
1194
+ save_name = os.path.join(dir, f"{file_name}_shorten.mp4")
1195
+ else:
1196
+ check_if_dir_exists(in_dir=os.path.dirname(save_path), source=f'{remove_end_of_video.__name__} save_path',
1197
+ create_if_not_exist=True)
1198
+ save_name = save_path
1199
+ duration = video_meta_data['video_length_s'] - time
1200
+ if gpu:
1201
+ cmd = f'ffmpeg -hwaccel auto -c:v h264_cuvid -i "{file_path}" -t {duration} -rc vbr -cq {quality_crf} -c:v h264_nvenc -c:a aac "{save_name}" -loglevel error -stats -hide_banner -y'
1202
+ else:
1203
+ cmd = f'ffmpeg -i "{file_path}" -t {duration} -c:v libx264 -crf {quality_crf} -c:a aac "{save_name}" -loglevel error -stats -hide_banner -y'
1204
+ print(f"Removing final {time}s from {file_name}... ")
1205
+ subprocess.call(cmd, shell=True, stdout=subprocess.PIPE)
1206
+ timer.stop_timer()
1207
+ stdout_success(msg=f"SIMBA COMPLETE: Video converted! {save_name} generated!", elapsed_time=timer.elapsed_time_str, source=remove_end_of_video.__name__)
1208
+
1209
+
1147
1210
  def clip_video_in_range(file_path: Union[str, os.PathLike],
1148
1211
  start_time: str,
1149
1212
  end_time: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simba-uw-tf-dev
3
- Version: 4.7.3
3
+ Version: 4.7.4
4
4
  Summary: Toolkit for computer classification and analysis of behaviors in experimental animals
5
5
  Home-page: https://github.com/sgoldenlab/simba
6
6
  Author: Simon Nilsson, Jia Jie Choong, Sophia Hwang
@@ -235,6 +235,8 @@ simba/assets/icons/last.png,sha256=FT0YsuoWI3OJviUlXvH_WCA6Xw91_hDsLS9datyMq74,3
235
235
  simba/assets/icons/last_frame_blue.png,sha256=lZMsTmZAAu6SyQU5L472d3U_WRF1axX34c4hkRPQGmU,511
236
236
  simba/assets/icons/last_red.png,sha256=qWwx3xKKPRAOeGUyMIrvv44xpo2vv_MEDsIa4vrG3OE,404
237
237
  simba/assets/icons/leaf.png,sha256=coTkYfpw9PKZVyNTSC4wDwjbMncKEKc5k06V7fW3hHE,591
238
+ simba/assets/icons/left_arrow_green.png,sha256=MHqoHc1Kdj7QT-734E7ykS9JrUkkpk41IJoWK0n9TR8,583
239
+ simba/assets/icons/left_arrow_red.png,sha256=7rd7F1Ew0cwmNXTuMOse90E55bwYVRLzcytoQ9FbJjE,565
238
240
  simba/assets/icons/left_ear.png,sha256=nFhf6MENIJdocklnzl4QXinxa3ZoX6nsgKFhVeuIi1s,530
239
241
  simba/assets/icons/light_bulb.png,sha256=V8TIhHTL3aDhowEd8ytrxaQPOsBEMJwqlLJm2rGtuUY,507
240
242
  simba/assets/icons/line.png,sha256=B_12LipvMHXooT4TgVjMAJB4xSZtnNEk2CjssThxk8k,230
@@ -337,6 +339,8 @@ simba/assets/icons/rewind.png,sha256=UMwE2s46EHZ0QOm5Cb_p4FdWhUE5mgs58bjxF6D2zVM
337
339
  simba/assets/icons/rewind_blue_2.png,sha256=eFl-heXp3JskJn25TvcC1fr3TLxVgNxgxjCWoiU7fZo,552
338
340
  simba/assets/icons/rewind_large.png,sha256=_fWLmuK4Bh8CwN3cPE0VPDUN2d3FR55nnGaEC62o4Q4,412
339
341
  simba/assets/icons/rewind_red.png,sha256=91dtjMAcQjmVB0CK5cwsSydtiOB8cnW-gaMFbW10ync,445
342
+ simba/assets/icons/right_arrow_green.png,sha256=AyHVYdcI9iUqHERJcHRi9erMXZL6TIAZvVttfB7lq9g,581
343
+ simba/assets/icons/right_arrow_red.png,sha256=rUC66nkmsrtU3DQu7NarVadiHgrj6qXr9HJuINeCpsw,570
340
344
  simba/assets/icons/rocket.png,sha256=iM7uzAol22gv27lpZU8uNHNFotOhIDhnEfLjey8aKrw,801
341
345
  simba/assets/icons/roi.png,sha256=lG_ifGWlysaoBX4wc1UMznSvAYsFm9ZHxLRux7eBfaA,4823
342
346
  simba/assets/icons/roi_green.png,sha256=ycX7hPtaYjCdtPDz_QL4MGRUQQ7XmVxnfkrdGHQ0JxY,1217
@@ -620,7 +624,7 @@ simba/mixins/config_reader.py,sha256=9su8ZGnGWghk5bAl12iaTQdqIOVfcW2ZQcxeEXmXtcI
620
624
  simba/mixins/feature_extraction_mixin.py,sha256=rUwHEG3wKpyreme8nXAeBEhktHG_Q75v3OJI2aFOOOA,59587
621
625
  simba/mixins/feature_extraction_supplement_mixin.py,sha256=6dPi1WFi26y-rmccsMgDe5sULm4fTQaBcSiH91DEkN0,44770
622
626
  simba/mixins/geometry_mixin.py,sha256=pBOCFZvq_AF2Snmmy8izmtOG6xy6KOu1td0lm5c6uNg,234551
623
- simba/mixins/image_mixin.py,sha256=CLhtCeDzLPVeRWUEzybBVG9-1Ll8Nl2OgN2PTfTZybM,117953
627
+ simba/mixins/image_mixin.py,sha256=C3FcZGp_kLMYMvbFxOedYPebIYm6KJIRPAB7rcu6J80,125807
624
628
  simba/mixins/network_mixin.py,sha256=s_OsF6VSlsa2vb5sysDQKErLp4D5EfBF1MVQ_yKWC_Y,41125
625
629
  simba/mixins/plotting_mixin.py,sha256=br3nWggOM776Ki30kq71P2geoGGgCLDn4MjDuiJ5IWs,95714
626
630
  simba/mixins/pop_up_mixin.py,sha256=tAKC3Axp1jgQ8VH2GfvKO5HbLUpZzo_aWou2xS-SYxc,24701
@@ -773,7 +777,7 @@ simba/roi_tools/roi_selector_rectangle_tkinter.py,sha256=j_dPyXfSqWMwBuMndoHxD4u
773
777
  simba/roi_tools/roi_time_bins_analyzer.py,sha256=fTt9iblXG0Y7ITdPGhRhSqwsjPiSMWIoKFr-U8VaVEo,25313
774
778
  simba/roi_tools/roi_ui.py,sha256=e2znZUpY9n8i6Dia1hXLyh7BkF3XtuGw6W6En2XK7qc,9469
775
779
  simba/roi_tools/roi_ui_mixin.py,sha256=ZVrzI_0L8QkAhwbCbi4GpOMYkBpUR7wPWIRBv5KalWM,93883
776
- simba/roi_tools/roi_utils.py,sha256=-N5cXI7qAJU8o1UuHwcUq959Zfso2GeD7y5FchLjbKw,37311
780
+ simba/roi_tools/roi_utils.py,sha256=dEpZvX_tFb_giNg8lDfAPWr5Gm8BHuvE9O_6eBzcr1Y,37295
777
781
  simba/sandbox/BackgroundRemoverDirectoryPopUp.py,sha256=wIoyJc9jhsjbnTAgoi_bHZBIaXFtCvQJ4IvqCXFuUCk,9249
778
782
  simba/sandbox/BlobTrackingUI.py,sha256=RGIxPMdkW0xMvHLntrXLhnwVPZw3rveox4l5633HyrU,21407
779
783
  simba/sandbox/BoundaryRearingFeaturizer.py,sha256=YPzISnoZD7u_EdbWqStjf0uoVE2-X-DiX7uv6IZTDzA,11096
@@ -1119,6 +1123,7 @@ simba/sandbox/reduce_imge_stack_size.py,sha256=JaOMzKVbRTV01SdcWkif0IgjrHd1Q0-rD
1119
1123
  simba/sandbox/regression_errors.py,sha256=JqgC28VGnkNwVPIRXwHvUhJ-Ef5vmX7x6HgOSgus-JI,8327
1120
1124
  simba/sandbox/relative_risk.py,sha256=_S_MlOUIks0vx3ow49--599UvoylMAICqdijWl3t8WA,2490
1121
1125
  simba/sandbox/remove_background_single_video.py,sha256=pASwjxUxxz1x7fCSFlIyYrX0cXa5_jGZdEQwo5fv_-o,7227
1126
+ simba/sandbox/remove_end_of_video.py,sha256=ObVJyl6eoUoE15cbrhRRo6EBsr6CnEDRgx4CqfNC0To,4096
1122
1127
  simba/sandbox/remove_substr_from_files.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1123
1128
  simba/sandbox/reverse_popup.py,sha256=qNCCwUa5u0KaPjOdv2Sux7M9eLBYNmtgx2uT-Fo2HfI,6723
1124
1129
  simba/sandbox/reverse_videos.py,sha256=-CfXmqWOsyIvqKUSfbY0JRmuh2RLrblc1t6z_TmFKEk,5659
@@ -1230,6 +1235,7 @@ simba/sandbox/video_color.py,sha256=Tl1x6ZbkB5PGj1D7QGTqXuZxIkbogLggYbfvqGumxJg,
1230
1235
  simba/sandbox/video_meta_data_get.py,sha256=foTzEuuqd0gYCVz54fWF4bITpANfBfCglNdLvRdpXj0,1979
1231
1236
  simba/sandbox/video_rotator.py,sha256=gpO31jW8UPVU7BQiWaG0UVlLV3EmtYBfE07sp3VsFS0,5515
1232
1237
  simba/sandbox/video_rotator_mp.py,sha256=gpO31jW8UPVU7BQiWaG0UVlLV3EmtYBfE07sp3VsFS0,5515
1238
+ simba/sandbox/video_timelaps.py,sha256=8FEGNmAgLgqMJIIuOcnMF3fBmk1PDrVEX5FmBOlVf8E,16327
1233
1239
  simba/sandbox/video_to_frames.py,sha256=nil-6OpRQoGRN9YWf3Krm28tFT7B4_QDQNPaMJhiFXc,5107
1234
1240
  simba/sandbox/violin.py,sha256=lUdumQm1CxgApmoXzvNIpy-d5ETJSbzjXi19H3CZRn8,1416
1235
1241
  simba/sandbox/visualize_networks.py,sha256=zwzugqWk59WJHeKLJJSD0X2nr9Ns-_joy3YuebhCGUg,8843
@@ -1367,9 +1373,10 @@ simba/ui/import_videos_frame.py,sha256=i0LnQzPLFne9dA_nvpVWAHYGmi0aTRXpiHzEog_-R
1367
1373
  simba/ui/machine_model_settings_ui.py,sha256=hTfpBxtfYGH9Rsf3JdQ5Sc8z874tYAoZefvjF1JD6gA,38292
1368
1374
  simba/ui/ml_settings_frm.py,sha256=f1-E6pEGjWJVF3I6n0azO9zAnsskpZjInViguHIDntw,3101
1369
1375
  simba/ui/px_to_mm_ui.py,sha256=ETedZPFkloU0l3JeGnhiSIAsGBQzzv0YrDWtiVGOmJ4,9387
1370
- simba/ui/tkinter_functions.py,sha256=u5QXx6iwlRj3UAsuwg2pC567l4-8IXX0cKppWRCT8P0,41392
1376
+ simba/ui/tkinter_functions.py,sha256=CUha5AY7iAuFL4Nt2oxHhV9eZIEuyDMq0J_VSoTjSio,41511
1371
1377
  simba/ui/user_defined_pose_creator.py,sha256=QAfdp8r225DONLaCgQJe6kXiZZLj20VQCLJEnCvZsKs,9249
1372
1378
  simba/ui/video_info_ui.py,sha256=ld_fN-3vCYJDFv1t9i5B-VVtoaBCspDmiDUIfOSJb_8,17262
1379
+ simba/ui/video_timelaps.py,sha256=ikqkCLpFCvifm1c6RKG6CT3xrfIlihgd1AUzMlz9ZkA,18736
1373
1380
  simba/ui/pop_ups/.DS_Store,sha256=4PUcfpTZzBMd8yz2YahGab5mqPUVIpvdxzjvQ2MmsBY,6148
1374
1381
  simba/ui/pop_ups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1375
1382
  simba/ui/pop_ups/about_simba_pop_up.py,sha256=IcmA97HVV7qgSfaf_5GLNwQfse5lA1_xL8ymA4e7aTU,14522
@@ -1464,7 +1471,7 @@ simba/ui/pop_ups/subset_feature_extractor_pop_up.py,sha256=M24iJSqh-DpYdpw1pSaIm
1464
1471
  simba/ui/pop_ups/targeted_annotation_clips_pop_up.py,sha256=PFh5ua2f_OMQ1Pth9Ha8Fo5lTPZNQV3bMnRGEoAPhTQ,6997
1465
1472
  simba/ui/pop_ups/third_party_annotator_appender_pop_up.py,sha256=Xnha2UwM-08djObCkL_EXK2L4pernyipzbyNKQvX5aQ,7694
1466
1473
  simba/ui/pop_ups/validation_plot_pop_up.py,sha256=yIo_el2dR_84ZAh_-2fYFg-BJDG0Eip_P_o9vzTQRkk,12174
1467
- simba/ui/pop_ups/video_processing_pop_up.py,sha256=hCurMClU2Oa-pB8PdHSP2pptQuQph97-S_FBV53Iowo,238382
1474
+ simba/ui/pop_ups/video_processing_pop_up.py,sha256=QnJtnAWKRoHHilfHdw993Z1U8EwK2-AG_XQQLhnG6mw,251956
1468
1475
  simba/ui/pop_ups/visualize_pose_in_dir_pop_up.py,sha256=PpFs0zaqF4dnHJ_yH-PqYgsjAyxYPVP427Soj-kYtM0,8838
1469
1476
  simba/ui/pop_ups/yolo_inference_popup.py,sha256=C4_WDvEHLp9JMUTjLZuRpKHxMCGpa_pxXELuj-zerCs,14679
1470
1477
  simba/ui/pop_ups/yolo_plot_results.py,sha256=yi9D3WquDu4L8PWJLZsODulojgakfy7Dzh_CpYK6Vgk,10096
@@ -1513,7 +1520,7 @@ simba/utils/enums.py,sha256=ZR2175N06ZHZNiHk8n757T-WGyt1-55LLpcg3Sbb91k,38668
1513
1520
  simba/utils/errors.py,sha256=aC-1qiGlh1vvHxUaPxBMQ9-LW-KKWXCGlH9acCPH0Cc,18788
1514
1521
  simba/utils/lookups.py,sha256=hVUIis9FxgoKvTa2S2Rhrqg_LKrzW13tEBr3Tt8ZP44,50458
1515
1522
  simba/utils/printing.py,sha256=2s-uESy1knuPiniqQ-q277uQ2teYM4OHo9Y4L20JQWM,5353
1516
- simba/utils/read_write.py,sha256=RadhCPmtgidCr866vxMB6tflQRu3hxc6siPf2eksZ6A,188719
1523
+ simba/utils/read_write.py,sha256=Rn4BavywRGn2MxioYlMMABAdcVGHcNLDDkqulR91_QM,189463
1517
1524
  simba/utils/warnings.py,sha256=K7w1RiDL4Un7rGaabOVCGc9fHcaKxk66iZyNLS_AtOE,8121
1518
1525
  simba/utils/yolo.py,sha256=UZzpnDqZj81SOMnwsWPQIhFAsHHSSaDawi1UUh0-uAA,19264
1519
1526
  simba/utils/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1521,7 +1528,7 @@ simba/utils/cli/cli_tools.py,sha256=vDPGuwqWTnHQ35pchC0uN3P8U_3GVnHalL3SFgGFc0g,
1521
1528
  simba/video_processors/.DS_Store,sha256=6gsgZL1uIfKqBNSk6EAKBP9lJ1qMrQy6XrEvluyc2GE,6148
1522
1529
  simba/video_processors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1523
1530
  simba/video_processors/async_frame_reader.py,sha256=_17735pfAKUDHE18snAbWIbxUhIFkx3m-HipWqoE6r8,8059
1524
- simba/video_processors/batch_process_create_ffmpeg_commands.py,sha256=LQmZXxxgBCFkeYUZiV4R2cB1syehPLbM0RnmpMMAZzQ,14233
1531
+ simba/video_processors/batch_process_create_ffmpeg_commands.py,sha256=qfGb6KeJ7kOpRreRfpV1Lb9zCOL2g7cDqUFbEk9or2k,14194
1525
1532
  simba/video_processors/batch_process_menus.py,sha256=cLgJCx90wvgzZJX2ygoPIQc1f4cWPaC6O02XH7Ta458,37627
1526
1533
  simba/video_processors/blob_tracking_executor.py,sha256=hyB-FYwbCmk44ytOmYQsiWHh7ecE0h5A0-ySjpYWyvY,18395
1527
1534
  simba/video_processors/brightness_contrast_ui.py,sha256=nWmzho1WeJuIp3CuDjJmqMIzge2sTZn6_H0lWyZYaz0,5202
@@ -1534,11 +1541,11 @@ simba/video_processors/multi_cropper.py,sha256=1BI0Ami4kB9rdMUHR0EistmIKqc-E5FK5
1534
1541
  simba/video_processors/roi_selector.py,sha256=5N3s0Bi1Ub6c9gjE_-mV7AWr8Fqg7HQKdBKBF6whurg,8522
1535
1542
  simba/video_processors/roi_selector_circle.py,sha256=SD_lv6V3MGiIQd0VtUFSKe83ySW_qvE1t8xsgAlr2hI,6436
1536
1543
  simba/video_processors/roi_selector_polygon.py,sha256=DMtilt__gGwNu6VV73CWbnPqrPBXkan1_akUqGEzfGw,6742
1537
- simba/video_processors/video_processing.py,sha256=yq0eMRyRPDQpahT1s85VeOO0fdPQH67M0UAgwfvGKRk,324031
1544
+ simba/video_processors/video_processing.py,sha256=ynHC-9-Pt020GMZa5xPDhfpouHS5i6D26mT-5F9NAY0,327790
1538
1545
  simba/video_processors/videos_to_frames.py,sha256=8hltNZpwUfb3GFi-63D0PsySmD5l59pbzQGJx8SscgU,7818
1539
- simba_uw_tf_dev-4.7.3.dist-info/LICENSE,sha256=Sjn362upcvYFypam-b-ziOXU1Wl5GGuTt5ICrGimzyA,1720
1540
- simba_uw_tf_dev-4.7.3.dist-info/METADATA,sha256=Azunk8PsRLqwufZzwA0xVgYMNvfH6V1auxUenpMa4BQ,11432
1541
- simba_uw_tf_dev-4.7.3.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
1542
- simba_uw_tf_dev-4.7.3.dist-info/entry_points.txt,sha256=Nfh_EbfDGdKftLjCnGWtQrBHENiDYMdgupwLyLpU5dc,44
1543
- simba_uw_tf_dev-4.7.3.dist-info/top_level.txt,sha256=ogtimvlqDxDTOBAPfT2WaQ2pGAAbKRXG8z8eUTzf6TU,14
1544
- simba_uw_tf_dev-4.7.3.dist-info/RECORD,,
1546
+ simba_uw_tf_dev-4.7.4.dist-info/LICENSE,sha256=Sjn362upcvYFypam-b-ziOXU1Wl5GGuTt5ICrGimzyA,1720
1547
+ simba_uw_tf_dev-4.7.4.dist-info/METADATA,sha256=3efqgf6cgV9K_10w20ZZ66GV5QKccmTQUpcrNI4cJTA,11432
1548
+ simba_uw_tf_dev-4.7.4.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
1549
+ simba_uw_tf_dev-4.7.4.dist-info/entry_points.txt,sha256=Nfh_EbfDGdKftLjCnGWtQrBHENiDYMdgupwLyLpU5dc,44
1550
+ simba_uw_tf_dev-4.7.4.dist-info/top_level.txt,sha256=ogtimvlqDxDTOBAPfT2WaQ2pGAAbKRXG8z8eUTzf6TU,14
1551
+ simba_uw_tf_dev-4.7.4.dist-info/RECORD,,