s2t 0.1.3__tar.gz → 0.1.4__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.
Files changed (31) hide show
  1. {s2t-0.1.3 → s2t-0.1.4}/PKG-INFO +1 -1
  2. s2t-0.1.4/src/s2t/recorder.py +336 -0
  3. {s2t-0.1.3 → s2t-0.1.4}/src/s2t.egg-info/PKG-INFO +1 -1
  4. s2t-0.1.3/src/s2t/recorder.py +0 -205
  5. {s2t-0.1.3 → s2t-0.1.4}/.gitignore +0 -0
  6. {s2t-0.1.3 → s2t-0.1.4}/.pre-commit-config.yaml +0 -0
  7. {s2t-0.1.3 → s2t-0.1.4}/AGENTS.md +0 -0
  8. {s2t-0.1.3 → s2t-0.1.4}/CONTRIBUTING.md +0 -0
  9. {s2t-0.1.3 → s2t-0.1.4}/MANIFEST.in +0 -0
  10. {s2t-0.1.3 → s2t-0.1.4}/Makefile +0 -0
  11. {s2t-0.1.3 → s2t-0.1.4}/README.md +0 -0
  12. {s2t-0.1.3 → s2t-0.1.4}/docs/RELEASING.md +0 -0
  13. {s2t-0.1.3 → s2t-0.1.4}/docs/SESSION_STATE.md +0 -0
  14. {s2t-0.1.3 → s2t-0.1.4}/pyproject.toml +0 -0
  15. {s2t-0.1.3 → s2t-0.1.4}/scripts/bench_transcribe.py +0 -0
  16. {s2t-0.1.3 → s2t-0.1.4}/setup.cfg +0 -0
  17. {s2t-0.1.3 → s2t-0.1.4}/src/s2t/__init__.py +0 -0
  18. {s2t-0.1.3 → s2t-0.1.4}/src/s2t/cli.py +0 -0
  19. {s2t-0.1.3 → s2t-0.1.4}/src/s2t/config.py +0 -0
  20. {s2t-0.1.3 → s2t-0.1.4}/src/s2t/outputs.py +0 -0
  21. {s2t-0.1.3 → s2t-0.1.4}/src/s2t/py.typed +0 -0
  22. {s2t-0.1.3 → s2t-0.1.4}/src/s2t/translator/__init__.py +0 -0
  23. {s2t-0.1.3 → s2t-0.1.4}/src/s2t/translator/argos_backend.py +0 -0
  24. {s2t-0.1.3 → s2t-0.1.4}/src/s2t/types.py +0 -0
  25. {s2t-0.1.3 → s2t-0.1.4}/src/s2t/utils.py +0 -0
  26. {s2t-0.1.3 → s2t-0.1.4}/src/s2t/whisper_engine.py +0 -0
  27. {s2t-0.1.3 → s2t-0.1.4}/src/s2t.egg-info/SOURCES.txt +0 -0
  28. {s2t-0.1.3 → s2t-0.1.4}/src/s2t.egg-info/dependency_links.txt +0 -0
  29. {s2t-0.1.3 → s2t-0.1.4}/src/s2t.egg-info/entry_points.txt +0 -0
  30. {s2t-0.1.3 → s2t-0.1.4}/src/s2t.egg-info/requires.txt +0 -0
  31. {s2t-0.1.3 → s2t-0.1.4}/src/s2t.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: s2t
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Speech to Text (s2t): Record audio, run Whisper, export formats, and copy transcript to clipboard.
5
5
  Author: Maintainers
6
6
  License-Expression: LicenseRef-Proprietary
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: s2t
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Speech to Text (s2t): Record audio, run Whisper, export formats, and copy transcript to clipboard.
5
5
  Author: Maintainers
6
6
  License-Expression: LicenseRef-Proprietary
@@ -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