s2t 0.1.3__tar.gz → 0.1.3.post1.dev1__tar.gz
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.
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/PKG-INFO +1 -1
- s2t-0.1.3.post1.dev1/src/s2t/recorder.py +336 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t.egg-info/PKG-INFO +1 -1
- s2t-0.1.3/src/s2t/recorder.py +0 -205
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/.gitignore +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/.pre-commit-config.yaml +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/AGENTS.md +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/CONTRIBUTING.md +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/MANIFEST.in +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/Makefile +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/README.md +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/docs/RELEASING.md +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/docs/SESSION_STATE.md +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/pyproject.toml +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/scripts/bench_transcribe.py +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/setup.cfg +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t/__init__.py +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t/cli.py +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t/config.py +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t/outputs.py +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t/py.typed +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t/translator/__init__.py +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t/translator/argos_backend.py +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t/types.py +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t/utils.py +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t/whisper_engine.py +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t.egg-info/SOURCES.txt +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t.egg-info/dependency_links.txt +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t.egg-info/entry_points.txt +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t.egg-info/requires.txt +0 -0
- {s2t-0.1.3 → s2t-0.1.3.post1.dev1}/src/s2t.egg-info/top_level.txt +0 -0
@@ -0,0 +1,336 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
import queue
|
5
|
+
import select
|
6
|
+
import sys
|
7
|
+
import threading
|
8
|
+
import time
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Any, Protocol, cast, runtime_checkable
|
11
|
+
|
12
|
+
|
13
|
+
class Recorder:
|
14
|
+
def __init__(
|
15
|
+
self,
|
16
|
+
session_dir: Path,
|
17
|
+
samplerate: int,
|
18
|
+
channels: int,
|
19
|
+
ext: str,
|
20
|
+
debounce_ms: int = 0,
|
21
|
+
verbose: bool = False,
|
22
|
+
pause_after_first_chunk: bool = False,
|
23
|
+
resume_event: threading.Event | None = None,
|
24
|
+
) -> None:
|
25
|
+
self.session_dir = session_dir
|
26
|
+
self.samplerate = samplerate
|
27
|
+
self.channels = channels
|
28
|
+
self.ext = ext
|
29
|
+
self.debounce_ms = max(0, int(debounce_ms))
|
30
|
+
self.verbose = verbose
|
31
|
+
self.pause_after_first_chunk = pause_after_first_chunk
|
32
|
+
self.resume_event = resume_event
|
33
|
+
self._paused = False
|
34
|
+
|
35
|
+
def run(
|
36
|
+
self,
|
37
|
+
tx_queue: queue.Queue[tuple[int, Path, int, float]],
|
38
|
+
) -> tuple[list[Path], list[int], list[float]]:
|
39
|
+
import platform
|
40
|
+
import termios
|
41
|
+
import tty
|
42
|
+
|
43
|
+
try:
|
44
|
+
import sounddevice as sd
|
45
|
+
import soundfile as sf
|
46
|
+
except Exception as e:
|
47
|
+
raise RuntimeError("sounddevice/soundfile required for recording.") from e
|
48
|
+
|
49
|
+
evt_q: queue.Queue[str] = queue.Queue()
|
50
|
+
# Control queue is separate from audio frames to avoid control backpressure.
|
51
|
+
ctrl_q: queue.Queue[str] = queue.Queue()
|
52
|
+
stop_evt = threading.Event()
|
53
|
+
|
54
|
+
def key_reader() -> None:
|
55
|
+
try:
|
56
|
+
if platform.system() == "Windows":
|
57
|
+
import msvcrt
|
58
|
+
|
59
|
+
@runtime_checkable
|
60
|
+
class _MSVCRT(Protocol):
|
61
|
+
def kbhit(self) -> int: ...
|
62
|
+
def getwch(self) -> str: ...
|
63
|
+
|
64
|
+
ms = cast(_MSVCRT, msvcrt)
|
65
|
+
|
66
|
+
last_space = 0.0
|
67
|
+
if self.verbose:
|
68
|
+
print("[key] using msvcrt (Windows)", file=sys.stderr)
|
69
|
+
while not stop_evt.is_set():
|
70
|
+
if ms.kbhit():
|
71
|
+
ch = ms.getwch()
|
72
|
+
if ch in ("\r", "\n"):
|
73
|
+
if self.verbose:
|
74
|
+
print("[key] ENTER", file=sys.stderr)
|
75
|
+
evt_q.put("ENTER")
|
76
|
+
break
|
77
|
+
if ch == " ":
|
78
|
+
now = time.perf_counter()
|
79
|
+
if self.debounce_ms and (now - last_space) < (
|
80
|
+
self.debounce_ms / 1000.0
|
81
|
+
):
|
82
|
+
continue
|
83
|
+
last_space = now
|
84
|
+
if self.verbose:
|
85
|
+
print("[key] SPACE", file=sys.stderr)
|
86
|
+
evt_q.put("SPACE")
|
87
|
+
time.sleep(0.01)
|
88
|
+
else:
|
89
|
+
# Prefer sys.stdin when it's a TTY (original, proven path). If not a TTY, try /dev/tty, else fallback to stdin line reads.
|
90
|
+
try:
|
91
|
+
if sys.stdin.isatty():
|
92
|
+
fd = sys.stdin.fileno()
|
93
|
+
if self.verbose:
|
94
|
+
print("[key] using sys.stdin (isatty, fd read)", file=sys.stderr)
|
95
|
+
old = termios.tcgetattr(fd)
|
96
|
+
tty.setcbreak(fd)
|
97
|
+
last_space = 0.0
|
98
|
+
try:
|
99
|
+
while not stop_evt.is_set():
|
100
|
+
r, _, _ = select.select([fd], [], [], 0.05)
|
101
|
+
if r:
|
102
|
+
try:
|
103
|
+
ch_b = os.read(fd, 1)
|
104
|
+
except BlockingIOError:
|
105
|
+
continue
|
106
|
+
if not ch_b:
|
107
|
+
continue
|
108
|
+
ch = ch_b.decode(errors="ignore")
|
109
|
+
if ch in ("\n", "\r"):
|
110
|
+
if self.verbose:
|
111
|
+
print("[key] ENTER", file=sys.stderr)
|
112
|
+
evt_q.put("ENTER")
|
113
|
+
break
|
114
|
+
if ch == " ":
|
115
|
+
now = time.perf_counter()
|
116
|
+
if self.debounce_ms and (now - last_space) < (
|
117
|
+
self.debounce_ms / 1000.0
|
118
|
+
):
|
119
|
+
continue
|
120
|
+
last_space = now
|
121
|
+
if self.verbose:
|
122
|
+
print("[key] SPACE", file=sys.stderr)
|
123
|
+
evt_q.put("SPACE")
|
124
|
+
finally:
|
125
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
126
|
+
else:
|
127
|
+
# Try /dev/tty when stdin is not a TTY
|
128
|
+
using_devtty = False
|
129
|
+
fd = None
|
130
|
+
try:
|
131
|
+
fd = os.open("/dev/tty", os.O_RDONLY)
|
132
|
+
using_devtty = True
|
133
|
+
if self.verbose:
|
134
|
+
print("[key] using /dev/tty (stdin not TTY)", file=sys.stderr)
|
135
|
+
old = termios.tcgetattr(fd)
|
136
|
+
tty.setcbreak(fd)
|
137
|
+
last_space = 0.0
|
138
|
+
try:
|
139
|
+
while not stop_evt.is_set():
|
140
|
+
r, _, _ = select.select([fd], [], [], 0.05)
|
141
|
+
if r:
|
142
|
+
ch_b = os.read(fd, 1)
|
143
|
+
if not ch_b:
|
144
|
+
continue
|
145
|
+
ch = ch_b.decode(errors="ignore")
|
146
|
+
if ch in ("\n", "\r"):
|
147
|
+
if self.verbose:
|
148
|
+
print("[key] ENTER", file=sys.stderr)
|
149
|
+
evt_q.put("ENTER")
|
150
|
+
break
|
151
|
+
if ch == " ":
|
152
|
+
now = time.perf_counter()
|
153
|
+
if self.debounce_ms and (now - last_space) < (
|
154
|
+
self.debounce_ms / 1000.0
|
155
|
+
):
|
156
|
+
continue
|
157
|
+
last_space = now
|
158
|
+
if self.verbose:
|
159
|
+
print("[key] SPACE", file=sys.stderr)
|
160
|
+
evt_q.put("SPACE")
|
161
|
+
finally:
|
162
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
163
|
+
except Exception:
|
164
|
+
if using_devtty and fd is not None:
|
165
|
+
try:
|
166
|
+
os.close(fd)
|
167
|
+
except Exception:
|
168
|
+
pass
|
169
|
+
print(
|
170
|
+
"Warning: no TTY for key input; falling back to stdin line mode.",
|
171
|
+
file=sys.stderr,
|
172
|
+
)
|
173
|
+
# Last resort: line-buffered stdin; Enter will still end.
|
174
|
+
while not stop_evt.is_set():
|
175
|
+
line = sys.stdin.readline()
|
176
|
+
if not line:
|
177
|
+
time.sleep(0.05)
|
178
|
+
continue
|
179
|
+
# If user hits Enter on empty line, treat as ENTER
|
180
|
+
if line == "\n" or line == "\r\n":
|
181
|
+
if self.verbose:
|
182
|
+
print("[key] ENTER (line mode)", file=sys.stderr)
|
183
|
+
evt_q.put("ENTER")
|
184
|
+
break
|
185
|
+
# If first non-empty char is space, treat as SPACE
|
186
|
+
if line and line[0] == " ":
|
187
|
+
if self.verbose:
|
188
|
+
print("[key] SPACE (line mode)", file=sys.stderr)
|
189
|
+
evt_q.put("SPACE")
|
190
|
+
except Exception as e:
|
191
|
+
print(f"Warning: key reader failed: {e}", file=sys.stderr)
|
192
|
+
|
193
|
+
except Exception as e:
|
194
|
+
# Log unexpected key reader errors to aid debugging, but keep recording running.
|
195
|
+
print(f"Warning: key reader stopped unexpectedly: {e}", file=sys.stderr)
|
196
|
+
|
197
|
+
audio_q: queue.Queue[tuple[str, Any]] = queue.Queue(maxsize=128)
|
198
|
+
chunk_index = 1
|
199
|
+
chunk_paths: list[Path] = []
|
200
|
+
chunk_frames: list[int] = []
|
201
|
+
chunk_offsets: list[float] = []
|
202
|
+
offset_seconds_total = 0.0
|
203
|
+
|
204
|
+
def writer_fn() -> None:
|
205
|
+
nonlocal chunk_index, offset_seconds_total
|
206
|
+
frames_written = 0
|
207
|
+
cur_path = self.session_dir / f"chunk_{chunk_index:04d}{self.ext}"
|
208
|
+
fh = sf.SoundFile(
|
209
|
+
str(cur_path), mode="w", samplerate=self.samplerate, channels=self.channels
|
210
|
+
)
|
211
|
+
while True:
|
212
|
+
# First, handle any pending control commands so SPACE/ENTER are never blocked by frames backlog.
|
213
|
+
try:
|
214
|
+
while True:
|
215
|
+
cmd = ctrl_q.get_nowait()
|
216
|
+
if cmd == "split":
|
217
|
+
fh.flush()
|
218
|
+
fh.close()
|
219
|
+
if frames_written > 0:
|
220
|
+
dur = frames_written / float(self.samplerate)
|
221
|
+
chunk_paths.append(cur_path)
|
222
|
+
chunk_frames.append(frames_written)
|
223
|
+
chunk_offsets.append(offset_seconds_total)
|
224
|
+
offset_seconds_total += dur
|
225
|
+
if self.verbose:
|
226
|
+
print(
|
227
|
+
f"Saved chunk: {cur_path.name} ({dur:.2f}s)",
|
228
|
+
file=sys.stderr,
|
229
|
+
)
|
230
|
+
tx_queue.put(
|
231
|
+
(chunk_index, cur_path, frames_written, chunk_offsets[-1])
|
232
|
+
)
|
233
|
+
else:
|
234
|
+
try:
|
235
|
+
cur_path.unlink(missing_ok=True)
|
236
|
+
except Exception:
|
237
|
+
pass
|
238
|
+
frames_written = 0
|
239
|
+
chunk_index += 1
|
240
|
+
if (
|
241
|
+
self.pause_after_first_chunk
|
242
|
+
and chunk_index == 2
|
243
|
+
and self.resume_event is not None
|
244
|
+
):
|
245
|
+
self._paused = True
|
246
|
+
self.resume_event.wait()
|
247
|
+
self._paused = False
|
248
|
+
cur_path = self.session_dir / f"chunk_{chunk_index:04d}{self.ext}"
|
249
|
+
fh = sf.SoundFile(
|
250
|
+
str(cur_path),
|
251
|
+
mode="w",
|
252
|
+
samplerate=self.samplerate,
|
253
|
+
channels=self.channels,
|
254
|
+
)
|
255
|
+
elif cmd == "finish":
|
256
|
+
fh.flush()
|
257
|
+
fh.close()
|
258
|
+
if frames_written > 0:
|
259
|
+
dur = frames_written / float(self.samplerate)
|
260
|
+
chunk_paths.append(cur_path)
|
261
|
+
chunk_frames.append(frames_written)
|
262
|
+
chunk_offsets.append(offset_seconds_total)
|
263
|
+
offset_seconds_total += dur
|
264
|
+
if self.verbose:
|
265
|
+
print(
|
266
|
+
f"Saved chunk: {cur_path.name} ({dur:.2f}s)",
|
267
|
+
file=sys.stderr,
|
268
|
+
)
|
269
|
+
tx_queue.put(
|
270
|
+
(chunk_index, cur_path, frames_written, chunk_offsets[-1])
|
271
|
+
)
|
272
|
+
else:
|
273
|
+
try:
|
274
|
+
cur_path.unlink(missing_ok=True)
|
275
|
+
except Exception:
|
276
|
+
pass
|
277
|
+
tx_queue.put((-1, Path(), 0, 0.0))
|
278
|
+
return
|
279
|
+
except queue.Empty:
|
280
|
+
pass
|
281
|
+
|
282
|
+
# Then, write frames if available; short timeout to re-check control queue regularly.
|
283
|
+
try:
|
284
|
+
kind, payload = audio_q.get(timeout=0.05)
|
285
|
+
except queue.Empty:
|
286
|
+
continue
|
287
|
+
if kind == "frames":
|
288
|
+
data = payload
|
289
|
+
fh.write(data)
|
290
|
+
frames_written += len(data)
|
291
|
+
tx_queue.put((-1, Path(), 0, 0.0))
|
292
|
+
|
293
|
+
# Timestamp of last dropped-frame warning (throttling for verbose mode)
|
294
|
+
last_drop_log = 0.0
|
295
|
+
|
296
|
+
def cb(indata: Any, frames: int, time_info: Any, status: Any) -> None:
|
297
|
+
nonlocal last_drop_log
|
298
|
+
if status:
|
299
|
+
print(status, file=sys.stderr)
|
300
|
+
if not self._paused:
|
301
|
+
try:
|
302
|
+
audio_q.put_nowait(("frames", indata.copy()))
|
303
|
+
except queue.Full:
|
304
|
+
# Drop frame if the queue is saturated; throttle warnings.
|
305
|
+
now = time.perf_counter()
|
306
|
+
if self.verbose and (now - last_drop_log) > 1.0:
|
307
|
+
print(
|
308
|
+
"Warning: audio queue full; dropping input frames.",
|
309
|
+
file=sys.stderr,
|
310
|
+
)
|
311
|
+
last_drop_log = now
|
312
|
+
|
313
|
+
key_t = threading.Thread(target=key_reader, daemon=True)
|
314
|
+
writer_t = threading.Thread(target=writer_fn, daemon=True)
|
315
|
+
key_t.start()
|
316
|
+
writer_t.start()
|
317
|
+
|
318
|
+
print("Recording… Press SPACE to split, Enter to finish.")
|
319
|
+
print("—" * 60)
|
320
|
+
print("")
|
321
|
+
|
322
|
+
import sounddevice as sd
|
323
|
+
|
324
|
+
with sd.InputStream(samplerate=self.samplerate, channels=self.channels, callback=cb):
|
325
|
+
while True:
|
326
|
+
try:
|
327
|
+
evt = evt_q.get(timeout=0.05)
|
328
|
+
except queue.Empty:
|
329
|
+
continue
|
330
|
+
if evt == "SPACE":
|
331
|
+
ctrl_q.put("split")
|
332
|
+
elif evt == "ENTER":
|
333
|
+
ctrl_q.put("finish")
|
334
|
+
break
|
335
|
+
writer_t.join()
|
336
|
+
return chunk_paths, chunk_frames, chunk_offsets
|
s2t-0.1.3/src/s2t/recorder.py
DELETED
@@ -1,205 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import queue
|
4
|
-
import select
|
5
|
-
import sys
|
6
|
-
import threading
|
7
|
-
import time
|
8
|
-
from pathlib import Path
|
9
|
-
from typing import Any, Protocol, cast, runtime_checkable
|
10
|
-
|
11
|
-
|
12
|
-
class Recorder:
|
13
|
-
def __init__(
|
14
|
-
self,
|
15
|
-
session_dir: Path,
|
16
|
-
samplerate: int,
|
17
|
-
channels: int,
|
18
|
-
ext: str,
|
19
|
-
debounce_ms: int = 0,
|
20
|
-
verbose: bool = False,
|
21
|
-
pause_after_first_chunk: bool = False,
|
22
|
-
resume_event: threading.Event | None = None,
|
23
|
-
) -> None:
|
24
|
-
self.session_dir = session_dir
|
25
|
-
self.samplerate = samplerate
|
26
|
-
self.channels = channels
|
27
|
-
self.ext = ext
|
28
|
-
self.debounce_ms = max(0, int(debounce_ms))
|
29
|
-
self.verbose = verbose
|
30
|
-
self.pause_after_first_chunk = pause_after_first_chunk
|
31
|
-
self.resume_event = resume_event
|
32
|
-
self._paused = False
|
33
|
-
|
34
|
-
def run(
|
35
|
-
self,
|
36
|
-
tx_queue: queue.Queue[tuple[int, Path, int, float]],
|
37
|
-
) -> tuple[list[Path], list[int], list[float]]:
|
38
|
-
import platform
|
39
|
-
import termios
|
40
|
-
import tty
|
41
|
-
|
42
|
-
try:
|
43
|
-
import sounddevice as sd
|
44
|
-
import soundfile as sf
|
45
|
-
except Exception as e:
|
46
|
-
raise RuntimeError("sounddevice/soundfile required for recording.") from e
|
47
|
-
|
48
|
-
evt_q: queue.Queue[str] = queue.Queue()
|
49
|
-
stop_evt = threading.Event()
|
50
|
-
|
51
|
-
def key_reader() -> None:
|
52
|
-
try:
|
53
|
-
if platform.system() == "Windows":
|
54
|
-
import msvcrt
|
55
|
-
|
56
|
-
@runtime_checkable
|
57
|
-
class _MSVCRT(Protocol):
|
58
|
-
def kbhit(self) -> int: ...
|
59
|
-
def getwch(self) -> str: ...
|
60
|
-
|
61
|
-
ms = cast(_MSVCRT, msvcrt)
|
62
|
-
|
63
|
-
last_space = 0.0
|
64
|
-
while not stop_evt.is_set():
|
65
|
-
if ms.kbhit():
|
66
|
-
ch = ms.getwch()
|
67
|
-
if ch in ("\r", "\n"):
|
68
|
-
evt_q.put("ENTER")
|
69
|
-
break
|
70
|
-
if ch == " ":
|
71
|
-
now = time.perf_counter()
|
72
|
-
if self.debounce_ms and (now - last_space) < (
|
73
|
-
self.debounce_ms / 1000.0
|
74
|
-
):
|
75
|
-
continue
|
76
|
-
last_space = now
|
77
|
-
evt_q.put("SPACE")
|
78
|
-
time.sleep(0.01)
|
79
|
-
else:
|
80
|
-
fd = sys.stdin.fileno()
|
81
|
-
old = termios.tcgetattr(fd)
|
82
|
-
tty.setcbreak(fd)
|
83
|
-
last_space = 0.0
|
84
|
-
try:
|
85
|
-
while not stop_evt.is_set():
|
86
|
-
r, _, _ = select.select([sys.stdin], [], [], 0.05)
|
87
|
-
if r:
|
88
|
-
ch = sys.stdin.read(1)
|
89
|
-
if ch in ("\n", "\r"):
|
90
|
-
evt_q.put("ENTER")
|
91
|
-
break
|
92
|
-
if ch == " ":
|
93
|
-
now = time.perf_counter()
|
94
|
-
if self.debounce_ms and (now - last_space) < (
|
95
|
-
self.debounce_ms / 1000.0
|
96
|
-
):
|
97
|
-
continue
|
98
|
-
last_space = now
|
99
|
-
evt_q.put("SPACE")
|
100
|
-
finally:
|
101
|
-
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
102
|
-
except Exception:
|
103
|
-
pass
|
104
|
-
|
105
|
-
audio_q: queue.Queue[tuple[str, Any]] = queue.Queue(maxsize=128)
|
106
|
-
chunk_index = 1
|
107
|
-
chunk_paths: list[Path] = []
|
108
|
-
chunk_frames: list[int] = []
|
109
|
-
chunk_offsets: list[float] = []
|
110
|
-
offset_seconds_total = 0.0
|
111
|
-
|
112
|
-
def writer_fn() -> None:
|
113
|
-
nonlocal chunk_index, offset_seconds_total
|
114
|
-
frames_written = 0
|
115
|
-
cur_path = self.session_dir / f"chunk_{chunk_index:04d}{self.ext}"
|
116
|
-
fh = sf.SoundFile(
|
117
|
-
str(cur_path), mode="w", samplerate=self.samplerate, channels=self.channels
|
118
|
-
)
|
119
|
-
while True:
|
120
|
-
kind, payload = audio_q.get()
|
121
|
-
if kind == "frames":
|
122
|
-
data = payload
|
123
|
-
fh.write(data)
|
124
|
-
frames_written += len(data)
|
125
|
-
elif kind == "split":
|
126
|
-
fh.flush()
|
127
|
-
fh.close()
|
128
|
-
if frames_written > 0:
|
129
|
-
dur = frames_written / float(self.samplerate)
|
130
|
-
chunk_paths.append(cur_path)
|
131
|
-
chunk_frames.append(frames_written)
|
132
|
-
chunk_offsets.append(offset_seconds_total)
|
133
|
-
offset_seconds_total += dur
|
134
|
-
if self.verbose:
|
135
|
-
print(f"Saved chunk: {cur_path.name} ({dur:.2f}s)", file=sys.stderr)
|
136
|
-
tx_queue.put((chunk_index, cur_path, frames_written, chunk_offsets[-1]))
|
137
|
-
else:
|
138
|
-
try:
|
139
|
-
cur_path.unlink(missing_ok=True)
|
140
|
-
except Exception:
|
141
|
-
pass
|
142
|
-
frames_written = 0
|
143
|
-
chunk_index += 1
|
144
|
-
if (
|
145
|
-
self.pause_after_first_chunk
|
146
|
-
and chunk_index == 2
|
147
|
-
and self.resume_event is not None
|
148
|
-
):
|
149
|
-
self._paused = True
|
150
|
-
self.resume_event.wait()
|
151
|
-
self._paused = False
|
152
|
-
cur_path = self.session_dir / f"chunk_{chunk_index:04d}{self.ext}"
|
153
|
-
fh = sf.SoundFile(
|
154
|
-
str(cur_path), mode="w", samplerate=self.samplerate, channels=self.channels
|
155
|
-
)
|
156
|
-
elif kind == "finish":
|
157
|
-
fh.flush()
|
158
|
-
fh.close()
|
159
|
-
if frames_written > 0:
|
160
|
-
dur = frames_written / float(self.samplerate)
|
161
|
-
chunk_paths.append(cur_path)
|
162
|
-
chunk_frames.append(frames_written)
|
163
|
-
chunk_offsets.append(offset_seconds_total)
|
164
|
-
offset_seconds_total += dur
|
165
|
-
if self.verbose:
|
166
|
-
print(f"Saved chunk: {cur_path.name} ({dur:.2f}s)", file=sys.stderr)
|
167
|
-
tx_queue.put((chunk_index, cur_path, frames_written, chunk_offsets[-1]))
|
168
|
-
else:
|
169
|
-
try:
|
170
|
-
cur_path.unlink(missing_ok=True)
|
171
|
-
except Exception:
|
172
|
-
pass
|
173
|
-
break
|
174
|
-
tx_queue.put((-1, Path(), 0, 0.0))
|
175
|
-
|
176
|
-
def cb(indata: Any, frames: int, time_info: Any, status: Any) -> None:
|
177
|
-
if status:
|
178
|
-
print(status, file=sys.stderr)
|
179
|
-
if not self._paused:
|
180
|
-
audio_q.put(("frames", indata.copy()))
|
181
|
-
|
182
|
-
key_t = threading.Thread(target=key_reader, daemon=True)
|
183
|
-
writer_t = threading.Thread(target=writer_fn, daemon=True)
|
184
|
-
key_t.start()
|
185
|
-
writer_t.start()
|
186
|
-
|
187
|
-
print("Recording… Press SPACE to split, Enter to finish.")
|
188
|
-
print("—" * 60)
|
189
|
-
print("")
|
190
|
-
|
191
|
-
import sounddevice as sd
|
192
|
-
|
193
|
-
with sd.InputStream(samplerate=self.samplerate, channels=self.channels, callback=cb):
|
194
|
-
while True:
|
195
|
-
try:
|
196
|
-
evt = evt_q.get(timeout=0.05)
|
197
|
-
except queue.Empty:
|
198
|
-
continue
|
199
|
-
if evt == "SPACE":
|
200
|
-
audio_q.put(("split", None))
|
201
|
-
elif evt == "ENTER":
|
202
|
-
audio_q.put(("finish", None))
|
203
|
-
break
|
204
|
-
writer_t.join()
|
205
|
-
return chunk_paths, chunk_frames, chunk_offsets
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|