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 ADDED
@@ -0,0 +1,3 @@
1
+ """fala package."""
2
+
3
+ __all__ = []
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()