inopyutils 1.4.5__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.
inopyutils/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ from .media_helper import InoMediaHelper
2
+ from .config_helper import InoConfigHelper
3
+ from .file_helper import InoFileHelper
4
+ from .log_helper import InoLogHelper, LogType
5
+ from .s3_helper import InoS3Helper
6
+ from .json_helper import InoJsonHelper
7
+ from .http_helper import InoHttpHelper
8
+ from .audio_helper import InoAudioHelper
9
+ from .util_helper import InoUtilHelper, ok, err, is_err
10
+ from .mongo_helper import InoMongoHelper
11
+
12
+ __all__ = [
13
+ "InoConfigHelper",
14
+ "InoMediaHelper",
15
+ "InoFileHelper",
16
+ "InoLogHelper",
17
+ "LogType",
18
+ "InoS3Helper",
19
+ "InoJsonHelper",
20
+ "InoHttpHelper",
21
+ "InoAudioHelper",
22
+ "InoUtilHelper",
23
+ "InoMongoHelper",
24
+ "ok",
25
+ "err",
26
+ "is_err"
27
+ ]
@@ -0,0 +1,253 @@
1
+ import re
2
+ import asyncio
3
+
4
+ class InoAudioHelper:
5
+ @staticmethod
6
+ async def transcode_raw_pcm(
7
+ pcm_bytes: bytes,
8
+ output:str = "ogg",
9
+ codec: str = "libopus",
10
+ to_format: str = "s16le",
11
+ application: str = "voip",
12
+ rate: int = 16000,
13
+ channel: int = 1,
14
+ gain_db: float | None = None,
15
+ limit_after_gain: bool = True,
16
+ limit_ceiling: float = 0.98
17
+ ) -> dict:
18
+ args = [
19
+ "ffmpeg",
20
+ "-f", to_format,
21
+ "-ar", str(rate),
22
+ "-ac", str(channel),
23
+ "-i", "pipe:0",
24
+ ]
25
+
26
+ afilters: list[str] = []
27
+ if gain_db is not None:
28
+ afilters.append(f"volume={gain_db}dB")
29
+ if limit_after_gain:
30
+ afilters.append(f"alimiter=limit={limit_ceiling}")
31
+
32
+ if afilters:
33
+ args += ["-filter:a", ",".join(afilters)]
34
+
35
+ args += [
36
+ "-c:a", codec,
37
+ "-b:a", "24k",
38
+ "-vbr", "on",
39
+ "-application", application,
40
+ "-sample_fmt", "s16",
41
+ "-f", output,
42
+ "pipe:1",
43
+ ]
44
+
45
+ process = await asyncio.create_subprocess_exec(
46
+ *args,
47
+ stdin=asyncio.subprocess.PIPE,
48
+ stdout=asyncio.subprocess.PIPE,
49
+ stderr=asyncio.subprocess.PIPE,
50
+ )
51
+
52
+ out, err = await process.communicate(input=pcm_bytes)
53
+ if process.returncode != 0:
54
+ return {
55
+ "success": False,
56
+ "msg": "ffmpeg failed",
57
+ "error_code": err.decode(),
58
+ "data": b""
59
+ }
60
+
61
+ return {
62
+ "success": True,
63
+ "msg": "Transcode successful",
64
+ "error_code": err.decode(),
65
+ "data": out
66
+ }
67
+
68
+ @staticmethod
69
+ async def audio_to_raw_pcm(
70
+ audio: bytes,
71
+ to_format: str = "s16le",
72
+ rate: int = 16000,
73
+ channel: int = 1,
74
+ ) -> dict:
75
+ """
76
+ Convert arbitrary encoded audio bytes to raw PCM stream via ffmpeg.
77
+
78
+ Parameters:
79
+ audio: Input audio bytes (e.g., mp3, wav, ogg, webm, etc.).
80
+ to_format: Raw PCM sample format for the output (e.g., "s16le", "f32le").
81
+ rate: Target sample rate (Hz).
82
+ channel: Number of channels (1=mono, 2=stereo).
83
+
84
+ Returns:
85
+ dict with keys:
86
+ success: bool
87
+ msg: str
88
+ error_code: str (ffmpeg stderr)
89
+ data: bytes (raw PCM)
90
+ """
91
+ # Build ffmpeg command to read from stdin and output raw PCM to stdout
92
+ # We avoid forcing input format, letting ffmpeg auto-detect from stream headers.
93
+ args = [
94
+ "ffmpeg",
95
+ "-hide_banner",
96
+ "-nostdin",
97
+ "-i", "pipe:0",
98
+ "-vn", # drop any video streams if present
99
+ "-ar", str(rate),
100
+ "-ac", str(channel),
101
+ "-f", to_format,
102
+ "pipe:1",
103
+ ]
104
+
105
+ process = await asyncio.create_subprocess_exec(
106
+ *args,
107
+ stdin=asyncio.subprocess.PIPE,
108
+ stdout=asyncio.subprocess.PIPE,
109
+ stderr=asyncio.subprocess.PIPE,
110
+ )
111
+
112
+ out, err = await process.communicate(input=audio)
113
+ if process.returncode != 0:
114
+ return {
115
+ "success": False,
116
+ "msg": "ffmpeg failed",
117
+ "error_code": err.decode(errors="ignore"),
118
+ "data": b"",
119
+ }
120
+
121
+ return {
122
+ "success": True,
123
+ "msg": "Decode to raw PCM successful",
124
+ "error_code": err.decode(errors="ignore"),
125
+ "data": out,
126
+ }
127
+
128
+ @staticmethod
129
+ async def chunks_raw_pcm(
130
+ audio: bytes,
131
+ chunk_size: int = 1024
132
+ ) -> dict:
133
+ """
134
+ Split a raw PCM byte stream into fixed-size chunks.
135
+
136
+ Parameters:
137
+ audio: Raw PCM bytes.
138
+ chunk_size: Size of each chunk in bytes.
139
+
140
+ Returns:
141
+ dict with keys:
142
+ success: bool
143
+ msg: str
144
+ count: int (number of chunks)
145
+ chunks: list[bytes] (chunks of raw PCM)
146
+ """
147
+ # Validate inputs
148
+ if not isinstance(audio, (bytes, bytearray)):
149
+ return {
150
+ "success": False,
151
+ "msg": "audio must be bytes or bytearray",
152
+ "count": 0,
153
+ "chunks": [],
154
+ }
155
+ if not isinstance(chunk_size, int) or chunk_size <= 0:
156
+ return {
157
+ "success": False,
158
+ "msg": "chunk_size must be a positive integer",
159
+ "count": 0,
160
+ "chunks": [],
161
+ }
162
+
163
+ data = bytes(audio)
164
+ chunks = [data[i:i + chunk_size] for i in range(0, len(data), chunk_size)] if data else []
165
+
166
+ return {
167
+ "success": True,
168
+ "msg": "Raw PCM chunked successfully",
169
+ "count": len(chunks),
170
+ "chunks": chunks,
171
+ }
172
+
173
+ @staticmethod
174
+ def get_audio_duration_from_text (text: str, wpm: float = 160.0) -> float:
175
+ cleaned = re.sub(r'\s+', ' ', text).strip()
176
+ if not cleaned:
177
+ return 0.0
178
+
179
+ words = len([t for t in cleaned.split(' ') if re.search(r'\w', t)])
180
+ minutes = words / max(wpm, 1e-6)
181
+ return minutes * 60.0
182
+
183
+ @staticmethod
184
+ def get_empty_audio_pcm_bytes (
185
+ duration: int = 1,
186
+ to_format: str = "s16le",
187
+ rate: int = 16000,
188
+ channel: int = 1,
189
+ ) -> bytes:
190
+ """
191
+ Generate a silent raw PCM byte buffer of the requested duration.
192
+
193
+ Parameters:
194
+ duration: Duration in seconds (integer seconds are typical; values <= 0 yield empty bytes).
195
+ to_format: PCM sample format string (e.g., "s16le", "f32le"). Only raw PCM formats are supported.
196
+ rate: Sample rate (Hz), e.g., 16000.
197
+ channel: Number of channels, e.g., 1 for mono, 2 for stereo.
198
+
199
+ Returns:
200
+ bytes: Raw PCM bytes representing silence for the given parameters.
201
+ """
202
+ # Basic validation and coercion
203
+ try:
204
+ dur_s = float(duration)
205
+ except Exception:
206
+ return b""
207
+ if dur_s <= 0:
208
+ return b""
209
+ if not isinstance(rate, int) or rate <= 0:
210
+ return b""
211
+ if not isinstance(channel, int) or channel <= 0:
212
+ return b""
213
+
214
+ fmt = (to_format or "s16le").lower()
215
+
216
+ # Map common raw PCM formats to bytes-per-sample and the silence byte value/pattern.
217
+ # For signed PCM and IEEE float, silence is all zero bytes regardless of endianness.
218
+ # For unsigned 8-bit PCM, silence is 0x80.
219
+ bytes_per_sample = None
220
+ silence_byte = 0x00
221
+
222
+ if fmt in ("s8",):
223
+ bytes_per_sample = 1
224
+ silence_byte = 0x00
225
+ elif fmt in ("u8",):
226
+ bytes_per_sample = 1
227
+ silence_byte = 0x80
228
+ elif fmt in ("s16le", "s16be", "s16"): # treat generic s16 as 2 bytes
229
+ bytes_per_sample = 2
230
+ elif fmt in ("s24le", "s24be", "s24"):
231
+ bytes_per_sample = 3
232
+ elif fmt in ("s32le", "s32be", "s32"):
233
+ bytes_per_sample = 4
234
+ elif fmt in ("f32le", "f32be", "f32"):
235
+ bytes_per_sample = 4
236
+ elif fmt in ("f64le", "f64be", "f64"):
237
+ bytes_per_sample = 8
238
+ else:
239
+ # Unsupported/unknown format
240
+ return b""
241
+
242
+ frames = int(round(dur_s * rate))
243
+ total_bytes = frames * channel * bytes_per_sample
244
+
245
+ if total_bytes <= 0:
246
+ return b""
247
+
248
+ # For 1-byte formats, we may need a non-zero bias for silence (u8 -> 0x80).
249
+ if bytes_per_sample == 1 and silence_byte != 0x00:
250
+ return bytes([silence_byte]) * total_bytes
251
+
252
+ # For multi-byte formats and signed/float 1-byte formats, silence is zeros.
253
+ return bytes(total_bytes)
@@ -0,0 +1,100 @@
1
+ import configparser
2
+ from pathlib import Path
3
+ import aiofiles
4
+
5
+ class InoConfigHelper:
6
+ def __init__(self, path='configs/base.ini', load_from: Path = None):
7
+ self.debug = False
8
+ self.path = Path(path)
9
+ self.config = configparser.ConfigParser()
10
+ if load_from is not None:
11
+ self.config.read(load_from)
12
+
13
+ self._load()
14
+
15
+ def _load(self):
16
+ self.config.read(self.path)
17
+
18
+ def get(self, section, key, fallback=None):
19
+ try:
20
+ value = self.config.get(section, key, fallback=fallback)
21
+ if isinstance(value, list):
22
+ if self.debug:
23
+ print(f"❌ Config value for [{section}][{key}] is a list: {value}")
24
+ return fallback
25
+ if self.debug:
26
+ print(f"🔎 Raw value for [{section}][{key}] = {value} ({type(value)})")
27
+ if value is not None and isinstance(value, str):
28
+ value = value.strip()
29
+ return value
30
+ except Exception as e:
31
+ print(f"❌ Failed to get str for [{section}][{key}]: {e}")
32
+ return fallback
33
+
34
+ def get_bool(self, section, key, fallback=False):
35
+ try:
36
+ value = self.config.getboolean(section, key, fallback=fallback)
37
+ if self.debug:
38
+ print(f"🔎 Raw value for [{section}][{key}] = {value} ({type(value)})")
39
+ return value
40
+ except Exception as e:
41
+ print(f"❌ Failed to get boolean for [{section}][{key}]: {e}")
42
+ return fallback
43
+
44
+ def set(self, section, key, value):
45
+ if section not in self.config:
46
+ self.config[section] = {}
47
+
48
+ if self.debug:
49
+ print(f"📝 Setting [{section}][{key}] = {value} ({type(value)})")
50
+
51
+ self.config[section][key] = str(value).strip()
52
+
53
+ self.save()
54
+
55
+ self._load()
56
+
57
+ async def set_async(self, section, key, value):
58
+ if section not in self.config:
59
+ self.config[section] = {}
60
+
61
+ if self.debug:
62
+ print(f"📝 Setting [{section}][{key}] = {value} ({type(value)})")
63
+
64
+ self.config[section][key] = str(value).strip()
65
+
66
+ await self.save_async()
67
+
68
+ self._load()
69
+
70
+ def _is_valid_config(self):
71
+ try:
72
+ self.config.read(self.path)
73
+ return bool(self.config.sections())
74
+ except Exception:
75
+ return False
76
+
77
+ def save(self):
78
+ try:
79
+ self.path.parent.mkdir(parents=True, exist_ok=True)
80
+ with open(self.path, "w") as configfile:
81
+ self.config.write(configfile)
82
+ except Exception as e:
83
+ print(f"❌ Failed to save config to {self.path}: {e}")
84
+ raise
85
+
86
+ async def save_async(self):
87
+ try:
88
+ self.path.parent.mkdir(parents=True, exist_ok=True)
89
+
90
+ from io import StringIO
91
+ buffer = StringIO()
92
+ self.config.write(buffer)
93
+ content = buffer.getvalue()
94
+ buffer.close()
95
+
96
+ async with aiofiles.open(self.path, "w") as configfile:
97
+ await configfile.write(content)
98
+ except Exception as e:
99
+ print(f"❌ Failed to save config asynchronously to {self.path}: {e}")
100
+ raise