voicepad 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voicepad-0.1.1/PKG-INFO +89 -0
- voicepad-0.1.1/README.md +56 -0
- voicepad-0.1.1/pyproject.toml +55 -0
- voicepad-0.1.1/src/voicepad/__init__.py +5 -0
- voicepad-0.1.1/src/voicepad/__main__.py +6 -0
- voicepad-0.1.1/src/voicepad/cli/__init__.py +4 -0
- voicepad-0.1.1/src/voicepad/cli/config.py +295 -0
- voicepad-0.1.1/src/voicepad/cli/record.py +282 -0
- voicepad-0.1.1/src/voicepad/main.py +18 -0
voicepad-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voicepad
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Command-line interface for voice recording and GPU-accelerated transcription.
|
|
5
|
+
Keywords: cli,voice,audio,transcription,recording,speech-to-text,whisper,command-line,gpu-acceleration
|
|
6
|
+
Author: Rajesh Das
|
|
7
|
+
Author-email: Rajesh Das <rajesh@hyperoot.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Capture/Recording
|
|
19
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Dist: voicepad-core>=0.1.0
|
|
23
|
+
Requires-Dist: typer>=0.23.1
|
|
24
|
+
Requires-Dist: textual>=7.5.0
|
|
25
|
+
Requires-Dist: voicepad-core[gpu] ; extra == 'gpu'
|
|
26
|
+
Requires-Python: >=3.13
|
|
27
|
+
Project-URL: HomePage, https://hyp3r00t.github.io/voicepad/
|
|
28
|
+
Project-URL: Repository, https://github.com/HYP3R00T/voicepad
|
|
29
|
+
Project-URL: Issues, https://github.com/HYP3R00T/voicepad/issues
|
|
30
|
+
Project-URL: Documentation, https://hyp3r00t.github.io/voicepad/
|
|
31
|
+
Provides-Extra: gpu
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# voicepad CLI
|
|
35
|
+
|
|
36
|
+
Simple command-line interface for recording audio and managing transcription configuration.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install voicepad
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Requirements:** Python 3.13+
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# List audio input devices
|
|
50
|
+
voicepad config input
|
|
51
|
+
|
|
52
|
+
# Start recording (press Ctrl+C to stop)
|
|
53
|
+
voicepad record start
|
|
54
|
+
|
|
55
|
+
# Check system capabilities
|
|
56
|
+
voicepad config system
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Example: Record and Transcribe
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Record a meeting (will auto-transcribe)
|
|
63
|
+
voicepad record start --prefix team_meeting
|
|
64
|
+
|
|
65
|
+
# Output:
|
|
66
|
+
# - data/recordings/team_meeting_20260218_103045.wav
|
|
67
|
+
# - data/markdown/team_meeting_20260218_103045.md
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
- [CLI Command Reference](https://voicepad.readthedocs.io/packages/voicepad/) - Full documentation
|
|
73
|
+
- [voicepad-core Library](https://voicepad.readthedocs.io/packages/voicepad-core/) - Python API
|
|
74
|
+
- [Main README](https://github.com/HYP3R00T/voicepad#readme) - Project overview
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
Edit `voicepad.yaml` to set defaults:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
recordings_path: data/recordings
|
|
82
|
+
markdown_path: data/markdown
|
|
83
|
+
input_device_index: null
|
|
84
|
+
transcription_model: tiny
|
|
85
|
+
transcription_device: auto
|
|
86
|
+
transcription_compute_type: auto
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See the [full documentation](https://voicepad.readthedocs.io/packages/voicepad/) for all configuration options.
|
voicepad-0.1.1/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# voicepad CLI
|
|
2
|
+
|
|
3
|
+
Simple command-line interface for recording audio and managing transcription configuration.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install voicepad
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Requirements:** Python 3.13+
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# List audio input devices
|
|
17
|
+
voicepad config input
|
|
18
|
+
|
|
19
|
+
# Start recording (press Ctrl+C to stop)
|
|
20
|
+
voicepad record start
|
|
21
|
+
|
|
22
|
+
# Check system capabilities
|
|
23
|
+
voicepad config system
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Example: Record and Transcribe
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Record a meeting (will auto-transcribe)
|
|
30
|
+
voicepad record start --prefix team_meeting
|
|
31
|
+
|
|
32
|
+
# Output:
|
|
33
|
+
# - data/recordings/team_meeting_20260218_103045.wav
|
|
34
|
+
# - data/markdown/team_meeting_20260218_103045.md
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Documentation
|
|
38
|
+
|
|
39
|
+
- [CLI Command Reference](https://voicepad.readthedocs.io/packages/voicepad/) - Full documentation
|
|
40
|
+
- [voicepad-core Library](https://voicepad.readthedocs.io/packages/voicepad-core/) - Python API
|
|
41
|
+
- [Main README](https://github.com/HYP3R00T/voicepad#readme) - Project overview
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
Edit `voicepad.yaml` to set defaults:
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
recordings_path: data/recordings
|
|
49
|
+
markdown_path: data/markdown
|
|
50
|
+
input_device_index: null
|
|
51
|
+
transcription_model: tiny
|
|
52
|
+
transcription_device: auto
|
|
53
|
+
transcription_compute_type: auto
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
See the [full documentation](https://voicepad.readthedocs.io/packages/voicepad/) for all configuration options.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "voicepad"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Command-line interface for voice recording and GPU-accelerated transcription."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "Rajesh Das", email = "rajesh@hyperoot.dev" }]
|
|
7
|
+
requires-python = ">=3.13"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
keywords = [
|
|
10
|
+
"cli",
|
|
11
|
+
"voice",
|
|
12
|
+
"audio",
|
|
13
|
+
"transcription",
|
|
14
|
+
"recording",
|
|
15
|
+
"speech-to-text",
|
|
16
|
+
"whisper",
|
|
17
|
+
"command-line",
|
|
18
|
+
"gpu-acceleration",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Environment :: Console",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Intended Audience :: End Users/Desktop",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
30
|
+
"Topic :: Multimedia :: Sound/Audio :: Capture/Recording",
|
|
31
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
32
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
33
|
+
"Typing :: Typed",
|
|
34
|
+
]
|
|
35
|
+
dependencies = ["voicepad-core>=0.1.0", "typer>=0.23.1", "textual>=7.5.0"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
HomePage = "https://hyp3r00t.github.io/voicepad/"
|
|
39
|
+
Repository = "https://github.com/HYP3R00T/voicepad"
|
|
40
|
+
Issues = "https://github.com/HYP3R00T/voicepad/issues"
|
|
41
|
+
Documentation = "https://hyp3r00t.github.io/voicepad/"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
gpu = [
|
|
45
|
+
# Chain to voicepad-core GPU support for NVIDIA acceleration
|
|
46
|
+
# Users can install with: pip install voicepad[gpu]
|
|
47
|
+
"voicepad-core[gpu]",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
voicepad = "voicepad:main"
|
|
52
|
+
|
|
53
|
+
[build-system]
|
|
54
|
+
requires = ["uv_build>=0.10.2,<0.11.0"]
|
|
55
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Configuration management commands."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TypedDict, cast
|
|
6
|
+
|
|
7
|
+
import sounddevice as sd
|
|
8
|
+
import typer
|
|
9
|
+
from voicepad_core import get_config, get_config_with_metadata
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
config_app = typer.Typer(help="Configuration management commands")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class AudioDevice:
|
|
18
|
+
index: int
|
|
19
|
+
name: str
|
|
20
|
+
channels: int
|
|
21
|
+
sample_rate: int
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
return f"[{self.index}] {self.name} ({self.channels} in, {self.sample_rate}Hz)"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_input_devices() -> list[AudioDevice]:
|
|
28
|
+
class _DeviceInfo(TypedDict, total=False):
|
|
29
|
+
name: str
|
|
30
|
+
max_input_channels: int
|
|
31
|
+
default_samplerate: float
|
|
32
|
+
|
|
33
|
+
devices: list[AudioDevice] = []
|
|
34
|
+
all_devices = cast(list[_DeviceInfo], sd.query_devices())
|
|
35
|
+
|
|
36
|
+
for idx, dev in enumerate(all_devices):
|
|
37
|
+
max_inputs = dev.get("max_input_channels") or 0
|
|
38
|
+
if max_inputs <= 0:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
name = dev.get("name", f"Device {idx}")
|
|
42
|
+
default_rate = dev.get("default_samplerate", 0.0)
|
|
43
|
+
rate = int(default_rate) if isinstance(default_rate, (int, float)) and default_rate > 0 else 44100
|
|
44
|
+
devices.append(AudioDevice(index=idx, name=name, channels=int(max_inputs), sample_rate=rate))
|
|
45
|
+
|
|
46
|
+
return devices
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@config_app.command("input")
|
|
50
|
+
def list_input_devices() -> None:
|
|
51
|
+
"""List available audio input devices for configuration."""
|
|
52
|
+
devices = _get_input_devices()
|
|
53
|
+
if not devices:
|
|
54
|
+
typer.secho("No audio input devices detected.", fg=typer.colors.RED, err=True)
|
|
55
|
+
raise typer.Exit(1)
|
|
56
|
+
|
|
57
|
+
config = get_config()
|
|
58
|
+
configured_index = config.input_device_index
|
|
59
|
+
|
|
60
|
+
typer.echo("Configured default input device:")
|
|
61
|
+
if configured_index is None:
|
|
62
|
+
typer.echo(" not set")
|
|
63
|
+
else:
|
|
64
|
+
try:
|
|
65
|
+
configured_device = next(dev for dev in devices if dev.index == configured_index)
|
|
66
|
+
typer.echo(f" {configured_index}: {configured_device.name}")
|
|
67
|
+
except StopIteration:
|
|
68
|
+
typer.echo(f" {configured_index}: not found")
|
|
69
|
+
|
|
70
|
+
typer.echo("\nAvailable audio input devices:")
|
|
71
|
+
typer.echo("-" * 60)
|
|
72
|
+
for dev in devices:
|
|
73
|
+
selected_marker = " (configured)" if dev.index == configured_index else ""
|
|
74
|
+
typer.echo(f"{dev}{selected_marker}")
|
|
75
|
+
typer.echo("-" * 60)
|
|
76
|
+
|
|
77
|
+
_show_config_hint()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _show_config_hint() -> None:
|
|
81
|
+
config_file = None
|
|
82
|
+
config_source = None
|
|
83
|
+
|
|
84
|
+
_config, metadata = get_config_with_metadata()
|
|
85
|
+
if metadata.per_field:
|
|
86
|
+
first_field_source = next(iter(metadata.per_field.values()))
|
|
87
|
+
config_file = first_field_source.source_path
|
|
88
|
+
config_source = first_field_source.source
|
|
89
|
+
|
|
90
|
+
if config_file and config_source != "defaults":
|
|
91
|
+
typer.echo(f"Config file: {config_file}")
|
|
92
|
+
typer.echo("Set input_device_index in that file to persist the default.")
|
|
93
|
+
else:
|
|
94
|
+
typer.echo("Config file: using defaults (no external config found)")
|
|
95
|
+
typer.echo("Set input_device_index in voicepad.yaml to persist the default.")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@config_app.command("system")
|
|
99
|
+
def system_info() -> None:
|
|
100
|
+
"""Display comprehensive system information for transcription."""
|
|
101
|
+
from voicepad_core import (
|
|
102
|
+
get_cpu_info,
|
|
103
|
+
get_ram_info,
|
|
104
|
+
gpu_diagnostics,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
typer.echo("System Information")
|
|
108
|
+
typer.echo("=" * 60)
|
|
109
|
+
|
|
110
|
+
# RAM info
|
|
111
|
+
ram = get_ram_info()
|
|
112
|
+
typer.echo("\nRAM:")
|
|
113
|
+
typer.echo(f" Total: {ram.total_gb:.1f} GB")
|
|
114
|
+
typer.echo(f" Available: {ram.available_gb:.1f} GB")
|
|
115
|
+
|
|
116
|
+
if ram.available_gb >= 8:
|
|
117
|
+
typer.secho(" [OK] Sufficient RAM for transcription", fg=typer.colors.GREEN)
|
|
118
|
+
elif ram.available_gb >= 4:
|
|
119
|
+
typer.secho(" [WARNING] Moderate RAM - use smaller models", fg=typer.colors.YELLOW)
|
|
120
|
+
else:
|
|
121
|
+
typer.secho(" [WARNING] Limited RAM - use tiny model only", fg=typer.colors.YELLOW)
|
|
122
|
+
|
|
123
|
+
# CPU info
|
|
124
|
+
cpu = get_cpu_info()
|
|
125
|
+
typer.echo("\nCPU:")
|
|
126
|
+
typer.echo(f" Cores: {cpu.count}")
|
|
127
|
+
if cpu.model_name:
|
|
128
|
+
typer.echo(f" Model: {cpu.model_name}")
|
|
129
|
+
|
|
130
|
+
# GPU diagnostics
|
|
131
|
+
gpu = gpu_diagnostics()
|
|
132
|
+
typer.echo("\nGPU:")
|
|
133
|
+
|
|
134
|
+
if gpu.nvidia_smi.success:
|
|
135
|
+
typer.secho(" NVIDIA Driver: [OK] Detected", fg=typer.colors.GREEN)
|
|
136
|
+
else:
|
|
137
|
+
typer.secho(" NVIDIA Driver: [NOT FOUND]", fg=typer.colors.YELLOW)
|
|
138
|
+
|
|
139
|
+
if gpu.ctranslate2_cuda.success:
|
|
140
|
+
count = gpu.ctranslate2_cuda.cuda_device_count or 0
|
|
141
|
+
typer.secho(f" CUDA Devices: {count} device(s) available", fg=typer.colors.GREEN)
|
|
142
|
+
else:
|
|
143
|
+
typer.echo(" CUDA Devices: Not available")
|
|
144
|
+
|
|
145
|
+
if gpu.faster_whisper_gpu.success:
|
|
146
|
+
typer.secho(" faster-whisper GPU: [OK] Compatible", fg=typer.colors.GREEN)
|
|
147
|
+
else:
|
|
148
|
+
typer.echo(" faster-whisper GPU: Not compatible (CPU mode only)")
|
|
149
|
+
|
|
150
|
+
typer.echo("=" * 60)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@config_app.command("recommend")
|
|
154
|
+
def recommend_model() -> None:
|
|
155
|
+
"""Get model recommendations based on system capabilities."""
|
|
156
|
+
from voicepad_core import (
|
|
157
|
+
get_available_models,
|
|
158
|
+
get_cpu_info,
|
|
159
|
+
get_model_recommendation,
|
|
160
|
+
get_ram_info,
|
|
161
|
+
gpu_diagnostics,
|
|
162
|
+
)
|
|
163
|
+
from voicepad_core.diagnostics.models import SystemInfo
|
|
164
|
+
|
|
165
|
+
# Gather system info
|
|
166
|
+
ram = get_ram_info()
|
|
167
|
+
cpu = get_cpu_info()
|
|
168
|
+
gpu = gpu_diagnostics()
|
|
169
|
+
available_models = get_available_models()
|
|
170
|
+
|
|
171
|
+
system_info = SystemInfo(ram=ram, cpu=cpu, gpu_diagnostics=gpu)
|
|
172
|
+
|
|
173
|
+
# Get recommendation
|
|
174
|
+
recommendation = get_model_recommendation(system_info, available_models)
|
|
175
|
+
|
|
176
|
+
# Display recommendation
|
|
177
|
+
typer.echo("Model Recommendation Based on System Capabilities")
|
|
178
|
+
typer.echo("=" * 60)
|
|
179
|
+
typer.echo("\nRecommended Configuration:")
|
|
180
|
+
typer.secho(f" Model: {recommendation.recommended_model}", fg=typer.colors.GREEN, bold=True)
|
|
181
|
+
typer.secho(f" Device: {recommendation.recommended_device}", fg=typer.colors.GREEN, bold=True)
|
|
182
|
+
typer.secho(f" Compute Type: {recommendation.recommended_compute_type}", fg=typer.colors.GREEN, bold=True)
|
|
183
|
+
|
|
184
|
+
typer.echo("\nReason:")
|
|
185
|
+
typer.echo(f" {recommendation.reason}")
|
|
186
|
+
|
|
187
|
+
if recommendation.alternative_models:
|
|
188
|
+
typer.echo("\nAlternative Models:")
|
|
189
|
+
for alt in recommendation.alternative_models[:5]: # Show top 5
|
|
190
|
+
typer.echo(f" - {alt}")
|
|
191
|
+
|
|
192
|
+
typer.echo("\nTo apply this configuration, add to voicepad.yaml:")
|
|
193
|
+
typer.echo(f" transcription_model: {recommendation.recommended_model}")
|
|
194
|
+
typer.echo(f" transcription_device: {recommendation.recommended_device}")
|
|
195
|
+
typer.echo(f" transcription_compute_type: {recommendation.recommended_compute_type}")
|
|
196
|
+
typer.echo("=" * 60)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@config_app.command("transcription")
|
|
200
|
+
def transcription_config() -> None:
|
|
201
|
+
"""View current transcription configuration."""
|
|
202
|
+
from voicepad_core import (
|
|
203
|
+
get_available_models,
|
|
204
|
+
get_config,
|
|
205
|
+
get_cpu_info,
|
|
206
|
+
get_model_recommendation,
|
|
207
|
+
get_ram_info,
|
|
208
|
+
gpu_diagnostics,
|
|
209
|
+
)
|
|
210
|
+
from voicepad_core.diagnostics.models import SystemInfo
|
|
211
|
+
|
|
212
|
+
config = get_config()
|
|
213
|
+
|
|
214
|
+
typer.echo("Transcription Configuration")
|
|
215
|
+
typer.echo("=" * 60)
|
|
216
|
+
typer.echo("\nCurrent Settings:")
|
|
217
|
+
typer.echo(f" Model: {config.transcription_model}")
|
|
218
|
+
typer.echo(f" Device: {config.transcription_device}")
|
|
219
|
+
typer.echo(f" Compute Type: {config.transcription_compute_type}")
|
|
220
|
+
|
|
221
|
+
# Get recommendation for comparison
|
|
222
|
+
ram = get_ram_info()
|
|
223
|
+
cpu = get_cpu_info()
|
|
224
|
+
gpu = gpu_diagnostics()
|
|
225
|
+
available_models = get_available_models()
|
|
226
|
+
system_info = SystemInfo(ram=ram, cpu=cpu, gpu_diagnostics=gpu)
|
|
227
|
+
recommendation = get_model_recommendation(system_info, available_models)
|
|
228
|
+
|
|
229
|
+
typer.echo("\nRecommended Settings:")
|
|
230
|
+
if (
|
|
231
|
+
config.transcription_model == recommendation.recommended_model
|
|
232
|
+
and config.transcription_device == recommendation.recommended_device
|
|
233
|
+
and config.transcription_compute_type == recommendation.recommended_compute_type
|
|
234
|
+
):
|
|
235
|
+
typer.secho(" [OK] Your configuration matches the recommendation!", fg=typer.colors.GREEN)
|
|
236
|
+
else:
|
|
237
|
+
typer.secho(" [INFO] Consider updating your configuration:", fg=typer.colors.YELLOW)
|
|
238
|
+
typer.echo(f" Model: {recommendation.recommended_model}")
|
|
239
|
+
typer.echo(f" Device: {recommendation.recommended_device}")
|
|
240
|
+
typer.echo(f" Compute Type: {recommendation.recommended_compute_type}")
|
|
241
|
+
typer.echo(f"\n Reason: {recommendation.reason}")
|
|
242
|
+
|
|
243
|
+
typer.echo("=" * 60)
|
|
244
|
+
_show_config_hint()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@config_app.command("models")
|
|
248
|
+
def list_models() -> None:
|
|
249
|
+
"""List all available Whisper models."""
|
|
250
|
+
from voicepad_core import get_available_models
|
|
251
|
+
|
|
252
|
+
models = get_available_models()
|
|
253
|
+
|
|
254
|
+
typer.echo("Available Whisper Models")
|
|
255
|
+
typer.echo("=" * 60)
|
|
256
|
+
|
|
257
|
+
# Categorize models
|
|
258
|
+
categories = {
|
|
259
|
+
"Tiny": [],
|
|
260
|
+
"Base": [],
|
|
261
|
+
"Small": [],
|
|
262
|
+
"Medium": [],
|
|
263
|
+
"Large": [],
|
|
264
|
+
"Turbo": [],
|
|
265
|
+
"Distil": [],
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for model in models:
|
|
269
|
+
model_lower = model.lower()
|
|
270
|
+
if "turbo" in model_lower:
|
|
271
|
+
categories["Turbo"].append(model)
|
|
272
|
+
elif "distil" in model_lower:
|
|
273
|
+
categories["Distil"].append(model)
|
|
274
|
+
elif "large" in model_lower:
|
|
275
|
+
categories["Large"].append(model)
|
|
276
|
+
elif "medium" in model_lower:
|
|
277
|
+
categories["Medium"].append(model)
|
|
278
|
+
elif "small" in model_lower:
|
|
279
|
+
categories["Small"].append(model)
|
|
280
|
+
elif "base" in model_lower:
|
|
281
|
+
categories["Base"].append(model)
|
|
282
|
+
elif "tiny" in model_lower:
|
|
283
|
+
categories["Tiny"].append(model)
|
|
284
|
+
|
|
285
|
+
for category, category_models in categories.items():
|
|
286
|
+
if category_models:
|
|
287
|
+
typer.echo(f"\n{category}:")
|
|
288
|
+
for model in sorted(category_models):
|
|
289
|
+
en_only = " (English-only)" if ".en" in model else ""
|
|
290
|
+
typer.echo(f" - {model}{en_only}")
|
|
291
|
+
|
|
292
|
+
typer.echo("\n" + "=" * 60)
|
|
293
|
+
typer.echo(f"Total: {len(models)} models available")
|
|
294
|
+
typer.echo("\nTip: Use 'voicepad config recommend' to get a model recommendation")
|
|
295
|
+
typer.echo(" based on your system capabilities.")
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Audio recording CLI commands for voicepad."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import signal
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from voicepad_core import AudioRecorder, AudioRecorderError, get_config
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
record_app = typer.Typer(help="Audio recording commands")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _handle_interrupt(recorder: AudioRecorder) -> Path | None:
|
|
17
|
+
"""Handle keyboard interrupt during recording.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
recorder: The AudioRecorder instance to stop.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Path to saved audio file, or None if failed.
|
|
24
|
+
"""
|
|
25
|
+
typer.echo("\n\n[!] Recording stopped by user")
|
|
26
|
+
try:
|
|
27
|
+
output_file = recorder.stop_recording()
|
|
28
|
+
if output_file:
|
|
29
|
+
typer.secho(f"[OK] Recording saved to: {output_file}", fg=typer.colors.GREEN)
|
|
30
|
+
return output_file
|
|
31
|
+
except AudioRecorderError as e:
|
|
32
|
+
typer.secho(f"[ERROR] Error stopping recording: {e}", fg=typer.colors.RED, err=True)
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _transcribe_audio_file(output_file: Path, config) -> None:
|
|
37
|
+
"""Transcribe audio file and display results.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
output_file: Path to the audio file to transcribe.
|
|
41
|
+
config: Configuration object.
|
|
42
|
+
"""
|
|
43
|
+
typer.echo()
|
|
44
|
+
typer.secho("[*] Transcribing audio...", fg=typer.colors.CYAN)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
from voicepad_core import TranscriptionError, transcribe_audio
|
|
48
|
+
|
|
49
|
+
# Generate output path
|
|
50
|
+
md_path = config.markdown_path / f"{output_file.stem}.md"
|
|
51
|
+
config.markdown_path.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
# Transcribe
|
|
54
|
+
stats = transcribe_audio(output_file, md_path, config)
|
|
55
|
+
|
|
56
|
+
# Check if GPU fallback occurred
|
|
57
|
+
fallback_info = stats.get("fallback_info", {})
|
|
58
|
+
if fallback_info.get("fallback_occurred"):
|
|
59
|
+
typer.echo()
|
|
60
|
+
typer.secho("[!] GPU requested but CUDA libraries not available", fg=typer.colors.YELLOW)
|
|
61
|
+
|
|
62
|
+
# Show what's missing
|
|
63
|
+
missing = fallback_info.get("missing_components", [])
|
|
64
|
+
if missing:
|
|
65
|
+
typer.echo(f" Missing: {', '.join(missing)}")
|
|
66
|
+
|
|
67
|
+
typer.echo()
|
|
68
|
+
typer.echo(" For 4x faster transcription, install GPU support:")
|
|
69
|
+
typer.secho(" pip install voicepad-core[gpu]", fg=typer.colors.CYAN)
|
|
70
|
+
typer.echo()
|
|
71
|
+
typer.echo(" Using CPU for now...")
|
|
72
|
+
|
|
73
|
+
# Success message
|
|
74
|
+
typer.echo()
|
|
75
|
+
device = stats.get("device", "cpu")
|
|
76
|
+
duration = stats.get("duration", 0)
|
|
77
|
+
typer.secho(f"[OK] Transcription saved! ({duration:.1f}s using {device})", fg=typer.colors.GREEN)
|
|
78
|
+
typer.echo(f" File: {md_path}")
|
|
79
|
+
typer.echo(f" Language: {stats['language']} ({stats['language_probability'] * 100:.1f}%)")
|
|
80
|
+
typer.echo(f" Words: {stats['word_count']}")
|
|
81
|
+
typer.echo(f" Segments: {stats['segment_count']}")
|
|
82
|
+
|
|
83
|
+
except TranscriptionError as e:
|
|
84
|
+
typer.secho(f"[ERROR] Transcription failed: {e}", fg=typer.colors.RED, err=True)
|
|
85
|
+
typer.echo(" Recording was saved successfully, but transcription failed.")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.exception("Unexpected error during transcription")
|
|
88
|
+
typer.secho(f"[ERROR] Unexpected transcription error: {e}", fg=typer.colors.RED, err=True)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@record_app.command("start")
|
|
92
|
+
def start_recording(
|
|
93
|
+
prefix: str | None = typer.Option(
|
|
94
|
+
None,
|
|
95
|
+
"--prefix",
|
|
96
|
+
"-p",
|
|
97
|
+
help="Custom prefix for the recording filename (overrides config)",
|
|
98
|
+
),
|
|
99
|
+
duration: float | None = typer.Option(
|
|
100
|
+
None,
|
|
101
|
+
"--duration",
|
|
102
|
+
"-d",
|
|
103
|
+
help="Duration in seconds for fixed-length recording (optional)",
|
|
104
|
+
min=0.1,
|
|
105
|
+
),
|
|
106
|
+
transcribe: bool = typer.Option(
|
|
107
|
+
True,
|
|
108
|
+
"--transcribe/--no-transcribe",
|
|
109
|
+
help="Transcribe audio after recording (default: enabled)",
|
|
110
|
+
),
|
|
111
|
+
vad: bool | None = typer.Option(
|
|
112
|
+
None,
|
|
113
|
+
"--vad/--no-vad",
|
|
114
|
+
help="Enable VAD-based chunking for long recordings (uses config default if not specified)",
|
|
115
|
+
),
|
|
116
|
+
min_chunk_duration: float | None = typer.Option(
|
|
117
|
+
None,
|
|
118
|
+
"--min-chunk-duration",
|
|
119
|
+
help="Minimum chunk duration in seconds (VAD mode only, default: 60)",
|
|
120
|
+
min=10.0,
|
|
121
|
+
max=600.0,
|
|
122
|
+
),
|
|
123
|
+
vad_threshold: float | None = typer.Option(
|
|
124
|
+
None,
|
|
125
|
+
"--vad-threshold",
|
|
126
|
+
help="VAD speech detection threshold 0.0-1.0 (VAD mode only, default: 0.5)",
|
|
127
|
+
min=0.0,
|
|
128
|
+
max=1.0,
|
|
129
|
+
),
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Start recording audio from the configured input device.
|
|
132
|
+
|
|
133
|
+
The recording will use the configured microphone (input_device_index) and
|
|
134
|
+
save to the configured recordings directory (recordings_path).
|
|
135
|
+
|
|
136
|
+
By default, the audio will be automatically transcribed after recording stops.
|
|
137
|
+
Use --no-transcribe to skip transcription.
|
|
138
|
+
|
|
139
|
+
Press Ctrl+C to stop recording manually, or use --duration for automatic stop.
|
|
140
|
+
"""
|
|
141
|
+
output_file: Path | None = None
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
# Load configuration
|
|
145
|
+
config = get_config()
|
|
146
|
+
|
|
147
|
+
# Apply CLI overrides for VAD settings
|
|
148
|
+
if vad is not None or min_chunk_duration is not None or vad_threshold is not None:
|
|
149
|
+
# Create modified config with CLI overrides
|
|
150
|
+
config_dict = config.model_dump()
|
|
151
|
+
|
|
152
|
+
if vad is not None:
|
|
153
|
+
config_dict["vad_enabled"] = vad
|
|
154
|
+
if min_chunk_duration is not None:
|
|
155
|
+
config_dict["vad_min_chunk_duration"] = min_chunk_duration
|
|
156
|
+
if vad_threshold is not None:
|
|
157
|
+
config_dict["vad_threshold"] = vad_threshold
|
|
158
|
+
|
|
159
|
+
# Reconstruct config with overrides
|
|
160
|
+
from voicepad_core.config import Config
|
|
161
|
+
|
|
162
|
+
config = Config(**config_dict)
|
|
163
|
+
|
|
164
|
+
# Display configuration info
|
|
165
|
+
typer.echo("Recording Configuration:")
|
|
166
|
+
typer.echo(f" Input device: {config.input_device_index or 'default'}")
|
|
167
|
+
typer.echo(f" Output directory: {config.recordings_path}")
|
|
168
|
+
typer.echo(f" Filename prefix: {prefix or config.recording_prefix}")
|
|
169
|
+
|
|
170
|
+
if config.vad_enabled:
|
|
171
|
+
typer.echo(" VAD chunking: enabled")
|
|
172
|
+
typer.echo(f" Min chunk duration: {config.vad_min_chunk_duration}s")
|
|
173
|
+
typer.echo(f" VAD threshold: {config.vad_threshold}")
|
|
174
|
+
typer.echo(f" Min silence: {config.vad_min_silence_duration_ms}ms")
|
|
175
|
+
else:
|
|
176
|
+
typer.echo(" VAD chunking: disabled")
|
|
177
|
+
|
|
178
|
+
if transcribe:
|
|
179
|
+
typer.echo(f" Transcription: enabled (model: {config.transcription_model})")
|
|
180
|
+
else:
|
|
181
|
+
typer.echo(" Transcription: disabled")
|
|
182
|
+
typer.echo()
|
|
183
|
+
|
|
184
|
+
# Create recorder
|
|
185
|
+
recorder = AudioRecorder(config)
|
|
186
|
+
|
|
187
|
+
# Start recording
|
|
188
|
+
output_file = recorder.start_recording(prefix=prefix, duration=duration)
|
|
189
|
+
|
|
190
|
+
if duration:
|
|
191
|
+
# Fixed-duration recording
|
|
192
|
+
typer.secho(f"[REC] Recording for {duration} seconds...", fg=typer.colors.YELLOW)
|
|
193
|
+
typer.echo(f" Output: {output_file}")
|
|
194
|
+
|
|
195
|
+
# Wait for recording to complete
|
|
196
|
+
time.sleep(duration + 0.5) # Add small buffer for processing
|
|
197
|
+
|
|
198
|
+
typer.secho("[OK] Recording completed!", fg=typer.colors.GREEN)
|
|
199
|
+
typer.echo(f" Saved to: {output_file}")
|
|
200
|
+
|
|
201
|
+
else:
|
|
202
|
+
# Manual stop recording
|
|
203
|
+
typer.secho("[REC] Recording in progress...", fg=typer.colors.YELLOW)
|
|
204
|
+
typer.echo(f" Output: {output_file}")
|
|
205
|
+
typer.echo()
|
|
206
|
+
typer.echo("Press Ctrl+C to stop recording")
|
|
207
|
+
|
|
208
|
+
# Set up signal handler for graceful shutdown
|
|
209
|
+
def signal_handler(sig: int, frame) -> None:
|
|
210
|
+
nonlocal output_file
|
|
211
|
+
output_file = _handle_interrupt(recorder)
|
|
212
|
+
# Don't exit here - let the finally block handle transcription
|
|
213
|
+
|
|
214
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
215
|
+
|
|
216
|
+
# Keep the main thread alive while recording
|
|
217
|
+
try:
|
|
218
|
+
while recorder.is_recording():
|
|
219
|
+
time.sleep(0.1)
|
|
220
|
+
except KeyboardInterrupt:
|
|
221
|
+
output_file = _handle_interrupt(recorder)
|
|
222
|
+
|
|
223
|
+
except AudioRecorderError as e:
|
|
224
|
+
typer.secho(f"[ERROR] Recording error: {e}", fg=typer.colors.RED, err=True)
|
|
225
|
+
raise typer.Exit(1) from e
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.exception("Unexpected error during recording")
|
|
228
|
+
typer.secho(f"[ERROR] Unexpected error: {e}", fg=typer.colors.RED, err=True)
|
|
229
|
+
raise typer.Exit(1) from e
|
|
230
|
+
finally:
|
|
231
|
+
# Transcribe if enabled and we have a valid output file
|
|
232
|
+
if transcribe and output_file and output_file.exists():
|
|
233
|
+
# For VAD mode, transcription already happened in background
|
|
234
|
+
# For non-VAD mode, transcribe now
|
|
235
|
+
if config.vad_enabled:
|
|
236
|
+
# Transcription already complete in background
|
|
237
|
+
typer.echo()
|
|
238
|
+
typer.secho("✓ Transcription already complete (background processing)", fg=typer.colors.GREEN)
|
|
239
|
+
md_path = output_file.with_suffix(".md")
|
|
240
|
+
if md_path.exists():
|
|
241
|
+
typer.echo(f" Markdown: {md_path}")
|
|
242
|
+
else:
|
|
243
|
+
# Non-VAD mode: transcribe now
|
|
244
|
+
_transcribe_audio_file(output_file, config)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@record_app.command("info")
|
|
248
|
+
def show_info() -> None:
|
|
249
|
+
"""Display current recording configuration and status."""
|
|
250
|
+
try:
|
|
251
|
+
config = get_config()
|
|
252
|
+
|
|
253
|
+
typer.echo("Current Recording Configuration")
|
|
254
|
+
typer.echo("=" * 60)
|
|
255
|
+
typer.echo(f"Input device index: {config.input_device_index or 'default (system)'}")
|
|
256
|
+
typer.echo(f"Recordings directory: {config.recordings_path}")
|
|
257
|
+
typer.echo(f"Filename prefix: {config.recording_prefix}")
|
|
258
|
+
if config.vad_enabled:
|
|
259
|
+
typer.echo("VAD chunking: enabled")
|
|
260
|
+
typer.echo(f" Min chunk duration: {config.vad_min_chunk_duration}s")
|
|
261
|
+
typer.echo(f" VAD threshold: {config.vad_threshold}")
|
|
262
|
+
typer.echo(f" Min silence: {config.vad_min_silence_duration_ms}ms")
|
|
263
|
+
else:
|
|
264
|
+
typer.echo("VAD chunking: disabled")
|
|
265
|
+
typer.echo("=" * 60)
|
|
266
|
+
|
|
267
|
+
# Check if recordings directory exists
|
|
268
|
+
if config.recordings_path.exists():
|
|
269
|
+
typer.secho("[OK] Recordings directory exists", fg=typer.colors.GREEN)
|
|
270
|
+
|
|
271
|
+
# Count existing recordings
|
|
272
|
+
recordings = list(config.recordings_path.glob("*.wav"))
|
|
273
|
+
typer.echo(f" {len(recordings)} recording(s) found")
|
|
274
|
+
else:
|
|
275
|
+
typer.secho("[WARN] Recordings directory does not exist (will be created)", fg=typer.colors.YELLOW)
|
|
276
|
+
|
|
277
|
+
typer.echo()
|
|
278
|
+
typer.echo("Tip: Use 'voicepad config input' to view and configure audio devices")
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
typer.secho(f"[ERROR] Error: {e}", fg=typer.colors.RED, err=True)
|
|
282
|
+
raise typer.Exit(1) from e
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from voicepad.cli import config_app, record_app
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
help="Voicepad - Voice recording and audio processing application",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
app.add_typer(config_app, name="config", help="Configuration management commands")
|
|
14
|
+
app.add_typer(record_app, name="record", help="Audio recording commands")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
app()
|