fala 0.0.1__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.
Potentially problematic release.
This version of fala might be problematic. Click here for more details.
- fala/__init__.py +3 -0
- fala/audio_capture.py +156 -0
- fala/cli.py +1395 -0
- fala/config.py +583 -0
- fala/deps.py +104 -0
- fala/record.py +65 -0
- fala/script_templates.py +532 -0
- fala/stats.py +328 -0
- fala/streaming.py +480 -0
- fala/transcribe.py +100 -0
- fala-0.0.1.dist-info/METADATA +205 -0
- fala-0.0.1.dist-info/RECORD +15 -0
- fala-0.0.1.dist-info/WHEEL +4 -0
- fala-0.0.1.dist-info/entry_points.txt +3 -0
- fala-0.0.1.dist-info/licenses/LICENSE +21 -0
fala/__init__.py
ADDED
fala/audio_capture.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Audio capture from microphone in real-time."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Self
|
|
8
|
+
|
|
9
|
+
import pyaudio
|
|
10
|
+
|
|
11
|
+
# Audio configuration for transcription APIs
|
|
12
|
+
SAMPLE_RATE = 16000
|
|
13
|
+
CHANNELS = 1
|
|
14
|
+
FORMAT = pyaudio.paInt16
|
|
15
|
+
FRAMES_PER_BUFFER = 800 # 50ms at 16kHz
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AudioCapture:
|
|
19
|
+
"""Captures audio from microphone and yields chunks in real-time."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
device_index: int | None = None,
|
|
24
|
+
sample_rate: int = SAMPLE_RATE,
|
|
25
|
+
channels: int = CHANNELS,
|
|
26
|
+
frames_per_buffer: int = FRAMES_PER_BUFFER,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Initialize audio capture with device and format settings."""
|
|
29
|
+
self.sample_rate = sample_rate
|
|
30
|
+
self.channels = channels
|
|
31
|
+
self.frames_per_buffer = frames_per_buffer
|
|
32
|
+
self.device_index = device_index
|
|
33
|
+
self._pyaudio: pyaudio.PyAudio | None = None
|
|
34
|
+
self._stream: pyaudio.Stream | None = None
|
|
35
|
+
self._stop_event = threading.Event()
|
|
36
|
+
|
|
37
|
+
def __enter__(self) -> Self:
|
|
38
|
+
"""Context manager entry: start capture."""
|
|
39
|
+
self.start()
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
43
|
+
"""Context manager exit: stop capture."""
|
|
44
|
+
self.stop()
|
|
45
|
+
|
|
46
|
+
def start(self) -> None:
|
|
47
|
+
"""Open the audio stream for capture."""
|
|
48
|
+
self._pyaudio = pyaudio.PyAudio()
|
|
49
|
+
self._stream = self._pyaudio.open(
|
|
50
|
+
format=FORMAT,
|
|
51
|
+
channels=self.channels,
|
|
52
|
+
rate=self.sample_rate,
|
|
53
|
+
input=True,
|
|
54
|
+
frames_per_buffer=self.frames_per_buffer,
|
|
55
|
+
input_device_index=self.device_index,
|
|
56
|
+
)
|
|
57
|
+
self._stop_event.clear()
|
|
58
|
+
|
|
59
|
+
def stop(self) -> None:
|
|
60
|
+
"""Stop capture and release resources."""
|
|
61
|
+
self.request_stop()
|
|
62
|
+
if self._stream:
|
|
63
|
+
try:
|
|
64
|
+
self._stream.stop_stream()
|
|
65
|
+
self._stream.close()
|
|
66
|
+
except Exception: # noqa: S110
|
|
67
|
+
pass
|
|
68
|
+
finally:
|
|
69
|
+
self._stream = None
|
|
70
|
+
if self._pyaudio:
|
|
71
|
+
try:
|
|
72
|
+
self._pyaudio.terminate()
|
|
73
|
+
except Exception: # noqa: S110
|
|
74
|
+
pass
|
|
75
|
+
finally:
|
|
76
|
+
self._pyaudio = None
|
|
77
|
+
|
|
78
|
+
def request_stop(self) -> None:
|
|
79
|
+
"""Signal the capture loop to stop without tearing down audio resources."""
|
|
80
|
+
self._stop_event.set()
|
|
81
|
+
|
|
82
|
+
def chunks(self) -> Iterator[bytes]:
|
|
83
|
+
"""Yield audio chunks from the microphone."""
|
|
84
|
+
if self._stream is None:
|
|
85
|
+
raise RuntimeError("AudioCapture not started. Call start() first.")
|
|
86
|
+
# Capture local reference to avoid None during iteration
|
|
87
|
+
stream = self._stream
|
|
88
|
+
while not self._stop_event.is_set():
|
|
89
|
+
try:
|
|
90
|
+
assert stream is not None # for type checker
|
|
91
|
+
data = stream.read(self.frames_per_buffer, exception_on_overflow=False)
|
|
92
|
+
yield data
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print(f"Audio capture error: {e}", flush=True)
|
|
95
|
+
if self._stop_event.is_set():
|
|
96
|
+
break
|
|
97
|
+
time.sleep(0.01)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def list_devices() -> list[dict]:
|
|
101
|
+
"""Return list of available audio input devices."""
|
|
102
|
+
p = pyaudio.PyAudio()
|
|
103
|
+
devices = []
|
|
104
|
+
try:
|
|
105
|
+
for i in range(p.get_device_count()):
|
|
106
|
+
info = p.get_device_info_by_index(i)
|
|
107
|
+
# Cast to int to satisfy type checker
|
|
108
|
+
if int(info["maxInputChannels"]) > 0:
|
|
109
|
+
devices.append(
|
|
110
|
+
{
|
|
111
|
+
"index": i,
|
|
112
|
+
"name": info["name"],
|
|
113
|
+
"channels": int(info["maxInputChannels"]),
|
|
114
|
+
"sample_rate": int(info["defaultSampleRate"]),
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
finally:
|
|
118
|
+
p.terminate()
|
|
119
|
+
return devices
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def record_to_file(
|
|
123
|
+
device_index: int | None,
|
|
124
|
+
duration_seconds: float,
|
|
125
|
+
output_path: Path,
|
|
126
|
+
sample_rate: int = SAMPLE_RATE,
|
|
127
|
+
channels: int = CHANNELS,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Record audio for a fixed duration and save to WAV file."""
|
|
130
|
+
import wave
|
|
131
|
+
|
|
132
|
+
p = pyaudio.PyAudio()
|
|
133
|
+
try:
|
|
134
|
+
stream = p.open(
|
|
135
|
+
format=FORMAT,
|
|
136
|
+
channels=channels,
|
|
137
|
+
rate=sample_rate,
|
|
138
|
+
input=True,
|
|
139
|
+
frames_per_buffer=FRAMES_PER_BUFFER,
|
|
140
|
+
input_device_index=device_index,
|
|
141
|
+
)
|
|
142
|
+
frames = []
|
|
143
|
+
chunks_needed = int(duration_seconds * sample_rate / FRAMES_PER_BUFFER)
|
|
144
|
+
for _ in range(chunks_needed):
|
|
145
|
+
data = stream.read(FRAMES_PER_BUFFER, exception_on_overflow=False)
|
|
146
|
+
frames.append(data)
|
|
147
|
+
stream.stop_stream()
|
|
148
|
+
stream.close()
|
|
149
|
+
|
|
150
|
+
with wave.open(str(output_path), "wb") as wf:
|
|
151
|
+
wf.setnchannels(channels)
|
|
152
|
+
wf.setsampwidth(p.get_sample_size(FORMAT))
|
|
153
|
+
wf.setframerate(sample_rate)
|
|
154
|
+
wf.writeframes(b"".join(frames))
|
|
155
|
+
finally:
|
|
156
|
+
p.terminate()
|