aioli-cli 0.1.28__tar.gz → 0.1.30__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.28 → aioli_cli-0.1.30}/PKG-INFO +1 -1
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/pyproject.toml +1 -1
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_cli/main.py +61 -1
- aioli_cli-0.1.30/src/aioli_cli/readiness.py +347 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_cli/setup.py +107 -4
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_cli/status.py +59 -7
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_contract/__init__.py +10 -1
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_contract/readiness.py +28 -2
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/data/models.toml +7 -5
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/doctor.py +11 -4
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/launcher.py +5 -3
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/models_data.py +3 -3
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/cli_commands/generate_cmd.py +36 -7
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/core/generate.py +215 -32
- aioli_cli-0.1.30/src/aioli_music/doctor.py +121 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/models_data.py +3 -3
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/transport/client.py +123 -31
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_video/cli.py +4 -1
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_video/doctor.py +17 -2
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/cli/doctor.py +19 -6
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/engines/piper.py +3 -1
- aioli_cli-0.1.28/src/aioli_music/doctor.py +0 -65
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/.gitignore +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/LICENSE +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/README.md +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_cli/_plugins.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_cli/fetch_ui.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_cli/registry.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_contract/advise.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_contract/config.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_contract/doctor.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_contract/fetch.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_contract/probe.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/__init__.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/cli.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/companions.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/config.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/gpu.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/models.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/plugin.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/presets.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/stub.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_image/ui.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_master/cli.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_master/doctor.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_master/ops.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_master/plugin.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/__init__.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/backends/__init__.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/backends/acestep.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/backends/base.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/backends/llm_caption.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/backends/minimax_comfy.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/backends/process.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/backends/stub.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/backends/yue2.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/cli_commands/__init__.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/cli_commands/backend_cmd.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/cli_commands/inspire_cmd.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/cli_commands/models_cmd.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/client.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/config.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/config_data/default.toml +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/core/__init__.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/core/formats.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/core/jobs.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/core/layout.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/generator.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/history.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/jsonstore.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/models.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/paths.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/plugin.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/presets.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/prompts.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/server.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/transport/__init__.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_music/tui.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_podcast/cast.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_podcast/cli.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_podcast/doctor.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_podcast/episode.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_podcast/plugin.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_podcast/script.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_video/models_data.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_video/paths.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_video/plugin.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_video/stub.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_video/wgp.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_video/worker.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/__init__.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/cli/engines_cmd.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/cli/generate.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/cli/models_cmd.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/cli/profiles.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/config/__init__.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/config/settings.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/core/__init__.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/core/generation.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/core/model_manager.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/core/profiles.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/core/registry.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/core/types.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/devices.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/doctor.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/engines/__init__.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/engines/chatterbox.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/engines/kokoro.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/engines/moss.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/engines/qwen3.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/engines/stub.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/engines/voxcpm.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/engines/xtts.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/models_data.py +0 -0
- {aioli_cli-0.1.28 → aioli_cli-0.1.30}/src/aioli_voice/plugin.py +0 -0
|
@@ -50,8 +50,14 @@ def _root(
|
|
|
50
50
|
def doctor() -> None:
|
|
51
51
|
"""Preflight: python, ffmpeg, CUDA. Plugins append their own checks.
|
|
52
52
|
|
|
53
|
+
V3: además emite la lectura unificada por backend (misma que
|
|
54
|
+
`status` y `setup --check`): `plugin/backend → capacidad/probado`
|
|
55
|
+
es OK; el resto es fallo con el nivel como detalle.
|
|
53
56
|
Exit 1 si algún check falla (usable en scripts/CI).
|
|
54
57
|
"""
|
|
58
|
+
from aioli_cli import _plugins as _plugmod # noqa: PLC0415
|
|
59
|
+
|
|
60
|
+
_plugmod.load_all()
|
|
55
61
|
cfg = AioliConfig.load()
|
|
56
62
|
table = Table(title="aioli doctor")
|
|
57
63
|
table.add_column("check")
|
|
@@ -71,6 +77,57 @@ def doctor() -> None:
|
|
|
71
77
|
check.hint,
|
|
72
78
|
)
|
|
73
79
|
ok = ok and check.ok
|
|
80
|
+
try:
|
|
81
|
+
from aioli_cli.readiness import doctor_checks_for, spec_for # noqa: PLC0415
|
|
82
|
+
from aioli_contract import ModelFile, default_bin_dir, default_models_dir # noqa: PLC0415
|
|
83
|
+
from aioli_contract.advise import read_machine # noqa: PLC0415
|
|
84
|
+
|
|
85
|
+
_md, _bd = default_models_dir(), default_bin_dir()
|
|
86
|
+
_machine = read_machine(_md)
|
|
87
|
+
for plugin in registry.all():
|
|
88
|
+
if plugin.models is None:
|
|
89
|
+
continue
|
|
90
|
+
grouped: dict[str, list] = {}
|
|
91
|
+
for raw in plugin.models():
|
|
92
|
+
entry = ModelFile(**raw)
|
|
93
|
+
if entry.optional:
|
|
94
|
+
continue
|
|
95
|
+
if entry.platform:
|
|
96
|
+
import sys as _sys
|
|
97
|
+
|
|
98
|
+
if entry.platform != _sys.platform:
|
|
99
|
+
continue
|
|
100
|
+
if entry.kind == "runtime":
|
|
101
|
+
continue
|
|
102
|
+
grouped.setdefault(entry.backend or "-", []).append(entry)
|
|
103
|
+
runtimes = []
|
|
104
|
+
for raw in plugin.models():
|
|
105
|
+
entry = ModelFile(**raw)
|
|
106
|
+
if entry.kind != "runtime" or entry.optional:
|
|
107
|
+
continue
|
|
108
|
+
if entry.platform:
|
|
109
|
+
import sys as _sys2
|
|
110
|
+
|
|
111
|
+
if entry.platform != _sys2.platform:
|
|
112
|
+
continue
|
|
113
|
+
runtimes.append(entry)
|
|
114
|
+
for backend, entries in sorted(grouped.items()):
|
|
115
|
+
if backend == "-":
|
|
116
|
+
continue
|
|
117
|
+
try:
|
|
118
|
+
spec = spec_for(plugin.name, backend)
|
|
119
|
+
except KeyError:
|
|
120
|
+
continue
|
|
121
|
+
files = [e for e in runtimes if e.backend in ("", backend)] + entries
|
|
122
|
+
for check in doctor_checks_for(spec, files, _md, _bd, _machine):
|
|
123
|
+
table.add_row(
|
|
124
|
+
f"readiness/{check.name}",
|
|
125
|
+
"✅" if check.ok else "❌",
|
|
126
|
+
check.hint,
|
|
127
|
+
)
|
|
128
|
+
ok = ok and check.ok
|
|
129
|
+
except (ImportError, OSError, RuntimeError):
|
|
130
|
+
pass
|
|
74
131
|
table.add_row("models_dir", "📁", str(cfg.models_dir))
|
|
75
132
|
table.add_row("plugins", "🔌", ", ".join(registry.names()) or "(none yet)")
|
|
76
133
|
console.print(table)
|
|
@@ -85,11 +142,14 @@ def setup(
|
|
|
85
142
|
None, "--plugins", "-p", help="Solo estos plugins (coma): image,video,music,voice."
|
|
86
143
|
),
|
|
87
144
|
ask: bool = typer.Option(False, "--ask", help="Pregunta qué instalar (por defecto: auto)."),
|
|
145
|
+
check: bool = typer.Option(
|
|
146
|
+
False, "--check", help="Solo muestra la lectura (misma que status) sin descargar."
|
|
147
|
+
),
|
|
88
148
|
) -> None:
|
|
89
149
|
"""Asistente de primera instalación: equipo → consejo → descarga."""
|
|
90
150
|
from aioli_cli.setup import run_setup
|
|
91
151
|
|
|
92
|
-
run_setup(yes=yes, plugins=plugins, ask=ask)
|
|
152
|
+
run_setup(yes=yes, plugins=plugins, ask=ask, check=check)
|
|
93
153
|
|
|
94
154
|
|
|
95
155
|
@app.command()
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""Evaluación readiness unificada V3 (D23): una sola lectura para status/doctor/setup.
|
|
2
|
+
|
|
3
|
+
Consume ``BackendSpec``/``ExecutionPlan``/``Readiness`` de
|
|
4
|
+
``aioli_contract.readiness`` sin duplicar detectores: las tres superficies
|
|
5
|
+
llaman aquí con el MISMO input y obtienen la MISMA lectura.
|
|
6
|
+
|
|
7
|
+
Mapa de detectores duplicados eliminados (V3.1):
|
|
8
|
+
- ``status._piper_binary`` → :func:`motor_probe` (voice/piper).
|
|
9
|
+
- ``setup._piper_ready`` → :func:`motor_probe` (voice/piper).
|
|
10
|
+
- ``image.launcher.resolve_sd_cli`` + ``image.doctor`` (exists sin
|
|
11
|
+
bit-X) → :func:`motor_probe` (image/*, exige is_file + bit X).
|
|
12
|
+
- ``music.paths.ace_server_binary`` + ``music.doctor`` (exists sin
|
|
13
|
+
probe unificado) → :func:`motor_probe` (music/*).
|
|
14
|
+
- ``video.wgp.resolve_*`` + ``video.doctor`` (exists/which bool) →
|
|
15
|
+
:func:`motor_probe` (video/*, directorio nunca vale).
|
|
16
|
+
- ``voice.cli.doctor._check_extras`` (which/find_spec bool) →
|
|
17
|
+
:func:`motor_probe` (voice/* SDK/binario, SDK faltante es desconocido).
|
|
18
|
+
- ``contract.doctor`` baseline (python/ffmpeg/cuda) se mantiene como
|
|
19
|
+
preflight genérico; la lectura por backend vive aquí.
|
|
20
|
+
|
|
21
|
+
Sin cambios de red/descarga: este módulo solo LEE (verify/quick/full,
|
|
22
|
+
stat del bit X, resolve sin ejecutar salvo ``run_probe=True`` explícito).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from aioli_contract.doctor import Check
|
|
30
|
+
from aioli_contract.readiness import (
|
|
31
|
+
BackendSpec,
|
|
32
|
+
ExecutionPlan,
|
|
33
|
+
ProbeOutcome,
|
|
34
|
+
Readiness,
|
|
35
|
+
check_exec_bit,
|
|
36
|
+
check_sdk,
|
|
37
|
+
probe_runtime,
|
|
38
|
+
rank,
|
|
39
|
+
resolve_backend,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"BACKEND_SPECS",
|
|
44
|
+
"build_execution_plan",
|
|
45
|
+
"doctor_checks_for",
|
|
46
|
+
"evaluate_plugin",
|
|
47
|
+
"is_ready",
|
|
48
|
+
"motor_probe",
|
|
49
|
+
"spec_for",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
BACKEND_SPECS: dict[tuple[str, str], BackendSpec] = {
|
|
53
|
+
("image", "flux-klein"): BackendSpec(
|
|
54
|
+
plugin="image",
|
|
55
|
+
backend="flux-klein",
|
|
56
|
+
kind="binary",
|
|
57
|
+
binary_env="IMAGE_SD_CLI_BIN",
|
|
58
|
+
binary_names=("sd-cli",),
|
|
59
|
+
),
|
|
60
|
+
("image", "flux-klein-4b"): BackendSpec(
|
|
61
|
+
plugin="image",
|
|
62
|
+
backend="flux-klein-4b",
|
|
63
|
+
kind="binary",
|
|
64
|
+
binary_env="IMAGE_SD_CLI_BIN",
|
|
65
|
+
binary_names=("sd-cli",),
|
|
66
|
+
),
|
|
67
|
+
("music", "acestep"): BackendSpec(
|
|
68
|
+
plugin="music",
|
|
69
|
+
backend="acestep",
|
|
70
|
+
kind="binary",
|
|
71
|
+
binary_names=("ace-server",),
|
|
72
|
+
),
|
|
73
|
+
("music", "yue2"): BackendSpec(
|
|
74
|
+
plugin="music",
|
|
75
|
+
backend="yue2",
|
|
76
|
+
kind="binary",
|
|
77
|
+
binary_names=("audiocpp_cli",),
|
|
78
|
+
),
|
|
79
|
+
("video", "ltx25"): BackendSpec(
|
|
80
|
+
plugin="video",
|
|
81
|
+
backend="ltx25",
|
|
82
|
+
kind="binary",
|
|
83
|
+
binary_env="VIDEO_PYTHON",
|
|
84
|
+
),
|
|
85
|
+
("voice", "piper"): BackendSpec(
|
|
86
|
+
plugin="voice",
|
|
87
|
+
backend="piper",
|
|
88
|
+
kind="binary",
|
|
89
|
+
binary_env="PIPER_BIN",
|
|
90
|
+
binary_names=("piper",),
|
|
91
|
+
),
|
|
92
|
+
("voice", "kokoro"): BackendSpec(
|
|
93
|
+
plugin="voice",
|
|
94
|
+
backend="kokoro",
|
|
95
|
+
kind="sdk",
|
|
96
|
+
sdk_import="kokoro_onnx",
|
|
97
|
+
extra_name="voice-kokoro",
|
|
98
|
+
),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def spec_for(plugin: str, backend: str) -> BackendSpec:
|
|
103
|
+
"""Spec unificado por backend (estable V3: tabla cerrada, KeyError si falta)."""
|
|
104
|
+
try:
|
|
105
|
+
return BACKEND_SPECS[(plugin, backend)]
|
|
106
|
+
except KeyError:
|
|
107
|
+
raise KeyError(f"backend sin spec unificado: {plugin}/{backend}") from None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _managed_candidates(spec: BackendSpec, bin_dir: Path | None) -> list[Path]:
|
|
111
|
+
"""Runtimes gestionados por `setup` en bin_dir (sin ejecutar nada)."""
|
|
112
|
+
if bin_dir is None:
|
|
113
|
+
return []
|
|
114
|
+
table: dict[tuple[str, str], list[str]] = {
|
|
115
|
+
("image", "flux-klein"): ["sd-cli/sd-cli", "sd-cli"],
|
|
116
|
+
("image", "flux-klein-4b"): ["sd-cli/sd-cli", "sd-cli"],
|
|
117
|
+
("music", "acestep"): ["ace-server/ace-server", "ace-server"],
|
|
118
|
+
("music", "yue2"): ["audiocpp/audiocpp_cli", "audiocpp_cli"],
|
|
119
|
+
}
|
|
120
|
+
return [bin_dir / rel for rel in table.get((spec.plugin, spec.backend), [])]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _legacy_candidates(spec: BackendSpec) -> list[Path]:
|
|
124
|
+
"""Resolutores legacy de plugins (solo lectura, envueltos con bit-X).
|
|
125
|
+
|
|
126
|
+
Reutiliza los resolvers existentes sin duplicar su lógica de rutas;
|
|
127
|
+
la ESTRICTEZ (is_file + bit X, directorio nunca vale) la pone el
|
|
128
|
+
contrato vía :func:`check_exec_bit`.
|
|
129
|
+
"""
|
|
130
|
+
out: list[Path] = []
|
|
131
|
+
try:
|
|
132
|
+
if spec.plugin == "image":
|
|
133
|
+
from aioli_image.launcher import resolve_sd_cli # noqa: PLC0415
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
out.append(resolve_sd_cli())
|
|
137
|
+
except (FileNotFoundError, OSError, RuntimeError, SystemExit):
|
|
138
|
+
pass
|
|
139
|
+
elif spec.plugin == "music" and spec.backend == "acestep":
|
|
140
|
+
from aioli_music.paths import ace_server_binary # noqa: PLC0415
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
out.append(ace_server_binary())
|
|
144
|
+
except (OSError, RuntimeError):
|
|
145
|
+
pass
|
|
146
|
+
elif spec.plugin == "music" and spec.backend == "yue2":
|
|
147
|
+
try:
|
|
148
|
+
from aioli_music.backends.yue2 import _cli as _yue2_cli # noqa: PLC0415
|
|
149
|
+
from aioli_music.config import load_config # noqa: PLC0415
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
out.append(_yue2_cli(load_config()))
|
|
153
|
+
except (OSError, RuntimeError, ValueError):
|
|
154
|
+
pass
|
|
155
|
+
except (ImportError, OSError):
|
|
156
|
+
pass
|
|
157
|
+
elif spec.plugin == "video":
|
|
158
|
+
from aioli_video.wgp import resolve_python # noqa: PLC0415
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
out.append(resolve_python())
|
|
162
|
+
except (OSError, RuntimeError):
|
|
163
|
+
pass
|
|
164
|
+
except ImportError:
|
|
165
|
+
pass
|
|
166
|
+
return out
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def motor_probe(
|
|
170
|
+
spec: BackendSpec, bin_dir: Path | None, *, run_probe: bool = False, timeout: int = 10
|
|
171
|
+
) -> ProbeOutcome:
|
|
172
|
+
"""Motor presente+ejecutable con la MISMA vara en las tres superficies.
|
|
173
|
+
|
|
174
|
+
Orden: contrato (env+PATH) → gestionados (bin_dir) → legacy (plugins).
|
|
175
|
+
Cada candidato pasa por ``is_file`` + bit X (directorio, inventado y
|
|
176
|
+
sin-permiso son ``desconocido``, nunca éxito). SDK faltante es
|
|
177
|
+
``desconocido``. Con ``run_probe=True`` se ejecuta ``--help`` con
|
|
178
|
+
timeout: colgado → ``desconocido`` (nunca éxito).
|
|
179
|
+
"""
|
|
180
|
+
if spec.kind == "sdk":
|
|
181
|
+
return check_sdk(spec)
|
|
182
|
+
if spec.kind == "hybrid":
|
|
183
|
+
try:
|
|
184
|
+
resolved = resolve_backend(spec)
|
|
185
|
+
ok = check_exec_bit(resolved)
|
|
186
|
+
if ok.readiness == Readiness.MOTOR:
|
|
187
|
+
return _maybe_probe(resolved, ok, run_probe=run_probe, timeout=timeout)
|
|
188
|
+
except (FileNotFoundError, OSError, RuntimeError):
|
|
189
|
+
pass
|
|
190
|
+
return check_sdk(spec)
|
|
191
|
+
candidates: list[Path] = []
|
|
192
|
+
try:
|
|
193
|
+
candidates.append(resolve_backend(spec))
|
|
194
|
+
except (FileNotFoundError, OSError, RuntimeError):
|
|
195
|
+
pass
|
|
196
|
+
candidates.extend(_managed_candidates(spec, bin_dir))
|
|
197
|
+
candidates.extend(_legacy_candidates(spec))
|
|
198
|
+
for cand in candidates:
|
|
199
|
+
outcome = check_exec_bit(cand)
|
|
200
|
+
if outcome.readiness == Readiness.MOTOR:
|
|
201
|
+
return _maybe_probe(cand, outcome, run_probe=run_probe, timeout=timeout)
|
|
202
|
+
if spec.kind == "hybrid" or spec.sdk_import:
|
|
203
|
+
sdk = check_sdk(spec)
|
|
204
|
+
if sdk.readiness == Readiness.MOTOR:
|
|
205
|
+
return sdk
|
|
206
|
+
first = candidates[0] if candidates else None
|
|
207
|
+
if first is not None:
|
|
208
|
+
return ProbeOutcome(Readiness.DESCONOCIDO, f"{first} no ejecutable")
|
|
209
|
+
return ProbeOutcome(Readiness.DESCONOCIDO, f"motor {spec.plugin}/{spec.backend} no encontrado")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _maybe_probe(path: Path, ok: ProbeOutcome, *, run_probe: bool, timeout: int) -> ProbeOutcome:
|
|
213
|
+
if not run_probe:
|
|
214
|
+
return ok
|
|
215
|
+
probed = probe_runtime(path, timeout=timeout)
|
|
216
|
+
if probed.readiness == Readiness.MOTOR:
|
|
217
|
+
return ProbeOutcome(Readiness.MOTOR, f"{path} responde --help")
|
|
218
|
+
return ProbeOutcome(Readiness.DESCONOCIDO, probed.note)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def evaluate_plugin(
|
|
222
|
+
spec: BackendSpec,
|
|
223
|
+
entries: list,
|
|
224
|
+
models_dir: Path,
|
|
225
|
+
bin_dir: Path | None,
|
|
226
|
+
*,
|
|
227
|
+
full: bool = False,
|
|
228
|
+
capacity_ok: bool | None = None,
|
|
229
|
+
tested: bool = False,
|
|
230
|
+
machine=None,
|
|
231
|
+
run_probe: bool = False,
|
|
232
|
+
) -> Readiness:
|
|
233
|
+
"""Escalera quick/full/probado con motor unificado (misma que el contrato).
|
|
234
|
+
|
|
235
|
+
Idéntica escalera que ``evaluate_readiness`` pero con :func:`motor_probe`
|
|
236
|
+
(contrato + gestionados + legacy con bit-X) en vez del solo-contrato:
|
|
237
|
+
mismo input → misma lectura en status, doctor y setup dry.
|
|
238
|
+
"""
|
|
239
|
+
if not entries:
|
|
240
|
+
return Readiness.DESCONOCIDO
|
|
241
|
+
from aioli_contract.fetch import verify # noqa: PLC0415
|
|
242
|
+
|
|
243
|
+
if machine is not None and capacity_ok is None:
|
|
244
|
+
try:
|
|
245
|
+
from aioli_contract.advise import judge # noqa: PLC0415
|
|
246
|
+
|
|
247
|
+
capacity_ok = bool(judge(spec.plugin, spec.backend, machine).fits)
|
|
248
|
+
except Exception: # noqa: BLE001
|
|
249
|
+
capacity_ok = None
|
|
250
|
+
for entry in entries:
|
|
251
|
+
if verify(entry, models_dir, bin_dir, quick=True) != "ok":
|
|
252
|
+
return Readiness.DESCONOCIDO
|
|
253
|
+
if not full:
|
|
254
|
+
level = Readiness.DESCARGADO
|
|
255
|
+
if motor_probe(spec, bin_dir, run_probe=run_probe).readiness == Readiness.MOTOR:
|
|
256
|
+
level = Readiness.MOTOR
|
|
257
|
+
if capacity_ok:
|
|
258
|
+
level = Readiness.CAPACIDAD
|
|
259
|
+
return level
|
|
260
|
+
for entry in entries:
|
|
261
|
+
if verify(entry, models_dir, bin_dir, quick=False) != "ok":
|
|
262
|
+
return Readiness.DESCARGADO
|
|
263
|
+
level = Readiness.INTEGRIDAD
|
|
264
|
+
if motor_probe(spec, bin_dir, run_probe=run_probe).readiness != Readiness.MOTOR:
|
|
265
|
+
return level
|
|
266
|
+
level = Readiness.MOTOR
|
|
267
|
+
if not capacity_ok:
|
|
268
|
+
return level
|
|
269
|
+
level = Readiness.CAPACIDAD
|
|
270
|
+
if tested:
|
|
271
|
+
return Readiness.PROBADO
|
|
272
|
+
return level
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def build_execution_plan(
|
|
276
|
+
spec: BackendSpec,
|
|
277
|
+
entries: list,
|
|
278
|
+
models_dir: Path,
|
|
279
|
+
bin_dir: Path | None,
|
|
280
|
+
*,
|
|
281
|
+
full: bool = True,
|
|
282
|
+
capacity_ok: bool | None = None,
|
|
283
|
+
tested: bool = False,
|
|
284
|
+
machine=None,
|
|
285
|
+
) -> ExecutionPlan:
|
|
286
|
+
"""Plan setup dry/check: ficheros, pasos, evidencia y faltantes (sin descargar)."""
|
|
287
|
+
from aioli_contract.fetch import verify # noqa: PLC0415
|
|
288
|
+
|
|
289
|
+
evidence = evaluate_plugin(
|
|
290
|
+
spec,
|
|
291
|
+
entries,
|
|
292
|
+
models_dir,
|
|
293
|
+
bin_dir,
|
|
294
|
+
full=full,
|
|
295
|
+
capacity_ok=capacity_ok,
|
|
296
|
+
tested=tested,
|
|
297
|
+
machine=machine,
|
|
298
|
+
)
|
|
299
|
+
missing = tuple(e for e in entries if verify(e, models_dir, bin_dir) != "ok")
|
|
300
|
+
steps: list[str] = []
|
|
301
|
+
if missing:
|
|
302
|
+
steps.append(f"descargar {len(missing)} fichero(s) de {spec.plugin}/{spec.backend}")
|
|
303
|
+
if rank(evidence) < rank(Readiness.MOTOR):
|
|
304
|
+
steps.append(f"resolver motor {spec.plugin}/{spec.backend}")
|
|
305
|
+
if not steps:
|
|
306
|
+
steps.append("nada pendiente")
|
|
307
|
+
return ExecutionPlan(
|
|
308
|
+
spec=spec, files=tuple(entries), steps=tuple(steps), evidence=evidence, missing=missing
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def is_ready(level: Readiness) -> bool:
|
|
313
|
+
"""Listo para generar: capacidad verificada (o probado)."""
|
|
314
|
+
return level in (Readiness.CAPACIDAD, Readiness.PROBADO)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def doctor_checks_for(
|
|
318
|
+
spec: BackendSpec,
|
|
319
|
+
entries: list,
|
|
320
|
+
models_dir: Path,
|
|
321
|
+
bin_dir: Path | None,
|
|
322
|
+
machine=None,
|
|
323
|
+
) -> list[Check]:
|
|
324
|
+
"""Checks doctor por backend con la misma lectura que status/setup."""
|
|
325
|
+
from aioli_contract.advise import judge # noqa: PLC0415
|
|
326
|
+
|
|
327
|
+
# V3 (F-4): None = capacidad sin medir (nunca se midió); no se
|
|
328
|
+
# confunde con el veredicto NEGATIVO medido del asesor, que es
|
|
329
|
+
# "no cabe". El hint no miente en ninguno de los tres casos.
|
|
330
|
+
fits: bool | None
|
|
331
|
+
if machine is None:
|
|
332
|
+
fits = None
|
|
333
|
+
else:
|
|
334
|
+
try:
|
|
335
|
+
fits = bool(judge(spec.plugin, spec.backend, machine).fits)
|
|
336
|
+
except Exception: # noqa: BLE001
|
|
337
|
+
fits = False
|
|
338
|
+
level = evaluate_plugin(
|
|
339
|
+
spec, entries, models_dir, bin_dir, full=False, capacity_ok=bool(fits), machine=None
|
|
340
|
+
)
|
|
341
|
+
ok = is_ready(level)
|
|
342
|
+
detail = level.value
|
|
343
|
+
if fits is False:
|
|
344
|
+
detail += " (no cabe en este equipo)"
|
|
345
|
+
elif fits is None:
|
|
346
|
+
detail += " (capacidad sin medir)"
|
|
347
|
+
return [Check(name=f"{spec.plugin}/{spec.backend}", ok=ok, hint=detail)]
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
import typer
|
|
8
9
|
from rich.console import Console
|
|
@@ -12,6 +13,9 @@ from rich.table import Table
|
|
|
12
13
|
|
|
13
14
|
console = Console()
|
|
14
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from aioli_contract.readiness import Readiness # noqa: F401 — anotación de _plan_readiness
|
|
18
|
+
|
|
15
19
|
LEAGUE_LINES = {
|
|
16
20
|
"patata": "🥔 Sin NVIDIA a la vista: tiraremos de CPU (voice) y poco más.",
|
|
17
21
|
"modesta": "🛵 GPU modesta: motores ligeros y sin prisas.",
|
|
@@ -119,7 +123,9 @@ def select_plan(registry, machine) -> dict[str, list]:
|
|
|
119
123
|
return plan
|
|
120
124
|
|
|
121
125
|
|
|
122
|
-
def run_setup(
|
|
126
|
+
def run_setup(
|
|
127
|
+
yes: bool = False, plugins: str | None = None, ask: bool = False, check: bool = False
|
|
128
|
+
) -> None:
|
|
123
129
|
from aioli_cli.fetch_ui import download_many, print_summary
|
|
124
130
|
from aioli_contract import (
|
|
125
131
|
check_space,
|
|
@@ -145,6 +151,22 @@ def run_setup(yes: bool = False, plugins: str | None = None, ask: bool = False)
|
|
|
145
151
|
# quepa. Runtimes y ficheros de plataforma correcta incluidos.
|
|
146
152
|
plan = select_plan(registry, machine)
|
|
147
153
|
|
|
154
|
+
if check:
|
|
155
|
+
# V3 --check: misma lectura que status --full, sin descargar ni preguntar.
|
|
156
|
+
from aioli_contract import default_bin_dir as _bdir # noqa: PLC0415
|
|
157
|
+
from aioli_contract import default_models_dir as _mdir # noqa: PLC0415
|
|
158
|
+
|
|
159
|
+
_md, _bd = _mdir(), _bdir()
|
|
160
|
+
for pn in sorted(plan):
|
|
161
|
+
missing, readiness = _plan_readiness(pn, plan[pn], _md, _bd, machine=machine)
|
|
162
|
+
state = (
|
|
163
|
+
"listo"
|
|
164
|
+
if not missing and readiness in ("capacidad", "probado")
|
|
165
|
+
else (readiness.value if readiness else "desconocido")
|
|
166
|
+
)
|
|
167
|
+
console.print(f"[dim]check {pn}: {state} ({len(missing)} pendiente(s))[/]")
|
|
168
|
+
return
|
|
169
|
+
|
|
148
170
|
if plugins:
|
|
149
171
|
requested = [p.strip() for p in plugins.split(",") if p.strip()]
|
|
150
172
|
known = {p.name for p in registry.all() if p.models is not None}
|
|
@@ -383,15 +405,96 @@ def _persist_engine_defaults(chosen: dict[str, str]) -> None:
|
|
|
383
405
|
|
|
384
406
|
|
|
385
407
|
def _piper_ready() -> bool:
|
|
408
|
+
"""V3: mismo criterio que status/doctor (evaluación unificada)."""
|
|
409
|
+
try:
|
|
410
|
+
from aioli_cli.readiness import motor_probe, spec_for # noqa: PLC0415
|
|
411
|
+
from aioli_contract import default_bin_dir # noqa: PLC0415
|
|
412
|
+
from aioli_contract.readiness import Readiness # noqa: PLC0415
|
|
413
|
+
|
|
414
|
+
spec = spec_for("voice", "piper")
|
|
415
|
+
return motor_probe(spec, default_bin_dir()).readiness == Readiness.MOTOR
|
|
416
|
+
except (KeyError, OSError, RuntimeError, ImportError):
|
|
417
|
+
pass
|
|
418
|
+
# V3 (F-6): el fallback tampoco gradua sin bit X - un binario
|
|
419
|
+
# presente pero no ejecutable es "pendiente", no exito.
|
|
386
420
|
try:
|
|
421
|
+
from aioli_contract.readiness import ( # noqa: PLC0415
|
|
422
|
+
Readiness as _Readiness,
|
|
423
|
+
)
|
|
424
|
+
from aioli_contract.readiness import (
|
|
425
|
+
check_exec_bit,
|
|
426
|
+
)
|
|
387
427
|
from aioli_voice.engines.piper import resolve_piper_bin # noqa: PLC0415
|
|
388
428
|
|
|
389
|
-
resolve_piper_bin()
|
|
390
|
-
|
|
391
|
-
except (OSError, RuntimeError):
|
|
429
|
+
return check_exec_bit(resolve_piper_bin()).readiness == _Readiness.MOTOR
|
|
430
|
+
except (OSError, RuntimeError, ImportError):
|
|
392
431
|
return False
|
|
393
432
|
|
|
394
433
|
|
|
434
|
+
def _plan_readiness(
|
|
435
|
+
pn: str, files: list, models_dir, bin_dir, *, machine=None
|
|
436
|
+
) -> tuple[list, Readiness | None]:
|
|
437
|
+
"""--check/dry_plan: pendientes + PEOR readiness de TODOS los backends (V3, F-2).
|
|
438
|
+
|
|
439
|
+
Antes solo se evaluaba el PRIMER backend del plan (un plan
|
|
440
|
+
multi-backend, p.ej. music acestep+yue2, quedaba subevaluado). Ahora
|
|
441
|
+
se agrupa por backend (como `aioli doctor`) y se agrega con la peor
|
|
442
|
+
lectura: un motor desconocido no es "listo" porque el otro gradue.
|
|
443
|
+
Backend sin spec unificada -> None (desconocido honesto, nunca exito).
|
|
444
|
+
"""
|
|
445
|
+
from aioli_contract import verify as _verify # noqa: PLC0415
|
|
446
|
+
from aioli_contract.readiness import rank as _rank
|
|
447
|
+
|
|
448
|
+
missing = [e for e in files if _verify(e, models_dir, bin_dir) != "ok"]
|
|
449
|
+
by_backend: dict[str, list] = {}
|
|
450
|
+
runtimes: list = []
|
|
451
|
+
for e in files:
|
|
452
|
+
if e.kind == "runtime":
|
|
453
|
+
runtimes.append(e)
|
|
454
|
+
elif e.backend:
|
|
455
|
+
by_backend.setdefault(e.backend, []).append(e)
|
|
456
|
+
if not by_backend:
|
|
457
|
+
return missing, None
|
|
458
|
+
levels: list[Readiness] = []
|
|
459
|
+
try:
|
|
460
|
+
from aioli_cli.readiness import build_execution_plan, spec_for # noqa: PLC0415
|
|
461
|
+
|
|
462
|
+
for backend in sorted(by_backend):
|
|
463
|
+
spec = spec_for(pn, backend)
|
|
464
|
+
backend_files = [r for r in runtimes if r.backend in ("", backend)]
|
|
465
|
+
levels.append(
|
|
466
|
+
build_execution_plan(
|
|
467
|
+
spec,
|
|
468
|
+
backend_files + by_backend[backend],
|
|
469
|
+
models_dir,
|
|
470
|
+
bin_dir,
|
|
471
|
+
machine=machine,
|
|
472
|
+
).evidence
|
|
473
|
+
)
|
|
474
|
+
except (KeyError, ImportError, OSError, RuntimeError):
|
|
475
|
+
return missing, None
|
|
476
|
+
return missing, min(levels, key=_rank)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def dry_plan(registry, machine) -> dict[str, object]:
|
|
480
|
+
"""Plan setup sin descargar (V3.2 coherencia): misma lectura que status.
|
|
481
|
+
|
|
482
|
+
Devuelve por plugin ``{"files": [...], "missing": [...], "readiness": Readiness}``
|
|
483
|
+
con verificación full (como `setup` sin descargas pendientes). No escribe
|
|
484
|
+
nada ni pregunta: los tests comparan este dict con status/doctor.
|
|
485
|
+
"""
|
|
486
|
+
from aioli_contract import default_bin_dir, default_models_dir # noqa: PLC0415
|
|
487
|
+
|
|
488
|
+
models_dir = default_models_dir()
|
|
489
|
+
bin_dir = default_bin_dir()
|
|
490
|
+
plan = select_plan(registry, machine)
|
|
491
|
+
out: dict[str, object] = {}
|
|
492
|
+
for pn, files in plan.items():
|
|
493
|
+
missing, readiness = _plan_readiness(pn, files, models_dir, bin_dir, machine=machine)
|
|
494
|
+
out[pn] = {"files": files, "missing": missing, "readiness": readiness}
|
|
495
|
+
return out
|
|
496
|
+
|
|
497
|
+
|
|
395
498
|
def _ensure_binaries(chosen: dict[str, str], yes: bool = False) -> list[str]:
|
|
396
499
|
"""Garantiza el motor ejecutable o devuelve la lista de pendientes.
|
|
397
500
|
|