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.
- {scribe_cli-0.7.8/scribe_cli.egg-info → scribe_cli-0.7.9}/PKG-INFO +1 -1
- scribe_cli-0.7.9/icon.xcf +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/_version.py +2 -2
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/app.py +79 -35
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/models.py +12 -4
- {scribe_cli-0.7.8 → scribe_cli-0.7.9/scribe_cli.egg-info}/PKG-INFO +1 -1
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_cli.egg-info/SOURCES.txt +4 -1
- scribe_cli-0.7.9/scribe_data/share/icon.png +0 -0
- scribe_cli-0.7.9/scribe_data/share/icon_recording.png +0 -0
- scribe_cli-0.7.9/scribe_data/share/icon_writing.png +0 -0
- scribe_cli-0.7.8/scribe_data/share/icon.jpg +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/.github/workflows/pypi.yml +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/.gitignore +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/LICENSE +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/README.md +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/pyproject.toml +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/__init__.py +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/audio.py +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/install_desktop.py +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/keyboard.py +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/models.toml +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/saverecording.py +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/testpynput.py +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe/util.py +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_cli.egg-info/dependency_links.txt +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_cli.egg-info/entry_points.txt +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_cli.egg-info/requires.txt +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_cli.egg-info/top_level.txt +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_data/__init__.py +0 -0
- {scribe_cli-0.7.8 → scribe_cli-0.7.9}/scribe_data/templates/scribe.desktop +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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(
|
|
120
|
-
hidden_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
|
-
|
|
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.
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|