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.
Files changed (54) hide show
  1. voice_agent/__init__.py +9 -0
  2. voice_agent/_vendor/__init__.py +0 -0
  3. voice_agent/_vendor/faster_whisper/LICENSE +21 -0
  4. voice_agent/_vendor/faster_whisper/VENDORED.md +7 -0
  5. voice_agent/_vendor/faster_whisper/__init__.py +14 -0
  6. voice_agent/_vendor/faster_whisper/audio.py +123 -0
  7. voice_agent/_vendor/faster_whisper/feature_extractor.py +230 -0
  8. voice_agent/_vendor/faster_whisper/tokenizer.py +320 -0
  9. voice_agent/_vendor/faster_whisper/transcribe.py +1941 -0
  10. voice_agent/_vendor/faster_whisper/utils.py +152 -0
  11. voice_agent/_vendor/faster_whisper/vad.py +351 -0
  12. voice_agent/_vendor/faster_whisper/version.py +3 -0
  13. voice_agent/activity.py +82 -0
  14. voice_agent/agents/__init__.py +196 -0
  15. voice_agent/agents/claude_code.py +189 -0
  16. voice_agent/agents/codex.py +238 -0
  17. voice_agent/audio.py +504 -0
  18. voice_agent/billing.py +396 -0
  19. voice_agent/cli.py +880 -0
  20. voice_agent/config.py +177 -0
  21. voice_agent/default_config.toml +160 -0
  22. voice_agent/drafter.py +178 -0
  23. voice_agent/events.py +51 -0
  24. voice_agent/hooks/__init__.py +0 -0
  25. voice_agent/hooks/on_notification.py +22 -0
  26. voice_agent/hooks/on_permission.py +45 -0
  27. voice_agent/hooks/on_prompt.py +28 -0
  28. voice_agent/hooks/on_stop.py +31 -0
  29. voice_agent/hooks/on_tool.py +20 -0
  30. voice_agent/jev.py +73 -0
  31. voice_agent/keys.py +66 -0
  32. voice_agent/listen.py +1755 -0
  33. voice_agent/llm/__init__.py +114 -0
  34. voice_agent/llm/anthropic_provider.py +113 -0
  35. voice_agent/llm/openai_provider.py +147 -0
  36. voice_agent/models.py +138 -0
  37. voice_agent/openai_decisions.py +87 -0
  38. voice_agent/permissions.py +51 -0
  39. voice_agent/research.py +300 -0
  40. voice_agent/router.py +256 -0
  41. voice_agent/session.py +97 -0
  42. voice_agent/settings.py +143 -0
  43. voice_agent/share.py +231 -0
  44. voice_agent/speaker_id.py +216 -0
  45. voice_agent/speech.py +97 -0
  46. voice_agent/tmux.py +275 -0
  47. voice_agent/transcribe.py +85 -0
  48. voice_agent/tts.py +126 -0
  49. vybespk-0.1.0.dist-info/METADATA +424 -0
  50. vybespk-0.1.0.dist-info/RECORD +54 -0
  51. vybespk-0.1.0.dist-info/WHEEL +4 -0
  52. vybespk-0.1.0.dist-info/entry_points.txt +2 -0
  53. vybespk-0.1.0.dist-info/licenses/LICENSE.md +105 -0
  54. vybespk-0.1.0.dist-info/licenses/voice_agent/_vendor/faster_whisper/LICENSE +21 -0
@@ -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