scribe-cli 1.2.0__tar.gz → 1.2.1__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 (78) hide show
  1. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/PKG-INFO +1 -1
  2. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/docs/cli.md +1 -0
  3. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/_version.py +3 -3
  4. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/app.py +7 -0
  5. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/backends/whisper.py +1 -0
  6. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/backends/whisper_futo.py +1 -0
  7. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/models.py +14 -0
  8. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/output.py +3 -1
  9. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/session.py +11 -0
  10. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/typers/eitype.py +7 -1
  11. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/typers/wtype.py +7 -1
  12. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_cli.egg-info/PKG-INFO +1 -1
  13. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_cli.egg-info/SOURCES.txt +2 -0
  14. scribe_cli-1.2.1/scribe_cli.egg-info/scm_file_list.json +71 -0
  15. scribe_cli-1.2.1/scribe_cli.egg-info/scm_version.json +8 -0
  16. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_clip_silence_trim.py +26 -2
  17. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_typers_ascii_fallback.py +23 -0
  18. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/.github/FUNDING.yml +0 -0
  19. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/.github/workflows/docs.yml +0 -0
  20. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/.github/workflows/pypi.yml +0 -0
  21. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/.gitignore +0 -0
  22. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/LICENSE +0 -0
  23. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/README.md +0 -0
  24. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/docs/app-tray-menu.png +0 -0
  25. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/docs/backends.md +0 -0
  26. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/docs/desktop-install.md +0 -0
  27. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/docs/index.md +0 -0
  28. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/docs/installation.md +0 -0
  29. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/docs/output.md +0 -0
  30. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/docs/quickstart.md +0 -0
  31. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/docs/roadmap-libei.md +0 -0
  32. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/docs/tray.md +0 -0
  33. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/icon.xcf +0 -0
  34. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/mkdocs.yml +0 -0
  35. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/pyproject.toml +0 -0
  36. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/__init__.py +0 -0
  37. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/audio.py +0 -0
  38. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/backends/__init__.py +0 -0
  39. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/backends/groq.py +0 -0
  40. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/backends/openai_api.py +0 -0
  41. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/backends/openai_realtime.py +0 -0
  42. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/backends/vosk.py +0 -0
  43. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/dialog.py +0 -0
  44. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/install_desktop.py +0 -0
  45. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/keyboard.py +0 -0
  46. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/menu.py +0 -0
  47. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/models.toml +0 -0
  48. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/saverecording.py +0 -0
  49. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/testpynput.py +0 -0
  50. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/typers/__init__.py +0 -0
  51. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/typers/base.py +0 -0
  52. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/typers/pynput.py +0 -0
  53. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/typers/ydotool.py +0 -0
  54. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe/util.py +0 -0
  55. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_cli.egg-info/dependency_links.txt +0 -0
  56. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_cli.egg-info/entry_points.txt +0 -0
  57. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_cli.egg-info/requires.txt +0 -0
  58. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_cli.egg-info/top_level.txt +0 -0
  59. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_data/__init__.py +0 -0
  60. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_data/share/icon.png +0 -0
  61. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_data/share/icon_recording.png +0 -0
  62. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_data/share/icon_writing.png +0 -0
  63. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_data/silero_vad.LICENSE +0 -0
  64. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_data/silero_vad.onnx +0 -0
  65. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scribe_data/templates/scribe.desktop +0 -0
  66. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scripts/bench_whisper_local.py +0 -0
  67. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/scripts/test_python_versions_install.sh +0 -0
  68. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/setup.cfg +0 -0
  69. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_backend_matrix.py +0 -0
  70. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_clipboard_backend.py +0 -0
  71. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_compose_prompt.py +0 -0
  72. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_debug_logging.py +0 -0
  73. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_openai_realtime_coalesce.py +0 -0
  74. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_output.py +0 -0
  75. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_output_file_picker.py +0 -0
  76. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_prompt_file_picker.py +0 -0
  77. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_pseudo_streaming.py +0 -0
  78. {scribe_cli-1.2.0 → scribe_cli-1.2.1}/tests/test_whisper_futo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scribe-cli
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Speech-to-text CLI and system-tray app for dictating into any focused window. Local (vosk, faster-whisper) or cloud (groq, openai) backends, batch or streaming.
5
5
  Author-email: Mahé Perrette <mahe.perrette@gmail.com>
6
6
  License: MIT License
@@ -65,6 +65,7 @@ flag suppresses only its own side (giving `--prompt ""` still loads
65
65
  | `-m, --mode {keystroke,clipboard,terminal,file}` | Where transcribed text goes (default `keystroke`). `file` routes the transcript exclusively to `--output-file` and suppresses keyboard/clipboard output. See [output.md](output.md). |
66
66
  | `--typer {auto,eitype,pynput,wtype,ydotool}` | Keystroke-injection backend (default `auto`). |
67
67
  | `--type-direct` | In keystroke mode, type the transcription as keystrokes instead of synthesising Ctrl+V. |
68
+ | `--typer-delay MS` | With `--type-direct`, milliseconds between key events for eitype/wtype (default `2`). Some apps, such as the Claude Code prompt, drop the end of text typed at 0. |
68
69
  | `-o, --output-file FILE` | Path the transcription is appended to when `--mode file`. Defaults to `<user-desktop>/scribe-notes.txt` (the platform Desktop folder — `~/Desktop` on Linux/macOS, `%USERPROFILE%\Desktop` on Windows; falls back to home dir if Desktop is absent). Ignored when `--mode` is anything other than `file` (the four output modes are mutually exclusive). |
69
70
 
70
71
  ## Silence detection
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.2.0'
22
- __version_tuple__ = version_tuple = (1, 2, 0)
21
+ __version__ = version = '1.2.1'
22
+ __version_tuple__ = version_tuple = (1, 2, 1)
23
23
 
24
- __commit_id__ = commit_id = 'gc1c4cb389'
24
+ __commit_id__ = commit_id = 'g7a322c71b'
@@ -467,6 +467,12 @@ def get_parser():
467
467
  "is the ^V control character. Cost: slower for long text; on wtype/ydotool "
468
468
  "non-ASCII characters fall back to their ASCII equivalents (eitype is "
469
469
  "Unicode-correct).")
470
+ group.add_argument("--typer-delay", default=2, type=int,
471
+ help="With --type-direct, delay in milliseconds between key events "
472
+ "for the eitype and wtype typers (default: %(default)s). Some "
473
+ "apps drop keystrokes that arrive faster than they can read them "
474
+ "(e.g. the Claude Code prompt loses the end of the text at 0). "
475
+ "Costs about 2 x delay per character.")
470
476
  group.add_argument("-o", "--output-file",
471
477
  default=DEFAULT_OUTPUT_FILE,
472
478
  help=f"Path the transcription is appended to when "
@@ -675,6 +681,7 @@ def _resolve_output(o, *, is_streaming, backend_obj):
675
681
  typer=getattr(o, "typer", None) if mode == "keystroke" else None,
676
682
  type_direct=getattr(o, "type_direct", False),
677
683
  output_file=getattr(o, "output_file", None) if mode == "file" else None,
684
+ typer_delay=getattr(o, "typer_delay", 0),
678
685
  is_streaming=is_streaming,
679
686
  backend_obj=backend_obj,
680
687
  )
@@ -54,6 +54,7 @@ class WhisperTranscriber(AbstractTranscriber):
54
54
  return {"text": text}
55
55
 
56
56
  def finalize(self):
57
+ self.flush_trailing_silence()
57
58
  if len(self.session.audio_buffer) == 0:
58
59
  return {"text": ""}
59
60
  result = self.transcribe_audio(self.session.audio_buffer)
@@ -199,6 +199,7 @@ class WhisperFutoTranscriber(AbstractTranscriber):
199
199
  return {"text": text}
200
200
 
201
201
  def finalize(self):
202
+ self.flush_trailing_silence()
202
203
  if len(self.session.audio_buffer) == 0:
203
204
  return {"text": ""}
204
205
  result = self.transcribe_audio(self.session.audio_buffer)
@@ -482,6 +482,20 @@ class AbstractTranscriber(STTBackend):
482
482
  def clear_streaming_context(self):
483
483
  self._streaming_context = ""
484
484
 
485
+ def flush_trailing_silence(self):
486
+ """Clip mode: move the silence held since the last speech block into
487
+ audio_buffer before finalize() transcribes it. The silence gate can
488
+ classify a quiet word ending as silence, and Whisper tends to drop
489
+ the last word when the audio stops right at the end of speech.
490
+ silence_buffer is already capped at clip_max_silence seconds, so at
491
+ most that much is appended. No-op when nothing was spoken, so a
492
+ silent recording still finalizes to empty text."""
493
+ session = self.session
494
+ if self.pseudo_streaming or not session.audio_buffer:
495
+ return
496
+ session.audio_buffer += session.silence_buffer
497
+ session.silence_buffer = b''
498
+
485
499
  def transcribe_audio(self, audio_data):
486
500
  raise NotImplementedError()
487
501
 
@@ -194,7 +194,7 @@ class KeyboardOutput(Output):
194
194
 
195
195
  def make_output(mode: str, *, typer: Optional[str], type_direct: bool,
196
196
  output_file: Optional[str], is_streaming: bool,
197
- backend_obj=None) -> Output:
197
+ backend_obj=None, typer_delay: int = 0) -> Output:
198
198
  """Resolve ``(mode, typer, type_direct, output_file, is_streaming)``
199
199
  into the right Output subclass.
200
200
 
@@ -230,6 +230,8 @@ def make_output(mode: str, *, typer: Optional[str], type_direct: bool,
230
230
  if type_direct:
231
231
  from scribe.typers import pick_typer
232
232
  typer_obj = pick_typer(typer if typer and typer != "auto" else None)
233
+ if hasattr(typer_obj, "key_delay_ms"):
234
+ typer_obj.key_delay_ms = typer_delay
233
235
  else:
234
236
  typer_obj = None
235
237
  return KeyboardOutput(
@@ -168,6 +168,17 @@ class RecordingSession:
168
168
  microphone.q.queue.clear()
169
169
  result = {"text": ""}
170
170
  else:
171
+ # The loop above polls the queue every 100 ms, so blocks
172
+ # captured just before stop can still be waiting here. Feed
173
+ # them to batch backends before finalizing so the last words
174
+ # aren't lost. A pseudo-streaming silence cut at this point
175
+ # is moot: finalize() runs right after anyway.
176
+ if not streaming:
177
+ try:
178
+ while not microphone.q.empty():
179
+ self.backend.transcribe_realtime_audio(microphone.q.get())
180
+ except SilenceDetected:
181
+ pass
171
182
  try:
172
183
  result = self.backend.finalize()
173
184
  except Exception as exc:
@@ -12,6 +12,9 @@ from scribe.typers.base import Typer
12
12
 
13
13
  class EitypeTyper:
14
14
  name = "eitype"
15
+ # Milliseconds between key events (`eitype -d`). Set from --typer-delay
16
+ # by scribe.output.make_output; 0 sends the whole string as one burst.
17
+ key_delay_ms: int = 0
15
18
 
16
19
  def compatible(self) -> bool:
17
20
  """Linux Wayland session; libei is a Wayland-specific protocol."""
@@ -29,7 +32,10 @@ class EitypeTyper:
29
32
 
30
33
  def _emit(self, text: str) -> None:
31
34
  try:
32
- subprocess.run(["eitype", "--", text], check=True, capture_output=True)
35
+ cmd = ["eitype"]
36
+ if self.key_delay_ms > 0:
37
+ cmd += ["-d", str(self.key_delay_ms)]
38
+ subprocess.run(cmd + ["--", text], check=True, capture_output=True)
33
39
  except subprocess.CalledProcessError as e:
34
40
  raise RuntimeError(
35
41
  f"eitype failed: {e.stderr.decode(errors='replace')}"
@@ -13,6 +13,9 @@ from scribe.typers.base import Typer
13
13
 
14
14
  class WtypeTyper:
15
15
  name = "wtype"
16
+ # Milliseconds between key events (`wtype -d`). Set from --typer-delay
17
+ # by scribe.output.make_output; 0 sends the whole string as one burst.
18
+ key_delay_ms: int = 0
16
19
 
17
20
  def compatible(self) -> bool:
18
21
  """Linux Wayland on a wlroots-based compositor (Sway, Hyprland, …).
@@ -42,7 +45,10 @@ class WtypeTyper:
42
45
 
43
46
  def _emit(self, text: str) -> None:
44
47
  try:
45
- subprocess.run(["wtype", "--", text], check=True, capture_output=True)
48
+ cmd = ["wtype"]
49
+ if self.key_delay_ms > 0:
50
+ cmd += ["-d", str(self.key_delay_ms)]
51
+ subprocess.run(cmd + ["--", text], check=True, capture_output=True)
46
52
  except subprocess.CalledProcessError as e:
47
53
  raise RuntimeError(
48
54
  f"wtype failed: {e.stderr.decode(errors='replace')}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scribe-cli
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Speech-to-text CLI and system-tray app for dictating into any focused window. Local (vosk, faster-whisper) or cloud (groq, openai) backends, batch or streaming.
5
5
  Author-email: Mahé Perrette <mahe.perrette@gmail.com>
6
6
  License: MIT License
@@ -50,6 +50,8 @@ scribe_cli.egg-info/SOURCES.txt
50
50
  scribe_cli.egg-info/dependency_links.txt
51
51
  scribe_cli.egg-info/entry_points.txt
52
52
  scribe_cli.egg-info/requires.txt
53
+ scribe_cli.egg-info/scm_file_list.json
54
+ scribe_cli.egg-info/scm_version.json
53
55
  scribe_cli.egg-info/top_level.txt
54
56
  scribe_data/__init__.py
55
57
  scribe_data/silero_vad.LICENSE
@@ -0,0 +1,71 @@
1
+ {
2
+ "files": [
3
+ ".github/FUNDING.yml",
4
+ ".github/workflows/docs.yml",
5
+ ".github/workflows/pypi.yml",
6
+ ".gitignore",
7
+ "LICENSE",
8
+ "README.md",
9
+ "docs/app-tray-menu.png",
10
+ "docs/backends.md",
11
+ "docs/cli.md",
12
+ "docs/desktop-install.md",
13
+ "docs/index.md",
14
+ "docs/installation.md",
15
+ "docs/output.md",
16
+ "docs/quickstart.md",
17
+ "docs/roadmap-libei.md",
18
+ "docs/tray.md",
19
+ "icon.xcf",
20
+ "mkdocs.yml",
21
+ "pyproject.toml",
22
+ "scribe/__init__.py",
23
+ "scribe/app.py",
24
+ "scribe/audio.py",
25
+ "scribe/backends/__init__.py",
26
+ "scribe/backends/groq.py",
27
+ "scribe/backends/openai_api.py",
28
+ "scribe/backends/openai_realtime.py",
29
+ "scribe/backends/vosk.py",
30
+ "scribe/backends/whisper.py",
31
+ "scribe/backends/whisper_futo.py",
32
+ "scribe/dialog.py",
33
+ "scribe/install_desktop.py",
34
+ "scribe/keyboard.py",
35
+ "scribe/menu.py",
36
+ "scribe/models.py",
37
+ "scribe/models.toml",
38
+ "scribe/output.py",
39
+ "scribe/saverecording.py",
40
+ "scribe/session.py",
41
+ "scribe/testpynput.py",
42
+ "scribe/typers/__init__.py",
43
+ "scribe/typers/base.py",
44
+ "scribe/typers/eitype.py",
45
+ "scribe/typers/pynput.py",
46
+ "scribe/typers/wtype.py",
47
+ "scribe/typers/ydotool.py",
48
+ "scribe/util.py",
49
+ "scribe_data/__init__.py",
50
+ "scribe_data/share/icon.png",
51
+ "scribe_data/share/icon_recording.png",
52
+ "scribe_data/share/icon_writing.png",
53
+ "scribe_data/silero_vad.LICENSE",
54
+ "scribe_data/silero_vad.onnx",
55
+ "scribe_data/templates/scribe.desktop",
56
+ "scripts/bench_whisper_local.py",
57
+ "scripts/test_python_versions_install.sh",
58
+ "tests/test_backend_matrix.py",
59
+ "tests/test_clip_silence_trim.py",
60
+ "tests/test_clipboard_backend.py",
61
+ "tests/test_compose_prompt.py",
62
+ "tests/test_debug_logging.py",
63
+ "tests/test_openai_realtime_coalesce.py",
64
+ "tests/test_output.py",
65
+ "tests/test_output_file_picker.py",
66
+ "tests/test_prompt_file_picker.py",
67
+ "tests/test_pseudo_streaming.py",
68
+ "tests/test_typers_ascii_fallback.py",
69
+ "tests/test_whisper_futo.py"
70
+ ]
71
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.2.1",
3
+ "distance": 0,
4
+ "node": "g7a322c71ba43f6e2a90702f6cfaad3e4713c3193",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-01"
8
+ }
@@ -90,14 +90,38 @@ def test_short_pause_kept_verbatim():
90
90
  assert sess.trimmed_silence_bytes == 0
91
91
 
92
92
 
93
- def test_trailing_silence_dropped():
93
+ def test_trailing_silence_capped_at_stop():
94
94
  sess = make_session()
95
95
  backend = make_backend(sess, clip_max_silence=2.0)
96
96
  backend.transcribe_realtime_audio(loud_chunk(1.0))
97
97
  for _ in range(50): # 5 s of trailing silence, never followed by speech
98
98
  backend.transcribe_realtime_audio(silent_chunk(0.1))
99
- # finalize() reads audio_buffer only — the pause stays out of it.
100
99
  assert secs(sess.audio_buffer) == 1.0
100
+ # At stop, the retained tail (<= clip_max_silence) is appended so a
101
+ # quiet word ending misread as silence still reaches the backend.
102
+ backend.flush_trailing_silence()
103
+ assert secs(sess.audio_buffer) == 3.0
104
+ assert sess.silence_buffer == b''
105
+
106
+
107
+ def test_quiet_word_ending_kept_at_stop():
108
+ sess = make_session()
109
+ backend = make_backend(sess, clip_max_silence=2.0)
110
+ backend.transcribe_realtime_audio(loud_chunk(1.0))
111
+ # A trailing-off word ending, below the dB gate threshold.
112
+ quiet = loud_chunk(0.3, amplitude=100)
113
+ backend.transcribe_realtime_audio(quiet)
114
+ backend.flush_trailing_silence()
115
+ assert sess.audio_buffer.endswith(quiet)
116
+
117
+
118
+ def test_flush_noop_when_nothing_spoken():
119
+ sess = make_session()
120
+ backend = make_backend(sess, clip_max_silence=2.0)
121
+ for _ in range(10):
122
+ backend.transcribe_realtime_audio(silent_chunk(0.1))
123
+ backend.flush_trailing_silence()
124
+ assert sess.audio_buffer == b''
101
125
 
102
126
 
103
127
  def test_zero_disables_trimming():
@@ -78,3 +78,26 @@ def test_unrenderable_char_is_skipped_not_fatal():
78
78
 
79
79
  type_ascii_safe(emit, "xéy", (LayoutError,))
80
80
  assert "".join(typed) == "xy"
81
+
82
+
83
+ def _record_subprocess(monkeypatch, module):
84
+ calls = []
85
+ monkeypatch.setattr(module.subprocess, "run",
86
+ lambda cmd, **kw: calls.append(cmd))
87
+ return calls
88
+
89
+
90
+ def test_subprocess_typers_pass_key_delay(monkeypatch):
91
+ # --typer-delay reaches eitype/wtype as `-d MS`; some apps (the Claude
92
+ # Code prompt) drop the end of text typed as one zero-delay burst.
93
+ import scribe.typers.eitype as eitype_mod
94
+ import scribe.typers.wtype as wtype_mod
95
+ for module, cls in ((eitype_mod, eitype_mod.EitypeTyper),
96
+ (wtype_mod, wtype_mod.WtypeTyper)):
97
+ calls = _record_subprocess(monkeypatch, module)
98
+ typer = cls()
99
+ typer.type("hi")
100
+ typer.key_delay_ms = 2
101
+ typer.type("hi")
102
+ assert calls == [[cls.name, "--", "hi"],
103
+ [cls.name, "-d", "2", "--", "hi"]]
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