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.
Files changed (196) hide show
  1. agent_cli/__init__.py +5 -0
  2. agent_cli/__main__.py +6 -0
  3. agent_cli/_extras.json +14 -0
  4. agent_cli/_requirements/.gitkeep +0 -0
  5. agent_cli/_requirements/audio.txt +79 -0
  6. agent_cli/_requirements/faster-whisper.txt +215 -0
  7. agent_cli/_requirements/kokoro.txt +425 -0
  8. agent_cli/_requirements/llm.txt +183 -0
  9. agent_cli/_requirements/memory.txt +355 -0
  10. agent_cli/_requirements/mlx-whisper.txt +222 -0
  11. agent_cli/_requirements/piper.txt +176 -0
  12. agent_cli/_requirements/rag.txt +402 -0
  13. agent_cli/_requirements/server.txt +154 -0
  14. agent_cli/_requirements/speed.txt +77 -0
  15. agent_cli/_requirements/vad.txt +155 -0
  16. agent_cli/_requirements/wyoming.txt +71 -0
  17. agent_cli/_tools.py +368 -0
  18. agent_cli/agents/__init__.py +23 -0
  19. agent_cli/agents/_voice_agent_common.py +136 -0
  20. agent_cli/agents/assistant.py +383 -0
  21. agent_cli/agents/autocorrect.py +284 -0
  22. agent_cli/agents/chat.py +496 -0
  23. agent_cli/agents/memory/__init__.py +31 -0
  24. agent_cli/agents/memory/add.py +190 -0
  25. agent_cli/agents/memory/proxy.py +160 -0
  26. agent_cli/agents/rag_proxy.py +128 -0
  27. agent_cli/agents/speak.py +209 -0
  28. agent_cli/agents/transcribe.py +671 -0
  29. agent_cli/agents/transcribe_daemon.py +499 -0
  30. agent_cli/agents/voice_edit.py +291 -0
  31. agent_cli/api.py +22 -0
  32. agent_cli/cli.py +106 -0
  33. agent_cli/config.py +503 -0
  34. agent_cli/config_cmd.py +307 -0
  35. agent_cli/constants.py +27 -0
  36. agent_cli/core/__init__.py +1 -0
  37. agent_cli/core/audio.py +461 -0
  38. agent_cli/core/audio_format.py +299 -0
  39. agent_cli/core/chroma.py +88 -0
  40. agent_cli/core/deps.py +191 -0
  41. agent_cli/core/openai_proxy.py +139 -0
  42. agent_cli/core/process.py +195 -0
  43. agent_cli/core/reranker.py +120 -0
  44. agent_cli/core/sse.py +87 -0
  45. agent_cli/core/transcription_logger.py +70 -0
  46. agent_cli/core/utils.py +526 -0
  47. agent_cli/core/vad.py +175 -0
  48. agent_cli/core/watch.py +65 -0
  49. agent_cli/dev/__init__.py +14 -0
  50. agent_cli/dev/cli.py +1588 -0
  51. agent_cli/dev/coding_agents/__init__.py +19 -0
  52. agent_cli/dev/coding_agents/aider.py +24 -0
  53. agent_cli/dev/coding_agents/base.py +167 -0
  54. agent_cli/dev/coding_agents/claude.py +39 -0
  55. agent_cli/dev/coding_agents/codex.py +24 -0
  56. agent_cli/dev/coding_agents/continue_dev.py +15 -0
  57. agent_cli/dev/coding_agents/copilot.py +24 -0
  58. agent_cli/dev/coding_agents/cursor_agent.py +48 -0
  59. agent_cli/dev/coding_agents/gemini.py +28 -0
  60. agent_cli/dev/coding_agents/opencode.py +15 -0
  61. agent_cli/dev/coding_agents/registry.py +49 -0
  62. agent_cli/dev/editors/__init__.py +19 -0
  63. agent_cli/dev/editors/base.py +89 -0
  64. agent_cli/dev/editors/cursor.py +15 -0
  65. agent_cli/dev/editors/emacs.py +46 -0
  66. agent_cli/dev/editors/jetbrains.py +56 -0
  67. agent_cli/dev/editors/nano.py +31 -0
  68. agent_cli/dev/editors/neovim.py +33 -0
  69. agent_cli/dev/editors/registry.py +59 -0
  70. agent_cli/dev/editors/sublime.py +20 -0
  71. agent_cli/dev/editors/vim.py +42 -0
  72. agent_cli/dev/editors/vscode.py +15 -0
  73. agent_cli/dev/editors/zed.py +20 -0
  74. agent_cli/dev/project.py +568 -0
  75. agent_cli/dev/registry.py +52 -0
  76. agent_cli/dev/skill/SKILL.md +141 -0
  77. agent_cli/dev/skill/examples.md +571 -0
  78. agent_cli/dev/terminals/__init__.py +19 -0
  79. agent_cli/dev/terminals/apple_terminal.py +82 -0
  80. agent_cli/dev/terminals/base.py +56 -0
  81. agent_cli/dev/terminals/gnome.py +51 -0
  82. agent_cli/dev/terminals/iterm2.py +84 -0
  83. agent_cli/dev/terminals/kitty.py +77 -0
  84. agent_cli/dev/terminals/registry.py +48 -0
  85. agent_cli/dev/terminals/tmux.py +58 -0
  86. agent_cli/dev/terminals/warp.py +132 -0
  87. agent_cli/dev/terminals/zellij.py +78 -0
  88. agent_cli/dev/worktree.py +856 -0
  89. agent_cli/docs_gen.py +417 -0
  90. agent_cli/example-config.toml +185 -0
  91. agent_cli/install/__init__.py +5 -0
  92. agent_cli/install/common.py +89 -0
  93. agent_cli/install/extras.py +174 -0
  94. agent_cli/install/hotkeys.py +48 -0
  95. agent_cli/install/services.py +87 -0
  96. agent_cli/memory/__init__.py +7 -0
  97. agent_cli/memory/_files.py +250 -0
  98. agent_cli/memory/_filters.py +63 -0
  99. agent_cli/memory/_git.py +157 -0
  100. agent_cli/memory/_indexer.py +142 -0
  101. agent_cli/memory/_ingest.py +408 -0
  102. agent_cli/memory/_persistence.py +182 -0
  103. agent_cli/memory/_prompt.py +91 -0
  104. agent_cli/memory/_retrieval.py +294 -0
  105. agent_cli/memory/_store.py +169 -0
  106. agent_cli/memory/_streaming.py +44 -0
  107. agent_cli/memory/_tasks.py +48 -0
  108. agent_cli/memory/api.py +113 -0
  109. agent_cli/memory/client.py +272 -0
  110. agent_cli/memory/engine.py +361 -0
  111. agent_cli/memory/entities.py +43 -0
  112. agent_cli/memory/models.py +112 -0
  113. agent_cli/opts.py +433 -0
  114. agent_cli/py.typed +0 -0
  115. agent_cli/rag/__init__.py +3 -0
  116. agent_cli/rag/_indexer.py +67 -0
  117. agent_cli/rag/_indexing.py +226 -0
  118. agent_cli/rag/_prompt.py +30 -0
  119. agent_cli/rag/_retriever.py +156 -0
  120. agent_cli/rag/_store.py +48 -0
  121. agent_cli/rag/_utils.py +218 -0
  122. agent_cli/rag/api.py +175 -0
  123. agent_cli/rag/client.py +299 -0
  124. agent_cli/rag/engine.py +302 -0
  125. agent_cli/rag/models.py +55 -0
  126. agent_cli/scripts/.runtime/.gitkeep +0 -0
  127. agent_cli/scripts/__init__.py +1 -0
  128. agent_cli/scripts/check_plugin_skill_sync.py +50 -0
  129. agent_cli/scripts/linux-hotkeys/README.md +63 -0
  130. agent_cli/scripts/linux-hotkeys/toggle-autocorrect.sh +45 -0
  131. agent_cli/scripts/linux-hotkeys/toggle-transcription.sh +58 -0
  132. agent_cli/scripts/linux-hotkeys/toggle-voice-edit.sh +58 -0
  133. agent_cli/scripts/macos-hotkeys/README.md +45 -0
  134. agent_cli/scripts/macos-hotkeys/skhd-config-example +5 -0
  135. agent_cli/scripts/macos-hotkeys/toggle-autocorrect.sh +12 -0
  136. agent_cli/scripts/macos-hotkeys/toggle-transcription.sh +37 -0
  137. agent_cli/scripts/macos-hotkeys/toggle-voice-edit.sh +37 -0
  138. agent_cli/scripts/nvidia-asr-server/README.md +99 -0
  139. agent_cli/scripts/nvidia-asr-server/pyproject.toml +27 -0
  140. agent_cli/scripts/nvidia-asr-server/server.py +255 -0
  141. agent_cli/scripts/nvidia-asr-server/shell.nix +32 -0
  142. agent_cli/scripts/nvidia-asr-server/uv.lock +4654 -0
  143. agent_cli/scripts/run-openwakeword.sh +11 -0
  144. agent_cli/scripts/run-piper-windows.ps1 +30 -0
  145. agent_cli/scripts/run-piper.sh +24 -0
  146. agent_cli/scripts/run-whisper-linux.sh +40 -0
  147. agent_cli/scripts/run-whisper-macos.sh +6 -0
  148. agent_cli/scripts/run-whisper-windows.ps1 +51 -0
  149. agent_cli/scripts/run-whisper.sh +9 -0
  150. agent_cli/scripts/run_faster_whisper_server.py +136 -0
  151. agent_cli/scripts/setup-linux-hotkeys.sh +72 -0
  152. agent_cli/scripts/setup-linux.sh +108 -0
  153. agent_cli/scripts/setup-macos-hotkeys.sh +61 -0
  154. agent_cli/scripts/setup-macos.sh +76 -0
  155. agent_cli/scripts/setup-windows.ps1 +63 -0
  156. agent_cli/scripts/start-all-services-windows.ps1 +53 -0
  157. agent_cli/scripts/start-all-services.sh +178 -0
  158. agent_cli/scripts/sync_extras.py +138 -0
  159. agent_cli/server/__init__.py +3 -0
  160. agent_cli/server/cli.py +721 -0
  161. agent_cli/server/common.py +222 -0
  162. agent_cli/server/model_manager.py +288 -0
  163. agent_cli/server/model_registry.py +225 -0
  164. agent_cli/server/proxy/__init__.py +3 -0
  165. agent_cli/server/proxy/api.py +444 -0
  166. agent_cli/server/streaming.py +67 -0
  167. agent_cli/server/tts/__init__.py +3 -0
  168. agent_cli/server/tts/api.py +335 -0
  169. agent_cli/server/tts/backends/__init__.py +82 -0
  170. agent_cli/server/tts/backends/base.py +139 -0
  171. agent_cli/server/tts/backends/kokoro.py +403 -0
  172. agent_cli/server/tts/backends/piper.py +253 -0
  173. agent_cli/server/tts/model_manager.py +201 -0
  174. agent_cli/server/tts/model_registry.py +28 -0
  175. agent_cli/server/tts/wyoming_handler.py +249 -0
  176. agent_cli/server/whisper/__init__.py +3 -0
  177. agent_cli/server/whisper/api.py +413 -0
  178. agent_cli/server/whisper/backends/__init__.py +89 -0
  179. agent_cli/server/whisper/backends/base.py +97 -0
  180. agent_cli/server/whisper/backends/faster_whisper.py +225 -0
  181. agent_cli/server/whisper/backends/mlx.py +270 -0
  182. agent_cli/server/whisper/languages.py +116 -0
  183. agent_cli/server/whisper/model_manager.py +157 -0
  184. agent_cli/server/whisper/model_registry.py +28 -0
  185. agent_cli/server/whisper/wyoming_handler.py +203 -0
  186. agent_cli/services/__init__.py +343 -0
  187. agent_cli/services/_wyoming_utils.py +64 -0
  188. agent_cli/services/asr.py +506 -0
  189. agent_cli/services/llm.py +228 -0
  190. agent_cli/services/tts.py +450 -0
  191. agent_cli/services/wake_word.py +142 -0
  192. agent_cli-0.70.5.dist-info/METADATA +2118 -0
  193. agent_cli-0.70.5.dist-info/RECORD +196 -0
  194. agent_cli-0.70.5.dist-info/WHEEL +4 -0
  195. agent_cli-0.70.5.dist-info/entry_points.txt +4 -0
  196. agent_cli-0.70.5.dist-info/licenses/LICENSE +21 -0
@@ -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
+ )