code-loader 1.0.196.dev1__py3-none-any.whl → 1.0.197__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.
@@ -59,7 +59,8 @@ class PreprocessResponse:
59
59
  self.sample_ids = cast(List[SampleId], [i for i in range(self.length)])
60
60
  self.sample_id_type = int
61
61
  self._grouped = False
62
- elif self.length is None and self.sample_ids is not None:
62
+ elif self.sample_ids is not None:
63
+ given_length = self.length
63
64
  self._grouped = len(self.sample_ids) > 0 and isinstance(self.sample_ids[0], list)
64
65
  if self._grouped:
65
66
  groups = cast(List[List[SampleId]], self.sample_ids)
@@ -68,6 +69,8 @@ class PreprocessResponse:
68
69
  assert isinstance(group, list) and len(group) > 0, \
69
70
  "Each group in a grouped PreprocessResponse must be a non-empty list."
70
71
  self.sample_id_type = type(groups[0][0])
72
+ assert issubclass(self.sample_id_type, (str, int)), \
73
+ f"Sample id should be of type str or int. Got: {self.sample_id_type.__name__}"
71
74
  for group in groups:
72
75
  for sample_id in group:
73
76
  assert isinstance(sample_id, self.sample_id_type), \
@@ -81,6 +84,11 @@ class PreprocessResponse:
81
84
  if self.sample_id_type == str:
82
85
  for sample_id in flat_ids:
83
86
  assert isinstance(sample_id, str), f"Sample id should be of type str. Got: {type(sample_id)}"
87
+ # dataclasses.replace() re-invokes __init__ with both fields already populated
88
+ # (length previously derived from sample_ids); only reject a genuine mismatch.
89
+ if given_length is not None:
90
+ assert given_length == self.length, \
91
+ f"Inconsistent PreprocessResponse: length={given_length} but sample_ids implies {self.length}."
84
92
  else:
85
93
  raise Exception("length is deprecated, please use sample_ids instead.")
86
94
 
@@ -28,6 +28,7 @@ class LeapDataType(Enum):
28
28
  ImageWithBBox = 'ImageWithBBox'
29
29
  ImageWithHeatmap = 'ImageWithHeatmap'
30
30
  Video = 'Video'
31
+ Audio = 'Audio'
31
32
 
32
33
 
33
34
  class MetricDirection(Enum):
@@ -343,6 +343,47 @@ class LeapImageWithHeatmap:
343
343
  'Number of heatmaps and labels must be equal')
344
344
 
345
345
 
346
+ @dataclass
347
+ class LeapAudio:
348
+ """
349
+ Visualizer representing a playable audio sample for Tensorleap.
350
+
351
+ The user plays the sample in the right panel while a playhead tick runs
352
+ through `visual` — either a spectrogram (a LeapImage) or a time-series
353
+ waveform (a LeapGraph). Build one with `default_audio_visualizer`.
354
+
355
+ Attributes:
356
+ audio (npt.NDArray): Mono waveform samples, shaped [N]. Multi-channel input
357
+ should be mixed down before constructing LeapAudio.
358
+ sample_rate (int): Samples per second — needed to encode the clip and to map
359
+ the playhead position to time.
360
+ visual (Union[LeapImage, LeapGraph]): The thing you look at.
361
+ x_range (Optional[Tuple[float, float]]): Time extent (in seconds) that the
362
+ visual's horizontal axis spans, used to align the tick. Defaults to
363
+ (0, N / sample_rate).
364
+ type (LeapDataType): The data type, default is LeapDataType.Audio.
365
+ """
366
+ audio: npt.NDArray
367
+ sample_rate: int
368
+ visual: Union[LeapImage, LeapGraph]
369
+ x_range: Optional[Tuple[float, float]] = None
370
+ type: LeapDataType = LeapDataType.Audio
371
+
372
+ def __post_init__(self) -> None:
373
+ validate_type(self.type, LeapDataType.Audio)
374
+ validate_type(type(self.audio), np.ndarray)
375
+ validate_type(len(self.audio.shape), 1, 'Audio must be mono, of shape 1 [N]')
376
+ validate_type(type(self.visual), [LeapImage, LeapGraph],
377
+ 'Audio visual must be a LeapImage (spectrogram) or LeapGraph (waveform)')
378
+ if not isinstance(self.sample_rate, int) or self.sample_rate <= 0:
379
+ raise LeapValidationError(f'sample_rate must be a positive int, got {self.sample_rate}')
380
+ if self.x_range is not None:
381
+ if len(self.x_range) != 2:
382
+ raise LeapValidationError('x_range must be a tuple of length 2')
383
+ validate_type(type(self.x_range[0]), [float, int], 'x_range must be a tuple of floats or integers')
384
+ validate_type(type(self.x_range[1]), [float, int], 'x_range must be a tuple of floats or integers')
385
+
386
+
346
387
  map_leap_data_type_to_visualizer_class = {
347
388
  LeapDataType.Image.value: LeapImage,
348
389
  LeapDataType.Graph.value: LeapGraph,
@@ -352,5 +393,6 @@ map_leap_data_type_to_visualizer_class = {
352
393
  LeapDataType.ImageMask.value: LeapImageMask,
353
394
  LeapDataType.TextMask.value: LeapTextMask,
354
395
  LeapDataType.ImageWithBBox.value: LeapImageWithBBox,
355
- LeapDataType.ImageWithHeatmap.value: LeapImageWithHeatmap
396
+ LeapDataType.ImageWithHeatmap.value: LeapImageWithHeatmap,
397
+ LeapDataType.Audio.value: LeapAudio
356
398
  }
@@ -892,6 +892,10 @@ class LeapBinder:
892
892
  preprocess_response.tl_generated = True
893
893
  if not preprocess_response.length or preprocess_response.length < 1:
894
894
  raise Exception("Simulation '{}' returned PreprocessResponse with length < 1".format(sim.name))
895
+ if preprocess_response.is_grouped:
896
+ raise Exception(
897
+ "Simulation '{}' returned a grouped PreprocessResponse; simulations must "
898
+ "return a flat (non-grouped) PreprocessResponse.".format(sim.name))
895
899
  preprocess_response.sample_ids = cast(List[Union[int, str]], [0])
896
900
  sim_sample_ids = cast(List[Union[int, str]], preprocess_response.sample_ids)
897
901
  for handler in self.setup_container.inputs:
@@ -29,7 +29,7 @@ from code_loader.contract.enums import MetricDirection, LeapDataType, DatasetMet
29
29
  from code_loader import leap_binder, LeapLoader
30
30
  from code_loader.contract.mapping import NodeMapping, NodeMappingType, NodeConnection
31
31
  from code_loader.contract.visualizer_classes import LeapImage, LeapImageMask, LeapTextMask, LeapText, LeapGraph, \
32
- LeapHorizontalBar, LeapImageWithBBox, LeapImageWithHeatmap, LeapVideo, LeapValidationError, \
32
+ LeapHorizontalBar, LeapImageWithBBox, LeapImageWithHeatmap, LeapVideo, LeapAudio, LeapValidationError, \
33
33
  map_leap_data_type_to_visualizer_class
34
34
  from code_loader.inner_leap_binder.leapbinder import mapping_runtime_mode_env_var_mame
35
35
  from code_loader.mixpanel_tracker import clear_integration_events, AnalyticsEvent, emit_integration_event_once
@@ -686,10 +686,29 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
686
686
 
687
687
  class ModelPlaceholder:
688
688
 
689
+ @staticmethod
690
+ def _reject_reused_input_source(elem, seen):
691
+ # A model input slot is wired by mutating elem.node_mapping.type, so the same
692
+ # source object at two positions silently overwrites the first slot. Each input
693
+ # encoder mints its own NodeMapping, so a collision means one source fed two
694
+ # slots — e.g. indexing a single grouped input, model([imgs[0], imgs[1]]), where
695
+ # imgs[i] returns the same node (a group is the batch dim of ONE input, not many
696
+ # inputs), or the flat model([img, img]). Reject it instead of mis-wiring.
697
+ if id(elem.node_mapping) in seen:
698
+ raise Exception(
699
+ "The same input source was passed to more than one model input slot. Each "
700
+ "model input must come from a distinct input encoder. Indexing a single "
701
+ "grouped input (e.g. model([imgs[0], imgs[1]])) feeds several samples of ONE "
702
+ "input into separate slots — a group is the batch dimension of one input, "
703
+ "not multiple inputs.")
704
+ seen.add(id(elem.node_mapping))
705
+
689
706
  # keras interface
690
707
  def __call__(self, arg):
691
708
  if isinstance(arg, list):
709
+ seen: set = set()
692
710
  for i, elem in enumerate(arg):
711
+ self._reject_reused_input_source(elem, seen)
693
712
  elem.node_mapping.type = _safe_get_item(i)
694
713
  else:
695
714
  arg.node_mapping.type = NodeMappingType.Input0
@@ -703,7 +722,9 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
703
722
  "use model.run(None, inputs) to return all outputs.")
704
723
  assert isinstance(input_dict, dict), \
705
724
  f'Expected input_dict to be a dict, got {type(input_dict)} instead.'
725
+ seen: set = set()
706
726
  for i, (input_key, elem) in enumerate(input_dict.items()):
727
+ self._reject_reused_input_source(elem, seen)
707
728
  if isinstance(input_key, NodeMappingType):
708
729
  elem.node_mapping.type = input_key
709
730
  else:
@@ -1522,7 +1543,8 @@ def tensorleap_custom_visualizer(name: str, visualizer_type: LeapDataType,
1522
1543
  LeapDataType.HorizontalBar: LeapHorizontalBar,
1523
1544
  LeapDataType.ImageWithBBox: LeapImageWithBBox,
1524
1545
  LeapDataType.ImageWithHeatmap: LeapImageWithHeatmap,
1525
- LeapDataType.Video: LeapVideo
1546
+ LeapDataType.Video: LeapVideo,
1547
+ LeapDataType.Audio: LeapAudio
1526
1548
  }
1527
1549
  validate_output_structure(result, func_name=user_function.__name__,
1528
1550
  expected_type_name=result_type_map[visualizer_type])
@@ -2845,6 +2867,13 @@ def tensorleap_element_instance_preprocess(
2845
2867
  result = user_function()
2846
2868
  found_instance_metadata = False
2847
2869
  for preprocess_response in result:
2870
+ if preprocess_response.is_grouped:
2871
+ raise Exception(
2872
+ "tensorleap_element_instance_preprocess validation failed: a grouped "
2873
+ "(nested sample_ids) PreprocessResponse cannot also declare element "
2874
+ "instances -- instance ids are derived per scalar sample id, and the "
2875
+ "engine's grouped/batch fetch path has no per-instance id to mask by. "
2876
+ "Remove the instance encoders or don't group this dataset.")
2848
2877
  sample_ids_to_instance_mappings = {}
2849
2878
  instance_to_sample_ids_mappings = {}
2850
2879
  all_sample_ids = preprocess_response.sample_ids.copy()
@@ -3219,7 +3248,7 @@ def tensorleap_gt_encoder(name: str):
3219
3248
  set_current("tensorleap_gt_encoder")
3220
3249
  validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
3221
3250
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
3222
- sample_id, preprocess_response = args
3251
+ sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
3223
3252
  _validate_input_args(sample_id, preprocess_response)
3224
3253
 
3225
3254
  grouped = isinstance(sample_id, list)
code_loader/leaploader.py CHANGED
@@ -41,6 +41,11 @@ def _serialize_sim_bounds(bounds) -> dict:
41
41
 
42
42
 
43
43
  class LeapLoader(LeapLoaderBase):
44
+ # Opt-in ceiling (env var) on samples materialized per get_samples call — the engine sets it to
45
+ # turn a mistaken whole-group ask into a clear error instead of a silent worker OOM. Unset => no
46
+ # ceiling. See get_samples and grouped-fetch-oom-bug.md.
47
+ _grouped_fetch_max_samples_env = "LEAP_GROUPED_FETCH_MAX_SAMPLES_PER_CALL"
48
+
44
49
  def __init__(self, code_path: str, code_entry_name: str):
45
50
  super().__init__(code_path, code_entry_name)
46
51
 
@@ -283,8 +288,10 @@ class LeapLoader(LeapLoaderBase):
283
288
  latent_fn = global_leap_binder.setup_container.custom_latent_space.function
284
289
  preprocess_state = preprocess_result[state]
285
290
  if preprocess_state.is_grouped:
286
- group_ids, pos = self._locate_group(preprocess_state, sample_id)
287
- custom_latent_space = self._to_grouped_list(latent_fn(group_ids, preprocess_state))[pos]
291
+ # Single-sample fetch: encode this sample only, not the whole group (same
292
+ # memory rationale as _get_dataset_handlers; see grouped-fetch-oom-bug.md).
293
+ self._locate_group(preprocess_state, sample_id) # validates group membership
294
+ custom_latent_space = self._to_grouped_list(latent_fn([sample_id], preprocess_state))[0]
288
295
  else:
289
296
  custom_latent_space = latent_fn(sample_id, preprocess_state)
290
297
  instance_mask = self._get_instances_masks(state, sample_id, instance_id)
@@ -414,7 +421,7 @@ class LeapLoader(LeapLoaderBase):
414
421
  test_result = DatasetTestResultPayload('preprocess')
415
422
  try:
416
423
  preprocess_result = self._preprocess_result()
417
- if self.get_sample_id_type() is str:
424
+ if issubclass(self.get_sample_id_type(), str):
418
425
  max_allowed_item_size = np.dtype('<U256').itemsize
419
426
  for state, preprocess_response in preprocess_result.items():
420
427
  sample_ids_array = np.array(preprocess_response.flat_sample_ids)
@@ -478,6 +485,10 @@ class LeapLoader(LeapLoaderBase):
478
485
  preprocess_response.tl_generated = True
479
486
  if preprocess_response.length < 1:
480
487
  raise ValueError("Simulation returned PreprocessResponse with length < 1")
488
+ if preprocess_response.is_grouped:
489
+ raise ValueError(
490
+ "Simulation returned a grouped PreprocessResponse; simulations must "
491
+ "return a flat (non-grouped) PreprocessResponse.")
481
492
  preprocess_response.sample_ids = [0]
482
493
  for handler in global_leap_binder.setup_container.inputs:
483
494
  out1 = handler.function(preprocess_response.sample_ids[0], preprocess_response)
@@ -999,11 +1010,17 @@ class LeapLoader(LeapLoaderBase):
999
1010
  else:
1000
1011
  preprocess_state = preprocess_result[state]
1001
1012
  if preprocess_state.is_grouped:
1002
- # Grouped dataset: call the encoder once with the whole group, index out the sample.
1003
- group_ids, pos = self._locate_group(preprocess_state, sample_id)
1013
+ # Grouped dataset, SINGLE-sample fetch: encode only THIS sample, never the whole group.
1014
+ # Grouping is an I/O read-unit, not an encode-unit — the integration's per-file cache
1015
+ # makes the group's file decode once across consecutive per-sample calls, so a singleton
1016
+ # ([sample_id]) is both a valid single-group subset of the get_samples contract and
1017
+ # correct (samples in a group are independent; see _to_grouped_list). Handing the encoder
1018
+ # the whole group here just to index one sample out materialized B encoded tensors per
1019
+ # sample — a driver of the grouped-fetch worker OOM (see grouped-fetch-oom-bug.md).
1020
+ self._locate_group(preprocess_state, sample_id) # validates the id belongs to a group
1004
1021
  for handler in handlers:
1005
- grouped = self._to_grouped_list(handler.function(group_ids, preprocess_state))
1006
- result_agg[handler.name] = grouped[pos]
1022
+ result_agg[handler.name] = self._to_grouped_list(
1023
+ handler.function([sample_id], preprocess_state))[0]
1007
1024
  return result_agg
1008
1025
  for handler in handlers:
1009
1026
  handler_result = handler.function(sample_id, preprocess_state)
@@ -1036,6 +1053,23 @@ class LeapLoader(LeapLoaderBase):
1036
1053
  f"get_samples received sample ids spanning {len(distinct_groups)} groups; a request "
1037
1054
  f"must be confined to a single group. The engine partitions cross-group requests and "
1038
1055
  f"issues one get_samples call per group.")
1056
+ # Opt-in safety net for the grouped-fetch OOM (grouped-fetch-oom-bug.md). This call
1057
+ # materializes len(group_ids) encoded samples at once; on a large group/file that peak is
1058
+ # what kernel-OOM-kills the worker. A group's file is decoded once by the integration's
1059
+ # per-file cache regardless of how the encode is chunked, so the engine should request
1060
+ # memory-bounded sub-chunks of a group (each a valid single-group subset, e.g.
1061
+ # get_samples(state, group_ids[i:j])) rather than the whole group. When the engine sets the
1062
+ # ceiling below, an over-large ask fails here with a message that names the lever instead of
1063
+ # a silent GENERIC_MEMORY_LIMIT kill. Unset => no ceiling (preserves current whole-group callers).
1064
+ max_per_call = os.environ.get(self._grouped_fetch_max_samples_env)
1065
+ if max_per_call is not None and len(group_ids) > int(max_per_call):
1066
+ raise ValueError(
1067
+ f"get_samples asked to materialize {len(group_ids)} samples in a single call, above "
1068
+ f"the configured ceiling {max_per_call} ({self._grouped_fetch_max_samples_env}). The "
1069
+ f"group's file is decoded once via the integration's per-file cache regardless of "
1070
+ f"chunking, so request memory-bounded sub-chunks of the group "
1071
+ f"(get_samples(state, group_ids[i:j])) instead of the whole group. "
1072
+ f"See grouped-fetch-oom-bug.md.")
1039
1073
  inputs = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
1040
1074
  for handler in global_leap_binder.setup_container.inputs}
1041
1075
  autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
@@ -1057,8 +1091,9 @@ class LeapLoader(LeapLoaderBase):
1057
1091
  f'{type(step_zero_result)} for sample {sid}.')
1058
1092
  self._validate_autoregressive_step_inputs(step_zero_result[0], sid)
1059
1093
  step_zero_per_sample.append(step_zero_result[0])
1060
- for key in (step_zero_per_sample[0].keys() if step_zero_per_sample else []):
1061
- inputs[key] = [row[key] for row in step_zero_per_sample]
1094
+ if step_zero_per_sample:
1095
+ for key in self._grouped_dict_keys(step_zero_per_sample, autoregressive_handler.name):
1096
+ inputs[key] = [row[key] for row in step_zero_per_sample]
1062
1097
  gt = None
1063
1098
  if state != DataStateEnum.unlabeled:
1064
1099
  gt = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
@@ -1067,8 +1102,8 @@ class LeapLoader(LeapLoaderBase):
1067
1102
  metadata: Dict[str, Any] = {}
1068
1103
  metadata_is_none: Dict[str, Any] = {}
1069
1104
  for handler in global_leap_binder.setup_container.metadata:
1070
- rows = handler.function(group_ids, preprocess_state) # list of B scalars/dicts
1071
- if rows and isinstance(rows[0], dict):
1105
+ rows = handler.function(group_ids, preprocess_state) # length-B sequence of scalars/dicts
1106
+ if len(rows) > 0 and isinstance(rows[0], dict):
1072
1107
  for key in self._grouped_dict_keys(rows, handler.name):
1073
1108
  name = "{}_{}".format(handler.name, key)
1074
1109
  converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
@@ -1195,16 +1230,23 @@ class LeapLoader(LeapLoaderBase):
1195
1230
  sample_id = original_local_id
1196
1231
  else:
1197
1232
  preprocess_state = preprocess_result[state]
1198
- group_ids, pos = (None, None)
1199
1233
  if preprocess_state.is_grouped:
1200
- group_ids, pos = self._locate_group(preprocess_state, sample_id)
1234
+ self._locate_group(preprocess_state, sample_id) # validates the id belongs to a group
1201
1235
  for handler in global_leap_binder.setup_container.metadata:
1202
1236
  if requested_metadata_names:
1203
1237
  if not is_metadata_name_starts_with_handler_name(handler):
1204
1238
  continue
1205
1239
 
1206
1240
  if preprocess_state.is_grouped:
1207
- handler_result = handler.function(group_ids, preprocess_state)[pos]
1241
+ # Single-sample metadata fetch: encode only THIS sample, never the whole group —
1242
+ # same memory rationale as _get_dataset_handlers / the get_sample latent branch
1243
+ # (grouped-fetch-oom-bug.md). Metadata values are not guaranteed small scalars: a
1244
+ # metadata encoder may return an np.asarray or read the sample's heavy data to
1245
+ # compute a statistic, so materializing all B rows just to index one out is the same
1246
+ # OOM path this fix closes. The batched get_metadata_multiple_samples correctly keeps
1247
+ # the whole-group encode (amortized across many ids via its group_cache); a single
1248
+ # id does not need it. A singleton is a valid single-group subset of the contract.
1249
+ handler_result = handler.function([sample_id], preprocess_state)[0]
1208
1250
  else:
1209
1251
  handler_result = handler.function(sample_id, preprocess_state)
1210
1252
  if isinstance(handler_result, dict):
@@ -1243,7 +1285,7 @@ class LeapLoader(LeapLoaderBase):
1243
1285
  if requested_metadata_names and not is_wanted(handler):
1244
1286
  continue
1245
1287
  rows = handler.function(group_ids, preprocess_state)
1246
- if rows and isinstance(rows[0], dict):
1288
+ if len(rows) > 0 and isinstance(rows[0], dict):
1247
1289
  for key in self._grouped_dict_keys(rows, handler.name):
1248
1290
  name = f'{handler.name}_{key}'
1249
1291
  if requested_metadata_names and name not in requested_metadata_names:
code_loader/utils.py CHANGED
@@ -17,13 +17,16 @@ from code_loader.contract.enums import DatasetMetadataType
17
17
  def to_numpy_return_wrapper(encoder_function: SectionCallableInterface) -> SectionCallableInterface:
18
18
  def numpy_encoder_function(idx: Union[int, str], samples: PreprocessResponse) -> npt.NDArray[np.float32]:
19
19
  result = encoder_function(idx, samples)
20
- if isinstance(idx, list) and isinstance(result, list) and len(result) > 0 \
21
- and all(isinstance(sample, np.ndarray) for sample in result):
22
- # Grouped call (idx is a list of sample ids): keep the per-sample list, never stack
23
- # it into a (B, *) batch grouping is only a storage read-unit; batching the group to
24
- # the model's B is the engine's single responsibility. Gate on the call shape (idx is a
25
- # list), not the result shape, so a flat encoder returning a list of arrays still stacks.
26
- return result
20
+ if isinstance(idx, list):
21
+ # Grouped call (idx is a list of sample ids): never stack the per-sample results into a
22
+ # (B, *) batch — grouping is only a storage read-unit; batching the group to the model's
23
+ # B is the engine's single responsibility. Gate on the call shape (idx is a list), not
24
+ # the result shape, so a flat encoder returning a list of arrays still stacks below.
25
+ # Normalize each sample independently (a user-stacked (B, *) array is split back into its
26
+ # rows) so a ragged group — variable per-sample shapes, e.g. token lists of differing
27
+ # length — is preserved instead of crashing np.array() on an inhomogeneous shape.
28
+ rows = result if isinstance(result, list) else np.asarray(result)
29
+ return [np.asarray(sample) for sample in rows]
27
30
  numpy_result: npt.NDArray[np.float32] = np.array(result)
28
31
  return numpy_result
29
32
 
@@ -4,20 +4,21 @@ import numpy as np
4
4
  import numpy.typing as npt
5
5
 
6
6
  from code_loader.contract.visualizer_classes import LeapImage, LeapGraph, LeapHorizontalBar, LeapText, \
7
- LeapImageMask, LeapTextMask, LeapVideo
7
+ LeapImageMask, LeapTextMask, LeapVideo, LeapAudio
8
8
  from code_loader.utils import rescale_min_max
9
9
 
10
10
 
11
11
  class DefaultVisualizer(Enum):
12
12
  Image = 'Image'
13
13
  Video = 'Video'
14
+ Audio = 'Audio'
14
15
  Graph = 'Graph'
15
16
  HorizontalBar = 'HorizontalBar'
16
17
  Text = 'Text'
17
18
  ImageMask = 'ImageMask'
18
19
  TextMask = 'TextMask'
19
20
  RawData = 'RawData'
20
-
21
+
21
22
 
22
23
  def default_image_visualizer(data: npt.NDArray[np.float32]) -> LeapImage:
23
24
  rescaled_data = rescale_min_max(data[0])
@@ -28,6 +29,69 @@ def default_video_visualizer(data: npt.NDArray[np.float32]) -> LeapVideo:
28
29
  return LeapVideo(data[0])
29
30
 
30
31
 
32
+ _MAX_WAVEFORM_POINTS = 2000
33
+ _VISUAL_KINDS = ('waveform', 'spectrogram', 'image')
34
+
35
+
36
+ def _to_mono(audio: npt.NDArray) -> npt.NDArray:
37
+ a = np.squeeze(np.asarray(audio))
38
+ if a.ndim == 2: # mix down: channels are the shorter axis
39
+ a = a.mean(axis=int(np.argmin(a.shape)))
40
+ if a.ndim != 1:
41
+ raise ValueError(f'audio must be reducible to a mono 1-D signal, got shape {np.asarray(audio).shape}')
42
+ return a.astype(np.float32)
43
+
44
+
45
+ def _waveform_graph(series: npt.NDArray, duration: float) -> LeapGraph:
46
+ s = np.squeeze(np.asarray(series)).astype(np.float32)
47
+ # ponytail: recharts chokes on 100k+ points; stride down for the default view. x_range keeps full time span.
48
+ step = max(1, s.shape[0] // _MAX_WAVEFORM_POINTS)
49
+ s = s[::step].reshape(-1, 1)
50
+ return LeapGraph(s, x_label='Time [s]', y_label='Amplitude', x_range=(0.0, duration))
51
+
52
+
53
+ def default_audio_visualizer(audio: npt.NDArray,
54
+ sample_rate: int,
55
+ visual: npt.NDArray,
56
+ visual_kind: str,
57
+ time_axis_index: int = -1) -> LeapAudio:
58
+ """Package a playable audio sample plus the visual a playhead runs through.
59
+
60
+ audio: waveform samples (mono [N], or multi-channel that is mixed down).
61
+ sample_rate: samples per second.
62
+ visual: the array to display. For 'waveform' a 1-D series; for 'spectrogram'
63
+ a raw 2-D [freq, time] (or [time, freq]) array to be rendered grayscale;
64
+ for 'image' an already-rendered [H, W, 1] or [H, W, 3] image (e.g. a
65
+ spectrogram the caller already ran through their own colormap).
66
+ visual_kind: 'waveform', 'spectrogram', or 'image'. The caller states this
67
+ explicitly; the shape of `visual` is never inspected to guess it.
68
+ time_axis_index: for 'spectrogram', which axis of `visual` is time (moved to columns).
69
+ Unused for 'waveform'/'image'.
70
+ """
71
+ if visual_kind not in _VISUAL_KINDS:
72
+ raise ValueError(f"visual_kind must be one of {_VISUAL_KINDS}, got {visual_kind!r}")
73
+
74
+ mono = _to_mono(audio)
75
+ duration = mono.shape[0] / float(sample_rate)
76
+ x_range = (0.0, duration)
77
+ v = np.asarray(visual)
78
+
79
+ if visual_kind == 'waveform':
80
+ return LeapAudio(mono, sample_rate, _waveform_graph(v, duration), x_range)
81
+
82
+ if visual_kind == 'image':
83
+ return LeapAudio(mono, sample_rate, LeapImage(v), x_range)
84
+
85
+ # spectrogram
86
+ spec = np.squeeze(v)
87
+ if spec.ndim != 2:
88
+ raise ValueError(f'spectrogram visual must be 2-D (freq x time), got shape {v.shape}')
89
+ if time_axis_index % spec.ndim == 0: # move time onto the horizontal axis (columns)
90
+ spec = spec.T
91
+ img = rescale_min_max(spec.astype(np.float32))[..., None] # [freq, time, 1]
92
+ return LeapAudio(mono, sample_rate, LeapImage(img), x_range)
93
+
94
+
31
95
  def default_graph_visualizer(data: npt.NDArray[np.float32]) -> LeapGraph:
32
96
  return LeapGraph(data[0])
33
97
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: code-loader
3
- Version: 1.0.196.dev1
3
+ Version: 1.0.197
4
4
  Summary:
5
5
  Home-page: https://github.com/tensorleap/code-loader
6
6
  License: MIT
@@ -1,13 +1,13 @@
1
1
  LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
2
2
  code_loader/__init__.py,sha256=outxRQ0M-zMfV0QGVJmAed5qWfRmyD0TV6-goEGAzBw,406
3
3
  code_loader/contract/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- code_loader/contract/datasetclasses.py,sha256=CX4HO3xiuBUiXaeSBEyjAFV7i1NGS7KZ3JnLGxofH28,16164
5
- code_loader/contract/enums.py,sha256=2q-IV_5g9lLE306DIbWA1c0tn5IhDtxsKxyV1x_Lreg,1671
4
+ code_loader/contract/datasetclasses.py,sha256=qJoC2pPrVLwJiNGQlPUmnM4vgGNxEv4LHPjovVunTGc,16741
5
+ code_loader/contract/enums.py,sha256=__GkPkwAXi2agmDGtEQgbMucPm4-n80maG6n_MmObPA,1691
6
6
  code_loader/contract/exceptions.py,sha256=jWqu5i7t-0IG0jGRsKF4DjJdrsdpJjIYpUkN1F4RiyQ,51
7
7
  code_loader/contract/mapping.py,sha256=sWJhpng-IkOzQnWQdMT5w2ZZ3X1Z_OOzSwCLXIS7oxE,1446
8
8
  code_loader/contract/responsedataclasses.py,sha256=5VFgGjRubMW8ItMPils3rkBNejunCGLaa192AIi-xko,4925
9
9
  code_loader/contract/sim_config.py,sha256=le8KMALZiP0WU4UcuKnTOSWBW2rNjpnWYfII502NqDM,3493
10
- code_loader/contract/visualizer_classes.py,sha256=cWXkBBYk_YP7-VdNwLJvSPQfvBAvoNWircgIuZwHQVc,16519
10
+ code_loader/contract/visualizer_classes.py,sha256=vhdufY1nDhEBbtqrLgRe0bZiNCtOmsKPFxbvfbTZVUs,18624
11
11
  code_loader/default_losses.py,sha256=NoOQym1106bDN5dcIk56Elr7ZG5quUHArqfP5-Nyxyo,1139
12
12
  code_loader/default_metrics.py,sha256=2XSlyNw_XLDGSJDoz5W_Evi5wbL0dhwq24pPr15vSPc,5025
13
13
  code_loader/experiment_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -21,18 +21,18 @@ code_loader/experiment_api/types.py,sha256=MY8xFARHwdVA7p4dxyhD60ShmttgTvb4qdp1o
21
21
  code_loader/experiment_api/utils.py,sha256=XZHtxge12TS4H4-8PjV3sKuhp8Ud6ojAiIzTZJEqBqc,3304
22
22
  code_loader/experiment_api/workingspace_config_utils.py,sha256=DLzXQCg4dgTV_YgaSbeTVzq-2ja_SQw4zi7LXwKL9cY,990
23
23
  code_loader/inner_leap_binder/__init__.py,sha256=koOlJyMNYzGbEsoIbXathSmQ-L38N_pEXH_HvL7beXU,99
24
- code_loader/inner_leap_binder/leapbinder.py,sha256=yW9EBA4E7YwWoWaiVELWoSP9uWTQjnPl5PfLbmX4cFk,56617
25
- code_loader/inner_leap_binder/leapbinder_decorators.py,sha256=GyuyffnSmkELj63q_NBAxa6Z0szP6MsaNxDodwCo-pE,183746
26
- code_loader/leaploader.py,sha256=yWzSc-ynm_ykfup_tk8mwZX3TFq1ZW-fN18fEDXOrfc,83386
24
+ code_loader/inner_leap_binder/leapbinder.py,sha256=RSYiWtIxrIV7a466oPTEupP5SF9eOkZZotr_rp7yCHs,56880
25
+ code_loader/inner_leap_binder/leapbinder_decorators.py,sha256=g_fZw4OlRAJ2d_8bjRBnazNNpzi6pJY0TV3BYJgXP3s,185922
26
+ code_loader/leaploader.py,sha256=vJDQLC6rq12PiBiDAeKttgSXK72QFQMCYLvjp_Jf2Uo,87135
27
27
  code_loader/leaploaderbase.py,sha256=PvBU_2OQqBfJLDMsbK1s372954eYr4Y3O4r18y5S7uY,10739
28
28
  code_loader/mixpanel_tracker.py,sha256=rNwRmFifNbdUoqLQvvhhgpKczWpWiEmd8MfyJe27sxw,9131
29
29
  code_loader/plot_functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  code_loader/plot_functions/plot_functions.py,sha256=2DC-zlVaN13P4VNx5d8csgs80C6SisaeP1-Kq2LW7iM,16075
31
31
  code_loader/plot_functions/visualize.py,sha256=gsBAYYkwMh7jIpJeDMPS8G4CW-pxwx6LznoQIvi4vpo,657
32
- code_loader/utils.py,sha256=Grrp8OLagrVAOYjt83DDmE0CPfjE22DuOQftgqDASyU,7215
32
+ code_loader/utils.py,sha256=Yj06BbS1UIjN9Cv2eQBjDfdDbpQlABAg6KwD3sB1BZ0,7496
33
33
  code_loader/visualizers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- code_loader/visualizers/default_visualizers.py,sha256=onRnLE_TXfgLN4o52hQIOOhUcFexGlqJ3xSpQDVLuZM,2604
35
- code_loader-1.0.196.dev1.dist-info/LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
36
- code_loader-1.0.196.dev1.dist-info/METADATA,sha256=qjJUAz8M-nUFhOQa1jAFKQXk2Qm-pdh4zns9jWNEglY,1095
37
- code_loader-1.0.196.dev1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
38
- code_loader-1.0.196.dev1.dist-info/RECORD,,
34
+ code_loader/visualizers/default_visualizers.py,sha256=c60vSpw48yVlIF2jIZ-5B6aDdCYBXKXfKzFpTDsAZAw,5467
35
+ code_loader-1.0.197.dist-info/LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
36
+ code_loader-1.0.197.dist-info/METADATA,sha256=MDgLFFdlskU6Gf1ccQWZC9Xk9-epYyr-aMYB9jFaqz4,1090
37
+ code_loader-1.0.197.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
38
+ code_loader-1.0.197.dist-info/RECORD,,