code-loader 1.0.196.dev0__tar.gz → 1.0.197__tar.gz

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.
Files changed (38) hide show
  1. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/PKG-INFO +1 -1
  2. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/contract/datasetclasses.py +9 -1
  3. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/contract/enums.py +1 -0
  4. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/contract/visualizer_classes.py +43 -1
  5. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/inner_leap_binder/leapbinder.py +51 -3
  6. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/inner_leap_binder/leapbinder_decorators.py +48 -14
  7. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/leaploader.py +57 -17
  8. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/utils.py +33 -22
  9. code_loader-1.0.197/code_loader/visualizers/default_visualizers.py +149 -0
  10. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/pyproject.toml +1 -1
  11. code_loader-1.0.196.dev0/code_loader/visualizers/default_visualizers.py +0 -85
  12. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/LICENSE +0 -0
  13. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/README.md +0 -0
  14. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/__init__.py +0 -0
  15. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/contract/__init__.py +0 -0
  16. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/contract/exceptions.py +0 -0
  17. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/contract/mapping.py +0 -0
  18. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/contract/responsedataclasses.py +0 -0
  19. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/contract/sim_config.py +0 -0
  20. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/default_losses.py +0 -0
  21. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/default_metrics.py +0 -0
  22. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/experiment_api/__init__.py +0 -0
  23. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/experiment_api/api.py +0 -0
  24. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/experiment_api/cli_config_utils.py +0 -0
  25. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/experiment_api/client.py +0 -0
  26. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/experiment_api/epoch.py +0 -0
  27. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/experiment_api/experiment.py +0 -0
  28. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/experiment_api/experiment_context.py +0 -0
  29. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/experiment_api/types.py +0 -0
  30. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/experiment_api/utils.py +0 -0
  31. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/experiment_api/workingspace_config_utils.py +0 -0
  32. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/inner_leap_binder/__init__.py +0 -0
  33. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/leaploaderbase.py +0 -0
  34. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/mixpanel_tracker.py +0 -0
  35. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/plot_functions/__init__.py +0 -0
  36. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/plot_functions/plot_functions.py +0 -0
  37. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/plot_functions/visualize.py +0 -0
  38. {code_loader-1.0.196.dev0 → code_loader-1.0.197}/code_loader/visualizers/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: code-loader
3
- Version: 1.0.196.dev0
3
+ Version: 1.0.197
4
4
  Summary:
5
5
  Home-page: https://github.com/tensorleap/code-loader
6
6
  License: MIT
@@ -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
  }
@@ -1,6 +1,9 @@
1
1
 
2
+ import builtins
2
3
  import inspect
3
- from typing import Callable, List, Optional, Dict, Any, Type, Union, get_args, cast
4
+ import os
5
+ from contextlib import contextmanager
6
+ from typing import Callable, List, Optional, Dict, Any, Type, Union, get_args, cast, Iterator, Set
4
7
 
5
8
  import numpy as np
6
9
  import numpy.typing as npt
@@ -16,7 +19,8 @@ from code_loader.contract.datasetclasses import SectionCallableInterface, InputH
16
19
  SimulationHandler, _simulation_context, AutoregressiveStepHandler, AutoregressiveStepCallableInterface, \
17
20
  AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS, AUTOREGRESSIVE_IMPLICIT_ARG_NAMES, \
18
21
  AutoregressiveMetricHandler, AutoregressiveLossHandler, AutoregressiveVisualizerHandler
19
- from code_loader.contract.enums import LeapDataType, DataStateEnum, DataStateType, MetricDirection, DatasetMetadataType
22
+ from code_loader.contract.enums import LeapDataType, DataStateEnum, DataStateType, MetricDirection, DatasetMetadataType, \
23
+ TestingSectionEnum
20
24
  from code_loader.contract.mapping import NodeConnection, NodeMapping, NodeMappingType
21
25
  from code_loader.contract.responsedataclasses import DatasetTestResultPayload, LeapAnalysisConfiguration
22
26
  from code_loader.contract.visualizer_classes import map_leap_data_type_to_visualizer_class
@@ -33,6 +37,25 @@ from code_loader.visualizers.default_visualizers import DefaultVisualizer, \
33
37
  mapping_runtime_mode_env_var_mame = '__MAPPING_RUNTIME_MODE__'
34
38
 
35
39
 
40
+ @contextmanager
41
+ def _track_opened_files() -> Iterator[Set[str]]:
42
+ opened: Set[str] = set()
43
+ original_open = builtins.open
44
+
45
+ def tracking_open(file: Any, *args: Any, **kwargs: Any) -> Any:
46
+ mode = args[0] if args else kwargs.get('mode', 'r')
47
+ is_read_mode = isinstance(mode, str) and not any(c in mode for c in ('a', 'w', 'x'))
48
+ if is_read_mode and isinstance(file, (str, os.PathLike)):
49
+ opened.add(os.fspath(file))
50
+ return original_open(file, *args, **kwargs)
51
+
52
+ builtins.open = tracking_open
53
+ try:
54
+ yield opened
55
+ finally:
56
+ builtins.open = original_open
57
+
58
+
36
59
  def _stringized_annotation_type_name(annotation: Any) -> Optional[str]:
37
60
  """Bare type name if ``annotation`` is a stringized annotation (from ``from __future__
38
61
  import annotations`` or a quoted hint), else ``None``. code_loader inspects raw
@@ -744,15 +767,34 @@ class LeapBinder:
744
767
  preprocess_response: PreprocessResponse, test_result: List[DatasetTestResultPayload],
745
768
  dataset_base_handler: Union[DatasetBaseHandler, MetadataHandler], state: DataStateEnum) -> List[DatasetTestResultPayload]:
746
769
  assert preprocess_response.sample_ids is not None
770
+ opened_files: Optional[Set[str]] = None
747
771
  if preprocess_response.is_grouped:
748
772
  # Grouped: probe with the first group, then reduce to a single sample's result so the
749
773
  # recorded shape/type is per-sample (matching the flat case), not the (B, *) batch.
750
774
  # (casts: sample_ids[0] is a group list here, and the grouped result is a per-sample list.)
751
775
  group = preprocess_response.sample_ids[0]
752
- raw_result = cast(Any, dataset_base_handler.function(cast(Any, group), preprocess_response))[0]
776
+ with _track_opened_files() as opened_files:
777
+ raw_result = cast(Any, dataset_base_handler.function(cast(Any, group), preprocess_response))[0]
753
778
  else:
754
779
  raw_result = dataset_base_handler.function(
755
780
  cast(Union[int, str], preprocess_response.sample_ids[0]), preprocess_response)
781
+
782
+ def _warn_if_group_spans_multiple_files(payloads: List[DatasetTestResultPayload]) -> None:
783
+ if opened_files is None or len(opened_files) <= 1:
784
+ return
785
+ shown = sorted(opened_files)[:5]
786
+ suffix = f" (+{len(opened_files) - 5} more)" if len(opened_files) > 5 else ""
787
+ warning = (
788
+ f"Encoding declared group 0 (size {len(cast(List[Any], group))}) for '{dataset_base_handler.name}' opened "
789
+ f"{len(opened_files)} distinct files: {shown}{suffix}. If this group is meant to be one "
790
+ f"physical source (e.g. one Parquet file), it may be grouped incorrectly - check your "
791
+ f"preprocess grouping logic. (Only files opened via Python's open() are tracked, so this "
792
+ f"can miss other I/O paths or be a false positive if grouping isn't meant to reflect file "
793
+ f"locality.)")
794
+ for payload in payloads:
795
+ existing = payload.display.get(TestingSectionEnum.Errors.name, '')
796
+ payload.display[TestingSectionEnum.Errors.name] = (existing + '\n' if existing else '') + warning
797
+
756
798
  handler_type = 'metadata' if isinstance(dataset_base_handler, MetadataHandler) else None
757
799
  if isinstance(dataset_base_handler, MetadataHandler):
758
800
  if isinstance(raw_result, dict):
@@ -792,6 +834,7 @@ class LeapBinder:
792
834
  else:
793
835
  if raw_result is None:
794
836
  if state != DataStateEnum.training:
837
+ _warn_if_group_spans_multiple_files(test_result)
795
838
  return test_result
796
839
 
797
840
  if dataset_base_handler.metadata_type is None:
@@ -813,6 +856,7 @@ class LeapBinder:
813
856
  # setting shape in setup for all encoders
814
857
  if isinstance(dataset_base_handler, (InputHandler, GroundTruthHandler)):
815
858
  dataset_base_handler.shape = result_shape
859
+ _warn_if_group_spans_multiple_files(test_result)
816
860
  return test_result
817
861
 
818
862
  def check_handlers(self, preprocess_result: Dict[DataStateEnum, PreprocessResponse]) -> None:
@@ -848,6 +892,10 @@ class LeapBinder:
848
892
  preprocess_response.tl_generated = True
849
893
  if not preprocess_response.length or preprocess_response.length < 1:
850
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))
851
899
  preprocess_response.sample_ids = cast(List[Union[int, str]], [0])
852
900
  sim_sample_ids = cast(List[Union[int, str]], preprocess_response.sample_ids)
853
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])
@@ -2010,9 +2032,10 @@ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
2010
2032
  state returned by the previous call, and return the next step's full input dict together with
2011
2033
  the updated state; returning (None, state) ends the chain — the terminating call still
2012
2034
  returns state, so the final step participates in state aggregation. State is never fed to
2013
- the model; it may be any nest of dicts/lists holding numpy arrays, numbers, strings or bools,
2014
- and it rides the platform queue on every step accumulate reductions, not raw per-step
2015
- tensors. The hook must be stateless and deterministically seeded (seed stochasticity from
2035
+ the model; any picklable nest of builtin/numpy values works (dicts, lists, tuples, sets,
2036
+ numbers, strings, arrays not user-defined classes, lambdas or open resources, since the
2037
+ platform deserializes state without the integration code), and it rides the platform queue
2038
+ on every step — accumulate reductions, not raw per-step tensors. The hook must be stateless and deterministically seeded (seed stochasticity from
2016
2039
  sample_id) — the engine replays steps after crash recovery and relies on identical results.
2017
2040
 
2018
2041
  latent_space_aggregation ('last_step' | 'mean') declares how the chain's latent-space
@@ -2844,6 +2867,13 @@ def tensorleap_element_instance_preprocess(
2844
2867
  result = user_function()
2845
2868
  found_instance_metadata = False
2846
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.")
2847
2877
  sample_ids_to_instance_mappings = {}
2848
2878
  instance_to_sample_ids_mappings = {}
2849
2879
  all_sample_ids = preprocess_response.sample_ids.copy()
@@ -3114,10 +3144,12 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
3114
3144
  if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
3115
3145
  if grouped:
3116
3146
  # A grouped result is a list of per-sample arrays (never stacked, so a
3117
- # B=1-only model can be fed one sample at a time). Add a batch dim to each
3118
- # sample, mirroring the flat path's single-sample expand_dims.
3119
- if isinstance(result, list):
3120
- result = [np.expand_dims(r, axis=0) for r in result]
3147
+ # B=1-only model can be fed one sample at a time). A user may also return a
3148
+ # supported (B, *) array; split it back into per-sample rows first (mirrors
3149
+ # LeapLoader._to_grouped_list) so both forms match the runtime path. Then add
3150
+ # a batch dim to each sample, mirroring the flat path's single-sample expand_dims.
3151
+ rows = result if isinstance(result, list) else [row for row in np.asarray(result)]
3152
+ result = [np.expand_dims(r, axis=0) for r in rows]
3121
3153
  else:
3122
3154
  batch_warning(result, user_function.__name__)
3123
3155
  result = np.expand_dims(result, axis=0)
@@ -3216,7 +3248,7 @@ def tensorleap_gt_encoder(name: str):
3216
3248
  set_current("tensorleap_gt_encoder")
3217
3249
  validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
3218
3250
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
3219
- sample_id, preprocess_response = args
3251
+ sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
3220
3252
  _validate_input_args(sample_id, preprocess_response)
3221
3253
 
3222
3254
  grouped = isinstance(sample_id, list)
@@ -3228,10 +3260,12 @@ def tensorleap_gt_encoder(name: str):
3228
3260
  if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
3229
3261
  if grouped:
3230
3262
  # A grouped result is a list of per-sample arrays (never stacked, so a
3231
- # B=1-only model can be fed one sample at a time). Add a batch dim to each
3232
- # sample, mirroring the flat path's single-sample expand_dims.
3233
- if isinstance(result, list):
3234
- result = [np.expand_dims(r, axis=0) for r in result]
3263
+ # B=1-only model can be fed one sample at a time). A user may also return a
3264
+ # supported (B, *) array; split it back into per-sample rows first (mirrors
3265
+ # LeapLoader._to_grouped_list) so both forms match the runtime path. Then add
3266
+ # a batch dim to each sample, mirroring the flat path's single-sample expand_dims.
3267
+ rows = result if isinstance(result, list) else [row for row in np.asarray(result)]
3268
+ result = [np.expand_dims(r, axis=0) for r in rows]
3235
3269
  else:
3236
3270
  batch_warning(result, user_function.__name__)
3237
3271
  result = np.expand_dims(result, axis=0)
@@ -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
 
@@ -77,8 +82,6 @@ class LeapLoader(LeapLoaderBase):
77
82
  preprocess_result = global_leap_binder.get_preprocess_result()
78
83
  self._preprocess_result_cached = preprocess_result
79
84
  is_grouped = any(r.is_grouped for r in preprocess_result.values())
80
- except Exception:
81
- is_grouped = False
82
85
  finally:
83
86
  os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
84
87
 
@@ -285,8 +288,10 @@ class LeapLoader(LeapLoaderBase):
285
288
  latent_fn = global_leap_binder.setup_container.custom_latent_space.function
286
289
  preprocess_state = preprocess_result[state]
287
290
  if preprocess_state.is_grouped:
288
- group_ids, pos = self._locate_group(preprocess_state, sample_id)
289
- 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]
290
295
  else:
291
296
  custom_latent_space = latent_fn(sample_id, preprocess_state)
292
297
  instance_mask = self._get_instances_masks(state, sample_id, instance_id)
@@ -416,7 +421,7 @@ class LeapLoader(LeapLoaderBase):
416
421
  test_result = DatasetTestResultPayload('preprocess')
417
422
  try:
418
423
  preprocess_result = self._preprocess_result()
419
- if self.get_sample_id_type() is str:
424
+ if issubclass(self.get_sample_id_type(), str):
420
425
  max_allowed_item_size = np.dtype('<U256').itemsize
421
426
  for state, preprocess_response in preprocess_result.items():
422
427
  sample_ids_array = np.array(preprocess_response.flat_sample_ids)
@@ -480,6 +485,10 @@ class LeapLoader(LeapLoaderBase):
480
485
  preprocess_response.tl_generated = True
481
486
  if preprocess_response.length < 1:
482
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.")
483
492
  preprocess_response.sample_ids = [0]
484
493
  for handler in global_leap_binder.setup_container.inputs:
485
494
  out1 = handler.function(preprocess_response.sample_ids[0], preprocess_response)
@@ -1001,11 +1010,17 @@ class LeapLoader(LeapLoaderBase):
1001
1010
  else:
1002
1011
  preprocess_state = preprocess_result[state]
1003
1012
  if preprocess_state.is_grouped:
1004
- # Grouped dataset: call the encoder once with the whole group, index out the sample.
1005
- 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
1006
1021
  for handler in handlers:
1007
- grouped = self._to_grouped_list(handler.function(group_ids, preprocess_state))
1008
- result_agg[handler.name] = grouped[pos]
1022
+ result_agg[handler.name] = self._to_grouped_list(
1023
+ handler.function([sample_id], preprocess_state))[0]
1009
1024
  return result_agg
1010
1025
  for handler in handlers:
1011
1026
  handler_result = handler.function(sample_id, preprocess_state)
@@ -1038,6 +1053,23 @@ class LeapLoader(LeapLoaderBase):
1038
1053
  f"get_samples received sample ids spanning {len(distinct_groups)} groups; a request "
1039
1054
  f"must be confined to a single group. The engine partitions cross-group requests and "
1040
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.")
1041
1073
  inputs = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
1042
1074
  for handler in global_leap_binder.setup_container.inputs}
1043
1075
  autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
@@ -1059,8 +1091,9 @@ class LeapLoader(LeapLoaderBase):
1059
1091
  f'{type(step_zero_result)} for sample {sid}.')
1060
1092
  self._validate_autoregressive_step_inputs(step_zero_result[0], sid)
1061
1093
  step_zero_per_sample.append(step_zero_result[0])
1062
- for key in (step_zero_per_sample[0].keys() if step_zero_per_sample else []):
1063
- 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]
1064
1097
  gt = None
1065
1098
  if state != DataStateEnum.unlabeled:
1066
1099
  gt = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
@@ -1069,8 +1102,8 @@ class LeapLoader(LeapLoaderBase):
1069
1102
  metadata: Dict[str, Any] = {}
1070
1103
  metadata_is_none: Dict[str, Any] = {}
1071
1104
  for handler in global_leap_binder.setup_container.metadata:
1072
- rows = handler.function(group_ids, preprocess_state) # list of B scalars/dicts
1073
- 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):
1074
1107
  for key in self._grouped_dict_keys(rows, handler.name):
1075
1108
  name = "{}_{}".format(handler.name, key)
1076
1109
  converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
@@ -1197,16 +1230,23 @@ class LeapLoader(LeapLoaderBase):
1197
1230
  sample_id = original_local_id
1198
1231
  else:
1199
1232
  preprocess_state = preprocess_result[state]
1200
- group_ids, pos = (None, None)
1201
1233
  if preprocess_state.is_grouped:
1202
- 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
1203
1235
  for handler in global_leap_binder.setup_container.metadata:
1204
1236
  if requested_metadata_names:
1205
1237
  if not is_metadata_name_starts_with_handler_name(handler):
1206
1238
  continue
1207
1239
 
1208
1240
  if preprocess_state.is_grouped:
1209
- 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]
1210
1250
  else:
1211
1251
  handler_result = handler.function(sample_id, preprocess_state)
1212
1252
  if isinstance(handler_result, dict):
@@ -1245,7 +1285,7 @@ class LeapLoader(LeapLoaderBase):
1245
1285
  if requested_metadata_names and not is_wanted(handler):
1246
1286
  continue
1247
1287
  rows = handler.function(group_ids, preprocess_state)
1248
- if rows and isinstance(rows[0], dict):
1288
+ if len(rows) > 0 and isinstance(rows[0], dict):
1249
1289
  for key in self._grouped_dict_keys(rows, handler.name):
1250
1290
  name = f'{handler.name}_{key}'
1251
1291
  if requested_metadata_names and name not in requested_metadata_names:
@@ -1,4 +1,6 @@
1
+ import io
1
2
  import math
3
+ import pickle
2
4
  import sys
3
5
  from pathlib import Path
4
6
  from types import TracebackType
@@ -15,11 +17,16 @@ from code_loader.contract.enums import DatasetMetadataType
15
17
  def to_numpy_return_wrapper(encoder_function: SectionCallableInterface) -> SectionCallableInterface:
16
18
  def numpy_encoder_function(idx: Union[int, str], samples: PreprocessResponse) -> npt.NDArray[np.float32]:
17
19
  result = encoder_function(idx, samples)
18
- if isinstance(result, list) and len(result) > 0 and all(isinstance(sample, np.ndarray) for sample in result):
19
- # A list of per-sample arrays only comes from a grouped encoder (a flat encoder returns
20
- # a single array). Never stack it into a (B, *) batch — grouping is only a storage
21
- # read-unit; batching the group to the model's B is the engine's single responsibility.
22
- 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]
23
30
  numpy_result: npt.NDArray[np.float32] = np.array(result)
24
31
  return numpy_result
25
32
 
@@ -109,27 +116,31 @@ def get_metadata_type_from_variable(variable: Union[Optional[str], int, bool, Op
109
116
 
110
117
 
111
118
 
112
- AUTOREGRESSIVE_STATE_LEAF_TYPES = (np.ndarray, np.generic, int, float, str, bool, type(None))
119
+ # The platform queue transports state as pickled payloads which the engine deserializes
120
+ # WITHOUT the integration code loaded, so a class defined in the user's modules round-trips
121
+ # locally but crashes on the engine. Only modules importable on the engine may resolve.
122
+ _STATE_SAFE_PICKLE_MODULES = ('builtins', 'copyreg', 'collections', 'numpy', 'datetime')
123
+
124
+
125
+ class _EngineSafeUnpickler(pickle.Unpickler):
126
+ def find_class(self, module: str, name: str) -> Any:
127
+ if module.split('.')[0] in _STATE_SAFE_PICKLE_MODULES:
128
+ return super().find_class(module, name)
129
+ raise pickle.UnpicklingError(
130
+ f'{module}.{name} is defined in integration code, which is not importable on the '
131
+ f'platform engine')
113
132
 
114
133
 
115
134
  def validate_autoregressive_state_types(value: Any, path: str = 'state') -> None:
116
- if isinstance(value, dict):
117
- for key, item in value.items():
118
- if not isinstance(key, str):
119
- raise AssertionError(
120
- f'autoregressive state validation failed: dict key {key!r} at {path} must be '
121
- f'a string. Got {type(key).__name__}.')
122
- validate_autoregressive_state_types(item, f'{path}[{key!r}]')
123
- return
124
- if isinstance(value, (list, tuple)):
125
- for i, item in enumerate(value):
126
- validate_autoregressive_state_types(item, f'{path}[{i}]')
127
- return
128
- if not isinstance(value, AUTOREGRESSIVE_STATE_LEAF_TYPES):
135
+ try:
136
+ _EngineSafeUnpickler(io.BytesIO(pickle.dumps(value))).load()
137
+ except Exception as e:
129
138
  raise AssertionError(
130
- f'autoregressive state validation failed: {path} holds a {type(value).__name__}. '
131
- f'State must be built only from numpy arrays, numbers, strings, bools and nests of '
132
- f'dicts/lists of thoseit is serialized to the platform queue on every chain step.')
139
+ f'autoregressive state validation failed: {path} is not serializable ({e}). State '
140
+ 'is serialized to the platform queue on every chain step and deserialized without '
141
+ 'the integration codeany picklable nest of builtin/numpy values works (dicts, '
142
+ 'lists, tuples, sets, numbers, strings, arrays), but user-defined classes, lambdas '
143
+ 'and open resources do not.') from e
133
144
 
134
145
 
135
146
  def autoregressive_nests_equal(a: Any, b: Any) -> bool:
@@ -0,0 +1,149 @@
1
+ from enum import Enum
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+
6
+ from code_loader.contract.visualizer_classes import LeapImage, LeapGraph, LeapHorizontalBar, LeapText, \
7
+ LeapImageMask, LeapTextMask, LeapVideo, LeapAudio
8
+ from code_loader.utils import rescale_min_max
9
+
10
+
11
+ class DefaultVisualizer(Enum):
12
+ Image = 'Image'
13
+ Video = 'Video'
14
+ Audio = 'Audio'
15
+ Graph = 'Graph'
16
+ HorizontalBar = 'HorizontalBar'
17
+ Text = 'Text'
18
+ ImageMask = 'ImageMask'
19
+ TextMask = 'TextMask'
20
+ RawData = 'RawData'
21
+
22
+
23
+ def default_image_visualizer(data: npt.NDArray[np.float32]) -> LeapImage:
24
+ rescaled_data = rescale_min_max(data[0])
25
+ return LeapImage(rescaled_data)
26
+
27
+
28
+ def default_video_visualizer(data: npt.NDArray[np.float32]) -> LeapVideo:
29
+ return LeapVideo(data[0])
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
+
95
+ def default_graph_visualizer(data: npt.NDArray[np.float32]) -> LeapGraph:
96
+ return LeapGraph(data[0])
97
+
98
+
99
+ def default_horizontal_bar_visualizer(data: npt.NDArray[np.float32]) -> LeapHorizontalBar:
100
+ labels = [str(index) for index in range(data.shape[-1])]
101
+ return LeapHorizontalBar(data[0], labels)
102
+
103
+
104
+ def default_word_visualizer(data: npt.NDArray[np.float32]) -> LeapText:
105
+ if len(data.shape) == 2 and data.shape[0] == 1:
106
+ data = data[0]
107
+ if hasattr(data, 'tolist'):
108
+ data = data.tolist()
109
+ words = [str(index[0]) if type(index) is list else str(index) for index in data]
110
+ return LeapText(words)
111
+
112
+
113
+ def default_raw_data_visualizer(data: npt.NDArray[np.float32]) -> LeapText:
114
+ return LeapText([str(data)])
115
+
116
+
117
+ def default_image_mask_visualizer(mask: npt.NDArray[np.float32], image: npt.NDArray[np.float32]) -> LeapImageMask:
118
+ if image.shape[0] == 1:
119
+ image = image[0]
120
+ if mask.shape[0] == 1:
121
+ mask = mask[0]
122
+
123
+ n_different_labels = mask.shape[-1]
124
+ labels = [str(i) for i in range(n_different_labels)]
125
+
126
+ if len(mask.shape) > 2:
127
+ if mask.shape[-1] == 1:
128
+ mask = np.squeeze(mask, axis=-1)
129
+ else:
130
+ mask = np.argmax(mask, axis=-1)
131
+
132
+ return LeapImageMask(mask.astype(np.uint8), image.astype(np.float32), labels)
133
+
134
+
135
+ def default_text_mask_visualizer(mask: npt.NDArray[np.float32], text_data: npt.NDArray[np.float32]) -> LeapTextMask:
136
+ mask = mask[0]
137
+ text_data = text_data[0]
138
+
139
+ words = default_word_visualizer(text_data).data
140
+ n_different_labels = mask.shape[-1]
141
+ labels = [str(i) for i in range(n_different_labels)]
142
+
143
+ if len(mask.shape) > 1:
144
+ if mask.shape[-1] == 1:
145
+ mask = np.squeeze(mask, axis=-1)
146
+ else:
147
+ mask = np.argmax(mask, axis=-1)
148
+
149
+ return LeapTextMask(mask.astype(np.uint8), words, labels)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "code-loader"
3
- version = "1.0.196.dev"
3
+ version = "1.0.197"
4
4
  description = ""
5
5
  authors = ["dorhar <doron.harnoy@tensorleap.ai>"]
6
6
  license = "MIT"
@@ -1,85 +0,0 @@
1
- from enum import Enum
2
-
3
- import numpy as np
4
- import numpy.typing as npt
5
-
6
- from code_loader.contract.visualizer_classes import LeapImage, LeapGraph, LeapHorizontalBar, LeapText, \
7
- LeapImageMask, LeapTextMask, LeapVideo
8
- from code_loader.utils import rescale_min_max
9
-
10
-
11
- class DefaultVisualizer(Enum):
12
- Image = 'Image'
13
- Video = 'Video'
14
- Graph = 'Graph'
15
- HorizontalBar = 'HorizontalBar'
16
- Text = 'Text'
17
- ImageMask = 'ImageMask'
18
- TextMask = 'TextMask'
19
- RawData = 'RawData'
20
-
21
-
22
- def default_image_visualizer(data: npt.NDArray[np.float32]) -> LeapImage:
23
- rescaled_data = rescale_min_max(data[0])
24
- return LeapImage(rescaled_data)
25
-
26
-
27
- def default_video_visualizer(data: npt.NDArray[np.float32]) -> LeapVideo:
28
- return LeapVideo(data[0])
29
-
30
-
31
- def default_graph_visualizer(data: npt.NDArray[np.float32]) -> LeapGraph:
32
- return LeapGraph(data[0])
33
-
34
-
35
- def default_horizontal_bar_visualizer(data: npt.NDArray[np.float32]) -> LeapHorizontalBar:
36
- labels = [str(index) for index in range(data.shape[-1])]
37
- return LeapHorizontalBar(data[0], labels)
38
-
39
-
40
- def default_word_visualizer(data: npt.NDArray[np.float32]) -> LeapText:
41
- if len(data.shape) == 2 and data.shape[0] == 1:
42
- data = data[0]
43
- if hasattr(data, 'tolist'):
44
- data = data.tolist()
45
- words = [str(index[0]) if type(index) is list else str(index) for index in data]
46
- return LeapText(words)
47
-
48
-
49
- def default_raw_data_visualizer(data: npt.NDArray[np.float32]) -> LeapText:
50
- return LeapText([str(data)])
51
-
52
-
53
- def default_image_mask_visualizer(mask: npt.NDArray[np.float32], image: npt.NDArray[np.float32]) -> LeapImageMask:
54
- if image.shape[0] == 1:
55
- image = image[0]
56
- if mask.shape[0] == 1:
57
- mask = mask[0]
58
-
59
- n_different_labels = mask.shape[-1]
60
- labels = [str(i) for i in range(n_different_labels)]
61
-
62
- if len(mask.shape) > 2:
63
- if mask.shape[-1] == 1:
64
- mask = np.squeeze(mask, axis=-1)
65
- else:
66
- mask = np.argmax(mask, axis=-1)
67
-
68
- return LeapImageMask(mask.astype(np.uint8), image.astype(np.float32), labels)
69
-
70
-
71
- def default_text_mask_visualizer(mask: npt.NDArray[np.float32], text_data: npt.NDArray[np.float32]) -> LeapTextMask:
72
- mask = mask[0]
73
- text_data = text_data[0]
74
-
75
- words = default_word_visualizer(text_data).data
76
- n_different_labels = mask.shape[-1]
77
- labels = [str(i) for i in range(n_different_labels)]
78
-
79
- if len(mask.shape) > 1:
80
- if mask.shape[-1] == 1:
81
- mask = np.squeeze(mask, axis=-1)
82
- else:
83
- mask = np.argmax(mask, axis=-1)
84
-
85
- return LeapTextMask(mask.astype(np.uint8), words, labels)