scribe-cli 0.7.8__tar.gz → 0.7.9__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. {scribe_cli-0.7.8/scribe_cli.egg-info → scribe_cli-0.7.9}/PKG-INFO +1 -1
  2. scribe_cli-0.7.9/icon.xcf +0 -0
  3. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/_version.py +2 -2
  4. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/app.py +79 -35
  5. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/models.py +12 -4
  6. {scribe_cli-0.7.8 → scribe_cli-0.7.9/scribe_cli.egg-info}/PKG-INFO +1 -1
  7. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_cli.egg-info/SOURCES.txt +4 -1
  8. scribe_cli-0.7.9/scribe_data/share/icon.png +0 -0
  9. scribe_cli-0.7.9/scribe_data/share/icon_recording.png +0 -0
  10. scribe_cli-0.7.9/scribe_data/share/icon_writing.png +0 -0
  11. scribe_cli-0.7.8/scribe_data/share/icon.jpg +0 -0
  12. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/.github/workflows/pypi.yml +0 -0
  13. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/.gitignore +0 -0
  14. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/LICENSE +0 -0
  15. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/README.md +0 -0
  16. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/pyproject.toml +0 -0
  17. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/__init__.py +0 -0
  18. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/audio.py +0 -0
  19. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/install_desktop.py +0 -0
  20. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/keyboard.py +0 -0
  21. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/models.toml +0 -0
  22. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/saverecording.py +0 -0
  23. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/testpynput.py +0 -0
  24. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/util.py +0 -0
  25. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_cli.egg-info/dependency_links.txt +0 -0
  26. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_cli.egg-info/entry_points.txt +0 -0
  27. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_cli.egg-info/requires.txt +0 -0
  28. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_cli.egg-info/top_level.txt +0 -0
  29. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_data/__init__.py +0 -0
  30. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_data/templates/scribe.desktop +0 -0
  31. {scribe_cli-0.7.8 → scribe_cli-0.7.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: scribe-cli
3
- Version: 0.7.8
3
+ Version: 0.7.9
4
4
  Summary: scribe is a local speech recognition tool that provides real-time transcription using vosk and whisper AI, with the goal of serving as a virtual keyboard on a computer
5
5
  Author-email: Mahé Perrette <mahe.perrette@gmail.com>
6
6
  License: MIT License
Binary file
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.7.8'
16
- __version_tuple__ = version_tuple = (0, 7, 8)
15
+ __version__ = version = '0.7.9'
16
+ __version_tuple__ = version_tuple = (0, 7, 9)
@@ -1,9 +1,10 @@
1
1
  from pathlib import Path
2
2
  import tomllib
3
+ import time
3
4
  import argparse
4
5
  from scribe.audio import Microphone
5
6
  from scribe.util import print_partial, clear_line, prompt_choices, check_dependencies, ansi_link, colored
6
- from scribe.models import VoskTranscriber, WhisperTranscriber, StopRecording
7
+ from scribe.models import VoskTranscriber, WhisperTranscriber
7
8
 
8
9
  with open(Path(__file__).parent / "models.toml", "rb") as f:
9
10
  language_config_default = tomllib.load(f)
@@ -55,9 +56,18 @@ class DummyTranscriber:
55
56
 
56
57
  def get_transcriber(o, prompt=True):
57
58
 
59
+ whisper_models = ["tiny", "base", "small", "medium", "large", "turbo"]
60
+ whisper_english_models = ["tiny.en", "base.en", "small.en", "medium.en"]
61
+
58
62
  if o.dummy:
59
63
  return DummyTranscriber("whisper", "dummy")
60
64
 
65
+ if o.model and not o.backend:
66
+ if o.model.startswith("vosk-"):
67
+ o.backend = "vosk"
68
+ elif o.model in whisper_models + whisper_english_models:
69
+ o.backend = "whisper"
70
+
61
71
  if o.backend:
62
72
  checked_backend = check_dependencies(o.backend)
63
73
  if not checked_backend:
@@ -95,29 +105,25 @@ def get_transcriber(o, prompt=True):
95
105
  print(f"Or pick one of the pre-defined languages: ", " ".join(available_languages))
96
106
  exit(1)
97
107
  choices = [language_config[backend][o.language]["model"]]
98
- default_model = choices[0]
108
+ default_model = choices[0] # this is a string
99
109
 
100
110
  else:
101
111
  available_models = [language_config[backend][lang]["model"] for lang in available_languages]
102
112
  choices = list(zip(available_models, available_languages)) + [f" * [Any model from {ansi_link('https://alphacephei.com/vosk/models')}]"]
103
- default_model = choices[0]
113
+ default_model = choices[0] # this is a tuple !!
104
114
 
105
115
  print(f"For information about vosk models see: {ansi_link('https://alphacephei.com/vosk/models')}")
106
116
  if prompt:
107
- model = prompt_choices(choices, default=default_model, label="model")
117
+ model = prompt_choices(choices, default=default_model, label="model") # this always returns a string
108
118
  else:
109
- model = default_model
119
+ model = default_model[0] if isinstance(default_model, tuple) else default_model # tuple -> string
110
120
 
111
121
  elif backend == "whisper":
112
-
113
- models = ["tiny", "base", "small", "medium", "large", "turbo"]
114
- english_models = ["tiny.en", "base.en", "small.en", "medium.en"]
115
122
  default_model = "small"
116
-
117
123
  print("Some models have a specialized English version (.en) which will be selected as default is `-l en` was requested, but can also be requested explicitly below (option not listed). See [documentation](https://github.com/openai/whisper?tab=readme-ov-file#available-models-and-languages).")
118
124
  if prompt:
119
- model = prompt_choices(models, default=default_model, label="model",
120
- hidden_models=english_models)
125
+ model = prompt_choices(whisper_models, default=default_model, label="model",
126
+ hidden_models=whisper_english_models)
121
127
  else:
122
128
  model = default_model
123
129
 
@@ -186,7 +192,7 @@ def get_parser():
186
192
 
187
193
 
188
194
  # Commencer l'enregistrement
189
- def start_recording(micro, transcriber, clipboard=True, keyboard=False, latency=0, ascii=False, **greetings):
195
+ def start_recording(micro, transcriber, clipboard=True, keyboard=False, latency=0, ascii=False, callback=None, **greetings):
190
196
 
191
197
  if keyboard:
192
198
  from scribe.keyboard import type_text
@@ -210,7 +216,7 @@ def start_recording(micro, transcriber, clipboard=True, keyboard=False, latency=
210
216
 
211
217
  if clipboard:
212
218
  fulltext += result['text'] + " "
213
- pyperclip.copy(fulltext)
219
+ pyperclip.copy(fulltext.strip())
214
220
 
215
221
  else:
216
222
  print_partial(result.get('partial', ''))
@@ -218,22 +224,8 @@ def start_recording(micro, transcriber, clipboard=True, keyboard=False, latency=
218
224
  if clipboard:
219
225
  print("Copied to clipboard.")
220
226
 
221
-
222
- def interrupt_app_thread(icon):
223
- """Thanks Le Chat for this solution: https://stackoverflow.com/a/325528/2192272
224
- """
225
- import ctypes
226
- thread = icon._recording_thread
227
- # Raise an exception in the thread using ctypes
228
- thread_id = thread.ident
229
- if thread_id is not None:
230
- res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
231
- ctypes.c_long(thread_id),
232
- ctypes.py_object(StopRecording)
233
- )
234
- if res > 1:
235
- ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
236
- print("Failure to raise exception in thread")
227
+ if callback:
228
+ callback()
237
229
 
238
230
 
239
231
  def create_app(micro, transcriber, **kwargs):
@@ -246,7 +238,42 @@ def create_app(micro, transcriber, **kwargs):
246
238
  import threading
247
239
 
248
240
  # Load an image from a file
249
- image = Image.open(Path(scribe_data.__file__).parent / "share" / "icon.jpg")
241
+ image = Image.open(Path(scribe_data.__file__).parent / "share" / "icon.png")
242
+ image_recording = Image.open(Path(scribe_data.__file__).parent / "share" / "icon_recording.png")
243
+ image_writing = Image.open(Path(scribe_data.__file__).parent / "share" / "icon_writing.png")
244
+
245
+ if transcriber.backend == "vosk":
246
+ # Recording and writing happen at the same time in this backend
247
+ # Overlay the writing image on top of the base image
248
+ image_recording = Image.alpha_composite(image_recording.convert("RGBA"), image_writing.convert("RGBA"))
249
+
250
+ def update_icon(icon, force=False):
251
+ if transcriber.recording:
252
+ if force or getattr(icon, "_icon_label", None) != "recording":
253
+ icon.icon = image_recording
254
+ icon._icon_label = "recording"
255
+ icon.update_menu()
256
+
257
+ elif transcriber.busy:
258
+ if force or getattr(icon, "_icon_label", None) != "busy":
259
+ icon.icon = image_writing
260
+ icon._icon_label = "busy"
261
+ icon.update_menu()
262
+
263
+ else:
264
+ if force or getattr(icon, "_icon_label", None) != None:
265
+ icon.icon = image
266
+ icon._icon_label = None
267
+ icon.update_menu()
268
+
269
+ def start_monitoring(icon):
270
+ try:
271
+ while transcriber.busy:
272
+ update_icon(icon)
273
+ time.sleep(0.1)
274
+
275
+ finally:
276
+ update_icon(icon)
250
277
 
251
278
  def callback_quit(icon, item):
252
279
  icon.visible = False
@@ -255,16 +282,34 @@ def create_app(micro, transcriber, **kwargs):
255
282
  icon.stop()
256
283
 
257
284
  def callback_stop_recording(icon, item):
258
- ## Here we need to stop the recording thread
259
- interrupt_app_thread(icon)
260
- icon._recording_thread.join()
285
+ # Here we need to stop the recording thread
286
+
287
+ transcriber.recording = False
288
+ if hasattr(icon, "_recording_thread"):
289
+ icon._recording_thread.join()
290
+ if hasattr(icon, "_monitoring_thread"):
291
+ icon._monitoring_thread.join()
261
292
 
262
293
  def callback_record(icon, item):
294
+ # kwargs["callback"] = icon.update_menu # NOTE: the thread will finish AFTER the callback is complete
295
+ if transcriber.busy:
296
+ print("Still busy recording or transcribing.")
297
+ return
298
+
299
+ if hasattr(icon, "_recording_thread") and icon._recording_thread.is_alive():
300
+ icon._recording_thread.join()
301
+
302
+ if hasattr(icon, "_monitoring_thread") and icon._monitoring_thread.is_alive():
303
+ icon._monitoring_thread.join()
304
+
305
+ transcriber.busy = True # this is a hack to prevent race conditions between the below threads
263
306
  icon._recording_thread = threading.Thread(target=start_recording, args=(micro, transcriber), kwargs=kwargs)
264
307
  icon._recording_thread.start()
308
+ icon._monitoring_thread = threading.Thread(target=start_monitoring, args=(icon,))
309
+ icon._monitoring_thread.start()
265
310
 
266
311
  def is_recording(item):
267
- return hasattr(icon, "_recording_thread") and icon._recording_thread.is_alive()
312
+ return transcriber.busy
268
313
 
269
314
  def is_not_recording(item):
270
315
  return not is_recording(item)
@@ -272,7 +317,6 @@ def create_app(micro, transcriber, **kwargs):
272
317
 
273
318
  # Create a menu
274
319
  menu = pystrayMenu(
275
- # Item('Record', callback_record),
276
320
  Item("Record", callback_record, visible=is_not_recording),
277
321
  Item("Stop", callback_stop_recording, visible=is_recording),
278
322
  Item('Quit', callback_quit),
@@ -32,6 +32,8 @@ class AbstractTranscriber:
32
32
  self.silence_thresh = silence_thresh
33
33
  self.silence_duration = silence_duration
34
34
  self.restart_after_silence = restart_after_silence
35
+ self.recording = False
36
+ self.busy = False
35
37
  self.reset()
36
38
 
37
39
  def get_elapsed(self):
@@ -54,16 +56,18 @@ class AbstractTranscriber:
54
56
 
55
57
  def start_recording(self, microphone,
56
58
  start_message="Recording... Press Ctrl+C to stop.",
57
- stop_message="Stopped recording."):
59
+ stop_message="Done transcribing."):
58
60
 
59
61
  self.reset()
62
+ self.recording = True
63
+ self.busy = True
60
64
 
61
65
  try:
62
66
 
63
67
  with microphone.open_stream():
64
68
  print(start_message)
65
69
 
66
- while True:
70
+ while self.recording:
67
71
  while not microphone.q.empty():
68
72
  data = microphone.q.get()
69
73
 
@@ -78,7 +82,7 @@ class AbstractTranscriber:
78
82
  self.reset()
79
83
  yield result
80
84
  else:
81
- raise KeyboardInterrupt("Silence detected: {:.2f} seconds".format(silence_duration))
85
+ raise StopRecording("Silence detected: {:.2f} seconds".format(silence_duration))
82
86
 
83
87
  else:
84
88
  self.last_sound_time = time.time()
@@ -86,14 +90,18 @@ class AbstractTranscriber:
86
90
  yield self.transcribe_realtime_audio(data)
87
91
 
88
92
  if self.is_overtime():
89
- raise KeyboardInterrupt("Overtime: {:.2f} seconds".format(self.get_elapsed()))
93
+ raise StopRecording("Overtime: {:.2f} seconds".format(self.get_elapsed()))
94
+
95
+ time.sleep(0.1) # avoid overheating
90
96
 
91
97
  except (KeyboardInterrupt, StopRecording):
92
98
  pass
93
99
 
94
100
  finally:
101
+ self.recording = False
95
102
  result = self.finalize()
96
103
  microphone.q.queue.clear()
104
+ self.busy = False
97
105
  yield result
98
106
 
99
107
  print(stop_message)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: scribe-cli
3
- Version: 0.7.8
3
+ Version: 0.7.9
4
4
  Summary: scribe is a local speech recognition tool that provides real-time transcription using vosk and whisper AI, with the goal of serving as a virtual keyboard on a computer
5
5
  Author-email: Mahé Perrette <mahe.perrette@gmail.com>
6
6
  License: MIT License
@@ -1,6 +1,7 @@
1
1
  .gitignore
2
2
  LICENSE
3
3
  README.md
4
+ icon.xcf
4
5
  pyproject.toml
5
6
  .github/workflows/pypi.yml
6
7
  scribe/__init__.py
@@ -21,5 +22,7 @@ scribe_cli.egg-info/entry_points.txt
21
22
  scribe_cli.egg-info/requires.txt
22
23
  scribe_cli.egg-info/top_level.txt
23
24
  scribe_data/__init__.py
24
- scribe_data/share/icon.jpg
25
+ scribe_data/share/icon.png
26
+ scribe_data/share/icon_recording.png
27
+ scribe_data/share/icon_writing.png
25
28
  scribe_data/templates/scribe.desktop
Binary file
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes