sonusai 1.0.16__cp311-abi3-macosx_10_12_x86_64.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 (150) hide show
  1. sonusai/__init__.py +170 -0
  2. sonusai/aawscd_probwrite.py +148 -0
  3. sonusai/audiofe.py +481 -0
  4. sonusai/calc_metric_spenh.py +1136 -0
  5. sonusai/config/__init__.py +0 -0
  6. sonusai/config/asr.py +21 -0
  7. sonusai/config/config.py +65 -0
  8. sonusai/config/config.yml +49 -0
  9. sonusai/config/constants.py +53 -0
  10. sonusai/config/ir.py +124 -0
  11. sonusai/config/ir_delay.py +62 -0
  12. sonusai/config/source.py +275 -0
  13. sonusai/config/spectral_masks.py +15 -0
  14. sonusai/config/truth.py +64 -0
  15. sonusai/constants.py +14 -0
  16. sonusai/data/__init__.py +0 -0
  17. sonusai/data/silero_vad_v5.1.jit +0 -0
  18. sonusai/data/silero_vad_v5.1.onnx +0 -0
  19. sonusai/data/speech_ma01_01.wav +0 -0
  20. sonusai/data/whitenoise.wav +0 -0
  21. sonusai/datatypes.py +383 -0
  22. sonusai/deprecated/gentcst.py +632 -0
  23. sonusai/deprecated/plot.py +519 -0
  24. sonusai/deprecated/tplot.py +365 -0
  25. sonusai/doc.py +52 -0
  26. sonusai/doc_strings/__init__.py +1 -0
  27. sonusai/doc_strings/doc_strings.py +531 -0
  28. sonusai/genft.py +196 -0
  29. sonusai/genmetrics.py +183 -0
  30. sonusai/genmix.py +199 -0
  31. sonusai/genmixdb.py +235 -0
  32. sonusai/ir_metric.py +551 -0
  33. sonusai/lsdb.py +141 -0
  34. sonusai/main.py +134 -0
  35. sonusai/metrics/__init__.py +43 -0
  36. sonusai/metrics/calc_audio_stats.py +42 -0
  37. sonusai/metrics/calc_class_weights.py +90 -0
  38. sonusai/metrics/calc_optimal_thresholds.py +73 -0
  39. sonusai/metrics/calc_pcm.py +45 -0
  40. sonusai/metrics/calc_pesq.py +36 -0
  41. sonusai/metrics/calc_phase_distance.py +43 -0
  42. sonusai/metrics/calc_sa_sdr.py +64 -0
  43. sonusai/metrics/calc_sample_weights.py +25 -0
  44. sonusai/metrics/calc_segsnr_f.py +82 -0
  45. sonusai/metrics/calc_speech.py +382 -0
  46. sonusai/metrics/calc_wer.py +71 -0
  47. sonusai/metrics/calc_wsdr.py +57 -0
  48. sonusai/metrics/calculate_metrics.py +395 -0
  49. sonusai/metrics/class_summary.py +74 -0
  50. sonusai/metrics/confusion_matrix_summary.py +75 -0
  51. sonusai/metrics/one_hot.py +283 -0
  52. sonusai/metrics/snr_summary.py +128 -0
  53. sonusai/metrics_summary.py +314 -0
  54. sonusai/mixture/__init__.py +15 -0
  55. sonusai/mixture/audio.py +187 -0
  56. sonusai/mixture/class_balancing.py +103 -0
  57. sonusai/mixture/constants.py +3 -0
  58. sonusai/mixture/data_io.py +173 -0
  59. sonusai/mixture/db.py +169 -0
  60. sonusai/mixture/db_datatypes.py +92 -0
  61. sonusai/mixture/effects.py +344 -0
  62. sonusai/mixture/feature.py +78 -0
  63. sonusai/mixture/generation.py +1116 -0
  64. sonusai/mixture/helpers.py +351 -0
  65. sonusai/mixture/ir_effects.py +77 -0
  66. sonusai/mixture/log_duration_and_sizes.py +23 -0
  67. sonusai/mixture/mixdb.py +1857 -0
  68. sonusai/mixture/pad_audio.py +35 -0
  69. sonusai/mixture/resample.py +7 -0
  70. sonusai/mixture/sox_effects.py +195 -0
  71. sonusai/mixture/sox_help.py +650 -0
  72. sonusai/mixture/spectral_mask.py +51 -0
  73. sonusai/mixture/truth.py +61 -0
  74. sonusai/mixture/truth_functions/__init__.py +45 -0
  75. sonusai/mixture/truth_functions/crm.py +105 -0
  76. sonusai/mixture/truth_functions/energy.py +222 -0
  77. sonusai/mixture/truth_functions/file.py +48 -0
  78. sonusai/mixture/truth_functions/metadata.py +24 -0
  79. sonusai/mixture/truth_functions/metrics.py +28 -0
  80. sonusai/mixture/truth_functions/phoneme.py +18 -0
  81. sonusai/mixture/truth_functions/sed.py +98 -0
  82. sonusai/mixture/truth_functions/target.py +142 -0
  83. sonusai/mkwav.py +135 -0
  84. sonusai/onnx_predict.py +363 -0
  85. sonusai/parse/__init__.py +0 -0
  86. sonusai/parse/expand.py +156 -0
  87. sonusai/parse/parse_source_directive.py +129 -0
  88. sonusai/parse/rand.py +214 -0
  89. sonusai/py.typed +0 -0
  90. sonusai/queries/__init__.py +0 -0
  91. sonusai/queries/queries.py +239 -0
  92. sonusai/rs.abi3.so +0 -0
  93. sonusai/rs.pyi +1 -0
  94. sonusai/rust/__init__.py +0 -0
  95. sonusai/speech/__init__.py +0 -0
  96. sonusai/speech/l2arctic.py +121 -0
  97. sonusai/speech/librispeech.py +102 -0
  98. sonusai/speech/mcgill.py +71 -0
  99. sonusai/speech/textgrid.py +89 -0
  100. sonusai/speech/timit.py +138 -0
  101. sonusai/speech/types.py +12 -0
  102. sonusai/speech/vctk.py +53 -0
  103. sonusai/speech/voxceleb.py +108 -0
  104. sonusai/utils/__init__.py +3 -0
  105. sonusai/utils/asl_p56.py +130 -0
  106. sonusai/utils/asr.py +91 -0
  107. sonusai/utils/asr_functions/__init__.py +3 -0
  108. sonusai/utils/asr_functions/aaware_whisper.py +69 -0
  109. sonusai/utils/audio_devices.py +50 -0
  110. sonusai/utils/braced_glob.py +50 -0
  111. sonusai/utils/calculate_input_shape.py +26 -0
  112. sonusai/utils/choice.py +51 -0
  113. sonusai/utils/compress.py +25 -0
  114. sonusai/utils/convert_string_to_number.py +6 -0
  115. sonusai/utils/create_timestamp.py +5 -0
  116. sonusai/utils/create_ts_name.py +14 -0
  117. sonusai/utils/dataclass_from_dict.py +27 -0
  118. sonusai/utils/db.py +16 -0
  119. sonusai/utils/docstring.py +53 -0
  120. sonusai/utils/energy_f.py +44 -0
  121. sonusai/utils/engineering_number.py +166 -0
  122. sonusai/utils/evaluate_random_rule.py +15 -0
  123. sonusai/utils/get_frames_per_batch.py +2 -0
  124. sonusai/utils/get_label_names.py +20 -0
  125. sonusai/utils/grouper.py +6 -0
  126. sonusai/utils/human_readable_size.py +7 -0
  127. sonusai/utils/keyboard_interrupt.py +12 -0
  128. sonusai/utils/load_object.py +21 -0
  129. sonusai/utils/max_text_width.py +9 -0
  130. sonusai/utils/model_utils.py +28 -0
  131. sonusai/utils/numeric_conversion.py +11 -0
  132. sonusai/utils/onnx_utils.py +155 -0
  133. sonusai/utils/parallel.py +162 -0
  134. sonusai/utils/path_info.py +7 -0
  135. sonusai/utils/print_mixture_details.py +60 -0
  136. sonusai/utils/rand.py +13 -0
  137. sonusai/utils/ranges.py +43 -0
  138. sonusai/utils/read_predict_data.py +32 -0
  139. sonusai/utils/reshape.py +154 -0
  140. sonusai/utils/seconds_to_hms.py +7 -0
  141. sonusai/utils/stacked_complex.py +82 -0
  142. sonusai/utils/stratified_shuffle_split.py +170 -0
  143. sonusai/utils/tokenized_shell_vars.py +143 -0
  144. sonusai/utils/write_audio.py +26 -0
  145. sonusai/utils/yes_or_no.py +8 -0
  146. sonusai/vars.py +47 -0
  147. sonusai-1.0.16.dist-info/METADATA +56 -0
  148. sonusai-1.0.16.dist-info/RECORD +150 -0
  149. sonusai-1.0.16.dist-info/WHEEL +4 -0
  150. sonusai-1.0.16.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,64 @@
1
+ from sonusai.datatypes import TruthParameter
2
+
3
+
4
+ def validate_truth_configs(given: dict) -> None:
5
+ """Validate fields in given 'truth_configs'
6
+
7
+ :param given: The dictionary of the given config
8
+ """
9
+ from copy import deepcopy
10
+
11
+ from ..mixture import truth_functions
12
+ from .constants import REQUIRED_TRUTH_CONFIGS
13
+
14
+ sources = given["sources"]
15
+
16
+ for category, source in sources.items():
17
+ if "truth_configs" not in source:
18
+ continue
19
+
20
+ truth_configs = source["truth_configs"]
21
+ if len(truth_configs) == 0:
22
+ raise ValueError(f"'truth_configs' in config source '{category}' is empty")
23
+
24
+ for truth_name, truth_config in truth_configs.items():
25
+ for k in REQUIRED_TRUTH_CONFIGS:
26
+ if k not in truth_config:
27
+ raise AttributeError(
28
+ f"'{truth_name}' in source '{category}' truth_configs is missing required '{k}'"
29
+ )
30
+
31
+ optional_config = deepcopy(truth_config)
32
+ for k in REQUIRED_TRUTH_CONFIGS:
33
+ del optional_config[k]
34
+
35
+ getattr(truth_functions, truth_config["function"] + "_validate")(optional_config)
36
+
37
+
38
+ def get_truth_parameters(config: dict) -> list[TruthParameter]:
39
+ """Get the list of truth parameters from a config
40
+
41
+ :param config: Config dictionary
42
+ :return: List of truth parameters
43
+ """
44
+ from copy import deepcopy
45
+
46
+ from ..mixture import truth_functions
47
+ from .constants import REQUIRED_TRUTH_CONFIGS
48
+
49
+ truth_parameters: list[TruthParameter] = []
50
+ for category, source_config in config["sources"].items():
51
+ if "truth_configs" in source_config:
52
+ for truth_name, truth_config in source_config["truth_configs"].items():
53
+ optional_config = deepcopy(truth_config)
54
+ for key in REQUIRED_TRUTH_CONFIGS:
55
+ del optional_config[key]
56
+
57
+ parameters = getattr(truth_functions, truth_config["function"] + "_parameters")(
58
+ config["feature"],
59
+ config["num_classes"],
60
+ optional_config,
61
+ )
62
+ truth_parameters.append(TruthParameter(category, truth_name, parameters))
63
+
64
+ return truth_parameters
sonusai/constants.py ADDED
@@ -0,0 +1,14 @@
1
+ from importlib.resources import as_file
2
+ from importlib.resources import files
3
+
4
+ SAMPLE_RATE = 16000
5
+ CHANNEL_COUNT = 1
6
+ BIT_DEPTH = 32
7
+ SAMPLE_BYTES = BIT_DEPTH // 8
8
+ FLOAT_BYTES = 4
9
+
10
+ with as_file(files("sonusai.data").joinpath("whitenoise.wav")) as path:
11
+ DEFAULT_NOISE = str(path)
12
+
13
+ with as_file(files("sonusai.data").joinpath("speech_ma01_01.wav")) as path:
14
+ DEFAULT_SPEECH = str(path)
File without changes
Binary file
Binary file
Binary file
Binary file
sonusai/datatypes.py ADDED
@@ -0,0 +1,383 @@
1
+ from collections.abc import Iterable
2
+ from dataclasses import dataclass
3
+ from dataclasses import field
4
+ from typing import Any
5
+ from typing import NamedTuple
6
+ from typing import SupportsIndex
7
+ from typing import TypeAlias
8
+
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+ from dataclasses_json import DataClassJsonMixin
12
+ from praatio.utilities.constants import Interval
13
+
14
+ AudioT: TypeAlias = npt.NDArray[np.float32]
15
+
16
+ Truth: TypeAlias = Any
17
+ TruthDict: TypeAlias = dict[str, Truth]
18
+ TruthsDict: TypeAlias = dict[str, TruthDict]
19
+ Segsnr: TypeAlias = npt.NDArray[np.float32]
20
+
21
+ AudioF: TypeAlias = npt.NDArray[np.complex64]
22
+
23
+ EnergyT: TypeAlias = npt.NDArray[np.float32]
24
+ EnergyF: TypeAlias = npt.NDArray[np.float32]
25
+
26
+ Feature: TypeAlias = npt.NDArray[np.float32]
27
+
28
+ Predict: TypeAlias = npt.NDArray[np.float32]
29
+
30
+ # Json type defined to maintain compatibility with DataClassJsonMixin
31
+ Json: TypeAlias = dict | list | str | int | float | bool | None
32
+
33
+
34
+ class DataClassSonusAIMixin(DataClassJsonMixin):
35
+ def __str__(self):
36
+ return f"{self.to_dict()}"
37
+
38
+ # Override DataClassJsonMixin to remove dictionary keys with values of None
39
+ def to_dict(self, encode_json=False) -> dict[str, Json]:
40
+ def del_none(d):
41
+ if isinstance(d, dict):
42
+ for key, value in list(d.items()):
43
+ if value is None:
44
+ del d[key]
45
+ elif isinstance(value, dict):
46
+ del_none(value)
47
+ elif isinstance(value, list):
48
+ for item in value:
49
+ del_none(item)
50
+ elif isinstance(d, list):
51
+ for item in d:
52
+ del_none(item)
53
+ return d
54
+
55
+ return del_none(super().to_dict(encode_json))
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class TruthConfig(DataClassSonusAIMixin):
60
+ function: str
61
+ stride_reduction: str
62
+ config: dict = field(default_factory=dict)
63
+
64
+ def __hash__(self):
65
+ return hash(self.to_json())
66
+
67
+ def __eq__(self, other):
68
+ return isinstance(other, TruthConfig) and hash(self) == hash(other)
69
+
70
+
71
+ TruthConfigs: TypeAlias = dict[str, TruthConfig]
72
+ TruthsConfigs: TypeAlias = dict[str, TruthConfigs]
73
+
74
+
75
+ NumberStr: TypeAlias = float | int | str
76
+ OptionalNumberStr: TypeAlias = NumberStr | None
77
+ OptionalListNumberStr: TypeAlias = list[NumberStr] | None
78
+
79
+
80
+ EffectList: TypeAlias = list[str]
81
+
82
+
83
+ @dataclass
84
+ class Effects(DataClassSonusAIMixin):
85
+ pre: EffectList
86
+ post: EffectList = field(default_factory=EffectList)
87
+
88
+
89
+ class UniversalSNRGenerator:
90
+ def __init__(self, raw_value: float | str) -> None:
91
+ self._raw_value = str(raw_value)
92
+ self.is_random = isinstance(raw_value, str) and raw_value.startswith("rand")
93
+
94
+ @property
95
+ def value(self) -> float:
96
+ from sonusai.parse.rand import rand
97
+
98
+ if self.is_random:
99
+ return float(rand(self._raw_value))
100
+ return float(self._raw_value)
101
+
102
+
103
+ class UniversalSNR(float):
104
+ def __new__(cls, value: float, is_random: bool = False):
105
+ return float.__new__(cls, value)
106
+
107
+ def __init__(self, value: float, is_random: bool = False) -> None:
108
+ float.__init__(value)
109
+ self._is_random = bool(is_random)
110
+
111
+ @property
112
+ def is_random(self) -> bool:
113
+ return self._is_random
114
+
115
+
116
+ Speaker: TypeAlias = dict[str, str]
117
+
118
+
119
+ @dataclass
120
+ class SourceFile(DataClassSonusAIMixin):
121
+ category: str
122
+ class_indices: list[int]
123
+ name: str
124
+ samples: int
125
+ truth_configs: TruthConfigs
126
+ class_balancing_effect: EffectList | None = None
127
+ id: int = -1
128
+ level_type: str | None = None
129
+ speaker_id: int | None = None
130
+
131
+ @property
132
+ def duration(self) -> float:
133
+ from .constants import SAMPLE_RATE
134
+
135
+ return self.samples / SAMPLE_RATE
136
+
137
+
138
+ @dataclass
139
+ class EffectedFile(DataClassSonusAIMixin):
140
+ file_id: int
141
+ effect_id: int
142
+
143
+
144
+ ClassCount: TypeAlias = list[int]
145
+
146
+ GeneralizedIDs: TypeAlias = str | int | list[int] | range
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class SpectralMask(DataClassSonusAIMixin):
151
+ f_max_width: int
152
+ f_num: int
153
+ t_max_width: int
154
+ t_num: int
155
+ t_max_percent: int
156
+
157
+
158
+ @dataclass(frozen=True)
159
+ class TruthParameter(DataClassSonusAIMixin):
160
+ category: str
161
+ name: str
162
+ parameters: int | None
163
+
164
+
165
+ @dataclass
166
+ class Source(DataClassSonusAIMixin):
167
+ effects: Effects
168
+ file_id: int
169
+ pre_tempo: float = 1
170
+ loop: bool = False
171
+ snr: UniversalSNR = field(default_factory=lambda: UniversalSNR(0))
172
+ snr_gain: float = 0
173
+ start: int = 0
174
+
175
+
176
+ Sources: TypeAlias = dict[str, Source]
177
+ SourcesAudioT: TypeAlias = dict[str, AudioT]
178
+ SourcesAudioF: TypeAlias = dict[str, AudioF]
179
+
180
+
181
+ @dataclass
182
+ class Mixture(DataClassSonusAIMixin):
183
+ name: str
184
+ samples: int
185
+ all_sources: Sources
186
+ spectral_mask_id: int
187
+ spectral_mask_seed: int
188
+
189
+ @property
190
+ def all_source_ids(self) -> dict[str, int]:
191
+ return {category: source.file_id for category, source in self.all_sources.items()}
192
+
193
+ @property
194
+ def sources(self) -> Sources:
195
+ return {category: source for category, source in self.all_sources.items() if category != "noise"}
196
+
197
+ @property
198
+ def source_ids(self) -> dict[str, int]:
199
+ return {category: source.file_id for category, source in self.sources.items()}
200
+
201
+ @property
202
+ def noise(self) -> Source:
203
+ return self.all_sources["noise"]
204
+
205
+ @property
206
+ def noise_id(self) -> int:
207
+ return self.noise.file_id
208
+
209
+ @property
210
+ def source_effects(self) -> dict[str, Effects]:
211
+ return {category: source.effects for category, source in self.sources.items()}
212
+
213
+ @property
214
+ def noise_effects(self) -> Effects:
215
+ return self.noise.effects
216
+
217
+ @property
218
+ def is_noise_only(self) -> bool:
219
+ return self.noise.snr < -96
220
+
221
+ @property
222
+ def is_source_only(self) -> bool:
223
+ return self.noise.snr > 96
224
+
225
+
226
+ @dataclass(frozen=True)
227
+ class TransformConfig:
228
+ length: int
229
+ overlap: int
230
+ bin_start: int
231
+ bin_end: int
232
+ ttype: str
233
+
234
+
235
+ @dataclass(frozen=True)
236
+ class FeatureGeneratorConfig:
237
+ feature_mode: str
238
+ truth_parameters: dict[str, dict[str, int | None]]
239
+
240
+
241
+ @dataclass(frozen=True)
242
+ class FeatureGeneratorInfo:
243
+ decimation: int
244
+ stride: int
245
+ step: int
246
+ feature_parameters: int
247
+ ft_config: TransformConfig
248
+ eft_config: TransformConfig
249
+ it_config: TransformConfig
250
+
251
+
252
+ ASRConfigs: TypeAlias = dict[str, dict[str, Any]]
253
+
254
+
255
+ @dataclass
256
+ class GenMixData:
257
+ mixture: AudioT | None = None
258
+ truth_t: TruthsDict | None = None
259
+ segsnr_t: Segsnr | None = None
260
+ sources: SourcesAudioT | None = None
261
+ source: AudioT | None = None
262
+ noise: AudioT | None = None
263
+
264
+
265
+ @dataclass
266
+ class GenFTData:
267
+ feature: Feature | None = None
268
+ truth_f: TruthsDict | None = None
269
+ segsnr: Segsnr | None = None
270
+
271
+
272
+ @dataclass
273
+ class ImpulseResponseData:
274
+ data: AudioT
275
+ sample_rate: int
276
+ delay: int
277
+
278
+
279
+ @dataclass
280
+ class ImpulseResponseFile(DataClassSonusAIMixin):
281
+ name: str
282
+ tags: list[str]
283
+ delay: str | int = "auto"
284
+
285
+
286
+ @dataclass
287
+ class MixtureDatabaseConfig(DataClassSonusAIMixin):
288
+ asr_configs: ASRConfigs
289
+ class_balancing: bool
290
+ class_labels: list[str]
291
+ class_weights_threshold: list[float]
292
+ feature: str
293
+ ir_files: list[ImpulseResponseFile]
294
+ mixtures: list[Mixture]
295
+ num_classes: int
296
+ source_files: dict[str, list[SourceFile]]
297
+ spectral_masks: list[SpectralMask]
298
+
299
+
300
+ SpeechMetadata: TypeAlias = str | list[Interval] | None
301
+
302
+
303
+ class SnrFMetrics(NamedTuple):
304
+ avg: float | None = None
305
+ std: float | None = None
306
+ db_avg: float | None = None
307
+ db_std: float | None = None
308
+
309
+
310
+ class SnrFBinMetrics(NamedTuple):
311
+ avg: np.ndarray | None = None
312
+ std: np.ndarray | None = None
313
+ db_avg: np.ndarray | None = None
314
+ db_std: np.ndarray | None = None
315
+
316
+
317
+ class SpeechMetrics(NamedTuple):
318
+ csig: float | None = None
319
+ cbak: float | None = None
320
+ covl: float | None = None
321
+
322
+
323
+ class AudioStatsMetrics(NamedTuple):
324
+ dco: float | None = None
325
+ min: float | None = None
326
+ max: float | None = None
327
+ pkdb: float | None = None
328
+ lrms: float | None = None
329
+ pkr: float | None = None
330
+ tr: float | None = None
331
+ cr: float | None = None
332
+ fl: float | None = None
333
+ pkc: float | None = None
334
+
335
+
336
+ @dataclass
337
+ class MetricDoc:
338
+ category: str
339
+ name: str
340
+ description: str
341
+
342
+
343
+ class MetricDocs(list[MetricDoc]):
344
+ def __init__(self, __iterable: Iterable[MetricDoc]) -> None:
345
+ super().__init__(item for item in __iterable)
346
+
347
+ def __setitem__(self, __key: SupportsIndex, __value: MetricDoc) -> None: # type: ignore[override]
348
+ super().__setitem__(__key, __value)
349
+
350
+ def insert(self, __index: SupportsIndex, __object: MetricDoc) -> None:
351
+ super().insert(__index, __object)
352
+
353
+ def append(self, __object: MetricDoc) -> None:
354
+ super().append(__object)
355
+
356
+ def extend(self, __iterable: Iterable[MetricDoc]) -> None:
357
+ if isinstance(__iterable, type(self)):
358
+ super().extend(__iterable)
359
+ else:
360
+ super().extend(item for item in __iterable)
361
+
362
+ @property
363
+ def pretty(self) -> str:
364
+ max_category_len = ((max([len(item.category) for item in self]) + 9) // 10) * 10
365
+ max_name_len = 2 + ((max([len(item.name) for item in self]) + 1) // 2) * 2
366
+ categories: list[str] = []
367
+ for item in self:
368
+ if item.category not in categories:
369
+ categories.append(item.category)
370
+
371
+ result = ""
372
+ for category in categories:
373
+ result += f"{category}\n"
374
+ result += "-" * max_category_len + "\n"
375
+ for item in [sub for sub in self if sub.category == category]:
376
+ result += f" {item.name:<{max_name_len}}{item.description}\n"
377
+ result += "\n"
378
+
379
+ return result
380
+
381
+ @property
382
+ def names(self) -> set[str]:
383
+ return {item.name for item in self}