deepctl-cmd-debug-audio 0.1.13__tar.gz → 0.1.15__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-cmd-debug-audio
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Audio debug subcommand for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepctl-cmd-debug-audio"
7
- version = "0.1.13" # x-release-please-version
7
+ version = "0.1.15" # x-release-please-version
8
8
  description = "Audio debug subcommand for deepctl"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,7 +2,7 @@
2
2
 
3
3
  import json
4
4
  import os
5
- import shutil
5
+ import shlex
6
6
  import subprocess
7
7
  import tempfile
8
8
  from typing import Any
@@ -10,15 +10,46 @@ from urllib.parse import urlparse
10
10
 
11
11
  import ffmpeg # type: ignore[import-untyped]
12
12
  import httpx
13
- from deepctl_core import AuthManager, BaseCommand, Config, DeepgramClient
13
+ from deepctl_core import (
14
+ AuthManager,
15
+ BaseCommand,
16
+ Config,
17
+ DeepgramClient,
18
+ get_console,
19
+ get_output_format,
20
+ get_status_console,
21
+ )
22
+ from deepctl_core.tools import resolve_tool
14
23
  from rich import box
15
- from rich.console import Console
16
24
  from rich.panel import Panel
17
25
  from rich.table import Table
18
26
 
19
27
  from .models import AudioDebugResult, AudioFormat, AudioInfo, AudioStream
20
28
 
21
- console = Console()
29
+
30
+ def _ffprobe_input(file_path: str) -> str:
31
+ """Name ``file_path`` for ffprobe as an input file, never as an option.
32
+
33
+ ffprobe has no ``--`` end-of-options marker, so a file named ``-f lavfi``
34
+ would be read as options. The ``file:`` protocol prefix with an absolute
35
+ path makes ffprobe treat it as a plain file, whatever it is called.
36
+ """
37
+ return "file:" + os.path.abspath(file_path)
38
+
39
+
40
+ # Two channels, deliberately (#104):
41
+ # console -> stdout, the human rendering of the analysis. Only
42
+ # written in default (human) output mode; in json/yaml/
43
+ # csv/table mode the serialized AudioDebugResult is the
44
+ # whole of stdout, so `dg -o json debug audio | jq` parses.
45
+ # status_console -> stderr, always: progress lines and failure panels.
46
+ console = get_console()
47
+ status_console = get_status_console()
48
+
49
+
50
+ def _human_output() -> bool:
51
+ """True when stdout is for a person, not a parser."""
52
+ return get_output_format() == "default"
22
53
 
23
54
 
24
55
  class AudioCommand(BaseCommand):
@@ -78,7 +109,14 @@ class AudioCommand(BaseCommand):
78
109
 
79
110
  def check_ffmpeg_installed(self) -> bool:
80
111
  """Check if ffmpeg/ffprobe is installed."""
81
- return shutil.which("ffprobe") is not None
112
+ return self._ffprobe_bin() is not None
113
+
114
+ def _ffprobe_bin(self) -> str | None:
115
+ """Absolute ffprobe path: the user config's, or one found on PATH
116
+ (never in the current directory)."""
117
+ return resolve_tool(
118
+ getattr(self, "_config", None), "ffprobe", "tools.ffprobe_path"
119
+ )
82
120
 
83
121
  def _is_url(self, path: str) -> bool:
84
122
  """Check if the path is a URL."""
@@ -87,7 +125,7 @@ class AudioCommand(BaseCommand):
87
125
 
88
126
  def _download_url(self, url: str) -> str:
89
127
  """Download a URL to a temporary file and return the path."""
90
- console.print(f"[blue]Downloading:[/blue] {url}")
128
+ status_console.print(f"[blue]Downloading:[/blue] {url}")
91
129
 
92
130
  parsed = urlparse(url)
93
131
  ext = os.path.splitext(parsed.path)[1] or ".audio"
@@ -107,7 +145,7 @@ class AudioCommand(BaseCommand):
107
145
  tmp.write(chunk)
108
146
  total += len(chunk)
109
147
  size_mb = total / (1024 * 1024)
110
- console.print(
148
+ status_console.print(
111
149
  f"[green]Downloaded[/green] {size_mb:.2f} MB → {tmp_name}"
112
150
  )
113
151
  return tmp_name
@@ -191,12 +229,16 @@ class AudioCommand(BaseCommand):
191
229
  self, file_path: str, custom_args: str | None = None
192
230
  ) -> dict[str, Any]:
193
231
  """Run ffprobe on the given file."""
232
+ ffprobe_bin = self._ffprobe_bin()
233
+ if ffprobe_bin is None:
234
+ raise Exception("Failed to probe file: ffprobe not found")
194
235
  try:
195
236
  if custom_args:
196
- # Parse custom arguments and run ffprobe directly
197
- cmd = ["ffprobe", "-v", "quiet", "-print_format", "json"]
198
- cmd.extend(custom_args.split())
199
- cmd.append(file_path)
237
+ # Split custom arguments like a POSIX shell on every OS, so a quoted
238
+ # value stays one argument, and run ffprobe directly
239
+ cmd = [ffprobe_bin, "-v", "quiet", "-print_format", "json"]
240
+ cmd.extend(shlex.split(custom_args))
241
+ cmd.append(_ffprobe_input(file_path))
200
242
 
201
243
  result = subprocess.run(cmd, capture_output=True, text=True)
202
244
  if result.returncode != 0:
@@ -205,7 +247,7 @@ class AudioCommand(BaseCommand):
205
247
  return dict(json.loads(result.stdout))
206
248
  else:
207
249
  # Use python-ffmpeg for standard probe
208
- probe = ffmpeg.probe(file_path)
250
+ probe = ffmpeg.probe(_ffprobe_input(file_path), cmd=ffprobe_bin)
209
251
  return dict(probe)
210
252
  except Exception as e:
211
253
  raise Exception(f"Failed to probe file: {e!s}")
@@ -390,6 +432,7 @@ class AudioCommand(BaseCommand):
390
432
  **kwargs: Any,
391
433
  ) -> Any:
392
434
  """Handle audio debug command execution."""
435
+ self._config = config
393
436
  audio_file = kwargs.get("file")
394
437
  verbose = kwargs.get("verbose", False)
395
438
  extra_verbose = kwargs.get("extra_verbose", False)
@@ -406,7 +449,7 @@ class AudioCommand(BaseCommand):
406
449
 
407
450
  # Check if ffmpeg is installed
408
451
  if not self.check_ffmpeg_installed():
409
- console.print(
452
+ status_console.print(
410
453
  Panel(
411
454
  "[red]✗ FFmpeg not found![/red]\n\n"
412
455
  "The audio debug command requires FFmpeg to be installed "
@@ -438,7 +481,7 @@ class AudioCommand(BaseCommand):
438
481
  downloaded_file = self._download_url(audio_file)
439
482
  file_to_analyze = downloaded_file
440
483
  except Exception as e:
441
- console.print(
484
+ status_console.print(
442
485
  Panel(
443
486
  f"[red]✗ Failed to download URL[/red]\n\n[dim]{e!s}[/dim]",
444
487
  title="Download Failed",
@@ -454,7 +497,9 @@ class AudioCommand(BaseCommand):
454
497
 
455
498
  # Process the audio file
456
499
  try:
457
- console.print(f"[blue]Analyzing audio file:[/blue] {file_to_analyze}")
500
+ status_console.print(
501
+ f"[blue]Analyzing audio file:[/blue] {file_to_analyze}"
502
+ )
458
503
 
459
504
  # Run ffprobe
460
505
  probe_data = self.run_ffprobe(file_to_analyze, ffprobe_args)
@@ -462,45 +507,52 @@ class AudioCommand(BaseCommand):
462
507
  # Parse the data
463
508
  audio_info = self.parse_audio_info(probe_data)
464
509
 
465
- # Display results based on verbosity
466
- if extra_verbose or ffprobe_args:
467
- self.display_extra_verbose_info(audio_info)
468
- elif verbose:
469
- self.display_verbose_info(audio_info)
470
- else:
471
- self.display_basic_info(audio_info)
472
-
473
- # Check for Deepgram compatibility
474
- console.print("\n[bold]Deepgram Compatibility Check:[/bold]")
475
- compatibility_issues = []
476
-
477
- if audio_info.streams:
478
- for stream in audio_info.streams:
479
- # Check sample rate
480
- if stream.sample_rate and int(stream.sample_rate) < 8000:
481
- compatibility_issues.append(
482
- f"⚠️ Low sample rate ({stream.sample_rate} Hz) - "
483
- f"Deepgram works best with 8kHz or higher"
484
- )
485
-
486
- # Check channels
487
- if stream.channels and stream.channels > 2:
488
- compatibility_issues.append(
489
- f"⚠️ Multi-channel audio ({stream.channels} "
490
- f"channels) - Consider converting to mono or "
491
- f"stereo"
492
- )
493
-
494
- if compatibility_issues:
495
- for issue in compatibility_issues:
496
- console.print(f" {issue}")
497
- else:
498
- console.print(
499
- " [green]✓[/green] Audio appears to be compatible with Deepgram"
500
- )
510
+ # Everything below is the human rendering of the analysis. It is
511
+ # the same information the returned AudioDebugResult carries, so
512
+ # in a machine-readable format it would be duplicate prose sitting
513
+ # in front of the payload -- exactly the #104 break. Print it only
514
+ # when stdout is for a person.
515
+ if _human_output():
516
+ # Display results based on verbosity
517
+ if extra_verbose or ffprobe_args:
518
+ self.display_extra_verbose_info(audio_info)
519
+ elif verbose:
520
+ self.display_verbose_info(audio_info)
521
+ else:
522
+ self.display_basic_info(audio_info)
523
+
524
+ # Check for Deepgram compatibility
525
+ console.print("\n[bold]Deepgram Compatibility Check:[/bold]")
526
+ compatibility_issues = []
527
+
528
+ if audio_info.streams:
529
+ for stream in audio_info.streams:
530
+ # Check sample rate
531
+ if stream.sample_rate and int(stream.sample_rate) < 8000:
532
+ compatibility_issues.append(
533
+ f"⚠️ Low sample rate ({stream.sample_rate} Hz) - "
534
+ f"Deepgram works best with 8kHz or higher"
535
+ )
536
+
537
+ # Check channels
538
+ if stream.channels and stream.channels > 2:
539
+ compatibility_issues.append(
540
+ f"⚠️ Multi-channel audio ({stream.channels} "
541
+ f"channels) - Consider converting to mono or "
542
+ f"stereo"
543
+ )
544
+
545
+ if compatibility_issues:
546
+ for issue in compatibility_issues:
547
+ console.print(f" {issue}")
548
+ else:
549
+ console.print(
550
+ " [green]✓[/green] Audio appears to be compatible "
551
+ "with Deepgram"
552
+ )
501
553
 
502
- # Encoding suggestions
503
- self._suggest_encoding(audio_info)
554
+ # Encoding suggestions
555
+ self._suggest_encoding(audio_info)
504
556
 
505
557
  return AudioDebugResult(
506
558
  status="success",
@@ -510,7 +562,7 @@ class AudioCommand(BaseCommand):
510
562
  )
511
563
 
512
564
  except Exception as e:
513
- console.print(
565
+ status_console.print(
514
566
  Panel(
515
567
  f"[red]✗ Error analyzing audio file[/red]\n\n[dim]{e!s}[/dim]",
516
568
  title="Analysis Failed",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-cmd-debug-audio
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Audio debug subcommand for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>