agent-cli 0.70.5__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/__init__.py +5 -0
- agent_cli/__main__.py +6 -0
- agent_cli/_extras.json +14 -0
- agent_cli/_requirements/.gitkeep +0 -0
- agent_cli/_requirements/audio.txt +79 -0
- agent_cli/_requirements/faster-whisper.txt +215 -0
- agent_cli/_requirements/kokoro.txt +425 -0
- agent_cli/_requirements/llm.txt +183 -0
- agent_cli/_requirements/memory.txt +355 -0
- agent_cli/_requirements/mlx-whisper.txt +222 -0
- agent_cli/_requirements/piper.txt +176 -0
- agent_cli/_requirements/rag.txt +402 -0
- agent_cli/_requirements/server.txt +154 -0
- agent_cli/_requirements/speed.txt +77 -0
- agent_cli/_requirements/vad.txt +155 -0
- agent_cli/_requirements/wyoming.txt +71 -0
- agent_cli/_tools.py +368 -0
- agent_cli/agents/__init__.py +23 -0
- agent_cli/agents/_voice_agent_common.py +136 -0
- agent_cli/agents/assistant.py +383 -0
- agent_cli/agents/autocorrect.py +284 -0
- agent_cli/agents/chat.py +496 -0
- agent_cli/agents/memory/__init__.py +31 -0
- agent_cli/agents/memory/add.py +190 -0
- agent_cli/agents/memory/proxy.py +160 -0
- agent_cli/agents/rag_proxy.py +128 -0
- agent_cli/agents/speak.py +209 -0
- agent_cli/agents/transcribe.py +671 -0
- agent_cli/agents/transcribe_daemon.py +499 -0
- agent_cli/agents/voice_edit.py +291 -0
- agent_cli/api.py +22 -0
- agent_cli/cli.py +106 -0
- agent_cli/config.py +503 -0
- agent_cli/config_cmd.py +307 -0
- agent_cli/constants.py +27 -0
- agent_cli/core/__init__.py +1 -0
- agent_cli/core/audio.py +461 -0
- agent_cli/core/audio_format.py +299 -0
- agent_cli/core/chroma.py +88 -0
- agent_cli/core/deps.py +191 -0
- agent_cli/core/openai_proxy.py +139 -0
- agent_cli/core/process.py +195 -0
- agent_cli/core/reranker.py +120 -0
- agent_cli/core/sse.py +87 -0
- agent_cli/core/transcription_logger.py +70 -0
- agent_cli/core/utils.py +526 -0
- agent_cli/core/vad.py +175 -0
- agent_cli/core/watch.py +65 -0
- agent_cli/dev/__init__.py +14 -0
- agent_cli/dev/cli.py +1588 -0
- agent_cli/dev/coding_agents/__init__.py +19 -0
- agent_cli/dev/coding_agents/aider.py +24 -0
- agent_cli/dev/coding_agents/base.py +167 -0
- agent_cli/dev/coding_agents/claude.py +39 -0
- agent_cli/dev/coding_agents/codex.py +24 -0
- agent_cli/dev/coding_agents/continue_dev.py +15 -0
- agent_cli/dev/coding_agents/copilot.py +24 -0
- agent_cli/dev/coding_agents/cursor_agent.py +48 -0
- agent_cli/dev/coding_agents/gemini.py +28 -0
- agent_cli/dev/coding_agents/opencode.py +15 -0
- agent_cli/dev/coding_agents/registry.py +49 -0
- agent_cli/dev/editors/__init__.py +19 -0
- agent_cli/dev/editors/base.py +89 -0
- agent_cli/dev/editors/cursor.py +15 -0
- agent_cli/dev/editors/emacs.py +46 -0
- agent_cli/dev/editors/jetbrains.py +56 -0
- agent_cli/dev/editors/nano.py +31 -0
- agent_cli/dev/editors/neovim.py +33 -0
- agent_cli/dev/editors/registry.py +59 -0
- agent_cli/dev/editors/sublime.py +20 -0
- agent_cli/dev/editors/vim.py +42 -0
- agent_cli/dev/editors/vscode.py +15 -0
- agent_cli/dev/editors/zed.py +20 -0
- agent_cli/dev/project.py +568 -0
- agent_cli/dev/registry.py +52 -0
- agent_cli/dev/skill/SKILL.md +141 -0
- agent_cli/dev/skill/examples.md +571 -0
- agent_cli/dev/terminals/__init__.py +19 -0
- agent_cli/dev/terminals/apple_terminal.py +82 -0
- agent_cli/dev/terminals/base.py +56 -0
- agent_cli/dev/terminals/gnome.py +51 -0
- agent_cli/dev/terminals/iterm2.py +84 -0
- agent_cli/dev/terminals/kitty.py +77 -0
- agent_cli/dev/terminals/registry.py +48 -0
- agent_cli/dev/terminals/tmux.py +58 -0
- agent_cli/dev/terminals/warp.py +132 -0
- agent_cli/dev/terminals/zellij.py +78 -0
- agent_cli/dev/worktree.py +856 -0
- agent_cli/docs_gen.py +417 -0
- agent_cli/example-config.toml +185 -0
- agent_cli/install/__init__.py +5 -0
- agent_cli/install/common.py +89 -0
- agent_cli/install/extras.py +174 -0
- agent_cli/install/hotkeys.py +48 -0
- agent_cli/install/services.py +87 -0
- agent_cli/memory/__init__.py +7 -0
- agent_cli/memory/_files.py +250 -0
- agent_cli/memory/_filters.py +63 -0
- agent_cli/memory/_git.py +157 -0
- agent_cli/memory/_indexer.py +142 -0
- agent_cli/memory/_ingest.py +408 -0
- agent_cli/memory/_persistence.py +182 -0
- agent_cli/memory/_prompt.py +91 -0
- agent_cli/memory/_retrieval.py +294 -0
- agent_cli/memory/_store.py +169 -0
- agent_cli/memory/_streaming.py +44 -0
- agent_cli/memory/_tasks.py +48 -0
- agent_cli/memory/api.py +113 -0
- agent_cli/memory/client.py +272 -0
- agent_cli/memory/engine.py +361 -0
- agent_cli/memory/entities.py +43 -0
- agent_cli/memory/models.py +112 -0
- agent_cli/opts.py +433 -0
- agent_cli/py.typed +0 -0
- agent_cli/rag/__init__.py +3 -0
- agent_cli/rag/_indexer.py +67 -0
- agent_cli/rag/_indexing.py +226 -0
- agent_cli/rag/_prompt.py +30 -0
- agent_cli/rag/_retriever.py +156 -0
- agent_cli/rag/_store.py +48 -0
- agent_cli/rag/_utils.py +218 -0
- agent_cli/rag/api.py +175 -0
- agent_cli/rag/client.py +299 -0
- agent_cli/rag/engine.py +302 -0
- agent_cli/rag/models.py +55 -0
- agent_cli/scripts/.runtime/.gitkeep +0 -0
- agent_cli/scripts/__init__.py +1 -0
- agent_cli/scripts/check_plugin_skill_sync.py +50 -0
- agent_cli/scripts/linux-hotkeys/README.md +63 -0
- agent_cli/scripts/linux-hotkeys/toggle-autocorrect.sh +45 -0
- agent_cli/scripts/linux-hotkeys/toggle-transcription.sh +58 -0
- agent_cli/scripts/linux-hotkeys/toggle-voice-edit.sh +58 -0
- agent_cli/scripts/macos-hotkeys/README.md +45 -0
- agent_cli/scripts/macos-hotkeys/skhd-config-example +5 -0
- agent_cli/scripts/macos-hotkeys/toggle-autocorrect.sh +12 -0
- agent_cli/scripts/macos-hotkeys/toggle-transcription.sh +37 -0
- agent_cli/scripts/macos-hotkeys/toggle-voice-edit.sh +37 -0
- agent_cli/scripts/nvidia-asr-server/README.md +99 -0
- agent_cli/scripts/nvidia-asr-server/pyproject.toml +27 -0
- agent_cli/scripts/nvidia-asr-server/server.py +255 -0
- agent_cli/scripts/nvidia-asr-server/shell.nix +32 -0
- agent_cli/scripts/nvidia-asr-server/uv.lock +4654 -0
- agent_cli/scripts/run-openwakeword.sh +11 -0
- agent_cli/scripts/run-piper-windows.ps1 +30 -0
- agent_cli/scripts/run-piper.sh +24 -0
- agent_cli/scripts/run-whisper-linux.sh +40 -0
- agent_cli/scripts/run-whisper-macos.sh +6 -0
- agent_cli/scripts/run-whisper-windows.ps1 +51 -0
- agent_cli/scripts/run-whisper.sh +9 -0
- agent_cli/scripts/run_faster_whisper_server.py +136 -0
- agent_cli/scripts/setup-linux-hotkeys.sh +72 -0
- agent_cli/scripts/setup-linux.sh +108 -0
- agent_cli/scripts/setup-macos-hotkeys.sh +61 -0
- agent_cli/scripts/setup-macos.sh +76 -0
- agent_cli/scripts/setup-windows.ps1 +63 -0
- agent_cli/scripts/start-all-services-windows.ps1 +53 -0
- agent_cli/scripts/start-all-services.sh +178 -0
- agent_cli/scripts/sync_extras.py +138 -0
- agent_cli/server/__init__.py +3 -0
- agent_cli/server/cli.py +721 -0
- agent_cli/server/common.py +222 -0
- agent_cli/server/model_manager.py +288 -0
- agent_cli/server/model_registry.py +225 -0
- agent_cli/server/proxy/__init__.py +3 -0
- agent_cli/server/proxy/api.py +444 -0
- agent_cli/server/streaming.py +67 -0
- agent_cli/server/tts/__init__.py +3 -0
- agent_cli/server/tts/api.py +335 -0
- agent_cli/server/tts/backends/__init__.py +82 -0
- agent_cli/server/tts/backends/base.py +139 -0
- agent_cli/server/tts/backends/kokoro.py +403 -0
- agent_cli/server/tts/backends/piper.py +253 -0
- agent_cli/server/tts/model_manager.py +201 -0
- agent_cli/server/tts/model_registry.py +28 -0
- agent_cli/server/tts/wyoming_handler.py +249 -0
- agent_cli/server/whisper/__init__.py +3 -0
- agent_cli/server/whisper/api.py +413 -0
- agent_cli/server/whisper/backends/__init__.py +89 -0
- agent_cli/server/whisper/backends/base.py +97 -0
- agent_cli/server/whisper/backends/faster_whisper.py +225 -0
- agent_cli/server/whisper/backends/mlx.py +270 -0
- agent_cli/server/whisper/languages.py +116 -0
- agent_cli/server/whisper/model_manager.py +157 -0
- agent_cli/server/whisper/model_registry.py +28 -0
- agent_cli/server/whisper/wyoming_handler.py +203 -0
- agent_cli/services/__init__.py +343 -0
- agent_cli/services/_wyoming_utils.py +64 -0
- agent_cli/services/asr.py +506 -0
- agent_cli/services/llm.py +228 -0
- agent_cli/services/tts.py +450 -0
- agent_cli/services/wake_word.py +142 -0
- agent_cli-0.70.5.dist-info/METADATA +2118 -0
- agent_cli-0.70.5.dist-info/RECORD +196 -0
- agent_cli-0.70.5.dist-info/WHEEL +4 -0
- agent_cli-0.70.5.dist-info/entry_points.txt +4 -0
- agent_cli-0.70.5.dist-info/licenses/LICENSE +21 -0
agent_cli/server/cli.py
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
"""CLI commands for the server module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from importlib.util import find_spec
|
|
8
|
+
from pathlib import Path # noqa: TC003 - Typer needs this at runtime
|
|
9
|
+
from typing import Annotated
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from agent_cli import opts
|
|
14
|
+
from agent_cli.cli import app as main_app
|
|
15
|
+
from agent_cli.core.deps import requires_extras
|
|
16
|
+
from agent_cli.core.process import set_process_title
|
|
17
|
+
from agent_cli.core.utils import console, err_console
|
|
18
|
+
from agent_cli.server.common import setup_rich_logging
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Check for optional dependencies at call time (not module load time)
|
|
23
|
+
# This is important because auto-install may install packages after the module is loaded
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _has(package: str) -> bool:
|
|
27
|
+
return find_spec(package) is not None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
app = typer.Typer(
|
|
31
|
+
name="server",
|
|
32
|
+
help="Run ASR/TTS servers (Whisper, TTS, or proxy mode).",
|
|
33
|
+
add_completion=True,
|
|
34
|
+
rich_markup_mode="markdown",
|
|
35
|
+
no_args_is_help=True,
|
|
36
|
+
)
|
|
37
|
+
main_app.add_typer(app, name="server", rich_help_panel="Servers")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.callback()
|
|
41
|
+
def server_callback(ctx: typer.Context) -> None:
|
|
42
|
+
"""Server command group callback."""
|
|
43
|
+
if ctx.invoked_subcommand is not None:
|
|
44
|
+
# Update process title to include full path: server-{subcommand}
|
|
45
|
+
set_process_title(f"server-{ctx.invoked_subcommand}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _check_server_deps() -> None:
|
|
49
|
+
"""Check that server dependencies are available."""
|
|
50
|
+
if not _has("uvicorn") or not _has("fastapi"):
|
|
51
|
+
err_console.print(
|
|
52
|
+
"[bold red]Error:[/bold red] Server dependencies not installed. "
|
|
53
|
+
"Run: [cyan]pip install agent-cli\\[server][/cyan] "
|
|
54
|
+
"or [cyan]uv sync --extra server[/cyan]",
|
|
55
|
+
)
|
|
56
|
+
raise typer.Exit(1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _check_tts_deps(backend: str = "auto") -> None:
|
|
60
|
+
"""Check that TTS dependencies are available."""
|
|
61
|
+
_check_server_deps()
|
|
62
|
+
|
|
63
|
+
if backend == "kokoro":
|
|
64
|
+
if not _has("kokoro"):
|
|
65
|
+
err_console.print(
|
|
66
|
+
"[bold red]Error:[/bold red] Kokoro backend requires kokoro. "
|
|
67
|
+
"Run: [cyan]pip install agent-cli\\[tts-kokoro][/cyan] "
|
|
68
|
+
"or [cyan]uv sync --extra tts-kokoro[/cyan]",
|
|
69
|
+
)
|
|
70
|
+
raise typer.Exit(1)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
if backend == "piper":
|
|
74
|
+
if not _has("piper"):
|
|
75
|
+
err_console.print(
|
|
76
|
+
"[bold red]Error:[/bold red] Piper backend requires piper-tts. "
|
|
77
|
+
"Run: [cyan]pip install agent-cli\\[tts][/cyan] "
|
|
78
|
+
"or [cyan]uv sync --extra tts[/cyan]",
|
|
79
|
+
)
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# For auto, check if either is available
|
|
84
|
+
if not _has("piper") and not _has("kokoro"):
|
|
85
|
+
err_console.print(
|
|
86
|
+
"[bold red]Error:[/bold red] No TTS backend available. "
|
|
87
|
+
"Run: [cyan]pip install agent-cli\\[tts][/cyan] for Piper "
|
|
88
|
+
"or [cyan]pip install agent-cli\\[tts-kokoro][/cyan] for Kokoro",
|
|
89
|
+
)
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _download_tts_models(
|
|
94
|
+
backend: str,
|
|
95
|
+
models: list[str],
|
|
96
|
+
cache_dir: Path | None,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Download TTS models/voices without starting the server."""
|
|
99
|
+
if backend == "kokoro":
|
|
100
|
+
from agent_cli.server.tts.backends.base import ( # noqa: PLC0415
|
|
101
|
+
get_backend_cache_dir,
|
|
102
|
+
)
|
|
103
|
+
from agent_cli.server.tts.backends.kokoro import ( # noqa: PLC0415
|
|
104
|
+
DEFAULT_VOICE,
|
|
105
|
+
_ensure_model,
|
|
106
|
+
_ensure_voice,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
download_dir = cache_dir or get_backend_cache_dir("kokoro")
|
|
110
|
+
console.print("[bold]Downloading Kokoro model...[/bold]")
|
|
111
|
+
_ensure_model(download_dir)
|
|
112
|
+
console.print(" [green]✓[/green] Model ready")
|
|
113
|
+
|
|
114
|
+
voices = [v for v in models if v != "kokoro"] or [DEFAULT_VOICE]
|
|
115
|
+
for voice in voices:
|
|
116
|
+
console.print(f" Downloading voice [cyan]{voice}[/cyan]...")
|
|
117
|
+
_ensure_voice(voice, download_dir)
|
|
118
|
+
console.print("[bold green]Download complete![/bold green]")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# Piper backend
|
|
122
|
+
from piper.download_voices import download_voice # noqa: PLC0415
|
|
123
|
+
|
|
124
|
+
from agent_cli.server.tts.backends.base import get_backend_cache_dir # noqa: PLC0415
|
|
125
|
+
|
|
126
|
+
download_dir = cache_dir or get_backend_cache_dir("piper")
|
|
127
|
+
console.print("[bold]Downloading Piper model(s)...[/bold]")
|
|
128
|
+
for model_name in models:
|
|
129
|
+
console.print(f" Downloading [cyan]{model_name}[/cyan]...")
|
|
130
|
+
download_voice(model_name, download_dir)
|
|
131
|
+
console.print("[bold green]Download complete![/bold green]")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _check_whisper_deps(backend: str, *, download_only: bool = False) -> None:
|
|
135
|
+
"""Check that Whisper dependencies are available."""
|
|
136
|
+
_check_server_deps()
|
|
137
|
+
if download_only:
|
|
138
|
+
if not _has("faster_whisper"):
|
|
139
|
+
err_console.print(
|
|
140
|
+
"[bold red]Error:[/bold red] faster-whisper is required for --download-only. "
|
|
141
|
+
"Run: [cyan]pip install agent-cli\\[whisper][/cyan] "
|
|
142
|
+
"or [cyan]uv sync --extra whisper[/cyan]",
|
|
143
|
+
)
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if backend == "mlx":
|
|
148
|
+
if not _has("mlx_whisper"):
|
|
149
|
+
err_console.print(
|
|
150
|
+
"[bold red]Error:[/bold red] MLX Whisper backend requires mlx-whisper. "
|
|
151
|
+
"Run: [cyan]pip install mlx-whisper[/cyan]",
|
|
152
|
+
)
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if not _has("faster_whisper"):
|
|
157
|
+
err_console.print(
|
|
158
|
+
"[bold red]Error:[/bold red] Whisper dependencies not installed. "
|
|
159
|
+
"Run: [cyan]pip install agent-cli\\[whisper][/cyan] "
|
|
160
|
+
"or [cyan]uv sync --extra whisper[/cyan]",
|
|
161
|
+
)
|
|
162
|
+
raise typer.Exit(1)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command("whisper")
|
|
166
|
+
@requires_extras("server", "faster-whisper|mlx-whisper")
|
|
167
|
+
def whisper_cmd( # noqa: PLR0912, PLR0915
|
|
168
|
+
model: Annotated[
|
|
169
|
+
list[str] | None,
|
|
170
|
+
typer.Option(
|
|
171
|
+
"--model",
|
|
172
|
+
"-m",
|
|
173
|
+
help="Model name(s) to load (can specify multiple)",
|
|
174
|
+
),
|
|
175
|
+
] = None,
|
|
176
|
+
default_model: Annotated[
|
|
177
|
+
str | None,
|
|
178
|
+
typer.Option(
|
|
179
|
+
"--default-model",
|
|
180
|
+
help="Default model when not specified in request",
|
|
181
|
+
),
|
|
182
|
+
] = None,
|
|
183
|
+
device: Annotated[
|
|
184
|
+
str,
|
|
185
|
+
typer.Option(
|
|
186
|
+
"--device",
|
|
187
|
+
"-d",
|
|
188
|
+
help="Device: auto, cuda, cuda:0, cpu",
|
|
189
|
+
),
|
|
190
|
+
] = "auto",
|
|
191
|
+
compute_type: Annotated[
|
|
192
|
+
str,
|
|
193
|
+
typer.Option(
|
|
194
|
+
"--compute-type",
|
|
195
|
+
help="Compute type: auto, float16, int8, int8_float16",
|
|
196
|
+
),
|
|
197
|
+
] = "auto",
|
|
198
|
+
cache_dir: Annotated[
|
|
199
|
+
Path | None,
|
|
200
|
+
typer.Option(
|
|
201
|
+
"--cache-dir",
|
|
202
|
+
help="Model cache directory",
|
|
203
|
+
),
|
|
204
|
+
] = None,
|
|
205
|
+
ttl: Annotated[
|
|
206
|
+
int,
|
|
207
|
+
typer.Option(
|
|
208
|
+
"--ttl",
|
|
209
|
+
help="Seconds before unloading idle model",
|
|
210
|
+
),
|
|
211
|
+
] = 300,
|
|
212
|
+
preload: Annotated[
|
|
213
|
+
bool,
|
|
214
|
+
typer.Option(
|
|
215
|
+
"--preload",
|
|
216
|
+
help="Load model(s) at startup and wait for completion",
|
|
217
|
+
),
|
|
218
|
+
] = False,
|
|
219
|
+
host: Annotated[
|
|
220
|
+
str,
|
|
221
|
+
typer.Option(
|
|
222
|
+
"--host",
|
|
223
|
+
help="Host to bind the server to",
|
|
224
|
+
),
|
|
225
|
+
] = "0.0.0.0", # noqa: S104
|
|
226
|
+
port: Annotated[
|
|
227
|
+
int,
|
|
228
|
+
typer.Option(
|
|
229
|
+
"--port",
|
|
230
|
+
"-p",
|
|
231
|
+
help="HTTP API port",
|
|
232
|
+
),
|
|
233
|
+
] = 10301,
|
|
234
|
+
wyoming_port: Annotated[
|
|
235
|
+
int,
|
|
236
|
+
typer.Option(
|
|
237
|
+
"--wyoming-port",
|
|
238
|
+
help="Wyoming protocol port",
|
|
239
|
+
),
|
|
240
|
+
] = 10300,
|
|
241
|
+
no_wyoming: Annotated[
|
|
242
|
+
bool,
|
|
243
|
+
typer.Option(
|
|
244
|
+
"--no-wyoming",
|
|
245
|
+
help="Disable Wyoming server",
|
|
246
|
+
),
|
|
247
|
+
] = False,
|
|
248
|
+
download_only: Annotated[
|
|
249
|
+
bool,
|
|
250
|
+
typer.Option(
|
|
251
|
+
"--download-only",
|
|
252
|
+
help="Download model(s) and exit without starting server",
|
|
253
|
+
),
|
|
254
|
+
] = False,
|
|
255
|
+
log_level: opts.LogLevel = opts.LOG_LEVEL,
|
|
256
|
+
backend: Annotated[
|
|
257
|
+
str,
|
|
258
|
+
typer.Option(
|
|
259
|
+
"--backend",
|
|
260
|
+
"-b",
|
|
261
|
+
help="Backend: auto (platform detection), faster-whisper, mlx",
|
|
262
|
+
),
|
|
263
|
+
] = "auto",
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Run Whisper ASR server with TTL-based model unloading.
|
|
266
|
+
|
|
267
|
+
The server provides:
|
|
268
|
+
- OpenAI-compatible /v1/audio/transcriptions endpoint
|
|
269
|
+
- Wyoming protocol for Home Assistant integration
|
|
270
|
+
- WebSocket streaming at /v1/audio/transcriptions/stream
|
|
271
|
+
|
|
272
|
+
Models are loaded lazily on first request and unloaded after being
|
|
273
|
+
idle for the TTL duration, freeing VRAM for other applications.
|
|
274
|
+
|
|
275
|
+
Examples:
|
|
276
|
+
# Run with default large-v3 model
|
|
277
|
+
agent-cli server whisper
|
|
278
|
+
|
|
279
|
+
# Run with specific model and 10-minute TTL
|
|
280
|
+
agent-cli server whisper --model large-v3 --ttl 600
|
|
281
|
+
|
|
282
|
+
# Run multiple models with different configs
|
|
283
|
+
agent-cli server whisper --model large-v3 --model small
|
|
284
|
+
|
|
285
|
+
# Download model without starting server
|
|
286
|
+
agent-cli server whisper --model large-v3 --download-only
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
# Setup Rich logging for consistent output
|
|
290
|
+
setup_rich_logging(log_level)
|
|
291
|
+
|
|
292
|
+
valid_backends = ("auto", "faster-whisper", "mlx")
|
|
293
|
+
if backend not in valid_backends:
|
|
294
|
+
err_console.print(
|
|
295
|
+
f"[bold red]Error:[/bold red] --backend must be one of: {', '.join(valid_backends)}",
|
|
296
|
+
)
|
|
297
|
+
raise typer.Exit(1)
|
|
298
|
+
|
|
299
|
+
resolved_backend = backend
|
|
300
|
+
if backend == "auto" and not download_only:
|
|
301
|
+
from agent_cli.server.whisper.backends import detect_backend # noqa: PLC0415
|
|
302
|
+
|
|
303
|
+
resolved_backend = detect_backend()
|
|
304
|
+
|
|
305
|
+
_check_whisper_deps(resolved_backend, download_only=download_only)
|
|
306
|
+
|
|
307
|
+
if backend == "auto" and not download_only:
|
|
308
|
+
logger.info("Selected %s backend (auto-detected)", resolved_backend)
|
|
309
|
+
|
|
310
|
+
from agent_cli.server.whisper.model_manager import WhisperModelConfig # noqa: PLC0415
|
|
311
|
+
from agent_cli.server.whisper.model_registry import create_whisper_registry # noqa: PLC0415
|
|
312
|
+
|
|
313
|
+
# Default model if none specified
|
|
314
|
+
if model is None:
|
|
315
|
+
model = ["large-v3"]
|
|
316
|
+
|
|
317
|
+
# Validate default model against model list
|
|
318
|
+
if default_model is not None and default_model not in model:
|
|
319
|
+
err_console.print(
|
|
320
|
+
f"[bold red]Error:[/bold red] --default-model '{default_model}' "
|
|
321
|
+
f"is not in the model list: {model}",
|
|
322
|
+
)
|
|
323
|
+
raise typer.Exit(1)
|
|
324
|
+
|
|
325
|
+
# Handle download-only mode
|
|
326
|
+
if download_only:
|
|
327
|
+
console.print("[bold]Downloading model(s)...[/bold]")
|
|
328
|
+
for model_name in model:
|
|
329
|
+
console.print(f" Downloading [cyan]{model_name}[/cyan]...")
|
|
330
|
+
try:
|
|
331
|
+
from faster_whisper import WhisperModel # noqa: PLC0415
|
|
332
|
+
|
|
333
|
+
_ = WhisperModel(
|
|
334
|
+
model_name,
|
|
335
|
+
device="cpu", # Don't need GPU for download
|
|
336
|
+
download_root=str(cache_dir) if cache_dir else None,
|
|
337
|
+
)
|
|
338
|
+
console.print(f" [green]✓[/green] Downloaded {model_name}")
|
|
339
|
+
except Exception as e:
|
|
340
|
+
err_console.print(f" [red]✗[/red] Failed to download {model_name}: {e}")
|
|
341
|
+
raise typer.Exit(1) from e
|
|
342
|
+
console.print("[bold green]All models downloaded successfully![/bold green]")
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
# Create registry and register models
|
|
346
|
+
registry = create_whisper_registry(default_model=default_model or model[0])
|
|
347
|
+
|
|
348
|
+
for model_name in model:
|
|
349
|
+
config = WhisperModelConfig(
|
|
350
|
+
model_name=model_name,
|
|
351
|
+
device=device,
|
|
352
|
+
compute_type=compute_type,
|
|
353
|
+
ttl_seconds=ttl,
|
|
354
|
+
cache_dir=cache_dir,
|
|
355
|
+
backend_type=resolved_backend, # type: ignore[arg-type]
|
|
356
|
+
)
|
|
357
|
+
registry.register(config)
|
|
358
|
+
|
|
359
|
+
# Preload models if requested
|
|
360
|
+
if preload:
|
|
361
|
+
console.print("[bold]Preloading model(s)...[/bold]")
|
|
362
|
+
asyncio.run(registry.preload())
|
|
363
|
+
|
|
364
|
+
# Build Wyoming URI
|
|
365
|
+
wyoming_uri = f"tcp://{host}:{wyoming_port}"
|
|
366
|
+
|
|
367
|
+
actual_backend = resolved_backend
|
|
368
|
+
|
|
369
|
+
# Print startup info
|
|
370
|
+
console.print()
|
|
371
|
+
console.print("[bold green]Starting Whisper ASR Server[/bold green]")
|
|
372
|
+
console.print()
|
|
373
|
+
console.print("[dim]Configuration:[/dim]")
|
|
374
|
+
console.print(f" Backend: [cyan]{actual_backend}[/cyan]")
|
|
375
|
+
console.print(f" Log level: [cyan]{log_level}[/cyan]")
|
|
376
|
+
console.print()
|
|
377
|
+
console.print("[dim]Endpoints:[/dim]")
|
|
378
|
+
console.print(f" HTTP API: [cyan]http://{host}:{port}[/cyan]")
|
|
379
|
+
if not no_wyoming:
|
|
380
|
+
console.print(f" Wyoming: [cyan]{wyoming_uri}[/cyan]")
|
|
381
|
+
console.print()
|
|
382
|
+
console.print("[dim]Models:[/dim]")
|
|
383
|
+
for m in model:
|
|
384
|
+
is_default = m == registry.default_model
|
|
385
|
+
suffix = " [yellow](default)[/yellow]" if is_default else ""
|
|
386
|
+
console.print(f" • {m} (ttl={ttl}s){suffix}")
|
|
387
|
+
console.print()
|
|
388
|
+
console.print("[dim]Usage with agent-cli:[/dim]")
|
|
389
|
+
console.print(
|
|
390
|
+
f" [cyan]ag transcribe --asr-provider openai "
|
|
391
|
+
f"--asr-openai-base-url http://localhost:{port}/v1[/cyan]",
|
|
392
|
+
)
|
|
393
|
+
if not no_wyoming:
|
|
394
|
+
console.print(
|
|
395
|
+
f" [cyan]ag transcribe --asr-provider wyoming --asr-wyoming-ip {host} "
|
|
396
|
+
f"--asr-wyoming-port {wyoming_port}[/cyan]",
|
|
397
|
+
)
|
|
398
|
+
console.print()
|
|
399
|
+
|
|
400
|
+
# Create and run the app
|
|
401
|
+
from agent_cli.server.whisper.api import create_app # noqa: PLC0415
|
|
402
|
+
|
|
403
|
+
fastapi_app = create_app(
|
|
404
|
+
registry,
|
|
405
|
+
enable_wyoming=not no_wyoming,
|
|
406
|
+
wyoming_uri=wyoming_uri,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
import uvicorn # noqa: PLC0415
|
|
410
|
+
|
|
411
|
+
uvicorn.run(
|
|
412
|
+
fastapi_app,
|
|
413
|
+
host=host,
|
|
414
|
+
port=port,
|
|
415
|
+
log_level=log_level.lower(),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@app.command("transcribe-proxy")
|
|
420
|
+
@requires_extras("server", "wyoming", "llm")
|
|
421
|
+
def transcribe_proxy_cmd(
|
|
422
|
+
host: Annotated[
|
|
423
|
+
str,
|
|
424
|
+
typer.Option("--host", help="Host to bind the server to"),
|
|
425
|
+
] = "0.0.0.0", # noqa: S104
|
|
426
|
+
port: Annotated[
|
|
427
|
+
int,
|
|
428
|
+
typer.Option("--port", "-p", help="Port to bind the server to"),
|
|
429
|
+
] = 61337,
|
|
430
|
+
reload: Annotated[
|
|
431
|
+
bool,
|
|
432
|
+
typer.Option("--reload", help="Enable auto-reload for development"),
|
|
433
|
+
] = False,
|
|
434
|
+
log_level: opts.LogLevel = opts.LOG_LEVEL,
|
|
435
|
+
) -> None:
|
|
436
|
+
"""Run transcription proxy server.
|
|
437
|
+
|
|
438
|
+
This server proxies transcription requests to configured ASR providers
|
|
439
|
+
(Wyoming, OpenAI, or Gemini) based on your agent-cli configuration.
|
|
440
|
+
|
|
441
|
+
It exposes:
|
|
442
|
+
- /transcribe endpoint for audio transcription
|
|
443
|
+
- /health endpoint for health checks
|
|
444
|
+
|
|
445
|
+
This is the original server command functionality.
|
|
446
|
+
|
|
447
|
+
Examples:
|
|
448
|
+
# Run on default port
|
|
449
|
+
agent-cli server transcribe-proxy
|
|
450
|
+
|
|
451
|
+
# Run on custom port
|
|
452
|
+
agent-cli server transcribe-proxy --port 8080
|
|
453
|
+
|
|
454
|
+
"""
|
|
455
|
+
_check_server_deps()
|
|
456
|
+
setup_rich_logging(log_level)
|
|
457
|
+
|
|
458
|
+
console.print(
|
|
459
|
+
f"[bold green]Starting Agent CLI transcription proxy on {host}:{port}[/bold green]",
|
|
460
|
+
)
|
|
461
|
+
console.print(f"[dim]Log level: {log_level}[/dim]")
|
|
462
|
+
if reload:
|
|
463
|
+
console.print("[yellow]Auto-reload enabled for development[/yellow]")
|
|
464
|
+
|
|
465
|
+
import uvicorn # noqa: PLC0415
|
|
466
|
+
|
|
467
|
+
uvicorn.run(
|
|
468
|
+
"agent_cli.server.proxy.api:app",
|
|
469
|
+
host=host,
|
|
470
|
+
port=port,
|
|
471
|
+
reload=reload,
|
|
472
|
+
log_level=log_level.lower(),
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@app.command("tts")
|
|
477
|
+
@requires_extras("server", "piper|kokoro")
|
|
478
|
+
def tts_cmd( # noqa: PLR0915
|
|
479
|
+
model: Annotated[
|
|
480
|
+
list[str] | None,
|
|
481
|
+
typer.Option(
|
|
482
|
+
"--model",
|
|
483
|
+
"-m",
|
|
484
|
+
help="Model name(s) to load. Piper: 'en_US-lessac-medium'. Kokoro: 'kokoro' (auto-downloads)",
|
|
485
|
+
),
|
|
486
|
+
] = None,
|
|
487
|
+
default_model: Annotated[
|
|
488
|
+
str | None,
|
|
489
|
+
typer.Option(
|
|
490
|
+
"--default-model",
|
|
491
|
+
help="Default model when not specified in request",
|
|
492
|
+
),
|
|
493
|
+
] = None,
|
|
494
|
+
device: Annotated[
|
|
495
|
+
str,
|
|
496
|
+
typer.Option(
|
|
497
|
+
"--device",
|
|
498
|
+
"-d",
|
|
499
|
+
help="Device: auto, cpu, cuda, mps (Piper is CPU-only, Kokoro supports GPU)",
|
|
500
|
+
),
|
|
501
|
+
] = "auto",
|
|
502
|
+
cache_dir: Annotated[
|
|
503
|
+
Path | None,
|
|
504
|
+
typer.Option(
|
|
505
|
+
"--cache-dir",
|
|
506
|
+
help="Model cache directory",
|
|
507
|
+
),
|
|
508
|
+
] = None,
|
|
509
|
+
ttl: Annotated[
|
|
510
|
+
int,
|
|
511
|
+
typer.Option(
|
|
512
|
+
"--ttl",
|
|
513
|
+
help="Seconds before unloading idle model",
|
|
514
|
+
),
|
|
515
|
+
] = 300,
|
|
516
|
+
preload: Annotated[
|
|
517
|
+
bool,
|
|
518
|
+
typer.Option(
|
|
519
|
+
"--preload",
|
|
520
|
+
help="Load model(s) at startup and wait for completion",
|
|
521
|
+
),
|
|
522
|
+
] = False,
|
|
523
|
+
host: Annotated[
|
|
524
|
+
str,
|
|
525
|
+
typer.Option(
|
|
526
|
+
"--host",
|
|
527
|
+
help="Host to bind the server to",
|
|
528
|
+
),
|
|
529
|
+
] = "0.0.0.0", # noqa: S104
|
|
530
|
+
port: Annotated[
|
|
531
|
+
int,
|
|
532
|
+
typer.Option(
|
|
533
|
+
"--port",
|
|
534
|
+
"-p",
|
|
535
|
+
help="HTTP API port",
|
|
536
|
+
),
|
|
537
|
+
] = 10201,
|
|
538
|
+
wyoming_port: Annotated[
|
|
539
|
+
int,
|
|
540
|
+
typer.Option(
|
|
541
|
+
"--wyoming-port",
|
|
542
|
+
help="Wyoming protocol port",
|
|
543
|
+
),
|
|
544
|
+
] = 10200,
|
|
545
|
+
no_wyoming: Annotated[
|
|
546
|
+
bool,
|
|
547
|
+
typer.Option(
|
|
548
|
+
"--no-wyoming",
|
|
549
|
+
help="Disable Wyoming server",
|
|
550
|
+
),
|
|
551
|
+
] = False,
|
|
552
|
+
download_only: Annotated[
|
|
553
|
+
bool,
|
|
554
|
+
typer.Option(
|
|
555
|
+
"--download-only",
|
|
556
|
+
help="Download model(s) and exit without starting server",
|
|
557
|
+
),
|
|
558
|
+
] = False,
|
|
559
|
+
log_level: opts.LogLevel = opts.LOG_LEVEL,
|
|
560
|
+
backend: Annotated[
|
|
561
|
+
str,
|
|
562
|
+
typer.Option(
|
|
563
|
+
"--backend",
|
|
564
|
+
"-b",
|
|
565
|
+
help="Backend: auto, piper, kokoro",
|
|
566
|
+
),
|
|
567
|
+
] = "auto",
|
|
568
|
+
) -> None:
|
|
569
|
+
"""Run TTS server with TTL-based model unloading.
|
|
570
|
+
|
|
571
|
+
The server provides:
|
|
572
|
+
- OpenAI-compatible /v1/audio/speech endpoint
|
|
573
|
+
- Wyoming protocol for Home Assistant integration
|
|
574
|
+
- Voice list at /v1/voices
|
|
575
|
+
|
|
576
|
+
Models are loaded lazily on first request and unloaded after being
|
|
577
|
+
idle for the TTL duration, freeing memory for other applications.
|
|
578
|
+
|
|
579
|
+
**Piper backend** (CPU-friendly):
|
|
580
|
+
Models use names like 'en_US-lessac-medium', 'en_GB-alan-medium'.
|
|
581
|
+
See https://github.com/rhasspy/piper for available models.
|
|
582
|
+
|
|
583
|
+
**Kokoro backend** (GPU-accelerated):
|
|
584
|
+
Model and voices auto-download from HuggingFace on first use.
|
|
585
|
+
Voices: af_heart, af_bella, am_adam, bf_emma, bm_george, etc.
|
|
586
|
+
See https://huggingface.co/hexgrad/Kokoro-82M for all voices.
|
|
587
|
+
|
|
588
|
+
Examples:
|
|
589
|
+
# Run with Kokoro (auto-downloads model and voices)
|
|
590
|
+
agent-cli server tts --backend kokoro
|
|
591
|
+
|
|
592
|
+
# Run with default Piper model
|
|
593
|
+
agent-cli server tts --backend piper
|
|
594
|
+
|
|
595
|
+
# Run with specific Piper model and 10-minute TTL
|
|
596
|
+
agent-cli server tts --model en_US-lessac-medium --ttl 600
|
|
597
|
+
|
|
598
|
+
# Download Kokoro model and voices without starting server
|
|
599
|
+
agent-cli server tts --backend kokoro --model af_bella --model am_adam --download-only
|
|
600
|
+
|
|
601
|
+
# Download Piper model without starting server
|
|
602
|
+
agent-cli server tts --backend piper --model en_US-lessac-medium --download-only
|
|
603
|
+
|
|
604
|
+
"""
|
|
605
|
+
# Setup Rich logging for consistent output
|
|
606
|
+
setup_rich_logging(log_level)
|
|
607
|
+
|
|
608
|
+
valid_backends = ("auto", "piper", "kokoro")
|
|
609
|
+
if backend not in valid_backends:
|
|
610
|
+
err_console.print(
|
|
611
|
+
f"[bold red]Error:[/bold red] --backend must be one of: {', '.join(valid_backends)}",
|
|
612
|
+
)
|
|
613
|
+
raise typer.Exit(1)
|
|
614
|
+
|
|
615
|
+
# Resolve backend for auto mode
|
|
616
|
+
resolved_backend = backend
|
|
617
|
+
if backend == "auto":
|
|
618
|
+
from agent_cli.server.tts.backends import ( # noqa: PLC0415
|
|
619
|
+
detect_backend as detect_tts_backend,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
resolved_backend = detect_tts_backend()
|
|
623
|
+
logger.info("Selected %s backend (auto-detected)", resolved_backend)
|
|
624
|
+
|
|
625
|
+
_check_tts_deps(resolved_backend)
|
|
626
|
+
|
|
627
|
+
from agent_cli.server.tts.model_manager import TTSModelConfig # noqa: PLC0415
|
|
628
|
+
from agent_cli.server.tts.model_registry import create_tts_registry # noqa: PLC0415
|
|
629
|
+
|
|
630
|
+
# Default model based on backend (Kokoro auto-downloads from HuggingFace)
|
|
631
|
+
if model is None:
|
|
632
|
+
model = ["kokoro"] if resolved_backend == "kokoro" else ["en_US-lessac-medium"]
|
|
633
|
+
|
|
634
|
+
# Validate default model against model list
|
|
635
|
+
if default_model is not None and default_model not in model:
|
|
636
|
+
err_console.print(
|
|
637
|
+
f"[bold red]Error:[/bold red] --default-model '{default_model}' "
|
|
638
|
+
f"is not in the model list: {model}",
|
|
639
|
+
)
|
|
640
|
+
raise typer.Exit(1)
|
|
641
|
+
|
|
642
|
+
if download_only:
|
|
643
|
+
_download_tts_models(resolved_backend, model, cache_dir)
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
# Create registry and register models
|
|
647
|
+
registry = create_tts_registry(default_model=default_model or model[0])
|
|
648
|
+
|
|
649
|
+
for model_name in model:
|
|
650
|
+
config = TTSModelConfig(
|
|
651
|
+
model_name=model_name,
|
|
652
|
+
device=device,
|
|
653
|
+
ttl_seconds=ttl,
|
|
654
|
+
cache_dir=cache_dir,
|
|
655
|
+
backend_type=resolved_backend, # type: ignore[arg-type]
|
|
656
|
+
)
|
|
657
|
+
registry.register(config)
|
|
658
|
+
|
|
659
|
+
# Preload models if requested
|
|
660
|
+
if preload:
|
|
661
|
+
console.print("[bold]Preloading model(s)...[/bold]")
|
|
662
|
+
asyncio.run(registry.preload())
|
|
663
|
+
|
|
664
|
+
# Build Wyoming URI
|
|
665
|
+
wyoming_uri = f"tcp://{host}:{wyoming_port}"
|
|
666
|
+
|
|
667
|
+
# Print startup info
|
|
668
|
+
console.print()
|
|
669
|
+
console.print("[bold green]Starting TTS Server[/bold green]")
|
|
670
|
+
console.print()
|
|
671
|
+
console.print("[dim]Configuration:[/dim]")
|
|
672
|
+
console.print(f" Backend: [cyan]{resolved_backend}[/cyan]")
|
|
673
|
+
console.print(f" Log level: [cyan]{log_level}[/cyan]")
|
|
674
|
+
console.print()
|
|
675
|
+
console.print("[dim]Endpoints:[/dim]")
|
|
676
|
+
console.print(f" HTTP API: [cyan]http://{host}:{port}[/cyan]")
|
|
677
|
+
if not no_wyoming:
|
|
678
|
+
console.print(f" Wyoming: [cyan]{wyoming_uri}[/cyan]")
|
|
679
|
+
console.print()
|
|
680
|
+
console.print("[dim]Models:[/dim]")
|
|
681
|
+
for m in model:
|
|
682
|
+
is_default = m == registry.default_model
|
|
683
|
+
suffix = " [yellow](default)[/yellow]" if is_default else ""
|
|
684
|
+
console.print(f" • {m} (ttl={ttl}s){suffix}")
|
|
685
|
+
console.print()
|
|
686
|
+
console.print("[dim]Usage with OpenAI client:[/dim]")
|
|
687
|
+
console.print(
|
|
688
|
+
" [cyan]from openai import OpenAI[/cyan]",
|
|
689
|
+
)
|
|
690
|
+
console.print(
|
|
691
|
+
f' [cyan]client = OpenAI(base_url="http://localhost:{port}/v1", api_key="x")[/cyan]',
|
|
692
|
+
)
|
|
693
|
+
if resolved_backend == "kokoro":
|
|
694
|
+
console.print(
|
|
695
|
+
' [cyan]response = client.audio.speech.create(model="tts-1", voice="af_heart", '
|
|
696
|
+
'input="Hello")[/cyan]',
|
|
697
|
+
)
|
|
698
|
+
else:
|
|
699
|
+
console.print(
|
|
700
|
+
' [cyan]response = client.audio.speech.create(model="tts-1", voice="alloy", '
|
|
701
|
+
'input="Hello")[/cyan]',
|
|
702
|
+
)
|
|
703
|
+
console.print()
|
|
704
|
+
|
|
705
|
+
# Create and run the app
|
|
706
|
+
from agent_cli.server.tts.api import create_app # noqa: PLC0415
|
|
707
|
+
|
|
708
|
+
fastapi_app = create_app(
|
|
709
|
+
registry,
|
|
710
|
+
enable_wyoming=not no_wyoming,
|
|
711
|
+
wyoming_uri=wyoming_uri,
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
import uvicorn # noqa: PLC0415
|
|
715
|
+
|
|
716
|
+
uvicorn.run(
|
|
717
|
+
fastapi_app,
|
|
718
|
+
host=host,
|
|
719
|
+
port=port,
|
|
720
|
+
log_level=log_level.lower(),
|
|
721
|
+
)
|