vybespk 0.1.0__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.
- voice_agent/__init__.py +9 -0
- voice_agent/_vendor/__init__.py +0 -0
- voice_agent/_vendor/faster_whisper/LICENSE +21 -0
- voice_agent/_vendor/faster_whisper/VENDORED.md +7 -0
- voice_agent/_vendor/faster_whisper/__init__.py +14 -0
- voice_agent/_vendor/faster_whisper/audio.py +123 -0
- voice_agent/_vendor/faster_whisper/feature_extractor.py +230 -0
- voice_agent/_vendor/faster_whisper/tokenizer.py +320 -0
- voice_agent/_vendor/faster_whisper/transcribe.py +1941 -0
- voice_agent/_vendor/faster_whisper/utils.py +152 -0
- voice_agent/_vendor/faster_whisper/vad.py +351 -0
- voice_agent/_vendor/faster_whisper/version.py +3 -0
- voice_agent/activity.py +82 -0
- voice_agent/agents/__init__.py +196 -0
- voice_agent/agents/claude_code.py +189 -0
- voice_agent/agents/codex.py +238 -0
- voice_agent/audio.py +504 -0
- voice_agent/billing.py +396 -0
- voice_agent/cli.py +880 -0
- voice_agent/config.py +177 -0
- voice_agent/default_config.toml +160 -0
- voice_agent/drafter.py +178 -0
- voice_agent/events.py +51 -0
- voice_agent/hooks/__init__.py +0 -0
- voice_agent/hooks/on_notification.py +22 -0
- voice_agent/hooks/on_permission.py +45 -0
- voice_agent/hooks/on_prompt.py +28 -0
- voice_agent/hooks/on_stop.py +31 -0
- voice_agent/hooks/on_tool.py +20 -0
- voice_agent/jev.py +73 -0
- voice_agent/keys.py +66 -0
- voice_agent/listen.py +1755 -0
- voice_agent/llm/__init__.py +114 -0
- voice_agent/llm/anthropic_provider.py +113 -0
- voice_agent/llm/openai_provider.py +147 -0
- voice_agent/models.py +138 -0
- voice_agent/openai_decisions.py +87 -0
- voice_agent/permissions.py +51 -0
- voice_agent/research.py +300 -0
- voice_agent/router.py +256 -0
- voice_agent/session.py +97 -0
- voice_agent/settings.py +143 -0
- voice_agent/share.py +231 -0
- voice_agent/speaker_id.py +216 -0
- voice_agent/speech.py +97 -0
- voice_agent/tmux.py +275 -0
- voice_agent/transcribe.py +85 -0
- voice_agent/tts.py +126 -0
- vybespk-0.1.0.dist-info/METADATA +424 -0
- vybespk-0.1.0.dist-info/RECORD +54 -0
- vybespk-0.1.0.dist-info/WHEEL +4 -0
- vybespk-0.1.0.dist-info/entry_points.txt +2 -0
- vybespk-0.1.0.dist-info/licenses/LICENSE.md +105 -0
- vybespk-0.1.0.dist-info/licenses/voice_agent/_vendor/faster_whisper/LICENSE +21 -0
voice_agent/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Vybespk: a hands-free voice front-end for Claude Code and Codex."""
|
|
2
|
+
__version__ = "0.1.0"
|
|
3
|
+
# Sent on every request we make. Python's default ("Python-urllib/3.x") is blocked by
|
|
4
|
+
# Cloudflare's bot protection in front of the relay.
|
|
5
|
+
USER_AGENT = f"vybespk/{__version__}"
|
|
6
|
+
|
|
7
|
+
from .keys import load_into_env as _load_keys
|
|
8
|
+
|
|
9
|
+
_load_keys()
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 SYSTRAN
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
faster-whisper 1.2.1 (MIT, https://github.com/SYSTRAN/faster-whisper), copied unchanged except:
|
|
2
|
+
- imports made relative, so it works inside voice_agent._vendor
|
|
3
|
+
- the Silero VAD asset is not included and onnxruntime is not a dependency: the voice agent
|
|
4
|
+
never passes vad_filter=True (it detects speech with silero-vad-lite). Asking for the VAD
|
|
5
|
+
raises a clear error instead.
|
|
6
|
+
Why: onnxruntime has no macOS Intel build for Python 3.13, so depending on faster-whisper
|
|
7
|
+
would make `pip install vybespk` fail on those Macs.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .audio import decode_audio
|
|
2
|
+
from .transcribe import BatchedInferencePipeline, WhisperModel
|
|
3
|
+
from .utils import available_models, download_model, format_timestamp
|
|
4
|
+
from .version import __version__
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"available_models",
|
|
8
|
+
"decode_audio",
|
|
9
|
+
"WhisperModel",
|
|
10
|
+
"BatchedInferencePipeline",
|
|
11
|
+
"download_model",
|
|
12
|
+
"format_timestamp",
|
|
13
|
+
"__version__",
|
|
14
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""We use the PyAV library to decode the audio: https://github.com/PyAV-Org/PyAV
|
|
2
|
+
|
|
3
|
+
The advantage of PyAV is that it bundles the FFmpeg libraries so there is no additional
|
|
4
|
+
system dependencies. FFmpeg does not need to be installed on the system.
|
|
5
|
+
|
|
6
|
+
However, the API is quite low-level so we need to manipulate audio frames directly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import gc
|
|
10
|
+
import io
|
|
11
|
+
import itertools
|
|
12
|
+
|
|
13
|
+
from typing import BinaryIO, Union
|
|
14
|
+
|
|
15
|
+
import av
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def decode_audio(
|
|
20
|
+
input_file: Union[str, BinaryIO],
|
|
21
|
+
sampling_rate: int = 16000,
|
|
22
|
+
split_stereo: bool = False,
|
|
23
|
+
):
|
|
24
|
+
"""Decodes the audio.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
input_file: Path to the input file or a file-like object.
|
|
28
|
+
sampling_rate: Resample the audio to this sample rate.
|
|
29
|
+
split_stereo: Return separate left and right channels.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A float32 Numpy array.
|
|
33
|
+
|
|
34
|
+
If `split_stereo` is enabled, the function returns a 2-tuple with the
|
|
35
|
+
separated left and right channels.
|
|
36
|
+
"""
|
|
37
|
+
resampler = av.audio.resampler.AudioResampler(
|
|
38
|
+
format="s16",
|
|
39
|
+
layout="mono" if not split_stereo else "stereo",
|
|
40
|
+
rate=sampling_rate,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
raw_buffer = io.BytesIO()
|
|
44
|
+
dtype = None
|
|
45
|
+
|
|
46
|
+
with av.open(input_file, mode="r", metadata_errors="ignore") as container:
|
|
47
|
+
frames = container.decode(audio=0)
|
|
48
|
+
frames = _ignore_invalid_frames(frames)
|
|
49
|
+
frames = _group_frames(frames, 500000)
|
|
50
|
+
frames = _resample_frames(frames, resampler)
|
|
51
|
+
|
|
52
|
+
for frame in frames:
|
|
53
|
+
array = frame.to_ndarray()
|
|
54
|
+
dtype = array.dtype
|
|
55
|
+
raw_buffer.write(array)
|
|
56
|
+
|
|
57
|
+
# It appears that some objects related to the resampler are not freed
|
|
58
|
+
# unless the garbage collector is manually run.
|
|
59
|
+
# https://github.com/SYSTRAN/faster-whisper/issues/390
|
|
60
|
+
# note that this slows down loading the audio a little bit
|
|
61
|
+
# if that is a concern, please use ffmpeg directly as in here:
|
|
62
|
+
# https://github.com/openai/whisper/blob/25639fc/whisper/audio.py#L25-L62
|
|
63
|
+
del resampler
|
|
64
|
+
gc.collect()
|
|
65
|
+
|
|
66
|
+
audio = np.frombuffer(raw_buffer.getbuffer(), dtype=dtype)
|
|
67
|
+
|
|
68
|
+
# Convert s16 back to f32.
|
|
69
|
+
audio = audio.astype(np.float32) / 32768.0
|
|
70
|
+
|
|
71
|
+
if split_stereo:
|
|
72
|
+
left_channel = audio[0::2]
|
|
73
|
+
right_channel = audio[1::2]
|
|
74
|
+
return left_channel, right_channel
|
|
75
|
+
|
|
76
|
+
return audio
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _ignore_invalid_frames(frames):
|
|
80
|
+
iterator = iter(frames)
|
|
81
|
+
|
|
82
|
+
while True:
|
|
83
|
+
try:
|
|
84
|
+
yield next(iterator)
|
|
85
|
+
except StopIteration:
|
|
86
|
+
break
|
|
87
|
+
except av.error.InvalidDataError:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _group_frames(frames, num_samples=None):
|
|
92
|
+
fifo = av.audio.fifo.AudioFifo()
|
|
93
|
+
|
|
94
|
+
for frame in frames:
|
|
95
|
+
frame.pts = None # Ignore timestamp check.
|
|
96
|
+
fifo.write(frame)
|
|
97
|
+
|
|
98
|
+
if num_samples is not None and fifo.samples >= num_samples:
|
|
99
|
+
yield fifo.read()
|
|
100
|
+
|
|
101
|
+
if fifo.samples > 0:
|
|
102
|
+
yield fifo.read()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _resample_frames(frames, resampler):
|
|
106
|
+
# Add None to flush the resampler.
|
|
107
|
+
for frame in itertools.chain(frames, [None]):
|
|
108
|
+
yield from resampler.resample(frame)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def pad_or_trim(array, length: int = 3000, *, axis: int = -1):
|
|
112
|
+
"""
|
|
113
|
+
Pad or trim the Mel features array to 3000, as expected by the encoder.
|
|
114
|
+
"""
|
|
115
|
+
if array.shape[axis] > length:
|
|
116
|
+
array = array.take(indices=range(length), axis=axis)
|
|
117
|
+
|
|
118
|
+
if array.shape[axis] < length:
|
|
119
|
+
pad_widths = [(0, 0)] * array.ndim
|
|
120
|
+
pad_widths[axis] = (0, length - array.shape[axis])
|
|
121
|
+
array = np.pad(array, pad_widths)
|
|
122
|
+
|
|
123
|
+
return array
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FeatureExtractor:
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
feature_size=80,
|
|
8
|
+
sampling_rate=16000,
|
|
9
|
+
hop_length=160,
|
|
10
|
+
chunk_length=30,
|
|
11
|
+
n_fft=400,
|
|
12
|
+
):
|
|
13
|
+
self.n_fft = n_fft
|
|
14
|
+
self.hop_length = hop_length
|
|
15
|
+
self.chunk_length = chunk_length
|
|
16
|
+
self.n_samples = chunk_length * sampling_rate
|
|
17
|
+
self.nb_max_frames = self.n_samples // hop_length
|
|
18
|
+
self.time_per_frame = hop_length / sampling_rate
|
|
19
|
+
self.sampling_rate = sampling_rate
|
|
20
|
+
self.mel_filters = self.get_mel_filters(
|
|
21
|
+
sampling_rate, n_fft, n_mels=feature_size
|
|
22
|
+
).astype("float32")
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def get_mel_filters(sr, n_fft, n_mels=128):
|
|
26
|
+
# Initialize the weights
|
|
27
|
+
n_mels = int(n_mels)
|
|
28
|
+
|
|
29
|
+
# Center freqs of each FFT bin
|
|
30
|
+
fftfreqs = np.fft.rfftfreq(n=n_fft, d=1.0 / sr)
|
|
31
|
+
|
|
32
|
+
# 'Center freqs' of mel bands - uniformly spaced between limits
|
|
33
|
+
min_mel = 0.0
|
|
34
|
+
max_mel = 45.245640471924965
|
|
35
|
+
|
|
36
|
+
mels = np.linspace(min_mel, max_mel, n_mels + 2)
|
|
37
|
+
|
|
38
|
+
# Fill in the linear scale
|
|
39
|
+
f_min = 0.0
|
|
40
|
+
f_sp = 200.0 / 3
|
|
41
|
+
freqs = f_min + f_sp * mels
|
|
42
|
+
|
|
43
|
+
# And now the nonlinear scale
|
|
44
|
+
min_log_hz = 1000.0 # beginning of log region (Hz)
|
|
45
|
+
min_log_mel = (min_log_hz - f_min) / f_sp # same (Mels)
|
|
46
|
+
logstep = np.log(6.4) / 27.0 # step size for log region
|
|
47
|
+
|
|
48
|
+
# If we have vector data, vectorize
|
|
49
|
+
log_t = mels >= min_log_mel
|
|
50
|
+
freqs[log_t] = min_log_hz * np.exp(logstep * (mels[log_t] - min_log_mel))
|
|
51
|
+
|
|
52
|
+
fdiff = np.diff(freqs)
|
|
53
|
+
ramps = freqs.reshape(-1, 1) - fftfreqs.reshape(1, -1)
|
|
54
|
+
|
|
55
|
+
lower = -ramps[:-2] / np.expand_dims(fdiff[:-1], axis=1)
|
|
56
|
+
upper = ramps[2:] / np.expand_dims(fdiff[1:], axis=1)
|
|
57
|
+
|
|
58
|
+
# Intersect them with each other and zero, vectorized across all i
|
|
59
|
+
weights = np.maximum(np.zeros_like(lower), np.minimum(lower, upper))
|
|
60
|
+
|
|
61
|
+
# Slaney-style mel is scaled to be approx constant energy per channel
|
|
62
|
+
enorm = 2.0 / (freqs[2 : n_mels + 2] - freqs[:n_mels])
|
|
63
|
+
weights *= np.expand_dims(enorm, axis=1)
|
|
64
|
+
|
|
65
|
+
return weights
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def stft(
|
|
69
|
+
input_array: np.ndarray,
|
|
70
|
+
n_fft: int,
|
|
71
|
+
hop_length: int = None,
|
|
72
|
+
win_length: int = None,
|
|
73
|
+
window: np.ndarray = None,
|
|
74
|
+
center: bool = True,
|
|
75
|
+
mode: str = "reflect",
|
|
76
|
+
normalized: bool = False,
|
|
77
|
+
onesided: bool = None,
|
|
78
|
+
return_complex: bool = None,
|
|
79
|
+
):
|
|
80
|
+
# Default initialization for hop_length and win_length
|
|
81
|
+
hop_length = hop_length if hop_length is not None else n_fft // 4
|
|
82
|
+
win_length = win_length if win_length is not None else n_fft
|
|
83
|
+
input_is_complex = np.iscomplexobj(input_array)
|
|
84
|
+
|
|
85
|
+
# Determine if the output should be complex
|
|
86
|
+
return_complex = (
|
|
87
|
+
return_complex
|
|
88
|
+
if return_complex is not None
|
|
89
|
+
else (input_is_complex or (window is not None and np.iscomplexobj(window)))
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if not return_complex and return_complex is None:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"stft requires the return_complex parameter for real inputs."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Input checks
|
|
98
|
+
if not np.issubdtype(input_array.dtype, np.floating) and not input_is_complex:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"stft: expected an array of floating point or complex values,"
|
|
101
|
+
f" got {input_array.dtype}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if input_array.ndim > 2 or input_array.ndim < 1:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"stft: expected a 1D or 2D array, but got {input_array.ndim}D array"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Handle 1D input
|
|
110
|
+
if input_array.ndim == 1:
|
|
111
|
+
input_array = np.expand_dims(input_array, axis=0)
|
|
112
|
+
input_array_1d = True
|
|
113
|
+
else:
|
|
114
|
+
input_array_1d = False
|
|
115
|
+
|
|
116
|
+
# Center padding if required
|
|
117
|
+
if center:
|
|
118
|
+
pad_amount = n_fft // 2
|
|
119
|
+
input_array = np.pad(
|
|
120
|
+
input_array, ((0, 0), (pad_amount, pad_amount)), mode=mode
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
batch, length = input_array.shape
|
|
124
|
+
|
|
125
|
+
# Additional input checks
|
|
126
|
+
if n_fft <= 0 or n_fft > length:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"stft: expected 0 < n_fft <= {length}, but got n_fft={n_fft}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if hop_length <= 0:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"stft: expected hop_length > 0, but got hop_length={hop_length}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if win_length <= 0 or win_length > n_fft:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"stft: expected 0 < win_length <= n_fft, but got win_length={win_length}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if window is not None:
|
|
142
|
+
if window.ndim != 1 or window.shape[0] != win_length:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"stft: expected a 1D window array of size equal to win_length={win_length}, "
|
|
145
|
+
f"but got window with size {window.shape}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Handle padding of the window if necessary
|
|
149
|
+
if win_length < n_fft:
|
|
150
|
+
left = (n_fft - win_length) // 2
|
|
151
|
+
window_ = np.zeros(n_fft, dtype=window.dtype)
|
|
152
|
+
window_[left : left + win_length] = window
|
|
153
|
+
else:
|
|
154
|
+
window_ = window
|
|
155
|
+
|
|
156
|
+
# Calculate the number of frames
|
|
157
|
+
n_frames = 1 + (length - n_fft) // hop_length
|
|
158
|
+
|
|
159
|
+
# Time to columns
|
|
160
|
+
input_array = np.lib.stride_tricks.as_strided(
|
|
161
|
+
input_array,
|
|
162
|
+
(batch, n_frames, n_fft),
|
|
163
|
+
(
|
|
164
|
+
input_array.strides[0],
|
|
165
|
+
hop_length * input_array.strides[1],
|
|
166
|
+
input_array.strides[1],
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if window_ is not None:
|
|
171
|
+
input_array = input_array * window_
|
|
172
|
+
|
|
173
|
+
# FFT and transpose
|
|
174
|
+
complex_fft = input_is_complex
|
|
175
|
+
onesided = onesided if onesided is not None else not complex_fft
|
|
176
|
+
|
|
177
|
+
if normalized:
|
|
178
|
+
norm = "ortho"
|
|
179
|
+
else:
|
|
180
|
+
norm = None
|
|
181
|
+
|
|
182
|
+
if complex_fft:
|
|
183
|
+
if onesided:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"Cannot have onesided output if window or input is complex"
|
|
186
|
+
)
|
|
187
|
+
output = np.fft.fft(input_array, n=n_fft, axis=-1, norm=norm)
|
|
188
|
+
else:
|
|
189
|
+
output = np.fft.rfft(input_array, n=n_fft, axis=-1, norm=norm)
|
|
190
|
+
|
|
191
|
+
output = output.transpose((0, 2, 1))
|
|
192
|
+
|
|
193
|
+
if input_array_1d:
|
|
194
|
+
output = output.squeeze(0)
|
|
195
|
+
|
|
196
|
+
return output if return_complex else np.real(output)
|
|
197
|
+
|
|
198
|
+
def __call__(self, waveform: np.ndarray, padding=160, chunk_length=None):
|
|
199
|
+
"""
|
|
200
|
+
Compute the log-Mel spectrogram of the provided audio.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
if chunk_length is not None:
|
|
204
|
+
self.n_samples = chunk_length * self.sampling_rate
|
|
205
|
+
self.nb_max_frames = self.n_samples // self.hop_length
|
|
206
|
+
|
|
207
|
+
if waveform.dtype is not np.float32:
|
|
208
|
+
waveform = waveform.astype(np.float32)
|
|
209
|
+
|
|
210
|
+
if padding:
|
|
211
|
+
waveform = np.pad(waveform, (0, padding))
|
|
212
|
+
|
|
213
|
+
window = np.hanning(self.n_fft + 1)[:-1].astype("float32")
|
|
214
|
+
|
|
215
|
+
stft = self.stft(
|
|
216
|
+
waveform,
|
|
217
|
+
self.n_fft,
|
|
218
|
+
self.hop_length,
|
|
219
|
+
window=window,
|
|
220
|
+
return_complex=True,
|
|
221
|
+
).astype("complex64")
|
|
222
|
+
magnitudes = np.abs(stft[..., :-1]) ** 2
|
|
223
|
+
|
|
224
|
+
mel_spec = self.mel_filters @ magnitudes
|
|
225
|
+
|
|
226
|
+
log_spec = np.log10(np.clip(mel_spec, a_min=1e-10, a_max=None))
|
|
227
|
+
log_spec = np.maximum(log_spec, log_spec.max() - 8.0)
|
|
228
|
+
log_spec = (log_spec + 4.0) / 4.0
|
|
229
|
+
|
|
230
|
+
return log_spec
|