deepctl-cmd-debug-audio 0.1.0__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.
@@ -0,0 +1,5 @@
1
+ """Audio debug subcommand for deepctl."""
2
+
3
+ from .command import AudioCommand
4
+
5
+ __all__ = ["AudioCommand"]
@@ -0,0 +1,393 @@
1
+ """Audio debug command for deepctl."""
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ from typing import Any
7
+
8
+ import ffmpeg # type: ignore[import-untyped]
9
+ from deepctl_core import AuthManager, BaseCommand, Config, DeepgramClient
10
+ from rich import box
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+
15
+ from .models import AudioDebugResult, AudioFormat, AudioInfo, AudioStream
16
+
17
+ console = Console()
18
+
19
+
20
+ class AudioCommand(BaseCommand):
21
+ """Debug audio file issues."""
22
+
23
+ name = "audio"
24
+ help = "Debug audio file issues for Deepgram transcription"
25
+ short_help = "Debug audio issues"
26
+
27
+ # Audio debug doesn't require auth
28
+ requires_auth = False
29
+ requires_project = False
30
+ ci_friendly = True
31
+
32
+ def get_arguments(self) -> list[dict[str, Any]]:
33
+ """Get command arguments and options."""
34
+ return [
35
+ {
36
+ "names": ["--file", "-f"],
37
+ "help": "Path to audio file or URL to debug",
38
+ "type": str,
39
+ "is_option": True,
40
+ "required": True,
41
+ },
42
+ {
43
+ "names": ["--verbose", "-v"],
44
+ "help": "Show detailed diagnostic information",
45
+ "is_flag": True,
46
+ "is_option": True,
47
+ },
48
+ {
49
+ "names": ["--extra-verbose", "-vv"],
50
+ "help": "Show raw ffprobe output",
51
+ "is_flag": True,
52
+ "is_option": True,
53
+ },
54
+ {
55
+ "names": ["--ffprobe-args"],
56
+ "help": (
57
+ "Custom ffprobe arguments "
58
+ "(e.g., '-show_streams -show_format')"
59
+ ),
60
+ "type": str,
61
+ "is_option": True,
62
+ },
63
+ ]
64
+
65
+ def check_ffmpeg_installed(self) -> bool:
66
+ """Check if ffmpeg/ffprobe is installed."""
67
+ return shutil.which("ffprobe") is not None
68
+
69
+ def run_ffprobe(
70
+ self, file_path: str, custom_args: str | None = None
71
+ ) -> dict[str, Any]:
72
+ """Run ffprobe on the given file."""
73
+ try:
74
+ if custom_args:
75
+ # Parse custom arguments and run ffprobe directly
76
+ cmd = ["ffprobe", "-v", "quiet", "-print_format", "json"]
77
+ cmd.extend(custom_args.split())
78
+ cmd.append(file_path)
79
+
80
+ result = subprocess.run(cmd, capture_output=True, text=True)
81
+ if result.returncode != 0:
82
+ raise Exception(f"ffprobe failed: {result.stderr}")
83
+
84
+ return dict(json.loads(result.stdout))
85
+ else:
86
+ # Use python-ffmpeg for standard probe
87
+ probe = ffmpeg.probe(file_path)
88
+ return dict(probe)
89
+ except Exception as e:
90
+ raise Exception(f"Failed to probe file: {e!s}")
91
+
92
+ def parse_audio_info(self, probe_data: dict[str, Any]) -> AudioInfo:
93
+ """Parse ffprobe output into AudioInfo model."""
94
+ audio_info = AudioInfo(raw_data=probe_data)
95
+
96
+ # Parse format information
97
+ if "format" in probe_data:
98
+ format_data = probe_data["format"]
99
+ audio_info.format = AudioFormat(
100
+ filename=format_data.get("filename", ""),
101
+ format_name=format_data.get("format_name"),
102
+ format_long_name=format_data.get("format_long_name"),
103
+ duration=(
104
+ float(format_data.get("duration", 0))
105
+ if format_data.get("duration")
106
+ else None
107
+ ),
108
+ size=(
109
+ int(format_data.get("size", 0))
110
+ if format_data.get("size")
111
+ else None
112
+ ),
113
+ bit_rate=format_data.get("bit_rate"),
114
+ nb_streams=(
115
+ int(format_data.get("nb_streams", 0))
116
+ if format_data.get("nb_streams")
117
+ else None
118
+ ),
119
+ )
120
+
121
+ # Parse audio streams
122
+ if "streams" in probe_data:
123
+ for stream in probe_data["streams"]:
124
+ if stream.get("codec_type") == "audio":
125
+ audio_stream = AudioStream(
126
+ codec_name=stream.get("codec_name"),
127
+ codec_long_name=stream.get("codec_long_name"),
128
+ sample_rate=stream.get("sample_rate"),
129
+ channels=stream.get("channels"),
130
+ channel_layout=stream.get("channel_layout"),
131
+ duration=(
132
+ float(stream.get("duration", 0))
133
+ if stream.get("duration")
134
+ else None
135
+ ),
136
+ bit_rate=stream.get("bit_rate"),
137
+ bits_per_sample=stream.get("bits_per_sample"),
138
+ )
139
+ audio_info.streams.append(audio_stream)
140
+
141
+ return audio_info
142
+
143
+ def display_basic_info(self, audio_info: AudioInfo) -> None:
144
+ """Display basic audio information."""
145
+ console.print("\n[green]✓[/green] Audio File Analysis Complete\n")
146
+
147
+ # Format info
148
+ if audio_info.format:
149
+ console.print("[bold]File Information:[/bold]")
150
+ format_name = (
151
+ audio_info.format.format_long_name
152
+ or audio_info.format.format_name
153
+ or "Unknown"
154
+ )
155
+ console.print(f" • Format: {format_name}")
156
+
157
+ if audio_info.format.duration:
158
+ minutes, seconds = divmod(audio_info.format.duration, 60)
159
+ console.print(
160
+ f" • Duration: {int(minutes):02d}:{seconds:06.3f}"
161
+ )
162
+
163
+ if audio_info.format.size:
164
+ size_mb = audio_info.format.size / (1024 * 1024)
165
+ console.print(f" • Size: {size_mb:.2f} MB")
166
+
167
+ if audio_info.format.bit_rate:
168
+ bit_rate_kbps = int(audio_info.format.bit_rate) / 1000
169
+ console.print(f" • Bit Rate: {bit_rate_kbps:.0f} kbps")
170
+
171
+ # Stream info
172
+ if audio_info.streams:
173
+ console.print("\n[bold]Audio Stream Information:[/bold]")
174
+ for i, stream in enumerate(audio_info.streams):
175
+ if len(audio_info.streams) > 1:
176
+ console.print(f"\n Stream {i + 1}:")
177
+ codec_name = (
178
+ stream.codec_long_name or stream.codec_name or "Unknown"
179
+ )
180
+ console.print(f" • Codec: {codec_name}")
181
+ if stream.sample_rate:
182
+ console.print(f" • Sample Rate: {stream.sample_rate} Hz")
183
+ if stream.channels:
184
+ console.print(
185
+ f" • Channels: {stream.channels} "
186
+ f"({stream.channel_layout or 'Unknown layout'})"
187
+ )
188
+ if stream.bit_rate:
189
+ bit_rate_kbps = int(stream.bit_rate) / 1000
190
+ console.print(
191
+ f" • Stream Bit Rate: {bit_rate_kbps:.0f} kbps"
192
+ )
193
+
194
+ def display_verbose_info(self, audio_info: AudioInfo) -> None:
195
+ """Display detailed audio information in table format."""
196
+ console.print("\n[green]✓[/green] Detailed Audio File Analysis\n")
197
+
198
+ # Format table
199
+ if audio_info.format:
200
+ format_table = Table(title="Format Information", box=box.ROUNDED)
201
+ format_table.add_column("Property", style="cyan")
202
+ format_table.add_column("Value", style="white")
203
+
204
+ format_data = audio_info.format.model_dump()
205
+ for key, value in format_data.items():
206
+ if value is not None:
207
+ display_key = key.replace("_", " ").title()
208
+ display_value = str(value)
209
+
210
+ if key == "duration" and value:
211
+ minutes, seconds = divmod(value, 60)
212
+ display_value = f"{int(minutes):02d}:{seconds:06.3f}"
213
+ elif key == "size" and value:
214
+ size_mb = value / (1024 * 1024)
215
+ display_value = f"{size_mb:.2f} MB ({value:,} bytes)"
216
+ elif key == "bit_rate" and value:
217
+ bit_rate_kbps = int(value) / 1000
218
+ display_value = f"{bit_rate_kbps:.0f} kbps"
219
+
220
+ format_table.add_row(display_key, display_value)
221
+
222
+ console.print(format_table)
223
+
224
+ # Streams table
225
+ if audio_info.streams:
226
+ for i, stream in enumerate(audio_info.streams):
227
+ stream_table = Table(
228
+ title=(
229
+ f"Audio Stream {i + 1}"
230
+ if len(audio_info.streams) > 1
231
+ else "Audio Stream"
232
+ ),
233
+ box=box.ROUNDED,
234
+ )
235
+ stream_table.add_column("Property", style="cyan")
236
+ stream_table.add_column("Value", style="white")
237
+
238
+ stream_data = stream.model_dump()
239
+ for key, value in stream_data.items():
240
+ if value is not None:
241
+ display_key = key.replace("_", " ").title()
242
+ display_value = str(value)
243
+
244
+ if key == "sample_rate" and value:
245
+ display_value = f"{value} Hz"
246
+ elif key == "bit_rate" and value:
247
+ bit_rate_kbps = int(value) / 1000
248
+ display_value = f"{bit_rate_kbps:.0f} kbps"
249
+ elif key == "duration" and value:
250
+ minutes, seconds = divmod(value, 60)
251
+ display_value = (
252
+ f"{int(minutes):02d}:{seconds:06.3f}"
253
+ )
254
+
255
+ stream_table.add_row(display_key, display_value)
256
+
257
+ console.print(stream_table)
258
+ if i < len(audio_info.streams) - 1:
259
+ console.print()
260
+
261
+ def display_extra_verbose_info(self, audio_info: AudioInfo) -> None:
262
+ """Display raw ffprobe output."""
263
+ console.print("\n[green]✓[/green] Raw FFprobe Output\n")
264
+
265
+ if audio_info.raw_data:
266
+ import json
267
+
268
+ from rich.syntax import Syntax
269
+
270
+ json_str = json.dumps(audio_info.raw_data, indent=2)
271
+ syntax = Syntax(
272
+ json_str, "json", theme="monokai", line_numbers=True
273
+ )
274
+ console.print(syntax)
275
+
276
+ def handle(
277
+ self,
278
+ config: Config,
279
+ auth_manager: AuthManager,
280
+ client: DeepgramClient,
281
+ **kwargs: Any,
282
+ ) -> Any:
283
+ """Handle audio debug command execution."""
284
+ audio_file = kwargs.get("file")
285
+ verbose = kwargs.get("verbose", False)
286
+ extra_verbose = kwargs.get("extra_verbose", False)
287
+ ffprobe_args = kwargs.get("ffprobe_args")
288
+
289
+ # Validate audio_file
290
+ if not audio_file:
291
+ return AudioDebugResult(
292
+ status="error",
293
+ message="No audio file specified",
294
+ audio_file="",
295
+ ffmpeg_installed=self.check_ffmpeg_installed(),
296
+ )
297
+
298
+ # Check if ffmpeg is installed
299
+ if not self.check_ffmpeg_installed():
300
+ console.print(
301
+ Panel(
302
+ "[red]✗ FFmpeg not found![/red]\n\n"
303
+ "The audio debug command requires FFmpeg to be installed "
304
+ "on your system.\n\n"
305
+ "[bold]To install FFmpeg:[/bold]\n"
306
+ "• Visit: [link]https://ffmpeg.org/download.html[/link]\n"
307
+ "• Or use your package manager:\n"
308
+ " - macOS: [dim]brew install ffmpeg[/dim]\n"
309
+ " - Ubuntu/Debian: [dim]sudo apt install ffmpeg[/dim]\n"
310
+ " - Windows: [dim]Download from ffmpeg.org[/dim]",
311
+ title="FFmpeg Required",
312
+ border_style="red",
313
+ )
314
+ )
315
+
316
+ return AudioDebugResult(
317
+ status="error",
318
+ message="FFmpeg is not installed",
319
+ audio_file=audio_file,
320
+ ffmpeg_installed=False,
321
+ )
322
+
323
+ # Process the audio file
324
+ try:
325
+ console.print(f"[blue]Analyzing audio file:[/blue] {audio_file}")
326
+
327
+ # Run ffprobe
328
+ probe_data = self.run_ffprobe(str(audio_file), ffprobe_args)
329
+
330
+ # Parse the data
331
+ audio_info = self.parse_audio_info(probe_data)
332
+
333
+ # Display results based on verbosity
334
+ if extra_verbose or ffprobe_args:
335
+ self.display_extra_verbose_info(audio_info)
336
+ elif verbose:
337
+ self.display_verbose_info(audio_info)
338
+ else:
339
+ self.display_basic_info(audio_info)
340
+
341
+ # Check for Deepgram compatibility
342
+ console.print("\n[bold]Deepgram Compatibility Check:[/bold]")
343
+ compatibility_issues = []
344
+
345
+ if audio_info.streams:
346
+ for stream in audio_info.streams:
347
+ # Check sample rate
348
+ if stream.sample_rate and int(stream.sample_rate) < 8000:
349
+ compatibility_issues.append(
350
+ f"⚠️ Low sample rate ({stream.sample_rate} Hz) - "
351
+ f"Deepgram works best with 8kHz or higher"
352
+ )
353
+
354
+ # Check channels
355
+ if stream.channels and stream.channels > 2:
356
+ compatibility_issues.append(
357
+ f"⚠️ Multi-channel audio ({stream.channels} "
358
+ f"channels) - Consider converting to mono or "
359
+ f"stereo"
360
+ )
361
+
362
+ if compatibility_issues:
363
+ for issue in compatibility_issues:
364
+ console.print(f" {issue}")
365
+ else:
366
+ console.print(
367
+ " [green]✓[/green] Audio appears to be compatible "
368
+ "with Deepgram"
369
+ )
370
+
371
+ return AudioDebugResult(
372
+ status="success",
373
+ message="Audio file analyzed successfully",
374
+ audio_file=audio_file,
375
+ audio_info=audio_info,
376
+ )
377
+
378
+ except Exception as e:
379
+ console.print(
380
+ Panel(
381
+ f"[red]✗ Error analyzing audio file[/red]\n\n"
382
+ f"[dim]{e!s}[/dim]",
383
+ title="Analysis Failed",
384
+ border_style="red",
385
+ )
386
+ )
387
+
388
+ return AudioDebugResult(
389
+ status="error",
390
+ message="Failed to analyze audio file",
391
+ audio_file=audio_file,
392
+ error_details=str(e),
393
+ )
@@ -0,0 +1,49 @@
1
+ """Data models for audio debug command."""
2
+
3
+ from typing import Any
4
+
5
+ from deepctl_core import BaseResult
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class AudioStream(BaseModel):
10
+ """Audio stream information from ffprobe."""
11
+
12
+ codec_name: str | None = None
13
+ codec_long_name: str | None = None
14
+ sample_rate: str | None = None
15
+ channels: int | None = None
16
+ channel_layout: str | None = None
17
+ duration: float | None = None
18
+ bit_rate: str | None = None
19
+ bits_per_sample: int | None = None
20
+
21
+
22
+ class AudioFormat(BaseModel):
23
+ """Audio format information from ffprobe."""
24
+
25
+ filename: str
26
+ format_name: str | None = None
27
+ format_long_name: str | None = None
28
+ duration: float | None = None
29
+ size: int | None = None
30
+ bit_rate: str | None = None
31
+ nb_streams: int | None = None
32
+
33
+
34
+ class AudioInfo(BaseModel):
35
+ """Detailed audio file information."""
36
+
37
+ format: AudioFormat | None = None
38
+ streams: list[AudioStream] = []
39
+ raw_data: dict[str, Any] | None = None # For verbose output
40
+
41
+
42
+ class AudioDebugResult(BaseResult):
43
+ """Result from audio debug command execution."""
44
+
45
+ message: str
46
+ audio_file: str | None = None
47
+ audio_info: AudioInfo | None = None
48
+ ffmpeg_installed: bool = True
49
+ error_details: str | None = None
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepctl-cmd-debug-audio
3
+ Version: 0.1.0
4
+ Summary: Audio debug subcommand for deepctl
5
+ Author-email: Deepgram <devrel@deepgram.com>
6
+ Maintainer-email: Deepgram <devrel@deepgram.com>
7
+ License-Expression: MIT
8
+ Keywords: deepgram,cli,debug,audio,diagnostics
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: deepctl-core>=0.1.0
18
+ Requires-Dist: click>=8.0.0
19
+ Requires-Dist: rich>=13.0.0
20
+ Requires-Dist: pydantic>=2.0.0
21
+ Requires-Dist: ffmpeg-python>=0.2.0
22
+
23
+ # deepctl-cmd-debug-audio
24
+
25
+ Audio debug subcommand for the Deepgram CLI (deepctl). This package provides the `audio` subcommand under the `debug` command group, allowing users to debug audio-related issues using ffprobe presets.
26
+
27
+ `ffmpeg` is required.
@@ -0,0 +1,8 @@
1
+ deepctl_cmd_debug_audio/__init__.py,sha256=Ci5YUT1HaKk33eAFkjxlrKI9e36M15M0XCpAXm3-HqM,105
2
+ deepctl_cmd_debug_audio/command.py,sha256=wkNWZ-cko_8EJjngJubaLk3juHfHXrLehq4JmRG7Id8,15082
3
+ deepctl_cmd_debug_audio/models.py,sha256=lWvCg4PeE0VACIUUxwmmUXhQBCDy0illNHrrswDCdEI,1270
4
+ deepctl_cmd_debug_audio-0.1.0.dist-info/METADATA,sha256=Lq0hrKzXluyjYBMLm4Hgp-nO7iGdT2xszpXcpR_MrH0,1039
5
+ deepctl_cmd_debug_audio-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ deepctl_cmd_debug_audio-0.1.0.dist-info/entry_points.txt,sha256=HT3JsfMbgYj8-CjEwXyXRwnfuVomFBwTVIRE4peGAoE,81
7
+ deepctl_cmd_debug_audio-0.1.0.dist-info/top_level.txt,sha256=GfWrPD2va7xlIhefBesVeDyMoBU8E5JdbZRUMAqUB88,24
8
+ deepctl_cmd_debug_audio-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [deepctl.subcommands.debug]
2
+ audio = deepctl_cmd_debug_audio.command:AudioCommand
@@ -0,0 +1 @@
1
+ deepctl_cmd_debug_audio