scribe-cli 0.7.7__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.7/scribe_cli.egg-info → scribe_cli-0.7.9}/PKG-INFO +3 -2
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/README.md +1 -1
- scribe_cli-0.7.9/icon.xcf +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/pyproject.toml +1 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/_version.py +2 -2
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/app.py +130 -46
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/models.py +13 -4
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/util.py +3 -41
- {scribe_cli-0.7.7 → scribe_cli-0.7.9/scribe_cli.egg-info}/PKG-INFO +3 -2
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe_cli.egg-info/SOURCES.txt +4 -1
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe_cli.egg-info/requires.txt +1 -0
- 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.7/scribe_data/share/icon.jpg +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/.github/workflows/pypi.yml +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/.gitignore +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/LICENSE +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/__init__.py +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/audio.py +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/install_desktop.py +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/keyboard.py +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/models.toml +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/saverecording.py +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe/testpynput.py +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe_cli.egg-info/dependency_links.txt +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe_cli.egg-info/entry_points.txt +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe_cli.egg-info/top_level.txt +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe_data/__init__.py +0 -0
- {scribe_cli-0.7.7 → scribe_cli-0.7.9}/scribe_data/templates/scribe.desktop +0 -0
- {scribe_cli-0.7.7 → 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
|
|
@@ -45,6 +45,7 @@ Requires-Dist: tqdm
|
|
|
45
45
|
Requires-Dist: requests
|
|
46
46
|
Requires-Dist: pyperclip
|
|
47
47
|
Requires-Dist: unidecode
|
|
48
|
+
Requires-Dist: termcolor
|
|
48
49
|
Provides-Extra: keyboard
|
|
49
50
|
Requires-Dist: pynput; extra == "keyboard"
|
|
50
51
|
Provides-Extra: whisper
|
|
@@ -61,7 +62,7 @@ Requires-Dist: vosk; extra == "all"
|
|
|
61
62
|
Requires-Dist: pystray; extra == "all"
|
|
62
63
|
|
|
63
64
|
[]()
|
|
64
|
-
[](https://pypi.org/project/scribe-cli)
|
|
65
66
|
|
|
66
67
|
# Scribe
|
|
67
68
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[]()
|
|
2
|
-
[](https://pypi.org/project/scribe-cli)
|
|
3
3
|
|
|
4
4
|
# Scribe
|
|
5
5
|
|
|
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)
|
|
@@ -37,8 +38,36 @@ def pick_specialist_model(model, language, backend):
|
|
|
37
38
|
return model
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
class DummyTranscriber:
|
|
42
|
+
|
|
43
|
+
def __init__(self, backend, model_name):
|
|
44
|
+
self.backend = backend
|
|
45
|
+
self.model_name = model_name
|
|
46
|
+
|
|
47
|
+
def start_recording(self, micro, **kwargs):
|
|
48
|
+
while True:
|
|
49
|
+
try:
|
|
50
|
+
yield {"text": input()}
|
|
51
|
+
except KeyboardInterrupt:
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
def __getattr__(self, item):
|
|
55
|
+
return None
|
|
56
|
+
|
|
40
57
|
def get_transcriber(o, prompt=True):
|
|
41
58
|
|
|
59
|
+
whisper_models = ["tiny", "base", "small", "medium", "large", "turbo"]
|
|
60
|
+
whisper_english_models = ["tiny.en", "base.en", "small.en", "medium.en"]
|
|
61
|
+
|
|
62
|
+
if o.dummy:
|
|
63
|
+
return DummyTranscriber("whisper", "dummy")
|
|
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
|
+
|
|
42
71
|
if o.backend:
|
|
43
72
|
checked_backend = check_dependencies(o.backend)
|
|
44
73
|
if not checked_backend:
|
|
@@ -76,29 +105,25 @@ def get_transcriber(o, prompt=True):
|
|
|
76
105
|
print(f"Or pick one of the pre-defined languages: ", " ".join(available_languages))
|
|
77
106
|
exit(1)
|
|
78
107
|
choices = [language_config[backend][o.language]["model"]]
|
|
79
|
-
default_model = choices[0]
|
|
108
|
+
default_model = choices[0] # this is a string
|
|
80
109
|
|
|
81
110
|
else:
|
|
82
111
|
available_models = [language_config[backend][lang]["model"] for lang in available_languages]
|
|
83
112
|
choices = list(zip(available_models, available_languages)) + [f" * [Any model from {ansi_link('https://alphacephei.com/vosk/models')}]"]
|
|
84
|
-
default_model = choices[0]
|
|
113
|
+
default_model = choices[0] # this is a tuple !!
|
|
85
114
|
|
|
86
115
|
print(f"For information about vosk models see: {ansi_link('https://alphacephei.com/vosk/models')}")
|
|
87
116
|
if prompt:
|
|
88
|
-
model = prompt_choices(choices, default=default_model, label="model")
|
|
117
|
+
model = prompt_choices(choices, default=default_model, label="model") # this always returns a string
|
|
89
118
|
else:
|
|
90
|
-
model = default_model
|
|
119
|
+
model = default_model[0] if isinstance(default_model, tuple) else default_model # tuple -> string
|
|
91
120
|
|
|
92
121
|
elif backend == "whisper":
|
|
93
|
-
|
|
94
|
-
models = ["tiny", "base", "small", "medium", "large", "turbo"]
|
|
95
|
-
english_models = ["tiny.en", "base.en", "small.en", "medium.en"]
|
|
96
122
|
default_model = "small"
|
|
97
|
-
|
|
98
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).")
|
|
99
124
|
if prompt:
|
|
100
|
-
model = prompt_choices(
|
|
101
|
-
hidden_models=
|
|
125
|
+
model = prompt_choices(whisper_models, default=default_model, label="model",
|
|
126
|
+
hidden_models=whisper_english_models)
|
|
102
127
|
else:
|
|
103
128
|
model = default_model
|
|
104
129
|
|
|
@@ -143,6 +168,8 @@ def get_parser():
|
|
|
143
168
|
parser.add_argument("-l", "--language", choices=list(language_config["vosk"]),
|
|
144
169
|
help="An alias for preselected models when using the vosk backend, or 'en' for the English version of whisper models.")
|
|
145
170
|
|
|
171
|
+
parser.add_argument("--dummy", action="store_true", help=argparse.SUPPRESS)
|
|
172
|
+
|
|
146
173
|
parser.add_argument("--no-prompt", action="store_false", dest="prompt", help="Disable prompts for backend and model selection and jump to recording")
|
|
147
174
|
parser.add_argument("--app", action="store_true", help="Start in app mode (relies on pystray)")
|
|
148
175
|
|
|
@@ -165,7 +192,7 @@ def get_parser():
|
|
|
165
192
|
|
|
166
193
|
|
|
167
194
|
# Commencer l'enregistrement
|
|
168
|
-
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):
|
|
169
196
|
|
|
170
197
|
if keyboard:
|
|
171
198
|
from scribe.keyboard import type_text
|
|
@@ -189,7 +216,7 @@ def start_recording(micro, transcriber, clipboard=True, keyboard=False, latency=
|
|
|
189
216
|
|
|
190
217
|
if clipboard:
|
|
191
218
|
fulltext += result['text'] + " "
|
|
192
|
-
pyperclip.copy(fulltext)
|
|
219
|
+
pyperclip.copy(fulltext.strip())
|
|
193
220
|
|
|
194
221
|
else:
|
|
195
222
|
print_partial(result.get('partial', ''))
|
|
@@ -197,22 +224,8 @@ def start_recording(micro, transcriber, clipboard=True, keyboard=False, latency=
|
|
|
197
224
|
if clipboard:
|
|
198
225
|
print("Copied to clipboard.")
|
|
199
226
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"""Thanks Le Chat for this solution: https://stackoverflow.com/a/325528/2192272
|
|
203
|
-
"""
|
|
204
|
-
import ctypes
|
|
205
|
-
thread = icon._recording_thread
|
|
206
|
-
# Raise an exception in the thread using ctypes
|
|
207
|
-
thread_id = thread.ident
|
|
208
|
-
if thread_id is not None:
|
|
209
|
-
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
210
|
-
ctypes.c_long(thread_id),
|
|
211
|
-
ctypes.py_object(StopRecording)
|
|
212
|
-
)
|
|
213
|
-
if res > 1:
|
|
214
|
-
ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
|
|
215
|
-
print("Failure to raise exception in thread")
|
|
227
|
+
if callback:
|
|
228
|
+
callback()
|
|
216
229
|
|
|
217
230
|
|
|
218
231
|
def create_app(micro, transcriber, **kwargs):
|
|
@@ -225,7 +238,42 @@ def create_app(micro, transcriber, **kwargs):
|
|
|
225
238
|
import threading
|
|
226
239
|
|
|
227
240
|
# Load an image from a file
|
|
228
|
-
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)
|
|
229
277
|
|
|
230
278
|
def callback_quit(icon, item):
|
|
231
279
|
icon.visible = False
|
|
@@ -234,16 +282,34 @@ def create_app(micro, transcriber, **kwargs):
|
|
|
234
282
|
icon.stop()
|
|
235
283
|
|
|
236
284
|
def callback_stop_recording(icon, item):
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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()
|
|
240
292
|
|
|
241
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
|
|
242
306
|
icon._recording_thread = threading.Thread(target=start_recording, args=(micro, transcriber), kwargs=kwargs)
|
|
243
307
|
icon._recording_thread.start()
|
|
308
|
+
icon._monitoring_thread = threading.Thread(target=start_monitoring, args=(icon,))
|
|
309
|
+
icon._monitoring_thread.start()
|
|
244
310
|
|
|
245
311
|
def is_recording(item):
|
|
246
|
-
return
|
|
312
|
+
return transcriber.busy
|
|
247
313
|
|
|
248
314
|
def is_not_recording(item):
|
|
249
315
|
return not is_recording(item)
|
|
@@ -251,7 +317,6 @@ def create_app(micro, transcriber, **kwargs):
|
|
|
251
317
|
|
|
252
318
|
# Create a menu
|
|
253
319
|
menu = pystrayMenu(
|
|
254
|
-
# Item('Record', callback_record),
|
|
255
320
|
Item("Record", callback_record, visible=is_not_recording),
|
|
256
321
|
Item("Stop", callback_stop_recording, visible=is_recording),
|
|
257
322
|
Item('Quit', callback_quit),
|
|
@@ -279,25 +344,37 @@ def main(args=None):
|
|
|
279
344
|
while True:
|
|
280
345
|
if transcriber is None:
|
|
281
346
|
transcriber = get_transcriber(o, prompt=o.prompt)
|
|
282
|
-
print(f"
|
|
347
|
+
print(f"Model [{colored(transcriber.model_name, 'light_blue', attrs=['bold'])}] from [{colored(transcriber.backend, 'light_blue', attrs=['bold'])}] selected.")
|
|
283
348
|
if o.prompt:
|
|
284
|
-
print(f"Choose any of the following actions
|
|
285
|
-
print(f"[q] quit")
|
|
286
|
-
print(f"[e] change model")
|
|
287
|
-
print(f"[x]
|
|
288
|
-
print(f"[
|
|
289
|
-
print(f"[
|
|
349
|
+
print(f"Choose any of the following actions")
|
|
350
|
+
print(f"{colored('[q]', 'light_yellow')} quit")
|
|
351
|
+
print(f"{colored('[e]', 'light_yellow')} change model")
|
|
352
|
+
print(f"{colored('[x]', 'light_yellow')} app is {colored(o.app, 'light_blue')} toggle?")
|
|
353
|
+
print(f"{colored('[c]', 'light_yellow')} clipboard is {colored(o.clipboard, 'light_blue')} toggle?")
|
|
354
|
+
print(f"{colored('[k]', 'light_yellow')} keyboard is {colored(o.keyboard, 'light_blue')} toggle?")
|
|
355
|
+
if o.keyboard:
|
|
356
|
+
print(f"{colored('[latency]', 'light_yellow')} between keystrokes is {colored(o.latency, 'light_blue')} s")
|
|
290
357
|
if transcriber.backend == "whisper":
|
|
291
|
-
print(f"[t] change duration (currently {transcriber.timeout}s)")
|
|
292
|
-
print(f"[b] change silence duration (currently {transcriber.silence_duration}s)")
|
|
293
|
-
print(f"[a]
|
|
294
|
-
|
|
358
|
+
print(f"{colored('[t]', 'light_yellow')} change duration (currently {colored(transcriber.timeout, 'light_blue')} s)")
|
|
359
|
+
print(f"{colored('[b]', 'light_yellow')} change silence duration (currently {colored(transcriber.silence_duration, 'light_blue')} s)")
|
|
360
|
+
print(f"{colored('[a]', 'light_yellow')} auto-restart after silence is {colored(transcriber.restart_after_silence, 'light_blue')} toggle?")
|
|
361
|
+
exclude_flags = ["keyboard", "clipboard", "app", "prompt", "restart_after_silence"]
|
|
362
|
+
display_flags = [a.dest for a in parser._actions if a.help != argparse.SUPPRESS]
|
|
363
|
+
for key, value in vars(o).items():
|
|
364
|
+
if key not in display_flags or key in exclude_flags or not isinstance(value, bool):
|
|
365
|
+
continue
|
|
366
|
+
print(f"{colored(f'[{key}]', 'light_yellow')} is {colored(value, 'light_blue')} toggle?")
|
|
367
|
+
|
|
368
|
+
print(colored(f"Press [Enter] to start recording.", attrs=["bold"]))
|
|
295
369
|
|
|
296
370
|
key = input()
|
|
297
371
|
if key == "q":
|
|
298
372
|
exit(0)
|
|
299
373
|
if key == "e":
|
|
300
374
|
transcriber = None
|
|
375
|
+
o.model = None
|
|
376
|
+
o.backend = None
|
|
377
|
+
o.language = None
|
|
301
378
|
continue
|
|
302
379
|
if key == "k":
|
|
303
380
|
o.keyboard = not o.keyboard
|
|
@@ -318,6 +395,13 @@ def main(args=None):
|
|
|
318
395
|
except:
|
|
319
396
|
print("Invalid duration. Must be an integer.")
|
|
320
397
|
continue
|
|
398
|
+
if key == "latency":
|
|
399
|
+
ans = input(f"Enter new keyboard latency in seconds (current: {o.latency}): ")
|
|
400
|
+
try:
|
|
401
|
+
o.latency = float(ans)
|
|
402
|
+
except:
|
|
403
|
+
print("Invalid latency. Must be a float.")
|
|
404
|
+
continue
|
|
321
405
|
if key == "b":
|
|
322
406
|
ans = input(f"Enter new silence break duration in seconds (current: {transcriber.silence_duration}): ")
|
|
323
407
|
try:
|
|
@@ -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)
|
|
@@ -102,6 +110,7 @@ class AbstractTranscriber:
|
|
|
102
110
|
def get_vosk_model(model, download_root=None, url=None):
|
|
103
111
|
"""Load the Vosk recognizer"""
|
|
104
112
|
import vosk
|
|
113
|
+
vosk.SetLogLevel(-1)
|
|
105
114
|
if download_root is None:
|
|
106
115
|
download_root = VOSK_MODELS_FOLDER
|
|
107
116
|
model_path = os.path.join(download_root, model)
|
|
@@ -3,26 +3,7 @@ import re
|
|
|
3
3
|
import tqdm
|
|
4
4
|
import shutil
|
|
5
5
|
from functools import partial
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class bcolors:
|
|
9
|
-
# https://stackoverflow.com/a/287944/2192272
|
|
10
|
-
HEADER = '\033[95m'
|
|
11
|
-
OKBLUE = '\033[94m'
|
|
12
|
-
OKGREEN = '\033[92m'
|
|
13
|
-
WARNING = '\033[93m'
|
|
14
|
-
FAIL = '\033[91m'
|
|
15
|
-
ENDC = '\033[0m'
|
|
16
|
-
BOLD = '\033[1m'
|
|
17
|
-
UNDERLINE = '\033[4m'
|
|
18
|
-
|
|
19
|
-
def strip_colors(s):
|
|
20
|
-
for name, c in vars(bcolors).items():
|
|
21
|
-
if name.startswith("_"):
|
|
22
|
-
continue
|
|
23
|
-
s = s.replace(c, '')
|
|
24
|
-
return s
|
|
25
|
-
|
|
6
|
+
from termcolor import colored
|
|
26
7
|
|
|
27
8
|
def ansi_link(uri, label=None):
|
|
28
9
|
"""https://stackoverflow.com/a/71309268/2192272
|
|
@@ -36,25 +17,6 @@ def ansi_link(uri, label=None):
|
|
|
36
17
|
|
|
37
18
|
return escape_mask.format(parameters, uri, label)
|
|
38
19
|
|
|
39
|
-
def colored(text, color):
|
|
40
|
-
if hasattr(bcolors, color):
|
|
41
|
-
color = getattr(bcolors, color)
|
|
42
|
-
return f"{color}{text}{bcolors.ENDC}"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
ANSI_LINK_RE = re.compile(r'(?P<ansi_sequence>\033]8;(?P<parameter>.*?);(?P<uri>.*?)\033\\(?P<label>.*?)\033]8;;\033\\)')
|
|
46
|
-
|
|
47
|
-
def strip_ansi_link(s):
|
|
48
|
-
for m in ANSI_LINK_RE.findall(s):
|
|
49
|
-
s = s.replace(m[0], m[3])
|
|
50
|
-
return s
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def strip_all(s):
|
|
54
|
-
s = strip_colors(s)
|
|
55
|
-
s = strip_ansi_link(s)
|
|
56
|
-
return s
|
|
57
|
-
|
|
58
20
|
|
|
59
21
|
# Function to clear the terminal line
|
|
60
22
|
def clear_line():
|
|
@@ -119,9 +81,9 @@ def format_choice(enum, default=None, unavailable=None):
|
|
|
119
81
|
value_str = value
|
|
120
82
|
|
|
121
83
|
if (default is not None and value == default) or (default is None and i == 0):
|
|
122
|
-
return f' ' + colored(f'({i+1}) {value_str} [Press Enter]', '
|
|
84
|
+
return f' ' + colored(f'({i+1}) {value_str} [Press Enter]', attrs=['bold'])
|
|
123
85
|
elif unavailable and value in unavailable:
|
|
124
|
-
return f' ' + colored(f'{" "} {value_str} -> unavailable !!',
|
|
86
|
+
return f' ' + colored(f'{" "} {value_str} -> unavailable !!', attrs=["strike"])
|
|
125
87
|
else:
|
|
126
88
|
return f' ({i+1}) {value_str}'
|
|
127
89
|
|
|
@@ -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
|
|
@@ -45,6 +45,7 @@ Requires-Dist: tqdm
|
|
|
45
45
|
Requires-Dist: requests
|
|
46
46
|
Requires-Dist: pyperclip
|
|
47
47
|
Requires-Dist: unidecode
|
|
48
|
+
Requires-Dist: termcolor
|
|
48
49
|
Provides-Extra: keyboard
|
|
49
50
|
Requires-Dist: pynput; extra == "keyboard"
|
|
50
51
|
Provides-Extra: whisper
|
|
@@ -61,7 +62,7 @@ Requires-Dist: vosk; extra == "all"
|
|
|
61
62
|
Requires-Dist: pystray; extra == "all"
|
|
62
63
|
|
|
63
64
|
[]()
|
|
64
|
-
[](https://pypi.org/project/scribe-cli)
|
|
65
66
|
|
|
66
67
|
# Scribe
|
|
67
68
|
|
|
@@ -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
|