agent-cli 0.66.2__py3-none-any.whl → 0.67.1__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.
- agent_cli/_extras.json +13 -0
- agent_cli/_requirements/audio.txt +71 -0
- agent_cli/_requirements/{whisper.txt → faster-whisper.txt} +5 -113
- agent_cli/_requirements/{tts-kokoro.txt → kokoro.txt} +5 -108
- agent_cli/_requirements/llm.txt +176 -0
- agent_cli/_requirements/memory.txt +7 -98
- agent_cli/_requirements/{whisper-mlx.txt → mlx-whisper.txt} +62 -125
- agent_cli/_requirements/{tts.txt → piper.txt} +6 -124
- agent_cli/_requirements/rag.txt +10 -97
- agent_cli/_requirements/server.txt +4 -122
- agent_cli/_requirements/speed.txt +7 -131
- agent_cli/_requirements/vad.txt +6 -130
- agent_cli/agents/assistant.py +2 -0
- agent_cli/agents/autocorrect.py +2 -0
- agent_cli/agents/chat.py +2 -0
- agent_cli/agents/memory/add.py +2 -0
- agent_cli/agents/memory/proxy.py +2 -0
- agent_cli/agents/rag_proxy.py +2 -0
- agent_cli/agents/speak.py +2 -0
- agent_cli/agents/transcribe.py +2 -0
- agent_cli/agents/transcribe_daemon.py +2 -0
- agent_cli/agents/voice_edit.py +2 -0
- agent_cli/core/deps.py +130 -14
- agent_cli/dev/skill/SKILL.md +2 -2
- agent_cli/docs_gen.py +0 -42
- agent_cli/install/extras.py +6 -14
- agent_cli/memory/__init__.py +1 -18
- agent_cli/rag/__init__.py +0 -19
- agent_cli/scripts/sync_extras.py +138 -0
- agent_cli/server/cli.py +4 -0
- agent_cli/services/_wyoming_utils.py +4 -2
- agent_cli/services/asr.py +13 -3
- agent_cli/services/tts.py +5 -2
- agent_cli/services/wake_word.py +6 -3
- {agent_cli-0.66.2.dist-info → agent_cli-0.67.1.dist-info}/METADATA +35 -30
- {agent_cli-0.66.2.dist-info → agent_cli-0.67.1.dist-info}/RECORD +39 -35
- {agent_cli-0.66.2.dist-info → agent_cli-0.67.1.dist-info}/WHEEL +0 -0
- {agent_cli-0.66.2.dist-info → agent_cli-0.67.1.dist-info}/entry_points.txt +0 -0
- {agent_cli-0.66.2.dist-info → agent_cli-0.67.1.dist-info}/licenses/LICENSE +0 -0
agent_cli/core/deps.py
CHANGED
|
@@ -2,22 +2,138 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import functools
|
|
6
|
+
import json
|
|
5
7
|
from importlib.util import find_spec
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
6
10
|
|
|
11
|
+
import typer
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
from agent_cli.core.utils import print_error_message
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
|
|
18
|
+
F = TypeVar("F", bound="Callable[..., object]")
|
|
19
|
+
|
|
20
|
+
# Load extras from JSON file
|
|
21
|
+
_EXTRAS_FILE = Path(__file__).parent.parent / "_extras.json"
|
|
22
|
+
EXTRAS: dict[str, tuple[str, list[str]]] = {
|
|
23
|
+
k: (v[0], v[1]) for k, v in json.loads(_EXTRAS_FILE.read_text()).items()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _check_package_installed(pkg: str) -> bool:
|
|
28
|
+
"""Check if a single package is installed."""
|
|
29
|
+
top_module = pkg.split(".")[0]
|
|
30
|
+
try:
|
|
31
|
+
return find_spec(top_module) is not None
|
|
32
|
+
except (ValueError, ModuleNotFoundError):
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_extra_installed(extra: str) -> bool:
|
|
37
|
+
"""Check if packages for an extra are installed using find_spec (no actual import).
|
|
38
|
+
|
|
39
|
+
Supports `|` syntax for alternatives: "piper|kokoro" means ANY of these extras.
|
|
40
|
+
For regular extras, ALL packages must be installed.
|
|
41
|
+
"""
|
|
42
|
+
# Handle "extra1|extra2" syntax - any of these extras is sufficient
|
|
43
|
+
if "|" in extra:
|
|
44
|
+
return any(check_extra_installed(e) for e in extra.split("|"))
|
|
45
|
+
|
|
46
|
+
if extra not in EXTRAS:
|
|
47
|
+
return True # Unknown extra, assume OK
|
|
48
|
+
_, packages = EXTRAS[extra]
|
|
49
|
+
|
|
50
|
+
# All packages must be installed
|
|
51
|
+
return all(_check_package_installed(pkg) for pkg in packages)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_install_hint(extra: str) -> str:
|
|
55
|
+
"""Get install command hint for an extra.
|
|
56
|
+
|
|
57
|
+
Supports `|` syntax for alternatives: "piper|kokoro" shows both options.
|
|
58
|
+
"""
|
|
59
|
+
# Handle "extra1|extra2" syntax - show all options
|
|
60
|
+
if "|" in extra:
|
|
61
|
+
alternatives = extra.split("|")
|
|
62
|
+
options = []
|
|
63
|
+
for alt in alternatives:
|
|
64
|
+
desc, _ = EXTRAS.get(alt, ("", []))
|
|
65
|
+
options.append((alt, desc))
|
|
66
|
+
|
|
67
|
+
lines = ["This command requires one of:"]
|
|
68
|
+
for alt, desc in options:
|
|
69
|
+
if desc:
|
|
70
|
+
lines.append(f" - '{alt}' ({desc})")
|
|
71
|
+
else:
|
|
72
|
+
lines.append(f" - '{alt}'")
|
|
73
|
+
lines.append("")
|
|
74
|
+
lines.append("Install one with:")
|
|
75
|
+
for alt, _ in options:
|
|
76
|
+
lines.append(f' uv tool install "agent-cli[{alt}]" -p 3.13')
|
|
77
|
+
lines.append(" # or")
|
|
78
|
+
for alt, _ in options:
|
|
79
|
+
lines.append(f" agent-cli install-extras {alt}")
|
|
80
|
+
return "\n".join(lines)
|
|
81
|
+
|
|
82
|
+
desc, _ = EXTRAS.get(extra, ("", []))
|
|
83
|
+
lines = [
|
|
84
|
+
f"This command requires the '{extra}' extra",
|
|
17
85
|
]
|
|
18
|
-
if
|
|
19
|
-
|
|
86
|
+
if desc:
|
|
87
|
+
lines[0] += f" ({desc})"
|
|
88
|
+
lines[0] += "."
|
|
89
|
+
lines.append("")
|
|
90
|
+
lines.append("Install with:")
|
|
91
|
+
lines.append(f' uv tool install "agent-cli[{extra}]" -p 3.13')
|
|
92
|
+
lines.append(" # or")
|
|
93
|
+
lines.append(f" agent-cli install-extras {extra}")
|
|
94
|
+
return "\n".join(lines)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def requires_extras(*extras: str) -> Callable[[F], F]:
|
|
98
|
+
"""Decorator to declare required extras for a command.
|
|
99
|
+
|
|
100
|
+
When a required extra is missing, the decorator prints a helpful error
|
|
101
|
+
message and exits with code 1.
|
|
102
|
+
|
|
103
|
+
The decorator stores the required extras on the function for test validation.
|
|
104
|
+
|
|
105
|
+
Process management flags (--stop, --status, --toggle) skip the dependency
|
|
106
|
+
check since they just manage running processes without using the actual
|
|
107
|
+
dependencies.
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
@app.command("rag-proxy")
|
|
111
|
+
@requires_extras("rag")
|
|
112
|
+
def rag_proxy(...):
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def decorator(func: F) -> F:
|
|
118
|
+
# Store extras on function for test introspection
|
|
119
|
+
func._required_extras = extras # type: ignore[attr-defined]
|
|
120
|
+
|
|
121
|
+
@functools.wraps(func)
|
|
122
|
+
def wrapper(*args: object, **kwargs: object) -> object:
|
|
123
|
+
# Skip dependency check for process management and info operations
|
|
124
|
+
# These don't need the actual dependencies, just manage processes or list info
|
|
125
|
+
if any(kwargs.get(flag) for flag in ("stop", "status", "toggle", "list_devices")):
|
|
126
|
+
return func(*args, **kwargs)
|
|
127
|
+
|
|
128
|
+
missing = [e for e in extras if not check_extra_installed(e)]
|
|
129
|
+
if missing:
|
|
130
|
+
for extra in missing:
|
|
131
|
+
print_error_message(get_install_hint(extra))
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
return func(*args, **kwargs)
|
|
134
|
+
|
|
135
|
+
# Preserve the extras on wrapper too
|
|
136
|
+
wrapper._required_extras = extras # type: ignore[attr-defined]
|
|
137
|
+
return wrapper # type: ignore[return-value]
|
|
20
138
|
|
|
21
|
-
|
|
22
|
-
msg = f"Missing required dependencies for {extra_name}: {', '.join(missing)}. Please install with {hint}."
|
|
23
|
-
raise ImportError(msg)
|
|
139
|
+
return decorator
|
agent_cli/dev/skill/SKILL.md
CHANGED
|
@@ -13,10 +13,10 @@ If `agent-cli` is not available, install it first:
|
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
# Install globally
|
|
16
|
-
uv tool install agent-cli
|
|
16
|
+
uv tool install agent-cli -p 3.13
|
|
17
17
|
|
|
18
18
|
# Or run directly without installing
|
|
19
|
-
uvx agent-cli dev new <branch-name> --agent --prompt "..."
|
|
19
|
+
uvx --python 3.13 agent-cli dev new <branch-name> --agent --prompt "..."
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## When to spawn parallel agents
|
agent_cli/docs_gen.py
CHANGED
|
@@ -16,7 +16,6 @@ Example usage in Markdown files:
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
-
from pathlib import Path
|
|
20
19
|
from typing import Any, get_origin
|
|
21
20
|
|
|
22
21
|
import click
|
|
@@ -369,47 +368,6 @@ def config_example(command_path: str | None = None) -> str:
|
|
|
369
368
|
return "\n".join(lines)
|
|
370
369
|
|
|
371
370
|
|
|
372
|
-
def readme_section(section_name: str) -> str:
|
|
373
|
-
"""Extract a section from README.md for reuse in other docs.
|
|
374
|
-
|
|
375
|
-
Sections are marked with HTML comments like:
|
|
376
|
-
<!-- SECTION:section_name:START -->
|
|
377
|
-
Content here...
|
|
378
|
-
<!-- SECTION:section_name:END -->
|
|
379
|
-
|
|
380
|
-
Args:
|
|
381
|
-
section_name: The name of the section to extract (e.g., "why-i-built-this")
|
|
382
|
-
|
|
383
|
-
Returns:
|
|
384
|
-
The content between the section markers (without the markers themselves)
|
|
385
|
-
|
|
386
|
-
"""
|
|
387
|
-
# Find the README.md relative to this module
|
|
388
|
-
readme_path = Path(__file__).parent.parent / "README.md"
|
|
389
|
-
if not readme_path.exists():
|
|
390
|
-
return f"*Could not find README.md at {readme_path}*"
|
|
391
|
-
|
|
392
|
-
content = readme_path.read_text()
|
|
393
|
-
|
|
394
|
-
# Look for section markers
|
|
395
|
-
start_marker = f"<!-- SECTION:{section_name}:START -->"
|
|
396
|
-
end_marker = f"<!-- SECTION:{section_name}:END -->"
|
|
397
|
-
|
|
398
|
-
start_idx = content.find(start_marker)
|
|
399
|
-
if start_idx == -1:
|
|
400
|
-
return f"*Section '{section_name}' not found in README.md*"
|
|
401
|
-
|
|
402
|
-
end_idx = content.find(end_marker, start_idx)
|
|
403
|
-
if end_idx == -1:
|
|
404
|
-
return f"*End marker for section '{section_name}' not found in README.md*"
|
|
405
|
-
|
|
406
|
-
# Extract content between markers (excluding the markers themselves)
|
|
407
|
-
section_content = content[start_idx + len(start_marker) : end_idx]
|
|
408
|
-
|
|
409
|
-
# Strip leading/trailing whitespace but preserve internal formatting
|
|
410
|
-
return section_content.strip()
|
|
411
|
-
|
|
412
|
-
|
|
413
371
|
def all_options_for_docs(command_path: str) -> str:
|
|
414
372
|
"""Generate complete options documentation for a command page.
|
|
415
373
|
|
agent_cli/install/extras.py
CHANGED
|
@@ -12,20 +12,11 @@ from typing import Annotated
|
|
|
12
12
|
import typer
|
|
13
13
|
|
|
14
14
|
from agent_cli.cli import app
|
|
15
|
+
from agent_cli.core.deps import EXTRAS as _EXTRAS_META
|
|
15
16
|
from agent_cli.core.utils import console, print_error_message
|
|
16
17
|
|
|
17
|
-
#
|
|
18
|
-
EXTRAS: dict[str, str] = {
|
|
19
|
-
"rag": "RAG proxy (ChromaDB, embeddings)",
|
|
20
|
-
"memory": "Long-term memory proxy",
|
|
21
|
-
"vad": "Voice Activity Detection (silero-vad)",
|
|
22
|
-
"whisper": "Local Whisper ASR (faster-whisper)",
|
|
23
|
-
"whisper-mlx": "MLX Whisper for Apple Silicon",
|
|
24
|
-
"tts": "Local Piper TTS",
|
|
25
|
-
"tts-kokoro": "Kokoro neural TTS",
|
|
26
|
-
"server": "FastAPI server components",
|
|
27
|
-
"speed": "Audio speed adjustment (audiostretchy)",
|
|
28
|
-
}
|
|
18
|
+
# Extract descriptions from the centralized EXTRAS metadata
|
|
19
|
+
EXTRAS: dict[str, str] = {name: desc for name, (desc, _) in _EXTRAS_META.items()}
|
|
29
20
|
|
|
30
21
|
|
|
31
22
|
def _requirements_dir() -> Path:
|
|
@@ -72,8 +63,9 @@ def _install_via_uv_tool(extras: list[str]) -> bool:
|
|
|
72
63
|
"""Reinstall agent-cli via uv tool with the specified extras."""
|
|
73
64
|
extras_str = ",".join(extras)
|
|
74
65
|
package_spec = f"agent-cli[{extras_str}]"
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
67
|
+
console.print(f"Reinstalling via uv tool: [cyan]{package_spec}[/] (Python {python_version})")
|
|
68
|
+
cmd = ["uv", "tool", "install", package_spec, "--force", "--python", python_version]
|
|
77
69
|
result = subprocess.run(cmd, check=False)
|
|
78
70
|
return result.returncode == 0
|
|
79
71
|
|
agent_cli/memory/__init__.py
CHANGED
|
@@ -2,23 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from agent_cli.
|
|
6
|
-
|
|
7
|
-
_REQUIRED_DEPS = {
|
|
8
|
-
"chromadb": "chromadb",
|
|
9
|
-
"fastapi": "fastapi",
|
|
10
|
-
"uvicorn": "uvicorn",
|
|
11
|
-
"onnxruntime": "onnxruntime",
|
|
12
|
-
"huggingface_hub": "huggingface-hub",
|
|
13
|
-
"transformers": "transformers",
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
ensure_optional_dependencies(
|
|
17
|
-
_REQUIRED_DEPS,
|
|
18
|
-
extra_name="memory",
|
|
19
|
-
install_hint="`pip install agent-cli[memory]` or `uv sync --extra memory`",
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
from agent_cli.memory.client import MemoryClient # noqa: E402
|
|
5
|
+
from agent_cli.memory.client import MemoryClient
|
|
23
6
|
|
|
24
7
|
__all__ = ["MemoryClient"]
|
agent_cli/rag/__init__.py
CHANGED
|
@@ -1,22 +1,3 @@
|
|
|
1
1
|
"""RAG module."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from agent_cli.core.deps import ensure_optional_dependencies
|
|
6
|
-
|
|
7
|
-
_REQUIRED_DEPS = {
|
|
8
|
-
"chromadb": "chromadb",
|
|
9
|
-
"watchfiles": "watchfiles",
|
|
10
|
-
"markitdown": "markitdown",
|
|
11
|
-
"fastapi": "fastapi",
|
|
12
|
-
"uvicorn": "uvicorn",
|
|
13
|
-
"onnxruntime": "onnxruntime",
|
|
14
|
-
"huggingface_hub": "huggingface-hub",
|
|
15
|
-
"transformers": "transformers",
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
ensure_optional_dependencies(
|
|
19
|
-
_REQUIRED_DEPS,
|
|
20
|
-
extra_name="rag",
|
|
21
|
-
install_hint="`pip install agent-cli[rag]` or `uv sync --extra rag`",
|
|
22
|
-
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate _extras.json from pyproject.toml.
|
|
3
|
+
|
|
4
|
+
This script parses the optional-dependencies in pyproject.toml and generates
|
|
5
|
+
the agent_cli/_extras.json file with package-to-import mappings.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python scripts/sync_extras.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import tomllib
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
REPO_ROOT = Path(__file__).parent.parent
|
|
20
|
+
PYPROJECT = REPO_ROOT / "pyproject.toml"
|
|
21
|
+
EXTRAS_FILE = REPO_ROOT / "agent_cli" / "_extras.json"
|
|
22
|
+
|
|
23
|
+
# Extras to skip (dev/test dependencies, not runtime installable)
|
|
24
|
+
SKIP_EXTRAS = {"dev", "test"}
|
|
25
|
+
|
|
26
|
+
# Manual mapping of extra name -> (description, list of import names)
|
|
27
|
+
# Import names should be the Python module name (how you import it)
|
|
28
|
+
# Bundle extras (voice, cloud, full) have empty import lists since they just install other extras
|
|
29
|
+
EXTRA_METADATA: dict[str, tuple[str, list[str]]] = {
|
|
30
|
+
# Provider extras (base dependencies now optional)
|
|
31
|
+
"audio": ("Audio recording/playback", ["sounddevice"]),
|
|
32
|
+
"wyoming": ("Wyoming protocol support", ["wyoming"]),
|
|
33
|
+
"openai": ("OpenAI API provider", ["openai"]),
|
|
34
|
+
"gemini": ("Google Gemini provider", ["google.genai"]),
|
|
35
|
+
"llm": ("LLM framework (pydantic-ai)", ["pydantic_ai"]),
|
|
36
|
+
# Feature extras
|
|
37
|
+
"rag": ("RAG proxy (ChromaDB, embeddings)", ["chromadb"]),
|
|
38
|
+
"memory": ("Long-term memory proxy", ["chromadb", "yaml"]),
|
|
39
|
+
"vad": ("Voice Activity Detection (silero-vad)", ["silero_vad"]),
|
|
40
|
+
"whisper": ("Local Whisper ASR (faster-whisper)", ["faster_whisper"]),
|
|
41
|
+
"whisper-mlx": ("MLX Whisper for Apple Silicon", ["mlx_whisper"]),
|
|
42
|
+
"tts": ("Local Piper TTS", ["piper"]),
|
|
43
|
+
"tts-kokoro": ("Kokoro neural TTS", ["kokoro"]),
|
|
44
|
+
"server": ("FastAPI server components", ["fastapi"]),
|
|
45
|
+
"speed": ("Audio speed adjustment (audiostretchy)", ["audiostretchy"]),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_extras_from_pyproject() -> set[str]:
|
|
50
|
+
"""Parse optional-dependencies from pyproject.toml."""
|
|
51
|
+
with PYPROJECT.open("rb") as f:
|
|
52
|
+
data = tomllib.load(f)
|
|
53
|
+
all_extras = set(data.get("project", {}).get("optional-dependencies", {}).keys())
|
|
54
|
+
return all_extras - SKIP_EXTRAS
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def extract_package_name(dep: str) -> str:
|
|
58
|
+
"""Extract the package name from a dependency specification.
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
"chromadb>=0.4.22" -> "chromadb"
|
|
62
|
+
"pydantic-ai-slim[openai,duckduckgo]" -> "pydantic-ai-slim"
|
|
63
|
+
'mlx-whisper>=0.4.0; sys_platform == "darwin"' -> "mlx-whisper"
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
# Remove markers (;...) and extras ([...])
|
|
67
|
+
dep = re.split(r"[;\[]", dep)[0]
|
|
68
|
+
# Remove version specifiers
|
|
69
|
+
dep = re.split(r"[<>=!~]", dep)[0]
|
|
70
|
+
return dep.strip()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def package_to_import_name(package: str) -> str:
|
|
74
|
+
"""Convert a package name to its Python import name.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
"google-genai" -> "google.genai"
|
|
78
|
+
"pydantic-ai-slim" -> "pydantic_ai"
|
|
79
|
+
"silero-vad" -> "silero_vad"
|
|
80
|
+
"faster-whisper" -> "faster_whisper"
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
# Special cases where the import name differs significantly
|
|
84
|
+
special_cases = {
|
|
85
|
+
"google-genai": "google.genai",
|
|
86
|
+
"pydantic-ai-slim": "pydantic_ai",
|
|
87
|
+
"silero-vad": "silero_vad",
|
|
88
|
+
"faster-whisper": "faster_whisper",
|
|
89
|
+
"mlx-whisper": "mlx_whisper",
|
|
90
|
+
"piper-tts": "piper",
|
|
91
|
+
"huggingface-hub": "huggingface_hub",
|
|
92
|
+
"fastapi": "fastapi",
|
|
93
|
+
"audiostretchy": "audiostretchy",
|
|
94
|
+
}
|
|
95
|
+
if package in special_cases:
|
|
96
|
+
return special_cases[package]
|
|
97
|
+
# Default: replace hyphens with underscores
|
|
98
|
+
return package.replace("-", "_")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def generate_extras_json(extras: set[str]) -> dict[str, list]:
|
|
102
|
+
"""Generate the content for _extras.json."""
|
|
103
|
+
result = {}
|
|
104
|
+
for extra in sorted(extras):
|
|
105
|
+
if extra in EXTRA_METADATA:
|
|
106
|
+
desc, imports = EXTRA_METADATA[extra]
|
|
107
|
+
result[extra] = [desc, imports]
|
|
108
|
+
else:
|
|
109
|
+
# Unknown extra - add a placeholder
|
|
110
|
+
result[extra] = ["TODO: add description", []]
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def check_missing_metadata(extras: set[str]) -> list[str]:
|
|
115
|
+
"""Check for extras that don't have metadata defined."""
|
|
116
|
+
return [e for e in extras if e not in EXTRA_METADATA]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def main() -> int:
|
|
120
|
+
"""Generate _extras.json from pyproject.toml."""
|
|
121
|
+
extras = get_extras_from_pyproject()
|
|
122
|
+
|
|
123
|
+
# Check for missing metadata
|
|
124
|
+
missing = check_missing_metadata(extras)
|
|
125
|
+
if missing:
|
|
126
|
+
print(f"Warning: The following extras need metadata in EXTRA_METADATA: {missing}")
|
|
127
|
+
print("Please update EXTRA_METADATA in scripts/sync_extras.py")
|
|
128
|
+
|
|
129
|
+
# Generate the file
|
|
130
|
+
content = generate_extras_json(extras)
|
|
131
|
+
EXTRAS_FILE.write_text(json.dumps(content, indent=2) + "\n")
|
|
132
|
+
print(f"Generated {EXTRAS_FILE}")
|
|
133
|
+
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
sys.exit(main())
|
agent_cli/server/cli.py
CHANGED
|
@@ -12,6 +12,7 @@ import typer
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
|
|
14
14
|
from agent_cli.cli import app as main_app
|
|
15
|
+
from agent_cli.core.deps import requires_extras
|
|
15
16
|
from agent_cli.core.process import set_process_title
|
|
16
17
|
from agent_cli.server.common import setup_rich_logging
|
|
17
18
|
|
|
@@ -163,6 +164,7 @@ def _check_whisper_deps(backend: str, *, download_only: bool = False) -> None:
|
|
|
163
164
|
|
|
164
165
|
|
|
165
166
|
@app.command("whisper")
|
|
167
|
+
@requires_extras("server", "faster-whisper|mlx-whisper")
|
|
166
168
|
def whisper_cmd( # noqa: PLR0912, PLR0915
|
|
167
169
|
model: Annotated[
|
|
168
170
|
list[str] | None,
|
|
@@ -422,6 +424,7 @@ def whisper_cmd( # noqa: PLR0912, PLR0915
|
|
|
422
424
|
|
|
423
425
|
|
|
424
426
|
@app.command("transcription-proxy")
|
|
427
|
+
@requires_extras("server")
|
|
425
428
|
def transcription_proxy_cmd(
|
|
426
429
|
host: Annotated[
|
|
427
430
|
str,
|
|
@@ -475,6 +478,7 @@ def transcription_proxy_cmd(
|
|
|
475
478
|
|
|
476
479
|
|
|
477
480
|
@app.command("tts")
|
|
481
|
+
@requires_extras("server", "piper|kokoro")
|
|
478
482
|
def tts_cmd( # noqa: PLR0915
|
|
479
483
|
model: Annotated[
|
|
480
484
|
list[str] | None,
|
|
@@ -5,14 +5,14 @@ from __future__ import annotations
|
|
|
5
5
|
from contextlib import asynccontextmanager
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
-
from wyoming.client import AsyncClient
|
|
9
|
-
|
|
10
8
|
from agent_cli.core.utils import print_error_message
|
|
11
9
|
|
|
12
10
|
if TYPE_CHECKING:
|
|
13
11
|
import logging
|
|
14
12
|
from collections.abc import AsyncGenerator
|
|
15
13
|
|
|
14
|
+
from wyoming.client import AsyncClient
|
|
15
|
+
|
|
16
16
|
|
|
17
17
|
@asynccontextmanager
|
|
18
18
|
async def wyoming_client_context(
|
|
@@ -40,6 +40,8 @@ async def wyoming_client_context(
|
|
|
40
40
|
Exception: For other connection errors
|
|
41
41
|
|
|
42
42
|
"""
|
|
43
|
+
from wyoming.client import AsyncClient # noqa: PLC0415
|
|
44
|
+
|
|
43
45
|
uri = f"tcp://{server_ip}:{server_port}"
|
|
44
46
|
logger.info("Connecting to Wyoming %s server at %s", server_type, uri)
|
|
45
47
|
|
agent_cli/services/asr.py
CHANGED
|
@@ -10,9 +10,6 @@ from functools import partial
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
|
-
from wyoming.asr import Transcribe, Transcript, TranscriptChunk, TranscriptStart, TranscriptStop
|
|
14
|
-
from wyoming.audio import AudioChunk, AudioStart, AudioStop
|
|
15
|
-
|
|
16
13
|
from agent_cli import constants
|
|
17
14
|
from agent_cli.core.audio import (
|
|
18
15
|
open_audio_stream,
|
|
@@ -225,6 +222,9 @@ async def _send_audio(
|
|
|
225
222
|
initial_prompt: str | None = None,
|
|
226
223
|
) -> None:
|
|
227
224
|
"""Read from mic and send to Wyoming server."""
|
|
225
|
+
from wyoming.asr import Transcribe # noqa: PLC0415
|
|
226
|
+
from wyoming.audio import AudioChunk, AudioStart, AudioStop # noqa: PLC0415
|
|
227
|
+
|
|
228
228
|
# Build context with initial_prompt if provided
|
|
229
229
|
context = {"initial_prompt": initial_prompt} if initial_prompt else None
|
|
230
230
|
await client.write_event(Transcribe(context=context).event())
|
|
@@ -282,6 +282,13 @@ async def _receive_transcript(
|
|
|
282
282
|
final_callback: Callable[[str], None] | None = None,
|
|
283
283
|
) -> str:
|
|
284
284
|
"""Receive transcription events and return the final transcript."""
|
|
285
|
+
from wyoming.asr import ( # noqa: PLC0415
|
|
286
|
+
Transcript,
|
|
287
|
+
TranscriptChunk,
|
|
288
|
+
TranscriptStart,
|
|
289
|
+
TranscriptStop,
|
|
290
|
+
)
|
|
291
|
+
|
|
285
292
|
transcript_text = ""
|
|
286
293
|
while True:
|
|
287
294
|
event = await client.read_event()
|
|
@@ -370,6 +377,9 @@ async def _transcribe_recorded_audio_wyoming(
|
|
|
370
377
|
**_kwargs: object,
|
|
371
378
|
) -> str:
|
|
372
379
|
"""Process pre-recorded audio data with Wyoming ASR server."""
|
|
380
|
+
from wyoming.asr import Transcribe # noqa: PLC0415
|
|
381
|
+
from wyoming.audio import AudioChunk, AudioStart, AudioStop # noqa: PLC0415
|
|
382
|
+
|
|
373
383
|
try:
|
|
374
384
|
async with wyoming_client_context(
|
|
375
385
|
wyoming_asr_cfg.asr_wyoming_ip,
|
agent_cli/services/tts.py
CHANGED
|
@@ -10,8 +10,6 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
12
|
from rich.live import Live
|
|
13
|
-
from wyoming.audio import AudioChunk, AudioStart, AudioStop
|
|
14
|
-
from wyoming.tts import Synthesize, SynthesizeVoice
|
|
15
13
|
|
|
16
14
|
from agent_cli import config, constants
|
|
17
15
|
from agent_cli.core.audio import open_audio_stream, setup_output_stream
|
|
@@ -32,6 +30,7 @@ if TYPE_CHECKING:
|
|
|
32
30
|
|
|
33
31
|
from rich.live import Live
|
|
34
32
|
from wyoming.client import AsyncClient
|
|
33
|
+
from wyoming.tts import Synthesize
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
has_audiostretchy = importlib.util.find_spec("audiostretchy") is not None
|
|
@@ -134,6 +133,8 @@ def _create_synthesis_request(
|
|
|
134
133
|
speaker: str | None = None,
|
|
135
134
|
) -> Synthesize:
|
|
136
135
|
"""Create a synthesis request with optional voice parameters."""
|
|
136
|
+
from wyoming.tts import Synthesize, SynthesizeVoice # noqa: PLC0415
|
|
137
|
+
|
|
137
138
|
synthesize_event = Synthesize(text=text)
|
|
138
139
|
|
|
139
140
|
# Add voice parameters if specified
|
|
@@ -152,6 +153,8 @@ async def _process_audio_events(
|
|
|
152
153
|
logger: logging.Logger,
|
|
153
154
|
) -> tuple[bytes, int | None, int | None, int | None]:
|
|
154
155
|
"""Process audio events from TTS server and return audio data with metadata."""
|
|
156
|
+
from wyoming.audio import AudioChunk, AudioStart, AudioStop # noqa: PLC0415
|
|
157
|
+
|
|
155
158
|
audio_data = io.BytesIO()
|
|
156
159
|
sample_rate = None
|
|
157
160
|
sample_width = None
|
agent_cli/services/wake_word.py
CHANGED
|
@@ -6,9 +6,6 @@ import asyncio
|
|
|
6
6
|
from functools import partial
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
from wyoming.audio import AudioChunk, AudioStart, AudioStop
|
|
10
|
-
from wyoming.wake import Detect, Detection, NotDetected
|
|
11
|
-
|
|
12
9
|
from agent_cli import config, constants
|
|
13
10
|
from agent_cli.core.audio import read_from_queue
|
|
14
11
|
from agent_cli.core.utils import manage_send_receive_tasks
|
|
@@ -38,6 +35,8 @@ async def _send_audio_from_queue_for_wake_detection(
|
|
|
38
35
|
progress_message: str,
|
|
39
36
|
) -> None:
|
|
40
37
|
"""Read from a queue and send to Wyoming wake word server."""
|
|
38
|
+
from wyoming.audio import AudioChunk, AudioStart, AudioStop # noqa: PLC0415
|
|
39
|
+
|
|
41
40
|
await client.write_event(AudioStart(**constants.WYOMING_AUDIO_CONFIG).event())
|
|
42
41
|
seconds_streamed = 0.0
|
|
43
42
|
|
|
@@ -76,6 +75,8 @@ async def _receive_wake_detection(
|
|
|
76
75
|
Name of detected wake word or None if no detection
|
|
77
76
|
|
|
78
77
|
"""
|
|
78
|
+
from wyoming.wake import Detection, NotDetected # noqa: PLC0415
|
|
79
|
+
|
|
79
80
|
while True:
|
|
80
81
|
event = await client.read_event()
|
|
81
82
|
if event is None:
|
|
@@ -108,6 +109,8 @@ async def _detect_wake_word_from_queue(
|
|
|
108
109
|
progress_message: str = "Listening for wake word",
|
|
109
110
|
) -> str | None:
|
|
110
111
|
"""Detect wake word from an audio queue."""
|
|
112
|
+
from wyoming.wake import Detect # noqa: PLC0415
|
|
113
|
+
|
|
111
114
|
try:
|
|
112
115
|
async with wyoming_client_context(
|
|
113
116
|
wake_word_cfg.wake_server_ip,
|