stt2desktop 0.0.1__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.
- stt2desktop/__init__.py +8 -0
- stt2desktop/__main__.py +10 -0
- stt2desktop/cli_app/__init__.py +41 -0
- stt2desktop/cli_app/listen.py +85 -0
- stt2desktop/cli_dev/__init__.py +70 -0
- stt2desktop/cli_dev/__main__.py +10 -0
- stt2desktop/cli_dev/code_style.py +12 -0
- stt2desktop/cli_dev/packaging.py +62 -0
- stt2desktop/cli_dev/testing.py +52 -0
- stt2desktop/cli_dev/update_readme_history.py +33 -0
- stt2desktop/constants.py +28 -0
- stt2desktop/get_bin.py +29 -0
- stt2desktop/stt.py +107 -0
- stt2desktop/tests/__init__.py +36 -0
- stt2desktop/tests/test_doctests.py +10 -0
- stt2desktop/tests/test_project_setup.py +49 -0
- stt2desktop/tests/test_readme.py +83 -0
- stt2desktop/tests/test_readme_history.py +9 -0
- stt2desktop-0.0.1.dist-info/METADATA +171 -0
- stt2desktop-0.0.1.dist-info/RECORD +22 -0
- stt2desktop-0.0.1.dist-info/WHEEL +4 -0
- stt2desktop-0.0.1.dist-info/entry_points.txt +2 -0
stt2desktop/__init__.py
ADDED
stt2desktop/__main__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI for usage
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from cli_base.autodiscover import import_all_files
|
|
10
|
+
from cli_base.cli_tools.version_info import print_version
|
|
11
|
+
from rich import print # noqa
|
|
12
|
+
from tyro.extras import SubcommandApp
|
|
13
|
+
|
|
14
|
+
import stt2desktop
|
|
15
|
+
from stt2desktop import constants
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
app = SubcommandApp()
|
|
21
|
+
|
|
22
|
+
# Register all CLI commands, just by import all files in this package:
|
|
23
|
+
import_all_files(package=__package__, init_file=__file__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command
|
|
27
|
+
def version():
|
|
28
|
+
"""Print version and exit"""
|
|
29
|
+
# Pseudo command, because the version always printed on every CLI call ;)
|
|
30
|
+
sys.exit(0)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main(args: Sequence[str] | None = None):
|
|
34
|
+
print_version(stt2desktop)
|
|
35
|
+
app.cli(
|
|
36
|
+
prog='./cli.py',
|
|
37
|
+
description=constants.CLI_EPILOG,
|
|
38
|
+
use_underscores=False, # use hyphens instead of underscores
|
|
39
|
+
sort_subcommands=True,
|
|
40
|
+
args=args,
|
|
41
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import tyro
|
|
5
|
+
from cli_base.cli_tools.verbosity import setup_logging
|
|
6
|
+
from cli_base.tyro_commands import TyroVerbosityArgType
|
|
7
|
+
from pynput import keyboard
|
|
8
|
+
from rich import print
|
|
9
|
+
|
|
10
|
+
from stt2desktop.cli_app import app
|
|
11
|
+
from stt2desktop.constants import (
|
|
12
|
+
DEFAULT_HOTKEY,
|
|
13
|
+
DEFAULT_SAMPLE_RATE,
|
|
14
|
+
DEFAULT_WHISPER_COMPUTE_TYPE,
|
|
15
|
+
DEFAULT_WHISPER_DEVICE,
|
|
16
|
+
DEFAULT_WHISPER_MODEL,
|
|
17
|
+
DEFAULT_WHISPER_NUM_WORKERS,
|
|
18
|
+
IS_WAYLAND,
|
|
19
|
+
WhisperModel,
|
|
20
|
+
)
|
|
21
|
+
from stt2desktop.get_bin import get_wtype_bin, get_xdotool_bin
|
|
22
|
+
from stt2desktop.stt import Recorder, load_whisper_model
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command
|
|
29
|
+
def listen(
|
|
30
|
+
verbosity: TyroVerbosityArgType,
|
|
31
|
+
model: Annotated[
|
|
32
|
+
WhisperModel, tyro.conf.arg(help='Whisper model to use for transcription.')
|
|
33
|
+
] = DEFAULT_WHISPER_MODEL,
|
|
34
|
+
hotkey: Annotated[
|
|
35
|
+
keyboard.Key,
|
|
36
|
+
tyro.conf.arg(help='Key to hold for recording. Release to transcribe and insert text.'),
|
|
37
|
+
] = DEFAULT_HOTKEY,
|
|
38
|
+
sample_rate: Annotated[
|
|
39
|
+
int, tyro.conf.arg(help='Audio sample rate in Hz. Whisper expects 16000.')
|
|
40
|
+
] = DEFAULT_SAMPLE_RATE,
|
|
41
|
+
device: Annotated[
|
|
42
|
+
str, tyro.conf.arg(help='Device to run inference on, e.g. cpu or cuda.')
|
|
43
|
+
] = DEFAULT_WHISPER_DEVICE,
|
|
44
|
+
compute_type: Annotated[
|
|
45
|
+
str, tyro.conf.arg(help='Quantization type, e.g. int8, float16, float32.')
|
|
46
|
+
] = DEFAULT_WHISPER_COMPUTE_TYPE,
|
|
47
|
+
num_workers: Annotated[
|
|
48
|
+
int,
|
|
49
|
+
tyro.conf.arg(help='Number of parallel transcription workers. Defaults to CPU count.'),
|
|
50
|
+
] = DEFAULT_WHISPER_NUM_WORKERS,
|
|
51
|
+
):
|
|
52
|
+
"""Start the STT listener. Hold the hotkey to record, release to transcribe and insert."""
|
|
53
|
+
setup_logging(verbosity=verbosity)
|
|
54
|
+
|
|
55
|
+
# Before start, check if needed binaries are available:
|
|
56
|
+
if IS_WAYLAND:
|
|
57
|
+
get_wtype_bin()
|
|
58
|
+
else:
|
|
59
|
+
get_xdotool_bin()
|
|
60
|
+
|
|
61
|
+
model = load_whisper_model(
|
|
62
|
+
model_size=model,
|
|
63
|
+
device=device,
|
|
64
|
+
compute_type=compute_type,
|
|
65
|
+
num_workers=num_workers,
|
|
66
|
+
)
|
|
67
|
+
recorder = Recorder(model=model, sample_rate=sample_rate)
|
|
68
|
+
|
|
69
|
+
def on_press(key):
|
|
70
|
+
if key == hotkey:
|
|
71
|
+
recorder.start()
|
|
72
|
+
|
|
73
|
+
def on_release(key):
|
|
74
|
+
if key == hotkey:
|
|
75
|
+
recorder.stop()
|
|
76
|
+
|
|
77
|
+
print(f'Ready. Hold [bold]{hotkey.name}[/bold] to record, release to transcribe.')
|
|
78
|
+
print('Press Ctrl+C to exit.')
|
|
79
|
+
try:
|
|
80
|
+
with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
|
|
81
|
+
listener.join()
|
|
82
|
+
except KeyboardInterrupt:
|
|
83
|
+
print('\nExiting...')
|
|
84
|
+
finally:
|
|
85
|
+
recorder.stop()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI for development
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from bx_py_utils.path import assert_is_file
|
|
11
|
+
from cli_base.autodiscover import import_all_files
|
|
12
|
+
from cli_base.cli_tools.dev_tools import run_coverage, run_nox, run_unittest_cli
|
|
13
|
+
from cli_base.cli_tools.version_info import print_version
|
|
14
|
+
from typeguard import install_import_hook
|
|
15
|
+
from tyro.extras import SubcommandApp
|
|
16
|
+
|
|
17
|
+
import stt2desktop
|
|
18
|
+
from stt2desktop import constants
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Check type annotations via typeguard in all tests.
|
|
22
|
+
# Sadly we must activate this here and can't do this in ./tests/__init__.py
|
|
23
|
+
install_import_hook(packages=('stt2desktop',))
|
|
24
|
+
|
|
25
|
+
# reload the module, after the typeguard import hook is activated:
|
|
26
|
+
importlib.reload(stt2desktop)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
PACKAGE_ROOT = constants.BASE_PATH.parent
|
|
33
|
+
assert_is_file(PACKAGE_ROOT / 'pyproject.toml') # Exists only in cloned git repo
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
app = SubcommandApp()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Register all CLI commands, just by import all files in this package:
|
|
40
|
+
import_all_files(package=__package__, init_file=__file__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command
|
|
44
|
+
def version():
|
|
45
|
+
"""Print version and exit"""
|
|
46
|
+
# Pseudo command, because the version always printed on every CLI call ;)
|
|
47
|
+
sys.exit(0)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(args: Sequence[str] | None = None):
|
|
51
|
+
print_version(stt2desktop)
|
|
52
|
+
|
|
53
|
+
if len(sys.argv) >= 2:
|
|
54
|
+
# Check if we can just pass a command call to origin CLI:
|
|
55
|
+
command = sys.argv[1]
|
|
56
|
+
command_map = {
|
|
57
|
+
'test': run_unittest_cli,
|
|
58
|
+
'nox': run_nox,
|
|
59
|
+
'coverage': run_coverage,
|
|
60
|
+
}
|
|
61
|
+
if real_func := command_map.get(command):
|
|
62
|
+
real_func(argv=sys.argv, exit_after_run=True)
|
|
63
|
+
|
|
64
|
+
app.cli(
|
|
65
|
+
prog='./dev-cli.py',
|
|
66
|
+
description=constants.CLI_EPILOG,
|
|
67
|
+
use_underscores=False, # use hyphens instead of underscores
|
|
68
|
+
sort_subcommands=True,
|
|
69
|
+
args=args,
|
|
70
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from cli_base.cli_tools.code_style import assert_code_style
|
|
2
|
+
from cli_base.tyro_commands import TyroVerbosityArgType
|
|
3
|
+
|
|
4
|
+
from stt2desktop.cli_dev import PACKAGE_ROOT, app
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@app.command
|
|
8
|
+
def lint(verbosity: TyroVerbosityArgType = 1):
|
|
9
|
+
"""
|
|
10
|
+
Check/fix code style by run: "ruff check --fix"
|
|
11
|
+
"""
|
|
12
|
+
assert_code_style(package_root=PACKAGE_ROOT, verbose=bool(verbosity), sys_exit=True)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from cli_base.cli_tools.dev_tools import run_unittest_cli
|
|
4
|
+
from cli_base.cli_tools.subprocess_utils import ToolsExecutor
|
|
5
|
+
from cli_base.cli_tools.verbosity import setup_logging
|
|
6
|
+
from cli_base.run_pip_audit import run_pip_audit
|
|
7
|
+
from cli_base.tyro_commands import TyroVerbosityArgType
|
|
8
|
+
from manageprojects.utilities.publish import publish_package
|
|
9
|
+
|
|
10
|
+
import stt2desktop
|
|
11
|
+
from stt2desktop.cli_dev import PACKAGE_ROOT, app
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command
|
|
18
|
+
def install():
|
|
19
|
+
"""
|
|
20
|
+
Install requirements and 'stt2desktop' via pip as editable.
|
|
21
|
+
"""
|
|
22
|
+
tools_executor = ToolsExecutor(cwd=PACKAGE_ROOT)
|
|
23
|
+
tools_executor.verbose_check_call('uv', 'sync')
|
|
24
|
+
tools_executor.verbose_check_call('uv', 'pip', 'install', '--no-deps', '-e', '.')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command
|
|
28
|
+
def pip_audit(verbosity: TyroVerbosityArgType):
|
|
29
|
+
"""
|
|
30
|
+
Run pip-audit check against current requirements files
|
|
31
|
+
"""
|
|
32
|
+
setup_logging(verbosity=verbosity)
|
|
33
|
+
run_pip_audit(base_path=PACKAGE_ROOT, verbosity=verbosity)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command
|
|
37
|
+
def update(verbosity: TyroVerbosityArgType):
|
|
38
|
+
"""
|
|
39
|
+
Update dependencies (uv.lock) and git pre-commit hooks
|
|
40
|
+
"""
|
|
41
|
+
setup_logging(verbosity=verbosity)
|
|
42
|
+
|
|
43
|
+
tools_executor = ToolsExecutor(cwd=PACKAGE_ROOT)
|
|
44
|
+
tools_executor.verbose_check_call('uv', 'lock', '--upgrade')
|
|
45
|
+
|
|
46
|
+
run_pip_audit(base_path=PACKAGE_ROOT, verbosity=verbosity)
|
|
47
|
+
|
|
48
|
+
# Install new dependencies in current .venv:
|
|
49
|
+
tools_executor.verbose_check_call('uv', 'sync')
|
|
50
|
+
|
|
51
|
+
# Update git pre-commit hooks:
|
|
52
|
+
tools_executor.verbose_check_call('pre-commit', 'autoupdate')
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command
|
|
56
|
+
def publish():
|
|
57
|
+
"""
|
|
58
|
+
Build and upload this project to PyPi
|
|
59
|
+
"""
|
|
60
|
+
run_unittest_cli(verbose=False, exit_after_run=False) # Don't publish a broken state
|
|
61
|
+
|
|
62
|
+
publish_package(module=stt2desktop, package_path=PACKAGE_ROOT)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from cli_base.cli_tools.dev_tools import run_coverage, run_nox, run_unittest_cli
|
|
2
|
+
from cli_base.cli_tools.subprocess_utils import verbose_check_call
|
|
3
|
+
from cli_base.cli_tools.test_utils.snapshot import UpdateTestSnapshotFiles
|
|
4
|
+
from cli_base.tyro_commands import TyroVerbosityArgType
|
|
5
|
+
|
|
6
|
+
from stt2desktop.cli_dev import PACKAGE_ROOT, app
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@app.command
|
|
10
|
+
def mypy(verbosity: TyroVerbosityArgType):
|
|
11
|
+
"""Run Mypy (configured in pyproject.toml)"""
|
|
12
|
+
verbose_check_call('mypy', '.', cwd=PACKAGE_ROOT, verbose=verbosity > 0, exit_on_error=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command
|
|
16
|
+
def update_test_snapshot_files(verbosity: TyroVerbosityArgType):
|
|
17
|
+
"""
|
|
18
|
+
Update all test snapshot files (by remove and recreate all snapshot files)
|
|
19
|
+
"""
|
|
20
|
+
with UpdateTestSnapshotFiles(root_path=PACKAGE_ROOT, verbose=verbosity > 0):
|
|
21
|
+
# Just recreate them by running tests:
|
|
22
|
+
run_unittest_cli(
|
|
23
|
+
extra_env={
|
|
24
|
+
'RAISE_SNAPSHOT_ERRORS': '0', # Recreate snapshot files without error
|
|
25
|
+
},
|
|
26
|
+
verbose=verbosity > 1,
|
|
27
|
+
exit_after_run=False,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command # Dummy command
|
|
32
|
+
def test():
|
|
33
|
+
"""
|
|
34
|
+
Run unittests
|
|
35
|
+
"""
|
|
36
|
+
run_unittest_cli()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command # Dummy command
|
|
40
|
+
def coverage():
|
|
41
|
+
"""
|
|
42
|
+
Run tests and show coverage report.
|
|
43
|
+
"""
|
|
44
|
+
run_coverage()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command # Dummy "nox" command
|
|
48
|
+
def nox():
|
|
49
|
+
"""
|
|
50
|
+
Run nox
|
|
51
|
+
"""
|
|
52
|
+
run_nox()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from cli_base.cli_tools import git_history
|
|
6
|
+
from cli_base.cli_tools.verbosity import setup_logging
|
|
7
|
+
from cli_base.tyro_commands import TyroVerbosityArgType
|
|
8
|
+
from rich import print
|
|
9
|
+
|
|
10
|
+
from stt2desktop.cli_dev import app
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command
|
|
17
|
+
def update_readme_history(verbosity: TyroVerbosityArgType):
|
|
18
|
+
"""
|
|
19
|
+
Update project history base on git commits/tags in README.md
|
|
20
|
+
|
|
21
|
+
Will be exited with 1 if the README.md was updated otherwise with 0.
|
|
22
|
+
|
|
23
|
+
Also, callable via e.g.:
|
|
24
|
+
python -m cli_base update-readme-history -v
|
|
25
|
+
"""
|
|
26
|
+
setup_logging(verbosity=verbosity)
|
|
27
|
+
|
|
28
|
+
logger.debug('%s called. CWD: %s', __name__, Path.cwd())
|
|
29
|
+
updated = git_history.update_readme_history(verbosity=verbosity)
|
|
30
|
+
exit_code = 1 if updated else 0
|
|
31
|
+
if verbosity:
|
|
32
|
+
print(f'{exit_code=}')
|
|
33
|
+
sys.exit(exit_code)
|
stt2desktop/constants.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from faster_whisper.utils import available_models
|
|
6
|
+
from pynput import keyboard
|
|
7
|
+
|
|
8
|
+
import stt2desktop
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
CLI_EPILOG = 'Project Homepage: https://github.com/jedie/stt2desktop'
|
|
12
|
+
|
|
13
|
+
BASE_PATH = Path(stt2desktop.__file__).parent
|
|
14
|
+
|
|
15
|
+
IS_WAYLAND = bool(os.environ.get('WAYLAND_DISPLAY'))
|
|
16
|
+
|
|
17
|
+
WhisperModel = StrEnum(
|
|
18
|
+
'WhisperModel',
|
|
19
|
+
[(m.replace('-', '_').replace('.', '_'), m) for m in available_models()],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Default values used in the CLI
|
|
23
|
+
DEFAULT_WHISPER_MODEL = WhisperModel.small
|
|
24
|
+
DEFAULT_WHISPER_DEVICE = 'auto' # Let faster-whisper decide the best device
|
|
25
|
+
DEFAULT_WHISPER_COMPUTE_TYPE = 'int8'
|
|
26
|
+
DEFAULT_WHISPER_NUM_WORKERS = os.cpu_count() or 1
|
|
27
|
+
DEFAULT_SAMPLE_RATE = 16000
|
|
28
|
+
DEFAULT_HOTKEY = keyboard.Key.scroll_lock
|
stt2desktop/get_bin.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from cli_base.cli_tools.subprocess_utils import verbose_check_call
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_bin(name: str, hint: str) -> Path:
|
|
10
|
+
bin_path = shutil.which(name)
|
|
11
|
+
if bin_path is None:
|
|
12
|
+
print(f'[red]Error: {name} is not installed or not found in PATH.[/red]')
|
|
13
|
+
print(hint)
|
|
14
|
+
sys.exit(1)
|
|
15
|
+
return Path(bin_path)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@functools.cache
|
|
19
|
+
def get_wtype_bin() -> Path:
|
|
20
|
+
wtype_bin = get_bin('wtype', hint='wtype is required for Wayland support: sudo apt install wtype')
|
|
21
|
+
verbose_check_call(wtype_bin, 'version')
|
|
22
|
+
return wtype_bin
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@functools.cache
|
|
26
|
+
def get_xdotool_bin() -> Path:
|
|
27
|
+
xdotool_bin = get_bin('xdotool', hint='xdotool is required for X11 support: sudo apt install xdotool')
|
|
28
|
+
verbose_check_call(xdotool_bin, 'version')
|
|
29
|
+
return xdotool_bin
|
stt2desktop/stt.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
import threading
|
|
4
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import sounddevice as sd
|
|
8
|
+
from faster_whisper import WhisperModel
|
|
9
|
+
from rich import print
|
|
10
|
+
|
|
11
|
+
from stt2desktop.constants import IS_WAYLAND
|
|
12
|
+
from stt2desktop.get_bin import get_wtype_bin, get_xdotool_bin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_whisper_model(*, model_size: str, device: str, compute_type: str, num_workers: int) -> WhisperModel:
|
|
19
|
+
print(f'Loading model {model_size} ({device=} {compute_type=} {num_workers=})...')
|
|
20
|
+
model = WhisperModel(model_size, device=device, compute_type=compute_type, num_workers=num_workers)
|
|
21
|
+
print('[green]Model loaded.[/green]')
|
|
22
|
+
return model
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def transcribe(model: WhisperModel, audio: np.ndarray) -> str:
|
|
26
|
+
if len(audio) == 0:
|
|
27
|
+
return ''
|
|
28
|
+
logger.debug('Transcribing %d samples', len(audio))
|
|
29
|
+
segments, _ = model.transcribe(audio, beam_size=5)
|
|
30
|
+
result = ' '.join(seg.text for seg in segments).strip()
|
|
31
|
+
logger.debug('Transcription: %r', result)
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def record_until_event(*, stop_event: threading.Event, sample_rate: int) -> np.ndarray:
|
|
36
|
+
"""Record from the default microphone until stop_event is set."""
|
|
37
|
+
chunks: list[np.ndarray] = []
|
|
38
|
+
|
|
39
|
+
def callback(indata, frames, time, status):
|
|
40
|
+
if status:
|
|
41
|
+
logger.warning('Audio input status: %r', status)
|
|
42
|
+
chunks.append(indata.copy())
|
|
43
|
+
|
|
44
|
+
logger.debug('Recording started')
|
|
45
|
+
with sd.InputStream(samplerate=sample_rate, channels=1, dtype='float32', callback=callback):
|
|
46
|
+
stop_event.wait()
|
|
47
|
+
logger.debug('Recording stopped, got %d chunks', len(chunks))
|
|
48
|
+
|
|
49
|
+
if not chunks:
|
|
50
|
+
return np.array([], dtype='float32')
|
|
51
|
+
return np.concatenate(chunks, axis=0).flatten()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def inject_text(text: str) -> None:
|
|
55
|
+
"""Inject text at the current cursor position."""
|
|
56
|
+
if IS_WAYLAND:
|
|
57
|
+
# wtype types text directly into the focused window
|
|
58
|
+
wtype_bin = get_wtype_bin()
|
|
59
|
+
subprocess.run((wtype_bin, '--', text), check=True)
|
|
60
|
+
else:
|
|
61
|
+
# xdotool types text by simulating key events
|
|
62
|
+
xdotool_bin = get_xdotool_bin()
|
|
63
|
+
subprocess.run(
|
|
64
|
+
(xdotool_bin, 'type', '--clearmodifiers', '--delay', '0', '--', text),
|
|
65
|
+
check=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def record_and_inject(
|
|
70
|
+
*,
|
|
71
|
+
model: WhisperModel,
|
|
72
|
+
stop_event: threading.Event,
|
|
73
|
+
sample_rate: int,
|
|
74
|
+
) -> None:
|
|
75
|
+
audio = record_until_event(stop_event=stop_event, sample_rate=sample_rate)
|
|
76
|
+
print('[cyan]Transcribing...[/cyan]')
|
|
77
|
+
text = transcribe(model, audio)
|
|
78
|
+
if text:
|
|
79
|
+
print(f'[green]Inserting:[/green] {text}')
|
|
80
|
+
inject_text(text)
|
|
81
|
+
else:
|
|
82
|
+
print('[dim]No speech detected.[/dim]')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Recorder:
|
|
86
|
+
def __init__(self, *, model: WhisperModel, sample_rate: int):
|
|
87
|
+
self._model = model
|
|
88
|
+
self._sample_rate = sample_rate
|
|
89
|
+
self._stop_event: threading.Event | None = None
|
|
90
|
+
self._future: Future | None = None
|
|
91
|
+
self._executor = ThreadPoolExecutor(max_workers=1)
|
|
92
|
+
|
|
93
|
+
def start(self):
|
|
94
|
+
if self._future and not self._future.done():
|
|
95
|
+
return
|
|
96
|
+
self._stop_event = threading.Event()
|
|
97
|
+
self._future = self._executor.submit(
|
|
98
|
+
record_and_inject,
|
|
99
|
+
model=self._model,
|
|
100
|
+
stop_event=self._stop_event,
|
|
101
|
+
sample_rate=self._sample_rate,
|
|
102
|
+
)
|
|
103
|
+
print('[yellow]Recording...[/yellow]')
|
|
104
|
+
|
|
105
|
+
def stop(self):
|
|
106
|
+
if self._stop_event is not None:
|
|
107
|
+
self._stop_event.set()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest.util
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from bx_py_utils.test_utils.deny_requests import deny_any_real_request
|
|
6
|
+
from cli_base.cli_tools.verbosity import MAX_LOG_LEVEL, setup_logging
|
|
7
|
+
from rich import print
|
|
8
|
+
from typeguard import install_import_hook
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Check type annotations via typeguard in all tests:
|
|
12
|
+
install_import_hook(packages=('stt2desktop',))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def pre_configure_tests() -> None:
|
|
16
|
+
print(f'Configure unittests via "load_tests Protocol" from {Path(__file__).relative_to(Path.cwd())}')
|
|
17
|
+
|
|
18
|
+
# Hacky way to display more "assert"-Context in failing tests:
|
|
19
|
+
_MIN_MAX_DIFF = unittest.util._MAX_LENGTH - unittest.util._MIN_DIFF_LEN
|
|
20
|
+
unittest.util._MAX_LENGTH = int(os.environ.get('UNITTEST_MAX_LENGTH', 2000))
|
|
21
|
+
unittest.util._MIN_DIFF_LEN = unittest.util._MAX_LENGTH - _MIN_MAX_DIFF
|
|
22
|
+
|
|
23
|
+
# Deny any request via docket/urllib3 because tests they should mock all requests:
|
|
24
|
+
deny_any_real_request()
|
|
25
|
+
|
|
26
|
+
# Display DEBUG logs in tests:
|
|
27
|
+
setup_logging(verbosity=MAX_LOG_LEVEL)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_tests(loader, tests, pattern):
|
|
31
|
+
"""
|
|
32
|
+
Use unittest "load_tests Protocol" as a hook to setup test environment before running tests.
|
|
33
|
+
https://docs.python.org/3/library/unittest.html#load-tests-protocol
|
|
34
|
+
"""
|
|
35
|
+
pre_configure_tests()
|
|
36
|
+
return loader.discover(start_dir=Path(__file__).parent, pattern=pattern)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from unittest import TestCase
|
|
3
|
+
|
|
4
|
+
from bx_py_utils.path import assert_is_file
|
|
5
|
+
from cli_base.cli_tools.code_style import assert_code_style
|
|
6
|
+
from cli_base.cli_tools.subprocess_utils import ToolsExecutor
|
|
7
|
+
from manageprojects.test_utils.project_setup import (
|
|
8
|
+
check_editor_config,
|
|
9
|
+
get_py_max_line_length,
|
|
10
|
+
)
|
|
11
|
+
from packaging.version import Version
|
|
12
|
+
|
|
13
|
+
from stt2desktop import __version__
|
|
14
|
+
from stt2desktop.cli_dev import PACKAGE_ROOT
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProjectSetupTestCase(TestCase):
|
|
18
|
+
def test_version(self):
|
|
19
|
+
self.assertIsNotNone(__version__)
|
|
20
|
+
|
|
21
|
+
version = Version(__version__) # Will raise InvalidVersion() if wrong formatted
|
|
22
|
+
self.assertEqual(str(version), __version__)
|
|
23
|
+
|
|
24
|
+
cli_bin = PACKAGE_ROOT / 'cli.py'
|
|
25
|
+
assert_is_file(cli_bin)
|
|
26
|
+
|
|
27
|
+
output = subprocess.check_output([cli_bin, 'version'], text=True)
|
|
28
|
+
self.assertIn(f'stt2desktop v{__version__}', output)
|
|
29
|
+
|
|
30
|
+
dev_cli_bin = PACKAGE_ROOT / 'dev-cli.py'
|
|
31
|
+
assert_is_file(dev_cli_bin)
|
|
32
|
+
|
|
33
|
+
output = subprocess.check_output([dev_cli_bin, 'version'], text=True)
|
|
34
|
+
self.assertIn(f'stt2desktop v{__version__}', output)
|
|
35
|
+
|
|
36
|
+
def test_code_style(self):
|
|
37
|
+
return_code = assert_code_style(package_root=PACKAGE_ROOT)
|
|
38
|
+
self.assertEqual(return_code, 0, 'Code style error, see output above!')
|
|
39
|
+
|
|
40
|
+
def test_check_editor_config(self):
|
|
41
|
+
check_editor_config(package_root=PACKAGE_ROOT)
|
|
42
|
+
|
|
43
|
+
max_line_length = get_py_max_line_length(package_root=PACKAGE_ROOT)
|
|
44
|
+
self.assertEqual(max_line_length, 119)
|
|
45
|
+
|
|
46
|
+
def test_pre_commit_hooks(self):
|
|
47
|
+
executor = ToolsExecutor(cwd=PACKAGE_ROOT)
|
|
48
|
+
for command in ('migrate-config', 'validate-config', 'validate-manifest'):
|
|
49
|
+
executor.verbose_check_call('pre-commit', command, exit_on_error=True)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from bx_py_utils.auto_doc import assert_readme_block
|
|
2
|
+
from bx_py_utils.path import assert_is_file
|
|
3
|
+
from cli_base.cli_tools.test_utils.assertion import assert_in
|
|
4
|
+
from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRich, invoke
|
|
5
|
+
from manageprojects.tests.base import BaseTestCase
|
|
6
|
+
|
|
7
|
+
from stt2desktop import constants
|
|
8
|
+
from stt2desktop.cli_dev import PACKAGE_ROOT
|
|
9
|
+
from stt2desktop.constants import DEFAULT_WHISPER_NUM_WORKERS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def assert_cli_help_in_readme(text_block: str, marker: str):
|
|
13
|
+
README_PATH = PACKAGE_ROOT / 'README.md'
|
|
14
|
+
assert_is_file(README_PATH)
|
|
15
|
+
|
|
16
|
+
text_block = text_block.replace(constants.CLI_EPILOG, '')
|
|
17
|
+
text_block = f'```\n{text_block.strip()}\n```'
|
|
18
|
+
assert_readme_block(
|
|
19
|
+
readme_path=README_PATH,
|
|
20
|
+
text_block=text_block,
|
|
21
|
+
start_marker_line=f'[comment]: <> (✂✂✂ auto generated {marker} start ✂✂✂)',
|
|
22
|
+
end_marker_line=f'[comment]: <> (✂✂✂ auto generated {marker} end ✂✂✂)',
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ReadmeTestCase(BaseTestCase):
|
|
27
|
+
def test_main_help(self):
|
|
28
|
+
with NoColorEnvRich():
|
|
29
|
+
stdout = invoke(
|
|
30
|
+
cli_bin=PACKAGE_ROOT / 'cli.py',
|
|
31
|
+
args=['--help'],
|
|
32
|
+
strip_line_prefix='usage: ',
|
|
33
|
+
)
|
|
34
|
+
assert_in(
|
|
35
|
+
content=stdout,
|
|
36
|
+
parts=(
|
|
37
|
+
'usage: ./cli.py [-h]',
|
|
38
|
+
' version ',
|
|
39
|
+
'Print version and exit',
|
|
40
|
+
constants.CLI_EPILOG,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
assert_cli_help_in_readme(text_block=stdout, marker='main help')
|
|
44
|
+
|
|
45
|
+
def test_listen_help(self):
|
|
46
|
+
with NoColorEnvRich():
|
|
47
|
+
stdout = invoke(
|
|
48
|
+
cli_bin=PACKAGE_ROOT / 'cli.py',
|
|
49
|
+
args=['listen', '--help'],
|
|
50
|
+
strip_line_prefix='usage: ./cli.py listen ',
|
|
51
|
+
)
|
|
52
|
+
cpu_count_part = f'Defaults to CPU count. (default: {DEFAULT_WHISPER_NUM_WORKERS})'
|
|
53
|
+
assert_in(
|
|
54
|
+
content=stdout,
|
|
55
|
+
parts=(
|
|
56
|
+
'usage: ./cli.py listen ',
|
|
57
|
+
' --hotkey ',
|
|
58
|
+
' --model ',
|
|
59
|
+
cpu_count_part,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
stdout = stdout.replace(cpu_count_part, 'Defaults to CPU count. (default:XY)').strip()
|
|
63
|
+
assert_cli_help_in_readme(text_block=stdout, marker='listen help')
|
|
64
|
+
|
|
65
|
+
def test_dev_help(self):
|
|
66
|
+
with NoColorEnvRich():
|
|
67
|
+
stdout = invoke(
|
|
68
|
+
cli_bin=PACKAGE_ROOT / 'dev-cli.py',
|
|
69
|
+
args=['--help'],
|
|
70
|
+
strip_line_prefix='usage: ',
|
|
71
|
+
)
|
|
72
|
+
assert_in(
|
|
73
|
+
content=stdout,
|
|
74
|
+
parts=(
|
|
75
|
+
'usage: ./dev-cli.py [-h]',
|
|
76
|
+
' lint ',
|
|
77
|
+
' coverage ',
|
|
78
|
+
' update-readme-history ',
|
|
79
|
+
' publish ',
|
|
80
|
+
constants.CLI_EPILOG,
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
assert_cli_help_in_readme(text_block=stdout, marker='dev help')
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stt2desktop
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Local speech-to-text for desktop using faster-whisper
|
|
5
|
+
Project-URL: Documentation, https://github.com/jedie/stt2desktop
|
|
6
|
+
Project-URL: Source, https://github.com/jedie/stt2desktop
|
|
7
|
+
Author-email: Jens Diemer <stt2desktop@jensdiemer.de>
|
|
8
|
+
License: GPL-3.0-or-later
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: bx-py-utils
|
|
11
|
+
Requires-Dist: cli-base-utilities>=0.27.1
|
|
12
|
+
Requires-Dist: faster-whisper
|
|
13
|
+
Requires-Dist: pynput
|
|
14
|
+
Requires-Dist: rich
|
|
15
|
+
Requires-Dist: sounddevice
|
|
16
|
+
Requires-Dist: tyro
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# stt2desktop
|
|
20
|
+
|
|
21
|
+
[](https://github.com/jedie/stt2desktop/actions/workflows/tests.yml)
|
|
22
|
+
[](https://app.codecov.io/github/jedie/stt2desktop)
|
|
23
|
+
[](https://pypi.org/project/stt2desktop/)
|
|
24
|
+
[](https://github.com/jedie/stt2desktop/blob/main/pyproject.toml)
|
|
25
|
+
[](https://github.com/jedie/stt2desktop/blob/main/LICENSE)
|
|
26
|
+
|
|
27
|
+
Local speech-to-text for desktop using [faster-whisper](https://github.com/SYSTRAN/faster-whisper).
|
|
28
|
+
|
|
29
|
+
Let's you dictate text into any application without sending audio to any cloud services.
|
|
30
|
+
Everything runs locally on your machine — no internet connection required after the initial model was download.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
1. Run `./cli.py listen` — the Whisper model is loaded into memory (downloaded on first run).
|
|
35
|
+
2. Hold **Scroll Lock** to record from your microphone.
|
|
36
|
+
3. Release **Scroll Lock** — the audio is transcribed locally by faster-whisper (CPU, `int8`).
|
|
37
|
+
4. The text is typed into the focused window via `wtype` (Wayland) or `xdotool` (X11).
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
Requirements: Python 3.12+, [`uv`](https://github.com/astral-sh/uv), a working microphone, and either `wtype` (Wayland) or `xdotool` (X11):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
sudo apt install wtype # Wayland
|
|
45
|
+
sudo apt install xdotool # X11
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then run:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
./cli.py listen
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
[comment]: <> (✂✂✂ auto generated listen help start ✂✂✂)
|
|
55
|
+
```
|
|
56
|
+
usage: ./cli.py listen [-h] [LISTEN OPTIONS]
|
|
57
|
+
|
|
58
|
+
Start the STT listener. Hold the hotkey to record, release to transcribe and insert.
|
|
59
|
+
|
|
60
|
+
╭─ options ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
61
|
+
│ -h, --help show this help message and exit │
|
|
62
|
+
│ -v, --verbosity Verbosity level; e.g.: -v, -vv, -vvv, etc. (repeatable) │
|
|
63
|
+
│ --model {tiny_en,tiny,base_en,base,small_en,small,medium_en,medium,large_v1,large_v2,large_v3,large,distil_large_v2, │
|
|
64
|
+
│ distil_medium_en,distil_small_en,distil_large_v3,distil_large_v3_5,large_v3_turbo,turbo} │
|
|
65
|
+
│ Whisper model to use for transcription. (default: small) │
|
|
66
|
+
│ --hotkey {alt,alt_l,alt_r,alt_gr,backspace,caps_lock,cmd,cmd_l,cmd_r,ctrl,ctrl_l,ctrl_r,delete,down,end,enter,esc, │
|
|
67
|
+
│ f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13,f14,f15,f16,f17,f18,f19,f20,home,left,page_down,page_up,right,shift, │
|
|
68
|
+
│ shift_l,shift_r,space,tab,up,media_play_pause,media_volume_mute,media_volume_down,media_volume_up,media_previous, │
|
|
69
|
+
│ media_next,insert,menu,num_lock,pause,print_screen,scroll_lock} │
|
|
70
|
+
│ Key to hold for recording. Release to transcribe and insert text. (default: scroll_lock) │
|
|
71
|
+
│ --sample-rate INT Audio sample rate in Hz. Whisper expects 16000. (default: 16000) │
|
|
72
|
+
│ --device STR Device to run inference on, e.g. cpu or cuda. (default: auto) │
|
|
73
|
+
│ --compute-type STR Quantization type, e.g. int8, float16, float32. (default: int8) │
|
|
74
|
+
│ --num-workers INT Number of parallel transcription workers. Defaults to CPU count. (default:XY) │
|
|
75
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
76
|
+
```
|
|
77
|
+
[comment]: <> (✂✂✂ auto generated listen help end ✂✂✂)
|
|
78
|
+
|
|
79
|
+
### Available models
|
|
80
|
+
|
|
81
|
+
| Model | Size | Speed | Accuracy |
|
|
82
|
+
|-------|------|-------|----------|
|
|
83
|
+
| `tiny` | ~75 MB | fastest | lowest |
|
|
84
|
+
| `base` | ~145 MB | fast | good (default) |
|
|
85
|
+
| `small` | ~460 MB | slower | better |
|
|
86
|
+
| `medium` | ~1.5 GB | slow | high |
|
|
87
|
+
|
|
88
|
+
Larger models produce more accurate transcriptions but take longer to process. For most use cases `base` or `small` is a good starting point.
|
|
89
|
+
|
|
90
|
+
## CLI
|
|
91
|
+
|
|
92
|
+
[comment]: <> (✂✂✂ auto generated main help start ✂✂✂)
|
|
93
|
+
```
|
|
94
|
+
usage: ./cli.py [-h] {listen,version}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
╭─ options ─────────────────────────────────────────────────────────────────────────────────────────╮
|
|
99
|
+
│ -h, --help show this help message and exit │
|
|
100
|
+
╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
101
|
+
╭─ subcommands ─────────────────────────────────────────────────────────────────────────────────────╮
|
|
102
|
+
│ (required) │
|
|
103
|
+
│ • listen Start the STT listener. Hold the hotkey to record, release to transcribe and insert. │
|
|
104
|
+
│ • version Print version and exit │
|
|
105
|
+
╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
106
|
+
```
|
|
107
|
+
[comment]: <> (✂✂✂ auto generated main help end ✂✂✂)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## start development
|
|
111
|
+
|
|
112
|
+
At least `uv` is needed. Install e.g.: via pipx:
|
|
113
|
+
```bash
|
|
114
|
+
apt-get install pipx
|
|
115
|
+
pipx install uv
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Clone the project and just start the CLI help commands.
|
|
119
|
+
A virtual environment will be created/updated automatically.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
~$ git clone https://github.com/jedie/stt2desktop.git
|
|
123
|
+
~$ cd stt2desktop
|
|
124
|
+
~/stt2desktop$ ./cli.py --help
|
|
125
|
+
~/stt2desktop$ ./dev-cli.py --help
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
[comment]: <> (✂✂✂ auto generated dev help start ✂✂✂)
|
|
129
|
+
```
|
|
130
|
+
usage: ./dev-cli.py [-h] {coverage,install,lint,mypy,nox,pip-audit,publish,test,update,update-readme-history,update-test-snapshot-files,version}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
╭─ options ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
135
|
+
│ -h, --help show this help message and exit │
|
|
136
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
137
|
+
╭─ subcommands ────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
138
|
+
│ (required) │
|
|
139
|
+
│ • coverage Run tests and show coverage report. │
|
|
140
|
+
│ • install Install requirements and 'stt2desktop' via pip as editable. │
|
|
141
|
+
│ • lint Check/fix code style by run: "ruff check --fix" │
|
|
142
|
+
│ • mypy Run Mypy (configured in pyproject.toml) │
|
|
143
|
+
│ • nox Run nox │
|
|
144
|
+
│ • pip-audit │
|
|
145
|
+
│ Run pip-audit check against current requirements files │
|
|
146
|
+
│ • publish Build and upload this project to PyPi │
|
|
147
|
+
│ • test Run unittests │
|
|
148
|
+
│ • update Update dependencies (uv.lock) and git pre-commit hooks │
|
|
149
|
+
│ • update-readme-history │
|
|
150
|
+
│ Update project history base on git commits/tags in README.md Will be exited with 1 if the README.md │
|
|
151
|
+
│ was updated otherwise with 0. │
|
|
152
|
+
│ │
|
|
153
|
+
│ Also, callable via e.g.: │
|
|
154
|
+
│ python -m cli_base update-readme-history -v │
|
|
155
|
+
│ • update-test-snapshot-files │
|
|
156
|
+
│ Update all test snapshot files (by remove and recreate all snapshot files) │
|
|
157
|
+
│ • version Print version and exit │
|
|
158
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
159
|
+
```
|
|
160
|
+
[comment]: <> (✂✂✂ auto generated dev help end ✂✂✂)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
## History
|
|
164
|
+
|
|
165
|
+
[comment]: <> (✂✂✂ auto generated history start ✂✂✂)
|
|
166
|
+
|
|
167
|
+
* [**dev**](https://github.com/jedie/stt2desktop/compare/b407f8f...main)
|
|
168
|
+
* 2026-03-26 - Add POC
|
|
169
|
+
* 2026-03-26 - init
|
|
170
|
+
|
|
171
|
+
[comment]: <> (✂✂✂ auto generated history end ✂✂✂)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
stt2desktop/__init__.py,sha256=D2_c_60RhAe2VD2IrFDTBzVzJgqchFg9lAevaag8f-s,217
|
|
2
|
+
stt2desktop/__main__.py,sha256=qoH1ZrA02HBmz5E16WpDaGfi3jUnm4s8tkyEbfaRjU0,154
|
|
3
|
+
stt2desktop/constants.py,sha256=nnjqZZ_Ym2oM_rqaxSxWPJum3nxOxYUT-U_L5pSbEfk,769
|
|
4
|
+
stt2desktop/get_bin.py,sha256=X-gV5BYOqlDdmHh--VNZsYS2U4rSyJNxHiKtwTQuDQo,831
|
|
5
|
+
stt2desktop/stt.py,sha256=25rjDtxWMknrn5lQ3ByV3v8JsrCxAUH6tR7KN9tHOfY,3508
|
|
6
|
+
stt2desktop/cli_app/__init__.py,sha256=DmwDpQ2ga-FbkOLtPCyllv6ID-_GelIpV_WTNcYK1s0,951
|
|
7
|
+
stt2desktop/cli_app/listen.py,sha256=siFfL17pt40oJSNVZWStqfGzG1WbLNMnGuJUeS_REhU,2618
|
|
8
|
+
stt2desktop/cli_dev/__init__.py,sha256=cCFldU21Py496ZIDxPWEvjCNKZT0x92sDJNMqgmGcII,1889
|
|
9
|
+
stt2desktop/cli_dev/__main__.py,sha256=e36uw5SUShOGKBRaC21xB5Rw2AmTQ-uKTDmCCnXjwx4,162
|
|
10
|
+
stt2desktop/cli_dev/code_style.py,sha256=BAXhqvmVPuDqC8D0AjIN0EJHPbT3GdzbIqePaEMHc6Y,386
|
|
11
|
+
stt2desktop/cli_dev/packaging.py,sha256=WvuV-kNHkoOnZSvwdqatNwMn8GoTcPsH9BpdxvpoWus,1814
|
|
12
|
+
stt2desktop/cli_dev/testing.py,sha256=yiXBX4LzVU6Eh12rj2ENJqt6V57l3XKB9D-gCMV2Kbw,1384
|
|
13
|
+
stt2desktop/cli_dev/update_readme_history.py,sha256=12swOa8O7y5Tkuw4pLN_cT_4zTTfSHe4VYlwHdnl_5k,896
|
|
14
|
+
stt2desktop/tests/__init__.py,sha256=-uUIzVrd-btVeP7UME5oDmUpNNVA9tr1ufaKhBUlRR4,1334
|
|
15
|
+
stt2desktop/tests/test_doctests.py,sha256=EGemZFe1HDN2HCBdxeIC_EDjA0OMVV0hfH3RoEbj-t4,217
|
|
16
|
+
stt2desktop/tests/test_project_setup.py,sha256=zkmzVwDDi6NW-ndSpvdWpYiwx9PLuuz8xEq-d9owmmM,1788
|
|
17
|
+
stt2desktop/tests/test_readme.py,sha256=R3_tXsr2CSzAnJomJ0sWPGfU9BV2R-tyC5ErerJXFtU,2910
|
|
18
|
+
stt2desktop/tests/test_readme_history.py,sha256=PbxF_yRspyRwkVCQosZXevIHrsla3WFBTUVeLI0bb-s,263
|
|
19
|
+
stt2desktop-0.0.1.dist-info/METADATA,sha256=2Dj2dEDxCtEsCqVDfCbiD3TnZzkEGNqquSaC89ju9l8,12134
|
|
20
|
+
stt2desktop-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
21
|
+
stt2desktop-0.0.1.dist-info/entry_points.txt,sha256=ezr7yveS5bFKyhDtKg_Xq2jd6_e3cfHm9soM8rnS_II,58
|
|
22
|
+
stt2desktop-0.0.1.dist-info/RECORD,,
|