s2t 0.1.3__py3-none-any.whl → 0.1.4__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.
s2t/recorder.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import queue
4
5
  import select
5
6
  import sys
@@ -46,6 +47,8 @@ class Recorder:
46
47
  raise RuntimeError("sounddevice/soundfile required for recording.") from e
47
48
 
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()
49
52
  stop_evt = threading.Event()
50
53
 
51
54
  def key_reader() -> None:
@@ -61,10 +64,14 @@ class Recorder:
61
64
  ms = cast(_MSVCRT, msvcrt)
62
65
 
63
66
  last_space = 0.0
67
+ if self.verbose:
68
+ print("[key] using msvcrt (Windows)", file=sys.stderr)
64
69
  while not stop_evt.is_set():
65
70
  if ms.kbhit():
66
71
  ch = ms.getwch()
67
72
  if ch in ("\r", "\n"):
73
+ if self.verbose:
74
+ print("[key] ENTER", file=sys.stderr)
68
75
  evt_q.put("ENTER")
69
76
  break
70
77
  if ch == " ":
@@ -74,33 +81,118 @@ class Recorder:
74
81
  ):
75
82
  continue
76
83
  last_space = now
84
+ if self.verbose:
85
+ print("[key] SPACE", file=sys.stderr)
77
86
  evt_q.put("SPACE")
78
87
  time.sleep(0.01)
79
88
  else:
80
- fd = sys.stdin.fileno()
81
- old = termios.tcgetattr(fd)
82
- tty.setcbreak(fd)
83
- last_space = 0.0
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.
84
90
  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
- ):
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)
97
178
  continue
98
- last_space = now
99
- evt_q.put("SPACE")
100
- finally:
101
- termios.tcsetattr(fd, termios.TCSADRAIN, old)
102
- except Exception:
103
- pass
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)
104
196
 
105
197
  audio_q: queue.Queue[tuple[str, Any]] = queue.Queue(maxsize=128)
106
198
  chunk_index = 1
@@ -117,67 +209,106 @@ class Recorder:
117
209
  str(cur_path), mode="w", samplerate=self.samplerate, channels=self.channels
118
210
  )
119
211
  while True:
120
- kind, payload = audio_q.get()
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
121
287
  if kind == "frames":
122
288
  data = payload
123
289
  fh.write(data)
124
290
  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
291
  tx_queue.put((-1, Path(), 0, 0.0))
175
292
 
293
+ # Timestamp of last dropped-frame warning (throttling for verbose mode)
294
+ last_drop_log = 0.0
295
+
176
296
  def cb(indata: Any, frames: int, time_info: Any, status: Any) -> None:
297
+ nonlocal last_drop_log
177
298
  if status:
178
299
  print(status, file=sys.stderr)
179
300
  if not self._paused:
180
- audio_q.put(("frames", indata.copy()))
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
181
312
 
182
313
  key_t = threading.Thread(target=key_reader, daemon=True)
183
314
  writer_t = threading.Thread(target=writer_fn, daemon=True)
@@ -197,9 +328,9 @@ class Recorder:
197
328
  except queue.Empty:
198
329
  continue
199
330
  if evt == "SPACE":
200
- audio_q.put(("split", None))
331
+ ctrl_q.put("split")
201
332
  elif evt == "ENTER":
202
- audio_q.put(("finish", None))
333
+ ctrl_q.put("finish")
203
334
  break
204
335
  writer_t.join()
205
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
@@ -3,14 +3,14 @@ s2t/cli.py,sha256=Qf6Hz0Ew9ncLbQQoCPDG7ZiYWeGbwBcZMZi_WbEu54w,20018
3
3
  s2t/config.py,sha256=lFc_x5fIx_q0JpTcI4Lm4aubxhIXVH34foBvLMUNFGs,437
4
4
  s2t/outputs.py,sha256=Lo8VcARZ7QPuuQQNu8myD5J4c4NO1Rs0L1DLnzLe9tM,1546
5
5
  s2t/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
- s2t/recorder.py,sha256=uBD9mYf-uUCkRJw8fQitVnDrX6PwRNXJycyY4dBfXL0,8076
6
+ s2t/recorder.py,sha256=0sw1UJqQIRdiJO5dugUxRjTN5kFU0CBETVjoQz99a8E,16055
7
7
  s2t/types.py,sha256=jBiRN-tr0qVw-lhaXvnsyKrVGDyLkqEbxs9qkQ6qGqI,339
8
8
  s2t/utils.py,sha256=YU6YhiuONmqhrKte4DY5tiC5PP-yFExJMMBzFUiA8qA,3416
9
9
  s2t/whisper_engine.py,sha256=x-V7ST9e3JnwMWdbMh4C7dHjA420jaOtXH2-igeh7vc,6492
10
10
  s2t/translator/__init__.py,sha256=K-MKves7kZ4-62POfrmWeOcBaTjsTzeFSu8QNHqYuus,239
11
11
  s2t/translator/argos_backend.py,sha256=VW_OYFFBuNZgcWM-fbvR6XGokuxS2fptkCMFIO9MD1I,19068
12
- s2t-0.1.3.dist-info/METADATA,sha256=V0l2MbvH4Kd5nt9Qk3jAoLQIoo1If0w2pSjjG901CQA,4642
13
- s2t-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- s2t-0.1.3.dist-info/entry_points.txt,sha256=JISIUlZAJ3DX1dB6zT3X_E3vcXI-eWEQKwHiT35fPKs,37
15
- s2t-0.1.3.dist-info/top_level.txt,sha256=o8N0JcuHdIrfX3iGHvntHiDC2XgN7__joyNu08ZOh0s,4
16
- s2t-0.1.3.dist-info/RECORD,,
12
+ s2t-0.1.4.dist-info/METADATA,sha256=oQYIN7eNFsSBvLQZTaORC_TvJtp0AUuhkuVmMIsfI28,4642
13
+ s2t-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ s2t-0.1.4.dist-info/entry_points.txt,sha256=JISIUlZAJ3DX1dB6zT3X_E3vcXI-eWEQKwHiT35fPKs,37
15
+ s2t-0.1.4.dist-info/top_level.txt,sha256=o8N0JcuHdIrfX3iGHvntHiDC2XgN7__joyNu08ZOh0s,4
16
+ s2t-0.1.4.dist-info/RECORD,,
File without changes