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 +27 -0
- inopyutils/audio_helper.py +253 -0
- inopyutils/config_helper.py +100 -0
- inopyutils/file_helper.py +559 -0
- inopyutils/http_helper.py +728 -0
- inopyutils/json_helper.py +424 -0
- inopyutils/log_helper.py +182 -0
- inopyutils/media_helper.py +297 -0
- inopyutils/mongo_helper.py +644 -0
- inopyutils/s3_helper.py +1430 -0
- inopyutils/util_helper.py +27 -0
- inopyutils-1.4.5.dist-info/METADATA +536 -0
- inopyutils-1.4.5.dist-info/RECORD +16 -0
- inopyutils-1.4.5.dist-info/WHEEL +5 -0
- inopyutils-1.4.5.dist-info/licenses/LICENSE +373 -0
- inopyutils-1.4.5.dist-info/top_level.txt +1 -0
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
|