aioli-cli 0.1.30__tar.gz → 0.1.32__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/PKG-INFO +6 -4
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/README.md +5 -3
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/pyproject.toml +13 -1
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_cli/main.py +133 -31
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_cli/setup.py +252 -48
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_cli/status.py +98 -12
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_contract/advise.py +149 -0
- aioli_cli-0.1.32/src/aioli_contract/config.py +99 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_contract/fetch.py +208 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/cli.py +45 -2
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/cli_commands/generate_cmd.py +55 -3
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_video/cli.py +64 -1
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_video/wgp.py +43 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/cli/engines_cmd.py +3 -1
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/cli/generate.py +92 -10
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/engines/chatterbox.py +103 -7
- aioli_cli-0.1.30/src/aioli_contract/config.py +0 -43
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/.gitignore +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/LICENSE +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_cli/_plugins.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_cli/fetch_ui.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_cli/readiness.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_cli/registry.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_contract/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_contract/doctor.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_contract/probe.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_contract/readiness.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/companions.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/config.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/data/models.toml +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/doctor.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/gpu.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/launcher.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/models.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/models_data.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/plugin.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/presets.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/stub.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_image/ui.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_master/cli.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_master/doctor.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_master/ops.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_master/plugin.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/backends/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/backends/acestep.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/backends/base.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/backends/llm_caption.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/backends/minimax_comfy.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/backends/process.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/backends/stub.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/backends/yue2.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/cli_commands/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/cli_commands/backend_cmd.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/cli_commands/inspire_cmd.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/cli_commands/models_cmd.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/client.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/config.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/config_data/default.toml +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/core/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/core/formats.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/core/generate.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/core/jobs.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/core/layout.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/doctor.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/generator.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/history.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/jsonstore.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/models.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/models_data.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/paths.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/plugin.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/presets.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/prompts.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/server.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/transport/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/transport/client.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_music/tui.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_podcast/cast.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_podcast/cli.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_podcast/doctor.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_podcast/episode.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_podcast/plugin.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_podcast/script.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_video/doctor.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_video/models_data.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_video/paths.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_video/plugin.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_video/stub.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_video/worker.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/cli/doctor.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/cli/models_cmd.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/cli/profiles.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/config/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/config/settings.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/core/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/core/generation.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/core/model_manager.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/core/profiles.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/core/registry.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/core/types.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/devices.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/doctor.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/engines/__init__.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/engines/kokoro.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/engines/moss.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/engines/piper.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/engines/qwen3.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/engines/stub.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/engines/voxcpm.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/engines/xtts.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/models_data.py +0 -0
- {aioli_cli-0.1.30 → aioli_cli-0.1.32}/src/aioli_voice/plugin.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: aioli-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.32
|
|
4
4
|
Summary: aioli — local-first AI media suite CLI (images, video, music, voice, podcast)
|
|
5
5
|
Author: aioli contributors
|
|
6
6
|
License: MIT
|
|
@@ -79,8 +79,8 @@ with these commands. No accounts, no subscription.
|
|
|
79
79
|
## Install
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
curl -fsSL https://aioli.es/install.sh | sh #
|
|
83
|
-
# …or by hand (needs `uv`, Python ≥3.12 and `ffmpeg`):
|
|
82
|
+
curl -fsSL https://aioli.es/install.sh | sh # installs uv+ffmpeg if missing → doctor → setup → status
|
|
83
|
+
# …or truly by hand (needs `uv`, Python ≥3.12 and `ffmpeg`):
|
|
84
84
|
uv tool install aioli-cli # extras: aioli-cli[voice-piper,music-storage]
|
|
85
85
|
aioli setup # tells you what fits your GPU, downloads weights
|
|
86
86
|
aioli doctor # preflight: CUDA, ffmpeg, models, VRAM
|
|
@@ -91,7 +91,9 @@ huggingface.co and export `HF_TOKEN` (read-only token) before `setup`.
|
|
|
91
91
|
|
|
92
92
|
> 🔒 Paranoid (good): the installer lives in the repo's `install.sh`.
|
|
93
93
|
> It only uses the network for `astral.sh` (uv), GitHub and HuggingFace.
|
|
94
|
-
>
|
|
94
|
+
> The only elevated step is installing `ffmpeg` if missing — it asks first
|
|
95
|
+
> (or skips with `--no-ffmpeg`). Re-running is safe: it upgrades in place
|
|
96
|
+
> and preserves your extras and `--with` engines.
|
|
95
97
|
|
|
96
98
|
## Requirements
|
|
97
99
|
|
|
@@ -47,8 +47,8 @@ with these commands. No accounts, no subscription.
|
|
|
47
47
|
## Install
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
curl -fsSL https://aioli.es/install.sh | sh #
|
|
51
|
-
# …or by hand (needs `uv`, Python ≥3.12 and `ffmpeg`):
|
|
50
|
+
curl -fsSL https://aioli.es/install.sh | sh # installs uv+ffmpeg if missing → doctor → setup → status
|
|
51
|
+
# …or truly by hand (needs `uv`, Python ≥3.12 and `ffmpeg`):
|
|
52
52
|
uv tool install aioli-cli # extras: aioli-cli[voice-piper,music-storage]
|
|
53
53
|
aioli setup # tells you what fits your GPU, downloads weights
|
|
54
54
|
aioli doctor # preflight: CUDA, ffmpeg, models, VRAM
|
|
@@ -59,7 +59,9 @@ huggingface.co and export `HF_TOKEN` (read-only token) before `setup`.
|
|
|
59
59
|
|
|
60
60
|
> 🔒 Paranoid (good): the installer lives in the repo's `install.sh`.
|
|
61
61
|
> It only uses the network for `astral.sh` (uv), GitHub and HuggingFace.
|
|
62
|
-
>
|
|
62
|
+
> The only elevated step is installing `ffmpeg` if missing — it asks first
|
|
63
|
+
> (or skips with `--no-ffmpeg`). Re-running is safe: it upgrades in place
|
|
64
|
+
> and preserves your extras and `--with` engines.
|
|
63
65
|
|
|
64
66
|
## Requirements
|
|
65
67
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aioli-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.32"
|
|
4
4
|
description = "aioli — local-first AI media suite CLI (images, video, music, voice, podcast)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -144,6 +144,7 @@ module = [
|
|
|
144
144
|
"voxcpm",
|
|
145
145
|
"qwen_tts", "qwen_tts.*",
|
|
146
146
|
"chatterbox", "chatterbox.*",
|
|
147
|
+
"perth",
|
|
147
148
|
"moss_tts_nano",
|
|
148
149
|
"transformers", "transformers.*",
|
|
149
150
|
"kokoro_onnx", "kokoro_onnx.*",
|
|
@@ -155,6 +156,17 @@ ignore_missing_imports = true
|
|
|
155
156
|
module = ["soundfile", "soundfile.*", "tqdm"]
|
|
156
157
|
ignore_missing_imports = true
|
|
157
158
|
|
|
159
|
+
# Out-of-venv checkers (session lens) run mypy WITHOUT the project venv:
|
|
160
|
+
# pytest (dev dep) and aioli_contract (workspace pkg in libs/contract, editable
|
|
161
|
+
# in .venv) don't resolve there and mypy reports import-not-found — false
|
|
162
|
+
# positive. In the real env they resolve (`.venv/bin/python -c "import pytest,
|
|
163
|
+
# aioli_contract"` OK; ruff + pytest --collect-only green on libs/contract/tests).
|
|
164
|
+
# ignore_missing_imports only fires when resolution fails, so the repo's own
|
|
165
|
+
# venv runs keep full checking.
|
|
166
|
+
[[tool.mypy.overrides]]
|
|
167
|
+
module = ["pytest", "pytest.*", "aioli_contract", "aioli_contract.*"]
|
|
168
|
+
ignore_missing_imports = true
|
|
169
|
+
|
|
158
170
|
# DEBT: aioli_music.core.jobs imports aioli_music.core.storage, which does not
|
|
159
171
|
# exist (unimplemented object-storage feature); nothing imports jobs. Decide:
|
|
160
172
|
# implement storage or delete the module. Until then its errors stay hidden
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""aioli CLI entry point."""
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
3
6
|
import typer
|
|
4
7
|
from rich.console import Console
|
|
5
8
|
from rich.table import Table
|
|
@@ -92,11 +95,10 @@ def doctor() -> None:
|
|
|
92
95
|
entry = ModelFile(**raw)
|
|
93
96
|
if entry.optional:
|
|
94
97
|
continue
|
|
95
|
-
|
|
96
|
-
import sys as _sys
|
|
98
|
+
from aioli_contract.fetch import current_platform, platform_matches
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
if not platform_matches(entry.platform, current_platform()):
|
|
101
|
+
continue
|
|
100
102
|
if entry.kind == "runtime":
|
|
101
103
|
continue
|
|
102
104
|
grouped.setdefault(entry.backend or "-", []).append(entry)
|
|
@@ -105,11 +107,10 @@ def doctor() -> None:
|
|
|
105
107
|
entry = ModelFile(**raw)
|
|
106
108
|
if entry.kind != "runtime" or entry.optional:
|
|
107
109
|
continue
|
|
108
|
-
|
|
109
|
-
import sys as _sys2
|
|
110
|
+
from aioli_contract.fetch import current_platform, platform_matches
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
if not platform_matches(entry.platform, current_platform()):
|
|
113
|
+
continue
|
|
113
114
|
runtimes.append(entry)
|
|
114
115
|
for backend, entries in sorted(grouped.items()):
|
|
115
116
|
if backend == "-":
|
|
@@ -174,6 +175,28 @@ def version() -> None:
|
|
|
174
175
|
models_app = typer.Typer(help="Download + verify weight files.")
|
|
175
176
|
app.add_typer(models_app, name="models")
|
|
176
177
|
|
|
178
|
+
backend_app = typer.Typer(help="Ver + cambiar el motor por defecto (music/voice).")
|
|
179
|
+
app.add_typer(backend_app, name="backend")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@backend_app.command("set")
|
|
183
|
+
def backend_set(
|
|
184
|
+
plugin: str = typer.Argument(..., help="Plugin con default persistido: music | voice."),
|
|
185
|
+
backend: str = typer.Argument(..., help="Motor a usar por defecto en generate."),
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Cambia el default persistido sin descargar (orden explicita, AC6).
|
|
188
|
+
|
|
189
|
+
`generate` usa este default salvo flag explicito (`--backend`/`--engine`,
|
|
190
|
+
que manda). Image/video son informativos y no tienen default persistido.
|
|
191
|
+
"""
|
|
192
|
+
from aioli_cli.setup import set_backend # noqa: PLC0415
|
|
193
|
+
|
|
194
|
+
set_backend(plugin, backend)
|
|
195
|
+
console.print(
|
|
196
|
+
f"✅ default de {plugin}: [bold]{backend}[/] "
|
|
197
|
+
"(el flag --backend/--engine sigue mandando en generate)"
|
|
198
|
+
)
|
|
199
|
+
|
|
177
200
|
|
|
178
201
|
def _wanted(plugin: str | None, backend: str | None = None) -> list[tuple[str, dict]]:
|
|
179
202
|
from aioli_cli import _plugins # noqa: PLC0415 — ensure loaded
|
|
@@ -202,6 +225,15 @@ def _wanted(plugin: str | None, backend: str | None = None) -> list[tuple[str, d
|
|
|
202
225
|
return out
|
|
203
226
|
|
|
204
227
|
|
|
228
|
+
def _plugin_roots(
|
|
229
|
+
wanted: list[tuple[str, Any]], cfg: AioliConfig, cli_flag: str | None
|
|
230
|
+
) -> dict[str, Path]:
|
|
231
|
+
roots: dict[str, Path] = {}
|
|
232
|
+
for pname, _ in wanted:
|
|
233
|
+
roots.setdefault(pname, cfg.resolve_models_root(pname, cli_flag=cli_flag))
|
|
234
|
+
return roots
|
|
235
|
+
|
|
236
|
+
|
|
205
237
|
def _backends() -> dict[str, list[str]]:
|
|
206
238
|
from aioli_cli import _plugins # noqa: PLC0415 — ensure loaded
|
|
207
239
|
|
|
@@ -222,24 +254,33 @@ def models_list(
|
|
|
222
254
|
None,
|
|
223
255
|
"--backend",
|
|
224
256
|
"-b",
|
|
225
|
-
help="Engine filter (default: plugin default; 'all' = every engine)."
|
|
257
|
+
help="Engine filter (default: plugin default; 'all' = every engine). "
|
|
258
|
+
"Disponible para tu equipo: ver `aioli status`.",
|
|
259
|
+
),
|
|
260
|
+
models_root: str | None = typer.Option(
|
|
261
|
+
None,
|
|
262
|
+
"--models-root",
|
|
263
|
+
help="Raíz de modelos (máxima precedencia, todos los plugins).",
|
|
226
264
|
),
|
|
227
265
|
) -> None:
|
|
228
266
|
"""Show manifest entries + local status."""
|
|
229
|
-
from aioli_contract import ModelFile, default_bin_dir,
|
|
267
|
+
from aioli_contract import ModelFile, default_bin_dir, human_gb, verify
|
|
230
268
|
|
|
231
|
-
|
|
269
|
+
cfg = AioliConfig.load()
|
|
232
270
|
bin_dir = default_bin_dir()
|
|
233
|
-
|
|
271
|
+
wanted = _wanted(plugin, backend)
|
|
272
|
+
roots = _plugin_roots(wanted, cfg, models_root)
|
|
273
|
+
distinct = ", ".join(sorted({str(r) for r in roots.values()}))
|
|
274
|
+
table = Table(title=f"aioli models → {distinct}")
|
|
234
275
|
table.add_column("plugin")
|
|
235
276
|
table.add_column("engine")
|
|
236
277
|
table.add_column("kind")
|
|
237
278
|
table.add_column("file")
|
|
238
279
|
table.add_column("size")
|
|
239
280
|
table.add_column("status")
|
|
240
|
-
for pname, raw in
|
|
281
|
+
for pname, raw in wanted:
|
|
241
282
|
entry = ModelFile(**raw)
|
|
242
|
-
status = verify(entry,
|
|
283
|
+
status = verify(entry, roots[pname], bin_dir, quick=True)
|
|
243
284
|
icon = {"ok": "✅", "missing": "⬇️", "corrupt": "⚠️", "unverified": "❔"}[status]
|
|
244
285
|
table.add_row(
|
|
245
286
|
pname,
|
|
@@ -260,11 +301,20 @@ def models_download(
|
|
|
260
301
|
plugin: str | None = typer.Argument(None),
|
|
261
302
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
262
303
|
backend: str | None = typer.Option(
|
|
263
|
-
None,
|
|
304
|
+
None,
|
|
305
|
+
"--backend",
|
|
306
|
+
"-b",
|
|
307
|
+
help="Engine only (default: plugin default; 'all' = every engine). "
|
|
308
|
+
"Disponible para tu equipo: ver `aioli status`.",
|
|
264
309
|
),
|
|
265
310
|
with_optional: bool = typer.Option(
|
|
266
311
|
False, "--with-optional", help="Include optional files (ASR, alt quants)."
|
|
267
312
|
),
|
|
313
|
+
models_root: str | None = typer.Option(
|
|
314
|
+
None,
|
|
315
|
+
"--models-root",
|
|
316
|
+
help="Raíz de modelos (máxima precedencia, todos los plugins).",
|
|
317
|
+
),
|
|
268
318
|
force: bool = typer.Option(
|
|
269
319
|
False, "--force", help="Re-download even if verified (repairs partial runtime bundles)."
|
|
270
320
|
),
|
|
@@ -276,14 +326,14 @@ def models_download(
|
|
|
276
326
|
ModelFile,
|
|
277
327
|
check_space,
|
|
278
328
|
default_bin_dir,
|
|
279
|
-
default_models_dir,
|
|
280
329
|
human_gb,
|
|
281
330
|
verify,
|
|
282
331
|
)
|
|
283
332
|
|
|
284
|
-
|
|
333
|
+
cfg = AioliConfig.load()
|
|
285
334
|
bin_dir = default_bin_dir()
|
|
286
335
|
wanted = [(pn, ModelFile(**raw)) for pn, raw in _wanted(plugin, backend)]
|
|
336
|
+
roots = _plugin_roots(wanted, cfg, models_root)
|
|
287
337
|
if not with_optional:
|
|
288
338
|
skipped = [e.rel for _, e in wanted if e.optional]
|
|
289
339
|
wanted = [(pn, e) for pn, e in wanted if not e.optional]
|
|
@@ -296,19 +346,29 @@ def models_download(
|
|
|
296
346
|
if force:
|
|
297
347
|
missing = wanted # re-descargar todo lo seleccionado
|
|
298
348
|
else:
|
|
299
|
-
missing = [(pn, e) for pn, e in wanted if verify(e,
|
|
349
|
+
missing = [(pn, e) for pn, e in wanted if verify(e, roots[pn], bin_dir) != "ok"]
|
|
300
350
|
if not missing:
|
|
301
351
|
console.print("✅ all models present + verified")
|
|
302
352
|
return
|
|
353
|
+
by_root: dict[Path, list] = {}
|
|
354
|
+
for pn, e in missing:
|
|
355
|
+
by_root.setdefault(roots[pn], []).append(e)
|
|
303
356
|
total = sum(e.size for _, e in missing)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
357
|
+
fits_all = True
|
|
358
|
+
free_min: int | None = None
|
|
359
|
+
for root, es in by_root.items():
|
|
360
|
+
fits, _needed, free = check_space(es, root, bin_dir, force=force)
|
|
361
|
+
fits_all = fits_all and fits
|
|
362
|
+
if free is not None:
|
|
363
|
+
free_min = free if free_min is None else min(free_min, free)
|
|
364
|
+
distinct = ", ".join(sorted(str(r) for r in by_root))
|
|
365
|
+
console.print(f"⬇️ {len(missing)} file(s), {human_gb(total)} → {distinct}")
|
|
366
|
+
if free_min is None:
|
|
307
367
|
console.print(" [yellow]⚠️ disco no medible: no puedo confirmar que quepa.[/]")
|
|
308
|
-
elif
|
|
309
|
-
console.print(f" disk free: {human_gb(
|
|
310
|
-
if not
|
|
311
|
-
have = human_gb(
|
|
368
|
+
elif free_min:
|
|
369
|
+
console.print(f" disk free: {human_gb(free_min)}")
|
|
370
|
+
if not fits_all:
|
|
371
|
+
have = human_gb(free_min) if free_min is not None else "desconocido"
|
|
312
372
|
raise typer.Abort(f"❌ not enough disk space for {human_gb(total)} (free: {have})")
|
|
313
373
|
notes = {registry._plugins[pn].gated_note for pn, _ in missing}
|
|
314
374
|
for note in sorted(n for n in notes if n):
|
|
@@ -323,28 +383,39 @@ def models_download(
|
|
|
323
383
|
|
|
324
384
|
from aioli_cli.fetch_ui import download_many, print_summary
|
|
325
385
|
|
|
326
|
-
failures
|
|
386
|
+
failures: list[tuple[str, str]] = []
|
|
387
|
+
for root, es in sorted(by_root.items(), key=lambda kv: str(kv[0])):
|
|
388
|
+
failures.extend(download_many(es, root, bin_dir, jobs=jobs, force=force))
|
|
327
389
|
ok_count = len(missing) - len(failures)
|
|
328
390
|
if not print_summary(failures, ok_count):
|
|
329
391
|
raise typer.Exit(code=1)
|
|
330
392
|
|
|
331
393
|
|
|
332
394
|
@models_app.command("verify")
|
|
333
|
-
def models_verify(
|
|
395
|
+
def models_verify(
|
|
396
|
+
plugin: str | None = typer.Argument(None),
|
|
397
|
+
models_root: str | None = typer.Option(
|
|
398
|
+
None,
|
|
399
|
+
"--models-root",
|
|
400
|
+
help="Raíz de modelos (máxima precedencia, todos los plugins).",
|
|
401
|
+
),
|
|
402
|
+
) -> None:
|
|
334
403
|
"""Re-hash local files against the manifest.
|
|
335
404
|
|
|
336
405
|
Exit 1 si algo falta, está corrupto o no es verificable (apto para
|
|
337
406
|
CI); 'unverified' (sin recibo legacy o sin hash) NO es "ok":
|
|
338
407
|
presencia no es integridad. Regenera con `models download --force`.
|
|
339
408
|
"""
|
|
340
|
-
from aioli_contract import ModelFile, default_bin_dir,
|
|
409
|
+
from aioli_contract import ModelFile, default_bin_dir, verify
|
|
341
410
|
|
|
342
|
-
|
|
411
|
+
cfg = AioliConfig.load()
|
|
343
412
|
bin_dir = default_bin_dir()
|
|
413
|
+
wanted = _wanted(plugin)
|
|
414
|
+
roots = _plugin_roots(wanted, cfg, models_root)
|
|
344
415
|
bad = 0
|
|
345
|
-
for pname, raw in
|
|
416
|
+
for pname, raw in wanted:
|
|
346
417
|
entry = ModelFile(**raw)
|
|
347
|
-
status = verify(entry,
|
|
418
|
+
status = verify(entry, roots[pname], bin_dir)
|
|
348
419
|
icon = {"ok": "✅", "missing": "⬇️", "corrupt": "⚠️", "unverified": "❔"}[status]
|
|
349
420
|
console.print(f"{icon} [{pname}] {entry.rel}: {status}")
|
|
350
421
|
if status in ("corrupt", "missing", "unverified"):
|
|
@@ -353,6 +424,37 @@ def models_verify(plugin: str | None = typer.Argument(None)) -> None:
|
|
|
353
424
|
raise typer.Exit(code=1)
|
|
354
425
|
|
|
355
426
|
|
|
427
|
+
@models_app.command("import")
|
|
428
|
+
def models_import(
|
|
429
|
+
plugin: str = typer.Argument(..., help="Plugin requerido: image,video,music,voice."),
|
|
430
|
+
src_dir: Path = typer.Argument(..., help="Directorio origen existente, no se borra."),
|
|
431
|
+
recursive: bool = typer.Option(False, "--recursive", "-r", help="Escanea subdirectorios."),
|
|
432
|
+
models_root: str | None = typer.Option(None, "--models-root", help="Raiz de modelos."),
|
|
433
|
+
) -> None:
|
|
434
|
+
from aioli_contract import default_bin_dir
|
|
435
|
+
from aioli_contract.fetch import ModelFile, import_models
|
|
436
|
+
|
|
437
|
+
if not src_dir.is_dir():
|
|
438
|
+
raise typer.BadParameter(f"dir origen inexistente: {src_dir}")
|
|
439
|
+
cfg = AioliConfig.load()
|
|
440
|
+
bin_dir = default_bin_dir()
|
|
441
|
+
root = cfg.resolve_models_root(plugin, cli_flag=models_root)
|
|
442
|
+
wanted = _wanted(plugin, backend="all")
|
|
443
|
+
entries = [ModelFile(**raw) for _, raw in wanted]
|
|
444
|
+
report = import_models(entries, src_dir, root, bin_dir, recursive=recursive)
|
|
445
|
+
console.print(f"adoptados: {len(report.adopted)}")
|
|
446
|
+
for rel in sorted(report.adopted):
|
|
447
|
+
console.print(f" adoptados {rel}")
|
|
448
|
+
console.print(f"desconocidos: {len(report.unknown)}")
|
|
449
|
+
for name in sorted(report.unknown):
|
|
450
|
+
console.print(f" desconocidos {name}")
|
|
451
|
+
console.print(f"discrepantes: {len(report.discrepant)}")
|
|
452
|
+
for name in sorted(report.discrepant):
|
|
453
|
+
console.print(f" discrepantes {name}")
|
|
454
|
+
if report.discrepant:
|
|
455
|
+
raise typer.Exit(code=1)
|
|
456
|
+
|
|
457
|
+
|
|
356
458
|
def main() -> None:
|
|
357
459
|
from aioli_cli import _plugins
|
|
358
460
|
|