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.
@@ -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.
@@ -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,5 @@
1
+ """Voicepad - Voice recording and audio processing application."""
2
+
3
+ from voicepad.main import main
4
+
5
+ __all__ = ["main"]
@@ -0,0 +1,6 @@
1
+ """Voicepad package entry point."""
2
+
3
+ from voicepad.main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,4 @@
1
+ from voicepad.cli.config import config_app
2
+ from voicepad.cli.record import record_app
3
+
4
+ __all__ = ["config_app", "record_app"]
@@ -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()