Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0f71836
fix(login): accept real API keys and ask for --yes instead of cancelling
dg-coreylweathers Nov 1, 2026
1f4dc37
feat(login): let agents finish device login with --complete
dg-coreylweathers Nov 1, 2026
16aeca8
perf(core): drop the pre-flight key check and reuse one HTTP client
dg-coreylweathers Oct 31, 2026
648576e
fix(output): drop status chrome from stderr in agent mode
dg-coreylweathers Oct 31, 2026
9a8a9c2
fix(listen): finish live streams cleanly on Ctrl-C and end of input
dg-coreylweathers Oct 31, 2026
b57d046
perf(listen): bound live-stream memory and frame latency
dg-coreylweathers Oct 31, 2026
2735cd9
fix(listen): keep machine output to the final result
dg-coreylweathers Oct 31, 2026
531f595
perf(speak): stream audio to pipes and players as it arrives
dg-coreylweathers Oct 31, 2026
cd70665
feat(speak): add --out-file and deprecate -o/--output for the audio file
dg-coreylweathers Oct 31, 2026
0524904
fix(speak): don't read stdin or write audio to a pipe in agent mode
dg-coreylweathers Oct 31, 2026
6b8907d
fix(listen): report a failed stdout write as an output error
dg-coreylweathers Oct 31, 2026
ad6f9f6
fix(cli): treat Ctrl-C while the CLI is loading as cancelled
dg-coreylweathers Nov 1, 2026
9f34dbc
perf(listen): stream prerecorded uploads from disk
dg-coreylweathers Oct 31, 2026
773cef5
feat(listen): add --timeout for prerecorded transcription
dg-coreylweathers Oct 31, 2026
7c39a56
perf(listen): check URL sources without fetching them
dg-coreylweathers Oct 31, 2026
3a2af7b
fix(debug): send no stored credentials from debug network
dg-coreylweathers Oct 31, 2026
b45f74d
build(deps): drop numpy, ffmpeg-python and requests from commands
dg-coreylweathers Oct 31, 2026
747f195
refactor(debug): run debug browser and probe on websockets
dg-coreylweathers Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ def _clean_deepgram_env(monkeypatch):
monkeypatch.delenv(key, raising=False)


@pytest.fixture(autouse=True)
def _speak_tests_run_as_a_person(request, monkeypatch):
"""Speak tests run as a person by default: the agent rules key off hard
agent markers, which a developer's own shell may set (CLAUDECODE=1).
Tests that need agent mode patch ``is_ai_agent`` themselves."""
if "deepctl-cmd-speak" not in request.path.parts:
return
from deepctl_core.output import AI_AGENT_ENV_MARKERS

for name in (*AI_AGENT_ENV_MARKERS, "OR_APP_NAME", "OR_SITE_URL"):
monkeypatch.delenv(name, raising=False)


@pytest.fixture(autouse=True)
def _disable_telemetry(monkeypatch):
"""Keep tests that call `main()` from initializing real telemetry.
Expand All @@ -64,3 +77,14 @@ def _disable_telemetry(monkeypatch):
"""
monkeypatch.setenv("DEEPCTL_TELEMETRY_DISABLED", "1")
monkeypatch.setenv("DEEPCTL_TELEMETRY_DSN", "http://k@127.0.0.1:9/1")


@pytest.fixture(autouse=True)
def _restore_sigint():
"""`main()` installs its own SIGINT handler; put pytest's back."""
import signal

before = signal.getsignal(signal.SIGINT)
yield
if before is not None:
signal.signal(signal.SIGINT, before)
1 change: 0 additions & 1 deletion packages/deepctl-cmd-debug-audio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ pipx run deepctl --help
- `click>=8.2.0,<9`
- `rich>=13.0.0,<16`
- `pydantic>=2.0.0,<3`
- `ffmpeg-python>=0.2.0`
- `httpx>=0.24.0,<1`

## License
Expand Down
1 change: 0 additions & 1 deletion packages/deepctl-cmd-debug-audio/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ dependencies = [
"click>=8.2.0,<9",
"rich>=13.0.0,<16",
"pydantic>=2.0.0,<3",
"ffmpeg-python>=0.2.0",
"httpx>=0.24.0,<1",
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from typing import Any
from urllib.parse import urlparse

import ffmpeg # type: ignore[import-untyped]
from deepctl_core import (
AuthManager,
BaseCommand,
Expand Down Expand Up @@ -45,6 +44,32 @@ def _ffprobe_input(file_path: str) -> str:
console = get_console()
status_console = get_status_console()

# Seconds a single ffprobe run may take before it is abandoned.
FFPROBE_TIMEOUT_S = 30


def _probe_input(path: str) -> list[str]:
"""ffprobe's input arguments, always last on the command line.

The input is always a local file (URLs are downloaded first), so ffprobe
may open it only through the file protocol: tcp:, concat:, pipe: and the
rest are refused. It is named as ``file:<absolute path>``
(``_ffprobe_input``), so ffprobe reads it as a file and never as an
option, whatever it is called.
"""
return ["-protocol_whitelist", "file", _ffprobe_input(path)]


def _run_ffprobe(cmd: list[str]) -> "subprocess.CompletedProcess[bytes]":
"""Run ffprobe from an argv list: no shell, no stdin, bounded in time."""
return subprocess.run(
cmd,
stdin=subprocess.DEVNULL,
capture_output=True,
timeout=FFPROBE_TIMEOUT_S,
check=False,
)


def _human_output() -> bool:
"""True when stdout is for a person, not a parser."""
Expand Down Expand Up @@ -239,17 +264,37 @@ def run_ffprobe(
# value stays one argument, and run ffprobe directly
cmd = [ffprobe_bin, "-v", "quiet", "-print_format", "json"]
cmd.extend(shlex.split(custom_args))
cmd.append(_ffprobe_input(file_path))
cmd.extend(_probe_input(file_path))

result = subprocess.run(cmd, capture_output=True, text=True)
result = _run_ffprobe(cmd)
if result.returncode != 0:
raise Exception(f"ffprobe failed: {result.stderr}")

return dict(json.loads(result.stdout))
else:
# Use python-ffmpeg for standard probe
probe = ffmpeg.probe(_ffprobe_input(file_path), cmd=ffprobe_bin)
return dict(probe)
stderr = result.stderr.decode("utf-8", errors="replace")
raise Exception(f"ffprobe failed: {stderr}")

return dict(json.loads(result.stdout.decode("utf-8")))
# The standard probe: the argv ffmpeg-python's probe() built,
# plus the file-only protocol whitelist.
cmd = [
ffprobe_bin,
"-show_format",
"-show_streams",
"-of",
"json",
*_probe_input(file_path),
]
probe = _run_ffprobe(cmd)
if probe.returncode != 0:
# stderr is captured, so say what ffprobe said: its last line.
said = [
line.strip()
for line in (probe.stderr or b"")
.decode("utf-8", "replace")
.splitlines()
if line.strip()
]
why = said[-1][:300] if said else f"exit {probe.returncode}"
raise Exception(f"ffprobe couldn't read the file: {why}")
return dict(json.loads(probe.stdout.decode("utf-8")))
except Exception as e:
raise Exception(f"Failed to probe file: {e!s}")

Expand Down
Loading
Loading