chugr 0.2.0.dev0__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.
Files changed (53) hide show
  1. chugr/__init__.py +46 -0
  2. chugr/app/test.py +105 -0
  3. chugr/common/__init__.py +9 -0
  4. chugr/common/cli.py +234 -0
  5. chugr/common/collate.py +104 -0
  6. chugr/common/config.py +335 -0
  7. chugr/common/random.py +76 -0
  8. chugr/common/serialize.py +150 -0
  9. chugr/common/task_config.py +24 -0
  10. chugr/common/types.py +122 -0
  11. chugr/common/urls.py +88 -0
  12. chugr/doc/__init__.py +17 -0
  13. chugr/doc/constants.py +15 -0
  14. chugr/doc/doc_processor.py +248 -0
  15. chugr/doc/doc_read_processor.py +249 -0
  16. chugr/doc/doc_vqa_processor.py +113 -0
  17. chugr/doc/synth_text.py +212 -0
  18. chugr/hfds/__init__.py +2 -0
  19. chugr/hfds/collate.py +83 -0
  20. chugr/hfds/loader.py +385 -0
  21. chugr/hfds/wrappers.py +108 -0
  22. chugr/image/__init__.py +4 -0
  23. chugr/image/build_transforms_doc.py +427 -0
  24. chugr/image/build_transforms_image.py +203 -0
  25. chugr/image/transforms_alb.py +112 -0
  26. chugr/image/transforms_factory.py +58 -0
  27. chugr/image/transforms_torch.py +137 -0
  28. chugr/loader.py +249 -0
  29. chugr/naflex.py +357 -0
  30. chugr/task_pipeline/__init__.py +7 -0
  31. chugr/task_pipeline/pipeline_doc_read.py +66 -0
  32. chugr/task_pipeline/pipeline_doc_vqa.py +124 -0
  33. chugr/task_pipeline/pipeline_factory.py +32 -0
  34. chugr/task_pipeline/pipeline_gtparse.py +93 -0
  35. chugr/task_pipeline/pipeline_image_text.py +90 -0
  36. chugr/task_pipeline/pipeline_manual.py +27 -0
  37. chugr/text/__init__.py +1 -0
  38. chugr/text/tokenization.py +222 -0
  39. chugr/version.py +1 -0
  40. chugr/wds/__init__.py +7 -0
  41. chugr/wds/dataset_info.py +65 -0
  42. chugr/wds/decode.py +319 -0
  43. chugr/wds/filters.py +142 -0
  44. chugr/wds/helpers.py +68 -0
  45. chugr/wds/loader.py +187 -0
  46. chugr/wds/pipeline.py +182 -0
  47. chugr/wds/shardlists.py +134 -0
  48. chugr/wds/tariterators.py +70 -0
  49. chugr-0.2.0.dev0.dist-info/METADATA +436 -0
  50. chugr-0.2.0.dev0.dist-info/RECORD +53 -0
  51. chugr-0.2.0.dev0.dist-info/WHEEL +4 -0
  52. chugr-0.2.0.dev0.dist-info/entry_points.txt +4 -0
  53. chugr-0.2.0.dev0.dist-info/licenses/LICENSE +201 -0
chugr/common/config.py ADDED
@@ -0,0 +1,335 @@
1
+ from dataclasses import dataclass, field, fields, replace
2
+ from typing import Any, Dict, List, Optional, Tuple, Union, Sequence, Callable
3
+
4
+ from .serialize import Serializable
5
+
6
+ from .types import ImageFeatureInfo, ShardSpec, SourceSpec
7
+ from .urls import expand_urls
8
+
9
+
10
+ def image_mode_to_chs(fmt: str):
11
+ if fmt is None:
12
+ return None
13
+ assert fmt in ('L', 'RGB') # could support more...
14
+ return 1 if fmt == 'L' else 3
15
+
16
+
17
+ @dataclass
18
+ class ImageInputCfg(Serializable):
19
+ size: Optional[Tuple[int, int]] = (512, 512)
20
+ mode: Optional[str] = 'L'
21
+ mean: Optional[Union[float, Tuple[float, ...]]] = 0.5
22
+ std: Optional[Union[float, Tuple[float, ...]]] = 0.5
23
+ interpolation: Optional[str] = 'bicubic'
24
+ fill_color: Optional[Union[int, Tuple[int, ...]]] = 255
25
+ crop_margin: Optional[bool] = False
26
+ align_long_axis: Optional[bool] = False
27
+ transform_type: Optional[str] = 'image_basic'
28
+ resize_mode: Optional[str] = 'shortest'
29
+ patch_size: Optional[int] = None # patch size for naflex (variable aspect / resolution) transforms
30
+ max_seq_len: Optional[int] = None # max patch sequence length for naflex transforms
31
+
32
+ @property
33
+ def image_chs(self):
34
+ return image_mode_to_chs(self.mode)
35
+
36
+ def __post_init__(self):
37
+ image_chs = self.image_chs
38
+ if image_chs is not None:
39
+ # ensue mean/std attributes match # image_chs
40
+ for attr_name in ('mean', 'std'):
41
+ attr = getattr(self, attr_name)
42
+ if attr is not None and not isinstance(attr, Sequence):
43
+ attr = (attr,)
44
+ if image_chs == 1 and len(attr) > image_chs:
45
+ attr = (sum(attr) / len(attr),)
46
+ if image_chs > 1 and len(attr) == 1:
47
+ attr = attr * image_chs
48
+ assert len(attr) == image_chs
49
+ setattr(self, attr_name, attr)
50
+
51
+ # ensure fill color matches image_chs
52
+ if self.fill_color is not None:
53
+ if not isinstance(self.fill_color, Sequence):
54
+ self.fill_color = (self.fill_color,)
55
+ if image_chs == 1 and len(self.fill_color) > image_chs:
56
+ self.fill_color = (int(sum(self.fill_color) / len(self.fill_color)),)
57
+ if image_chs > 1 and len(self.fill_color) == 1:
58
+ self.fill_color = self.fill_color * image_chs
59
+
60
+ @classmethod
61
+ def empty(cls):
62
+ return cls(**{f.name: None for f in fields(cls)})
63
+
64
+ def set_default(self, right, inplace=False):
65
+ # set left fields from right fields if left fields are not-initialized (None)
66
+ changes = {
67
+ f.name: v for f in fields(self)
68
+ if (v := getattr(right, f.name)) is not None and getattr(self, f.name) is None
69
+ }
70
+ if inplace:
71
+ for k, v in changes.items():
72
+ setattr(self, k, v)
73
+ return self
74
+ else:
75
+ return replace(self, **changes)
76
+
77
+ def merge(self, right, inplace=False):
78
+ # merge from right to left for right fields that are not None
79
+ changes = {f.name: v for f in fields(self) if (v := getattr(right, f.name)) is not None}
80
+ if inplace:
81
+ for k, v in changes.items():
82
+ setattr(self, k, v)
83
+ return self
84
+ else:
85
+ return replace(self, **changes)
86
+
87
+
88
+ @dataclass
89
+ class ImageAugCfg(Serializable):
90
+ """
91
+ A simple flat config struct for overriding common augmentation defaults.
92
+
93
+ Each image transform type supports different augmentations and have their own defaults,
94
+ this struct is intended to override defaults for common values, not necessarily to
95
+ cover all cases and define all augmentation possibilities across all schemes.
96
+ """
97
+
98
+ # resize scale bounds (1.0 = middle point = same scale)
99
+ scale: Optional[Tuple[float, float]] = None
100
+
101
+ # resize aspect ratio bounds (1.0 = 1:1)
102
+ ratio: Optional[Tuple[float, float]] = None
103
+
104
+ # color jitter, per item probs
105
+ color_jitter: Optional[Union[float, Tuple[float, float, float], Tuple[float, float, float, float]]] = None
106
+
107
+ # for simclr, control prob for applying any of the jitter probs above
108
+ color_jitter_prob: Optional[float] = None
109
+
110
+ # for preprocess w/ grayscale (simclr), control prob of converting to graysacle
111
+ grayscale_prob: Optional[float] = None
112
+
113
+ gaussian_blur_prob: Optional[float] = None
114
+ gaussian_blur_kernel_size: Optional[int] = None
115
+
116
+ # probability of applying random-erasing (timm style aug)
117
+ re_prob: Optional[float] = None
118
+
119
+ # number of random-erasing blocks (timm style aug)
120
+ re_count: Optional[int] = None
121
+
122
+ @classmethod
123
+ def clip(cls, **kwargs):
124
+ aug = cls(
125
+ scale=(0.9, 1.0),
126
+ ratio=(0.75, 1. / 0.75),
127
+ )
128
+ aug = replace(aug, **kwargs)
129
+ return aug
130
+
131
+ @classmethod
132
+ def imagenet(cls, **kwargs):
133
+ aug = cls(
134
+ scale=(0.08, 1.0),
135
+ ratio=(0.75, 1. / 0.75),
136
+ color_jitter=(0.4, 0.4, 0.4),
137
+ )
138
+ aug = replace(aug, **kwargs)
139
+ return aug
140
+
141
+ @classmethod
142
+ def simclr(cls, **kwargs):
143
+ aug = cls(
144
+ scale=(0.08, 1.0),
145
+ ratio=(0.75, 1. / 0.75),
146
+ color_jitter=(0.4, 0.4, 0.4, 0.1),
147
+ color_jitter_prob=0.8,
148
+ grayscale_prob=0.2,
149
+ gaussian_blur_prob=0.5,
150
+ #gaussian_blur_kernel_size=23,
151
+ )
152
+ aug = replace(aug, **kwargs)
153
+ return aug
154
+
155
+
156
+ # Vision preprocessing config
157
+ @dataclass
158
+ class PreprocessCfg(Serializable):
159
+ image_input: ImageInputCfg = field(default_factory=ImageInputCfg)
160
+ aug_cfg: Optional[ImageAugCfg] = None
161
+
162
+
163
+ @dataclass
164
+ class DataArg(Serializable):
165
+ """ Data source argument in an argument friendly form (multiple sources represented in string)
166
+ """
167
+ source: str
168
+ split: Optional[str] = None
169
+ sampling_weight: Optional[str] = None
170
+ template: Optional[str] = None # template to transform url for use
171
+ num_samples: Optional[Union[int, str]] = None
172
+ data_dir: Optional[str] = None
173
+
174
+ batch_size: int = 1
175
+ format: str = "wds" # e.g. "hfds", "hfids", or "wds"
176
+
177
+ resampled: bool = False # sample shards with replacement
178
+ multi_interval: bool = True
179
+ persistent_workers: bool = True
180
+ num_workers: int = 4
181
+
182
+ # NaFlex schedule driven batching (variable resolution buckets w/ token budget batch sizes), enabled when
183
+ # naflex_seq_lens is set (batch_size is then ignored for training).
184
+ naflex_seq_lens: Optional[List[int]] = None
185
+ naflex_seq_len_probs: Optional[List[float]] = None
186
+ naflex_max_tokens: Optional[int] = None # tokens (patches + text) per batch per rank
187
+ naflex_text_tokens: int = 0 # per row text token cost added when sizing batches
188
+ naflex_batch_divisor: int = 8
189
+ length_bucketing: bool = False # pool sort samples by annotation text length
190
+
191
+
192
+ def split_sources(
193
+ source: str,
194
+ split: Optional[str] = None,
195
+ sampling_weights: Optional[Union[str, List[float]]] = None,
196
+ num_samples: Optional[Union[int, str, List[int]]] = None,
197
+ ):
198
+ if '::' in source:
199
+ source_split = source.split('::')
200
+ else:
201
+ source_split = [source]
202
+ num_sources = len(source_split)
203
+
204
+ if sampling_weights is not None:
205
+ if isinstance(sampling_weights, str):
206
+ weights_split = sampling_weights.split('::')
207
+ sampling_weights = [float(w) for w in weights_split]
208
+ assert len(sampling_weights) == num_sources
209
+
210
+ num_samples_per_source = None
211
+ if num_samples is not None:
212
+ if isinstance(num_samples, str):
213
+ num_samples_split = num_samples.split('::')
214
+ num_samples = [int(s) for s in num_samples_split]
215
+
216
+ try:
217
+ len(num_samples)
218
+ except Exception:
219
+ num_samples_per_source = [None] * num_sources
220
+ else:
221
+ num_samples_per_source = num_samples
222
+ num_samples = sum(num_samples_per_source)
223
+ finally:
224
+ assert len(num_samples_per_source) == num_sources
225
+
226
+ output = []
227
+ for i, s in enumerate(source_split):
228
+ output.append(SourceSpec(
229
+ url=s,
230
+ split=split,
231
+ sampling_weight=None if sampling_weights is None else sampling_weights[i],
232
+ num_samples=None if num_samples_per_source is None else num_samples_per_source[i],
233
+ ))
234
+
235
+ return output, num_samples
236
+
237
+
238
+ # FIXME add code to resolve shard information from _info.yaml or .json files (see dataset_info.py)
239
+
240
+
241
+ def source_to_shard_spec(
242
+ source: Union[str, SourceSpec, List[SourceSpec]],
243
+ ):
244
+ if isinstance(source, str):
245
+ source_list = [SourceSpec(url=source)]
246
+ elif isinstance(source, SourceSpec):
247
+ source_list = [source]
248
+ else:
249
+ assert isinstance(source[0], SourceSpec)
250
+ source_list = source
251
+
252
+ # process weights first in case some are set and some are not
253
+ if not all(s.sampling_weight is None for s in source_list):
254
+ weights = [s.sampling_weight if s.sampling_weight else 1.0 for s in source_list]
255
+ else:
256
+ weights = [None] * len(source_list)
257
+
258
+ all_urls = []
259
+ all_weights = []
260
+ for s, w in zip(source_list, weights):
261
+ expanded_urls, expanded_weights = expand_urls(s.url, weights=w)
262
+ all_urls.extend(expanded_urls)
263
+ if expanded_weights:
264
+ all_weights.extend(expanded_weights)
265
+ all_weights = all_weights or None
266
+ sizes = None # FIXME resolve sizes
267
+
268
+ ss = ShardSpec(urls=all_urls, weights=all_weights, sizes=sizes)
269
+ return ss
270
+
271
+
272
+ @dataclass
273
+ class DataCfg(Serializable):
274
+ source: Union[str, SourceSpec, List[SourceSpec]]
275
+ split: Optional[str] = None
276
+ num_samples: Optional[int] = None # overrides num_samples across sources if set
277
+ data_dir: Optional[str] = None
278
+
279
+ batch_size: Optional[int] = 1
280
+ format: str = "wds" # e.g. "hfds", "hfids", or "wds".
281
+
282
+ resampled: bool = False # sample shards with replacement
283
+ multi_interval: bool = True
284
+ persistent_workers: bool = True
285
+ num_workers: int = 4
286
+
287
+ # NaFlex schedule driven batching (variable resolution buckets w/ token budget batch sizes), enabled when
288
+ # naflex_seq_lens is set (batch_size is then ignored for training).
289
+ naflex_seq_lens: Optional[List[int]] = None
290
+ naflex_seq_len_probs: Optional[List[float]] = None
291
+ naflex_max_tokens: Optional[int] = None # tokens (patches + text) per batch per rank
292
+ naflex_text_tokens: int = 0 # per row text token cost added when sizing batches
293
+ naflex_batch_divisor: int = 8
294
+ length_bucketing: bool = False # pool sort samples by annotation text length
295
+
296
+ @classmethod
297
+ def from_arg(cls, data_arg: DataArg):
298
+ sources, num_samples = split_sources(
299
+ data_arg.source,
300
+ split=data_arg.split,
301
+ sampling_weights=data_arg.sampling_weight,
302
+ num_samples=data_arg.num_samples,
303
+ )
304
+ return cls(
305
+ source=sources if len(sources) > 1 else sources[0],
306
+ split=data_arg.split,
307
+ num_samples=num_samples,
308
+ data_dir=data_arg.data_dir,
309
+ batch_size=data_arg.batch_size,
310
+ format=data_arg.format,
311
+ resampled=data_arg.resampled,
312
+ multi_interval=data_arg.multi_interval,
313
+ persistent_workers=data_arg.persistent_workers,
314
+ num_workers=data_arg.num_workers,
315
+ naflex_seq_lens=data_arg.naflex_seq_lens,
316
+ naflex_seq_len_probs=data_arg.naflex_seq_len_probs,
317
+ naflex_max_tokens=data_arg.naflex_max_tokens,
318
+ naflex_text_tokens=data_arg.naflex_text_tokens,
319
+ naflex_batch_divisor=data_arg.naflex_batch_divisor,
320
+ length_bucketing=data_arg.length_bucketing,
321
+ )
322
+
323
+ @property
324
+ def shard_spec(self):
325
+ return source_to_shard_spec(self.source)
326
+
327
+ def __post_init__(self):
328
+ if self.num_workers == 0:
329
+ self.persistent_workers = False
330
+
331
+ @dataclass
332
+ class DistributedCfg:
333
+ world_size: int = 1
334
+ local_rank: int = 0
335
+ global_rank: int = 0
chugr/common/random.py ADDED
@@ -0,0 +1,76 @@
1
+ import random
2
+
3
+ import numpy as np
4
+
5
+ from .types import SharedCount
6
+
7
+
8
+ _RANK_SEED_STRIDE = 1000003
9
+
10
+
11
+ def rank_seed(seed: int, rank: int = 0) -> int:
12
+ """ Seed for per rank randomness (process RNGs, dataloader worker seeds).
13
+
14
+ Ranks are spaced by a large stride (vs seed + rank) so runs w/ nearby seeds don't share random streams
15
+ (seed 42 rank 1 == seed 43 rank 0), rank 0 keeps `seed` so single process runs are unchanged.
16
+ """
17
+ return seed + _RANK_SEED_STRIDE * rank
18
+
19
+
20
+ def seed_worker(worker_id):
21
+ import torch
22
+ worker_seed = torch.initial_seed()
23
+ random.seed(worker_seed)
24
+ np.random.seed(worker_seed % 2**32)
25
+
26
+
27
+ def get_pytorch_worker_seed(increment=0, initial_seed=None):
28
+ """get dataloader worker seed from pytorch
29
+ """
30
+ from torch.utils.data import get_worker_info
31
+
32
+ increment_value = increment.get_value() if isinstance(increment, SharedCount) else increment
33
+ worker_info = get_worker_info()
34
+ if worker_info is not None:
35
+ # favour using the seed already created for pytorch dataloader workers if it exists
36
+ seed = worker_info.seed
37
+ num_workers = worker_info.num_workers
38
+ if increment_value:
39
+ # space out seed increments so they can't overlap across workers in different iterations
40
+ seed += increment_value * max(1, num_workers)
41
+ else:
42
+ # a fallback when no dataloader workers are present (num_workers=0)
43
+ import torch
44
+
45
+ if initial_seed is None:
46
+ initial_seed = torch.initial_seed()
47
+
48
+ # generate seed from initial via torch.Generator so it matches DL worker seeds
49
+ seed = torch.empty((), dtype=torch.int64).random_(
50
+ generator=torch.Generator().manual_seed(initial_seed)).item()
51
+
52
+ if increment_value:
53
+ seed += increment_value
54
+
55
+ return seed
56
+
57
+
58
+ def shuffled_interval_indices(dataset_len: int, num_indices: int, interval: int = 0, seed: int = 0):
59
+ """ Shuffled dataset indices for one interval of a map-style dataset (all ranks, split w/ [rank::world_size]).
60
+
61
+ Each pass over the dataset is a fresh permutation (seed + pass), intervals consume the permutations
62
+ sequentially (interval i starts where i - 1 ended), so intervals shorter than the dataset still cycle through
63
+ every sample before repeating. Identical across ranks / workers for the same args.
64
+ """
65
+ import torch
66
+ assert dataset_len > 0, 'dataset is empty'
67
+ epoch, offset = divmod(interval * num_indices, dataset_len)
68
+ indices = []
69
+ while len(indices) < num_indices:
70
+ g = torch.Generator()
71
+ g.manual_seed(seed + epoch)
72
+ perm = torch.randperm(dataset_len, generator=g).tolist()
73
+ indices.extend(perm[offset:])
74
+ offset = 0
75
+ epoch += 1
76
+ return indices[:num_indices]
@@ -0,0 +1,150 @@
1
+ """ Minimal dataclass (de)serialization
2
+
3
+ A small replacement for the subset of `simple_parsing.helpers.Serializable` used by chugr & pixparse.
4
+ Supports nested dataclasses, Optional / Union / Tuple / List / Dict fields, and json or yaml files.
5
+ """
6
+ import dataclasses
7
+ import json
8
+ import types
9
+ import typing
10
+ from pathlib import Path
11
+ from typing import Any, Dict, Type, TypeVar, Union
12
+
13
+ T = TypeVar('T')
14
+
15
+ _NONE_TYPE = type(None)
16
+
17
+
18
+ def _is_union(tp) -> bool:
19
+ origin = typing.get_origin(tp)
20
+ return origin is Union or origin is types.UnionType
21
+
22
+
23
+ def _unwrap_optional(tp):
24
+ """ Optional[X] -> X, leave other unions untouched """
25
+ if _is_union(tp):
26
+ args = [a for a in typing.get_args(tp) if a is not _NONE_TYPE]
27
+ if len(args) == 1:
28
+ return args[0]
29
+ return tp
30
+
31
+
32
+ def resolve_field_types(cls) -> Dict[str, Any]:
33
+ try:
34
+ return typing.get_type_hints(cls)
35
+ except Exception:
36
+ return {f.name: f.type for f in dataclasses.fields(cls)}
37
+
38
+
39
+ def to_dict(obj) -> Any:
40
+ """ Recursively convert dataclasses (and containers of) to plain python types. """
41
+ if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
42
+ out = {}
43
+ for f in dataclasses.fields(obj):
44
+ if not f.init and not f.metadata.get('serialize', False):
45
+ continue
46
+ out[f.name] = to_dict(getattr(obj, f.name))
47
+ return out
48
+ if isinstance(obj, (list, tuple)):
49
+ return [to_dict(v) for v in obj]
50
+ if isinstance(obj, dict):
51
+ return {k: to_dict(v) for k, v in obj.items()}
52
+ if isinstance(obj, Path):
53
+ return str(obj)
54
+ return obj
55
+
56
+
57
+ def from_value(tp, value):
58
+ """ Convert a plain python value to the annotated type `tp`, recursing into dataclasses. """
59
+ if value is None:
60
+ return None
61
+ tp = _unwrap_optional(tp)
62
+
63
+ if dataclasses.is_dataclass(tp):
64
+ if isinstance(value, tp):
65
+ return value
66
+ assert isinstance(value, dict), f'Expected dict for {tp}, got {type(value)}'
67
+ return from_dict(tp, value)
68
+
69
+ if _is_union(tp):
70
+ # try dataclass members first when given a dict, otherwise pass through unchanged
71
+ if isinstance(value, dict):
72
+ for a in typing.get_args(tp):
73
+ if dataclasses.is_dataclass(a):
74
+ try:
75
+ return from_dict(a, value)
76
+ except (TypeError, AssertionError):
77
+ continue
78
+ if isinstance(value, list):
79
+ for a in typing.get_args(tp):
80
+ if typing.get_origin(a) in (list, tuple):
81
+ return from_value(a, value)
82
+ return value
83
+
84
+ origin = typing.get_origin(tp)
85
+ args = typing.get_args(tp)
86
+ if origin is tuple:
87
+ if not isinstance(value, (list, tuple)):
88
+ value = (value,)
89
+ if len(args) == 2 and args[1] is Ellipsis:
90
+ return tuple(from_value(args[0], v) for v in value)
91
+ if args and len(args) == len(value):
92
+ return tuple(from_value(a, v) for a, v in zip(args, value))
93
+ return tuple(value)
94
+ if origin is list:
95
+ item_tp = args[0] if args else Any
96
+ return [from_value(item_tp, v) for v in value]
97
+ if origin is dict:
98
+ val_tp = args[1] if len(args) == 2 else Any
99
+ return {k: from_value(val_tp, v) for k, v in value.items()}
100
+ if tp is float and isinstance(value, int) and not isinstance(value, bool):
101
+ return float(value)
102
+ return value
103
+
104
+
105
+ def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
106
+ field_types = resolve_field_types(cls)
107
+ init_fields = {f.name for f in dataclasses.fields(cls) if f.init}
108
+ unknown = set(data.keys()) - init_fields
109
+ if unknown:
110
+ raise TypeError(f'Unknown fields {sorted(unknown)} for {cls.__name__}.')
111
+ kwargs = {k: from_value(field_types[k], v) for k, v in data.items()}
112
+ return cls(**kwargs)
113
+
114
+
115
+ def _load_file(path: Union[str, Path]) -> Dict[str, Any]:
116
+ path = Path(path)
117
+ text = path.read_text()
118
+ if path.suffix in ('.yaml', '.yml'):
119
+ import yaml
120
+ return yaml.safe_load(text) or {}
121
+ return json.loads(text)
122
+
123
+
124
+ class Serializable:
125
+ """ Mixin for dataclasses w/ dict, json, and yaml (de)serialization. """
126
+
127
+ def to_dict(self) -> Dict[str, Any]:
128
+ return to_dict(self)
129
+
130
+ @classmethod
131
+ def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
132
+ return from_dict(cls, data)
133
+
134
+ @classmethod
135
+ def load(cls: Type[T], path: Union[str, Path]) -> T:
136
+ return from_dict(cls, _load_file(path))
137
+
138
+ def save(self, path: Union[str, Path]):
139
+ path = Path(path)
140
+ if path.suffix in ('.yaml', '.yml'):
141
+ path.write_text(self.dumps_yaml())
142
+ else:
143
+ path.write_text(self.dumps_json())
144
+
145
+ def dumps_json(self, indent: int = 2) -> str:
146
+ return json.dumps(self.to_dict(), indent=indent)
147
+
148
+ def dumps_yaml(self) -> str:
149
+ import yaml
150
+ return yaml.safe_dump(self.to_dict(), sort_keys=False)
@@ -0,0 +1,24 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
3
+
4
+
5
+ @dataclass
6
+ class DataTaskCfg:
7
+ """
8
+ Attributes:
9
+ ...
10
+ output_tuple: Output tuples instead of dicts from task pipeline.
11
+ filter_valid: Filter out invalid / incomplete samples.
12
+ flatten_json: Flatten json dicts into parent sample dict. Common to have in wds datasets.
13
+ error_handler: 'specifies which error (exception) handler should be used, 'ignore_and_continue'
14
+ is good for training, 'reraise_exception' for debugging purposes.
15
+ """
16
+ decode_fn: Optional[Callable] = None
17
+ image_process_fn: Optional[Callable] = None
18
+ image_process_factory: Optional[Callable] = None # seq_len -> image process fn (NaFlex bucketed batching)
19
+ text_process_fn: Optional[Callable] = None
20
+ decode_and_process_fn: Optional[Callable] = None
21
+ output_tuple: bool = False # output features as tuple instead of dictionary
22
+ filter_valid: bool = False # enable filter to keep samples with valid key-values
23
+ flatten_json: bool = True # flatten nested 'json' dicts into parent sample
24
+ error_handler: str = 'reraise_exception'