modusa 0.2.22__py3-none-any.whl → 0.3__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 (70) hide show
  1. modusa/.DS_Store +0 -0
  2. modusa/__init__.py +8 -1
  3. modusa/decorators.py +4 -4
  4. modusa/devtools/generate_docs_source.py +96 -0
  5. modusa/devtools/generate_template.py +13 -13
  6. modusa/devtools/main.py +4 -3
  7. modusa/devtools/templates/generator.py +1 -1
  8. modusa/devtools/templates/io.py +1 -1
  9. modusa/devtools/templates/{signal.py → model.py} +18 -11
  10. modusa/devtools/templates/plugin.py +1 -1
  11. modusa/devtools/templates/test.py +2 -3
  12. modusa/devtools/templates/{engine.py → tool.py} +3 -8
  13. modusa/generators/__init__.py +9 -1
  14. modusa/generators/audio.py +188 -0
  15. modusa/generators/audio_waveforms.py +22 -13
  16. modusa/generators/base.py +1 -1
  17. modusa/generators/ftds.py +298 -0
  18. modusa/generators/s1d.py +270 -0
  19. modusa/generators/s2d.py +300 -0
  20. modusa/generators/s_ax.py +102 -0
  21. modusa/generators/t_ax.py +64 -0
  22. modusa/generators/tds.py +267 -0
  23. modusa/main.py +0 -30
  24. modusa/models/__init__.py +14 -0
  25. modusa/models/__pycache__/signal1D.cpython-312.pyc.4443461152 +0 -0
  26. modusa/models/audio.py +90 -0
  27. modusa/models/base.py +70 -0
  28. modusa/models/data.py +457 -0
  29. modusa/models/ftds.py +584 -0
  30. modusa/models/s1d.py +578 -0
  31. modusa/models/s2d.py +619 -0
  32. modusa/models/s_ax.py +448 -0
  33. modusa/models/t_ax.py +335 -0
  34. modusa/models/tds.py +465 -0
  35. modusa/plugins/__init__.py +3 -1
  36. modusa/tmp.py +98 -0
  37. modusa/tools/__init__.py +7 -0
  38. modusa/tools/audio_converter.py +73 -0
  39. modusa/tools/audio_loader.py +90 -0
  40. modusa/tools/audio_player.py +89 -0
  41. modusa/tools/base.py +43 -0
  42. modusa/tools/math_ops.py +335 -0
  43. modusa/tools/plotter.py +351 -0
  44. modusa/tools/youtube_downloader.py +72 -0
  45. modusa/utils/excp.py +15 -42
  46. modusa/utils/np_func_cat.py +44 -0
  47. modusa/utils/plot.py +142 -0
  48. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/METADATA +5 -16
  49. modusa-0.3.dist-info/RECORD +60 -0
  50. modusa/engines/.DS_Store +0 -0
  51. modusa/engines/__init__.py +0 -3
  52. modusa/engines/base.py +0 -14
  53. modusa/io/__init__.py +0 -9
  54. modusa/io/audio_converter.py +0 -76
  55. modusa/io/audio_loader.py +0 -214
  56. modusa/io/audio_player.py +0 -72
  57. modusa/io/base.py +0 -43
  58. modusa/io/plotter.py +0 -430
  59. modusa/io/youtube_downloader.py +0 -139
  60. modusa/signals/__init__.py +0 -7
  61. modusa/signals/audio_signal.py +0 -483
  62. modusa/signals/base.py +0 -34
  63. modusa/signals/frequency_domain_signal.py +0 -329
  64. modusa/signals/signal_ops.py +0 -158
  65. modusa/signals/spectrogram.py +0 -465
  66. modusa/signals/time_domain_signal.py +0 -309
  67. modusa-0.2.22.dist-info/RECORD +0 -47
  68. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/WHEEL +0 -0
  69. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/entry_points.txt +0 -0
  70. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/licenses/LICENSE.md +0 -0
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ import soundfile as sf
5
+ from scipy.signal import resample
6
+ from pathlib import Path
7
+ import tempfile
8
+ from scipy.signal import resample
9
+ from .youtube_downloader import download
10
+ from .audio_converter import convert
11
+
12
+
13
+ def load(path, sr=None):
14
+ """
15
+ Loads audio file from various sources.
16
+
17
+ .. code-block:: python
18
+
19
+ import modusa as ms
20
+ audio_fp = ms.load(
21
+ "https://www.youtube.com/watch?v=lIpw9-Y_N0g",
22
+ sr = None)
23
+
24
+ Parameters
25
+ ----------
26
+ path: str
27
+ - Path to the audio
28
+ - Youtube URL
29
+ sr: int
30
+ - Sampling rate to load the audio in.
31
+
32
+ Return
33
+ ------
34
+ np.ndarray
35
+ - Audio signal.
36
+ int
37
+ - Sampling rate of the loaded audio signal.
38
+ title
39
+ - Title of the loaded audio.
40
+ - Filename without extension or YouTube title.
41
+ """
42
+
43
+ # Check if the path is YouTube
44
+ if ".youtube." in str(path):
45
+ # Download the audio in temp directory using tempfile module
46
+ with tempfile.TemporaryDirectory() as tmpdir:
47
+ # Download
48
+ audio_fp: Path = download(url=path, content_type="audio", output_dir=Path(tmpdir))
49
+
50
+ # Convert the audio to ".wav" form for loading
51
+ wav_audio_fp: Path = convert(inp_audio_fp=audio_fp, output_audio_fp=audio_fp.with_suffix(".wav"))
52
+
53
+ # Load the audio in memory
54
+ audio_data, audio_sr = sf.read(wav_audio_fp)
55
+ title = audio_fp.stem
56
+
57
+ # Convert to mono if it's multi-channel
58
+ if audio_data.ndim > 1:
59
+ audio_data = audio_data.mean(axis=1)
60
+
61
+ # Resample if needed
62
+ if sr is not None:
63
+ if audio_sr != sr:
64
+ n_samples = int(len(audio_data) * sr / audio_sr)
65
+ audio_data = resample(audio_data, n_samples)
66
+ audio_sr = sr
67
+
68
+ else:
69
+ # Check if the file exists
70
+ fp = Path(path)
71
+
72
+ if not fp.exists():
73
+ raise FileNotFoundError(f"{path} does not exist.")
74
+
75
+ # Load the audio in memory
76
+ audio_data, audio_sr = sf.read(fp)
77
+ title = fp.stem
78
+
79
+ # Convert to mono if it's multi-channel
80
+ if audio_data.ndim > 1:
81
+ audio_data = audio_data.mean(axis=1)
82
+
83
+ # Resample if needed
84
+ if sr is not None:
85
+ if audio_sr != sr:
86
+ n_samples = int(len(audio_data) * sr / audio_sr)
87
+ audio_data = resample(audio_data, n_samples)
88
+ audio_sr = sr
89
+
90
+ return audio_data, audio_sr, title
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import numpy as np
4
+ from IPython.display import display, HTML, Audio
5
+
6
+ def play(y: np.ndarray, sr: float, t0: float = 0.0, regions = None, title = None) -> None:
7
+ """
8
+ Plays audio clips for given regions in Jupyter Notebooks.
9
+
10
+ Parameters
11
+ ----------
12
+ y : np.ndarray
13
+ - Audio data.
14
+ - Mono (1D) numpy array.
15
+ sr: float
16
+ - Sampling rate of the audio.
17
+ t0: float
18
+ - Starting timestamp, incase the audio is cropped
19
+ - Default: 0.0 → Starts from 0.0 sec
20
+ regions : list[tuple[float, float, str]] | tuple[float, float, str] | None
21
+ - Regions to extract and play (in sec), e.g. [(0, 10.2, "tag")]
22
+ - If there is only one region, a tuple should also work. e.g. (0, 10.2, "tag")
23
+ - Default: None → The entire song is selected.
24
+ title : str | None
25
+ - Title to display above audio players.
26
+
27
+ Returns
28
+ -------
29
+ None
30
+ """
31
+ if title:
32
+ display(HTML(f"<h4>{title}</h4>"))
33
+
34
+ clip_tags = []
35
+ timings = []
36
+ players = []
37
+
38
+ if isinstance(regions, tuple): regions = [regions] # (10, 20, "Region 1") -> [(10, 20, "Region 1")]
39
+
40
+ if regions is not None:
41
+ for region in regions:
42
+ assert len(region) == 3
43
+
44
+ start_sec = region[0] - t0
45
+ end_sec = region[1] - t0
46
+ tag = region[2]
47
+
48
+ start_sample, end_sample = int(start_sec * sr), int(end_sec * sr)
49
+ clip = y[start_sample: end_sample]
50
+ audio_player = Audio(data=clip, rate=sr)._repr_html_()
51
+
52
+ clip_tags.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>{tag}</td>")
53
+ timings.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>{start_sec:.2f}s → {end_sec:.2f}s</td>")
54
+ players.append(f"<td style='padding:6px;'>{audio_player}</td>")
55
+ else:
56
+ audio_player = Audio(data=y, rate=sr)._repr_html_()
57
+
58
+ start_sec = t0
59
+ end_sec = t0 + y.shape[0] / sr
60
+
61
+ clip_tags.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>1</td>")
62
+ timings.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>{start_sec:.2f}s → {end_sec:.2f}s</td>")
63
+ players.append(f"<td style='padding:6px;'>{audio_player}</td>")
64
+
65
+ # Wrap rows in a table with border
66
+ table_html = f"""
67
+ <div style="display:inline-block; border:1px solid #ccc; border-radius:6px; overflow:hidden;">
68
+ <table style="border-collapse:collapse;">
69
+ <tr style="background-color:#f2f2f2;">
70
+ <th style="text-align:left; padding:6px 12px;">Clip</th>
71
+ {''.join(clip_tags)}
72
+ </tr>
73
+ <tr style="background-color:#fcfcfc;">
74
+ <th style="text-align:left; padding:6px 12px;">Timing</th>
75
+ {''.join(timings)}
76
+ </tr>
77
+ <tr>
78
+ <th style="text-align:left; padding:6px 12px;">Player</th>
79
+ {''.join(players)}
80
+ </tr>
81
+ </table>
82
+ </div>
83
+ """
84
+
85
+ return HTML(table_html)
86
+
87
+
88
+
89
+
modusa/tools/base.py ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ class ModusaTool(ABC):
6
+ """
7
+ Base class for all tool: youtube downloader, audio converter, filter.
8
+
9
+ >>> modusa-dev create io
10
+
11
+ .. code-block:: python
12
+
13
+ # General template of a subclass of ModusaTool
14
+ from modusa.tools.base import ModusaTool
15
+
16
+ class MyCustomIOClass(ModusaIO):
17
+ #--------Meta Information----------
18
+ _name = "My Custom Tool"
19
+ _description = "My custom class for Tool."
20
+ _author_name = "Ankit Anand"
21
+ _author_email = "ankit0.anand0@gmail.com"
22
+ _created_at = "2025-07-06"
23
+ #----------------------------------
24
+
25
+ @staticmethod
26
+ def do_something():
27
+ pass
28
+
29
+
30
+ Note
31
+ ----
32
+ - This class is intended to be subclassed by any tool built for the modusa framework.
33
+ - In order to create a tool, you can use modusa-dev CLI to generate a template.
34
+ - It is recommended to treat subclasses of ModusaTool as namespaces and define @staticmethods with control parameters, rather than using instance-level __init__ methods.
35
+ """
36
+
37
+ #--------Meta Information----------
38
+ _name: str = "Modusa Tool"
39
+ _description: str = "Base class for any tool in the Modusa framework."
40
+ _author_name = "Ankit Anand"
41
+ _author_email = "ankit0.anand0@gmail.com"
42
+ _created_at = "2025-07-11"
43
+ #----------------------------------
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from modusa import excp
4
+ from modusa.tools.base import ModusaTool
5
+ from typing import Any
6
+ import numpy as np
7
+
8
+ class MathOps(ModusaTool):
9
+ """
10
+ Performs arithmetic and NumPy-style ops.
11
+
12
+ Note
13
+ ----
14
+ - Shape-changing operations like reshape, transpose, etc. are not yet supported. Use only element-wise or aggregation ops for now.
15
+ - Index alignment must be handled carefully in future extensions.
16
+ """
17
+
18
+ def _axes_match(a1: tuple[np.ndarray, ...], a2: tuple[np.ndarray, ...]) -> bool:
19
+ """
20
+ To check if two axes are same.
21
+
22
+ It checks the length of the axes and the corresponding values.
23
+ """
24
+ if len(a1) != len(a2):
25
+ return False
26
+ return all(np.allclose(x, y, atol=1e-8) for x, y in zip(a1, a2))
27
+
28
+
29
+ #----------------------------------
30
+ # To handle basic element wise
31
+ # math operations like
32
+ # +, -, *, **, / ...
33
+ #----------------------------------
34
+
35
+ @staticmethod
36
+ def add(a: Any, b: Any) -> np.generic | np.ndarray:
37
+ try:
38
+ result = np.add(a, b)
39
+ except Exception as e:
40
+ raise excp.InputError(f"`a` and `b` can't be added") from e
41
+
42
+ if isinstance(a, str) and isinstance(b, str): # numpy actually concatenates, we do not want that
43
+ raise excp.InputError(f"`a` and `b` can't be added")
44
+ return result
45
+
46
+ @staticmethod
47
+ def subtract(a: Any, b: Any) -> np.generic | np.ndarray:
48
+ try:
49
+ result = np.subtract(a, b)
50
+ except Exception as e:
51
+ raise excp.InputError(f"`a` and `b` can't be subtracted") from e
52
+ return result
53
+
54
+ @staticmethod
55
+ def multiply(a: Any, b: Any) -> np.generic | np.ndarray:
56
+ try:
57
+ result = np.multiply(a, b)
58
+ except Exception as e:
59
+ raise excp.InputError(f"`a` and `b` can't be multiplied") from e
60
+
61
+ if isinstance(a, str) and isinstance(b, str): # numpy actually concatenates, we do not want that
62
+ raise excp.InputError(f"`a` and `b` can't be multiplied")
63
+ return result
64
+
65
+ @staticmethod
66
+ def divide(a: Any, b: Any) -> np.generic | np.ndarray:
67
+ try:
68
+ result = np.divide(a, b)
69
+ except Exception as e:
70
+ raise excp.InputError(f"`a` and `b` can't be divided") from e
71
+ return result
72
+
73
+ @staticmethod
74
+ def power(a: Any, b: Any) -> np.generic | np.ndarray:
75
+ try:
76
+ result = np.power(a, b)
77
+ except Exception as e:
78
+ raise excp.InputError(f"`a` can't be exponentiated with `b`") from e
79
+ return result
80
+
81
+ @staticmethod
82
+ def floor_divide(a: Any, b: Any) -> np.generic | np.ndarray:
83
+ try:
84
+ result = np.floor_divide(a, b)
85
+ except Exception as e:
86
+ raise excp.InputError(f"`a` can't be floor divided by `b`") from e
87
+ return result
88
+
89
+ #----------------------------------
90
+ # To handle numpy aggregator ops
91
+ #----------------------------------
92
+ @staticmethod
93
+ def mean(a: Any, axis: int | None = None) -> np.generic | np.ndarray:
94
+ try:
95
+ result = np.mean(a, axis=axis)
96
+ except Exception as e:
97
+ raise excp.InputError(f"can't find mean for `a`") from e
98
+ return result
99
+
100
+ @staticmethod
101
+ def std(a: Any, axis: int | None = None) -> np.generic | np.ndarray:
102
+ """"""
103
+ try:
104
+ result = np.std(a, axis=axis)
105
+ except Exception as e:
106
+ raise excp.InputError(f"can't find std for `a`") from e
107
+ return result
108
+
109
+ @staticmethod
110
+ def min(a: Any, axis: int | None = None) -> np.generic | np.ndarray:
111
+ try:
112
+ result = np.min(a, axis=axis)
113
+ except Exception as e:
114
+ raise excp.InputError(f"can't find min for `a`") from e
115
+ return result
116
+
117
+ @staticmethod
118
+ def max(a: Any, axis: int | None = None) -> np.generic | np.ndarray:
119
+ try:
120
+ result = np.max(a, axis=axis)
121
+ except Exception as e:
122
+ raise excp.InputError(f"can't find max for `a`") from e
123
+ return result
124
+
125
+ @staticmethod
126
+ def sum(a: Any, axis: int | None = None) -> np.generic | np.ndarray:
127
+ try:
128
+ result = np.sum(a, axis=axis)
129
+ except Exception as e:
130
+ raise excp.InputError(f"can't find sum for `a`") from e
131
+ return result
132
+
133
+ #----------------------------------
134
+ # To handle numpy ops where the
135
+ # shapes are unaltered
136
+ # sin, cos, exp, log, ...
137
+ #----------------------------------
138
+
139
+ @staticmethod
140
+ def sin(a: Any) -> np.generic | np.ndarray:
141
+ try:
142
+ result = np.sin(a)
143
+ except Exception as e:
144
+ raise excp.InputError(f"can't find sin for `a`") from e
145
+ return result
146
+
147
+ @staticmethod
148
+ def cos(a: Any) -> np.generic | np.ndarray:
149
+ try:
150
+ result = np.cos(a)
151
+ except Exception as e:
152
+ raise excp.InputError(f"can't find cos for `a`") from e
153
+ return result
154
+
155
+ @staticmethod
156
+ def tanh(a: Any) -> np.generic | np.ndarray:
157
+ try:
158
+ result = np.tanh(a)
159
+ except Exception as e:
160
+ raise excp.InputError(f"can't find tanh for `a`") from e
161
+ return result
162
+
163
+ @staticmethod
164
+ def exp(a: Any) -> np.generic | np.ndarray:
165
+ try:
166
+ result = np.exp(a)
167
+ except Exception as e:
168
+ raise excp.InputError(f"can't find exp for `a`") from e
169
+ return result
170
+
171
+ @staticmethod
172
+ def log(a: Any) -> np.generic | np.ndarray:
173
+ try:
174
+ result = np.log(a)
175
+ except Exception as e:
176
+ raise excp.InputError(f"can't find log for `a`") from e
177
+ return result
178
+
179
+ @staticmethod
180
+ def log10(a: Any) -> np.generic | np.ndarray:
181
+ try:
182
+ result = np.log10(a)
183
+ except Exception as e:
184
+ raise excp.InputError(f"can't find log10 for `a`") from e
185
+ return result
186
+
187
+ @staticmethod
188
+ def log2(a: Any) -> np.generic | np.ndarray:
189
+ try:
190
+ result = np.log2(a)
191
+ except Exception as e:
192
+ raise excp.InputError(f"can't find log2 for `a`") from e
193
+ return result
194
+
195
+ @staticmethod
196
+ def log1p(a: Any) -> np.generic | np.ndarray:
197
+ try:
198
+ result = np.log1p(a)
199
+ except Exception as e:
200
+ raise excp.InputError(f"can't find log1p for `a`") from e
201
+ return result
202
+
203
+
204
+ @staticmethod
205
+ def sqrt(a: Any) -> np.generic | np.ndarray:
206
+ try:
207
+ result = np.sqrt(a)
208
+ except Exception as e:
209
+ raise excp.InputError(f"can't find sqrt for `a`") from e
210
+ return result
211
+
212
+ @staticmethod
213
+ def abs(a: Any) -> np.generic | np.ndarray:
214
+ try:
215
+ result = np.abs(a)
216
+ except Exception as e:
217
+ raise excp.InputError(f"can't find abs for `a`") from e
218
+ return result
219
+
220
+ @staticmethod
221
+ def floor(a: Any) -> np.generic | np.ndarray:
222
+ try:
223
+ result = np.floor(a)
224
+ except Exception as e:
225
+ raise excp.InputError(f"can't find floor for `a`") from e
226
+ return result
227
+
228
+ @staticmethod
229
+ def ceil(a: Any) -> np.generic | np.ndarray:
230
+ try:
231
+ result = np.ceil(a)
232
+ except Exception as e:
233
+ raise excp.InputError(f"can't find ceil for `a`") from e
234
+ return result
235
+
236
+ @staticmethod
237
+ def round(a: Any) -> np.generic | np.ndarray:
238
+ try:
239
+ result = np.round(a)
240
+ except Exception as e:
241
+ raise excp.InputError(f"can't find round for `a`") from e
242
+ return result
243
+
244
+ #------------------------------------
245
+ # TODO: Add shape-changing ops like
246
+ # reshape, transpose, squeeze later
247
+ #------------------------------------
248
+
249
+ @staticmethod
250
+ def reshape(a: Any, shape: int | tuple[int, ...]) -> np.ndarray:
251
+ try:
252
+ result = np.reshape(a, shape=shape)
253
+ except Exception as e:
254
+ raise excp.InputError(f"can't reshape `a`") from e
255
+ return result
256
+
257
+ #------------------------------------
258
+ # Complex numbers operations
259
+ #------------------------------------
260
+
261
+ @staticmethod
262
+ def real(a: Any) -> np.ndarray:
263
+ try:
264
+ result = np.real(a)
265
+ except Exception as e:
266
+ raise excp.InputError(f"can't find real for `a`") from e
267
+ return result
268
+
269
+ @staticmethod
270
+ def imag(a: Any) -> np.ndarray:
271
+ try:
272
+ result = np.imag(a)
273
+ except Exception as e:
274
+ raise excp.InputError(f"can't find imag for `a`") from e
275
+ return result
276
+
277
+ @staticmethod
278
+ def angle(a: Any) -> np.ndarray:
279
+ try:
280
+ result = np.angle(a)
281
+ except Exception as e:
282
+ raise excp.InputError(f"can't find angle for `a`") from e
283
+ return result
284
+
285
+ #------------------------------------
286
+ # Comparison
287
+ #------------------------------------
288
+
289
+ @staticmethod
290
+ def lt(a: Any, b: Any) -> np.ndarray:
291
+ try:
292
+ mask = a < b
293
+ except Exception as e:
294
+ raise excp.InputError(f"`a` and `b` can't be compared") from e
295
+ return mask
296
+
297
+ @staticmethod
298
+ def le(a: Any, b: Any) -> np.ndarray:
299
+ try:
300
+ mask = a <= b
301
+ except Exception as e:
302
+ raise excp.InputError(f"`a` and `b` can't be compared") from e
303
+ return mask
304
+
305
+ @staticmethod
306
+ def gt(a: Any, b: Any) -> np.ndarray:
307
+ try:
308
+ mask = a > b
309
+ except Exception as e:
310
+ raise excp.InputError(f"`a` and `b` can't be compared") from e
311
+ return mask
312
+
313
+ @staticmethod
314
+ def ge(a: Any, b: Any) -> np.ndarray:
315
+ try:
316
+ mask = a >= b
317
+ except Exception as e:
318
+ raise excp.InputError(f"`a` and `b` can't be compared") from e
319
+ return mask
320
+
321
+ @staticmethod
322
+ def eq(a: Any, b: Any) -> np.ndarray:
323
+ try:
324
+ mask = a == b
325
+ except Exception as e:
326
+ raise excp.InputError(f"`a` and `b` can't be compared") from e
327
+ return mask
328
+
329
+ @staticmethod
330
+ def ne(a: Any, b: Any) -> np.ndarray:
331
+ try:
332
+ mask = a != b
333
+ except Exception as e:
334
+ raise excp.InputError(f"`a` and `b` can't be compared") from e
335
+ return mask