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.
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/PKG-INFO +1 -1
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/pyproject.toml +1 -1
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/src/deepctl_cmd_debug_audio/command.py +106 -54
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/src/deepctl_cmd_debug_audio.egg-info/PKG-INFO +1 -1
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/README.md +0 -0
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/setup.cfg +0 -0
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/src/deepctl_cmd_debug_audio/__init__.py +0 -0
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/src/deepctl_cmd_debug_audio/models.py +0 -0
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/src/deepctl_cmd_debug_audio.egg-info/SOURCES.txt +0 -0
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/src/deepctl_cmd_debug_audio.egg-info/dependency_links.txt +0 -0
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/src/deepctl_cmd_debug_audio.egg-info/entry_points.txt +0 -0
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/src/deepctl_cmd_debug_audio.egg-info/requires.txt +0 -0
- {deepctl_cmd_debug_audio-0.1.13 → deepctl_cmd_debug_audio-0.1.15}/src/deepctl_cmd_debug_audio.egg-info/top_level.txt +0 -0
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
197
|
-
|
|
198
|
-
cmd
|
|
199
|
-
cmd.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|