aioli-cli 0.1.0__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.
- aioli_cli/_plugins.py +118 -0
- aioli_cli/main.py +215 -0
- aioli_cli/registry.py +45 -0
- aioli_cli/setup.py +151 -0
- aioli_cli-0.1.0.dist-info/METADATA +18 -0
- aioli_cli-0.1.0.dist-info/RECORD +8 -0
- aioli_cli-0.1.0.dist-info/WHEEL +4 -0
- aioli_cli-0.1.0.dist-info/entry_points.txt +2 -0
aioli_cli/_plugins.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Lazy plugin loader: import each installed plugin package and register it.
|
|
2
|
+
|
|
3
|
+
Plugins are optional — a missing package is silently skipped so the `aioli`
|
|
4
|
+
binary keeps working with whatever is installed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from aioli_cli.registry import Plugin, registry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_LOADED = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_all() -> None:
|
|
14
|
+
"""Import plugin packages and register their Typer apps + checks."""
|
|
15
|
+
global _LOADED
|
|
16
|
+
if _LOADED:
|
|
17
|
+
return
|
|
18
|
+
_LOADED = True
|
|
19
|
+
try:
|
|
20
|
+
from aioli_voice.models_data import DEFAULT_BACKEND as VOICE_DEFAULT
|
|
21
|
+
from aioli_voice.models_data import MODELS as VOICE_MODELS
|
|
22
|
+
from aioli_voice.plugin import DESCRIPTION, voice_app, voice_checks
|
|
23
|
+
|
|
24
|
+
registry.register(
|
|
25
|
+
Plugin(
|
|
26
|
+
name="voice",
|
|
27
|
+
description=DESCRIPTION,
|
|
28
|
+
app=voice_app,
|
|
29
|
+
checks=voice_checks,
|
|
30
|
+
models=lambda: VOICE_MODELS,
|
|
31
|
+
default_backend=VOICE_DEFAULT,
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
except ImportError:
|
|
35
|
+
pass
|
|
36
|
+
try:
|
|
37
|
+
from aioli_image.models_data import GATED_NOTE as IMAGE_GATED
|
|
38
|
+
from aioli_image.models_data import DEFAULT_BACKEND as IMAGE_DEFAULT
|
|
39
|
+
from aioli_image.models_data import MODELS as IMAGE_MODELS
|
|
40
|
+
from aioli_image.plugin import DESCRIPTION, image_app, image_checks
|
|
41
|
+
|
|
42
|
+
registry.register(
|
|
43
|
+
Plugin(
|
|
44
|
+
name="image",
|
|
45
|
+
description=DESCRIPTION,
|
|
46
|
+
app=image_app,
|
|
47
|
+
checks=image_checks,
|
|
48
|
+
models=lambda: IMAGE_MODELS,
|
|
49
|
+
default_backend=IMAGE_DEFAULT,
|
|
50
|
+
gated_note=IMAGE_GATED,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
except ImportError:
|
|
54
|
+
pass
|
|
55
|
+
try:
|
|
56
|
+
from aioli_video.models_data import GATED_NOTE as VIDEO_GATED
|
|
57
|
+
from aioli_video.models_data import DEFAULT_BACKEND as VIDEO_DEFAULT
|
|
58
|
+
from aioli_video.models_data import MODELS as VIDEO_MODELS
|
|
59
|
+
from aioli_video.plugin import DESCRIPTION, video_app, video_checks
|
|
60
|
+
|
|
61
|
+
registry.register(
|
|
62
|
+
Plugin(
|
|
63
|
+
name="video",
|
|
64
|
+
description=DESCRIPTION,
|
|
65
|
+
app=video_app,
|
|
66
|
+
checks=video_checks,
|
|
67
|
+
models=lambda: VIDEO_MODELS,
|
|
68
|
+
default_backend=VIDEO_DEFAULT,
|
|
69
|
+
gated_note=VIDEO_GATED,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
except ImportError:
|
|
73
|
+
pass
|
|
74
|
+
try:
|
|
75
|
+
from aioli_podcast.plugin import DESCRIPTION, podcast_app, podcast_checks
|
|
76
|
+
|
|
77
|
+
registry.register(
|
|
78
|
+
Plugin(
|
|
79
|
+
name="podcast",
|
|
80
|
+
description=DESCRIPTION,
|
|
81
|
+
app=podcast_app,
|
|
82
|
+
checks=podcast_checks,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
except ImportError:
|
|
86
|
+
pass
|
|
87
|
+
try:
|
|
88
|
+
from aioli_master.plugin import DESCRIPTION, master_app, master_checks
|
|
89
|
+
|
|
90
|
+
registry.register(
|
|
91
|
+
Plugin(
|
|
92
|
+
name="master",
|
|
93
|
+
description=DESCRIPTION,
|
|
94
|
+
app=master_app,
|
|
95
|
+
checks=master_checks,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
except ImportError:
|
|
99
|
+
pass
|
|
100
|
+
try:
|
|
101
|
+
from aioli_music.models_data import GATED_NOTE as MUSIC_GATED
|
|
102
|
+
from aioli_music.models_data import DEFAULT_BACKEND as MUSIC_DEFAULT
|
|
103
|
+
from aioli_music.models_data import MODELS as MUSIC_MODELS
|
|
104
|
+
from aioli_music.plugin import DESCRIPTION, music_app, music_checks
|
|
105
|
+
|
|
106
|
+
registry.register(
|
|
107
|
+
Plugin(
|
|
108
|
+
name="music",
|
|
109
|
+
description=DESCRIPTION,
|
|
110
|
+
app=music_app,
|
|
111
|
+
checks=music_checks,
|
|
112
|
+
models=lambda: MUSIC_MODELS,
|
|
113
|
+
default_backend=MUSIC_DEFAULT,
|
|
114
|
+
gated_note=MUSIC_GATED,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
except ImportError:
|
|
118
|
+
pass
|
aioli_cli/main.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""aioli CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from aioli_cli.registry import registry
|
|
8
|
+
from aioli_contract import AioliConfig, run_checks
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="aioli",
|
|
12
|
+
help="🧄 aioli — local-first AI media suite.",
|
|
13
|
+
rich_markup_mode="rich",
|
|
14
|
+
)
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command()
|
|
19
|
+
def doctor() -> None:
|
|
20
|
+
"""Preflight: python, ffmpeg, CUDA. Plugins append their own checks."""
|
|
21
|
+
cfg = AioliConfig.load()
|
|
22
|
+
table = Table(title="aioli doctor")
|
|
23
|
+
table.add_column("check")
|
|
24
|
+
table.add_column("status")
|
|
25
|
+
table.add_column("detail")
|
|
26
|
+
for check in run_checks():
|
|
27
|
+
table.add_row(check.name, "✅" if check.ok else "❌", check.hint)
|
|
28
|
+
for plugin in registry.all():
|
|
29
|
+
if plugin.checks is None:
|
|
30
|
+
continue
|
|
31
|
+
for check in plugin.checks():
|
|
32
|
+
table.add_row(
|
|
33
|
+
f"{plugin.name}/{check.name}",
|
|
34
|
+
"✅" if check.ok else "❌",
|
|
35
|
+
check.hint,
|
|
36
|
+
)
|
|
37
|
+
table.add_row("models_dir", "📁", str(cfg.models_dir))
|
|
38
|
+
table.add_row("plugins", "🔌", ", ".join(registry.names()) or "(none yet)")
|
|
39
|
+
console.print(table)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command()
|
|
43
|
+
def setup(
|
|
44
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Sin preguntas: instala todo lo que quepa."),
|
|
45
|
+
plugins: str | None = typer.Option(None, "--plugins", "-p", help="Solo estos plugins (coma): image,video,music,voice."),
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Asistente de primera instalación: equipo → consejo → descarga."""
|
|
48
|
+
from aioli_cli.setup import run_setup
|
|
49
|
+
|
|
50
|
+
run_setup(yes=yes, plugins=plugins)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command()
|
|
54
|
+
def version() -> None:
|
|
55
|
+
"""Show version."""
|
|
56
|
+
console.print("aioli 0.1.0")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
models_app = typer.Typer(help="Download + verify weight files.")
|
|
60
|
+
app.add_typer(models_app, name="models")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _wanted(plugin: str | None, backend: str | None = None) -> list[tuple[str, dict]]:
|
|
64
|
+
from aioli_cli import _plugins # noqa: PLC0415 — ensure loaded
|
|
65
|
+
|
|
66
|
+
_plugins.load_all()
|
|
67
|
+
out: list[tuple[str, dict]] = []
|
|
68
|
+
for p in registry.all():
|
|
69
|
+
if p.models is None:
|
|
70
|
+
continue
|
|
71
|
+
if plugin is not None and p.name != plugin:
|
|
72
|
+
continue
|
|
73
|
+
want = backend or p.default_backend or None
|
|
74
|
+
for entry in p.models():
|
|
75
|
+
if want is not None and want != "all" and entry.get("backend", "") != want:
|
|
76
|
+
continue
|
|
77
|
+
out.append((p.name, entry))
|
|
78
|
+
if plugin is not None and not out:
|
|
79
|
+
raise typer.BadParameter(f"no model manifest for plugin '{plugin}'")
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _backends() -> dict[str, list[str]]:
|
|
84
|
+
from aioli_cli import _plugins # noqa: PLC0415 — ensure loaded
|
|
85
|
+
|
|
86
|
+
_plugins.load_all()
|
|
87
|
+
out: dict[str, list[str]] = {}
|
|
88
|
+
for p in registry.all():
|
|
89
|
+
if p.models is None:
|
|
90
|
+
continue
|
|
91
|
+
seen = sorted({e.get("backend", "?") for e in p.models()})
|
|
92
|
+
out[p.name] = seen
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@models_app.command("list")
|
|
97
|
+
def models_list(
|
|
98
|
+
plugin: str | None = typer.Argument(None),
|
|
99
|
+
backend: str | None = typer.Option(None, "--backend", "-b", help="Engine filter (default: plugin default; 'all' = every engine)."),
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Show manifest entries + local status."""
|
|
102
|
+
from aioli_contract import ModelFile, default_models_dir, human_gb, verify
|
|
103
|
+
|
|
104
|
+
models_dir = default_models_dir()
|
|
105
|
+
table = Table(title=f"aioli models → {models_dir}")
|
|
106
|
+
table.add_column("plugin")
|
|
107
|
+
table.add_column("engine")
|
|
108
|
+
table.add_column("file")
|
|
109
|
+
table.add_column("size")
|
|
110
|
+
table.add_column("status")
|
|
111
|
+
for pname, raw in _wanted(plugin, backend):
|
|
112
|
+
entry = ModelFile(**raw)
|
|
113
|
+
status = verify(entry, models_dir)
|
|
114
|
+
icon = {"ok": "✅", "missing": "⬇️", "corrupt": "⚠️", "unverified": "❔"}[status]
|
|
115
|
+
table.add_row(pname, entry.backend or "-", entry.rel, human_gb(entry.size), f"{icon} {status}")
|
|
116
|
+
console.print(table)
|
|
117
|
+
if plugin is None and backend in (None, "all"):
|
|
118
|
+
for pname, engines in _backends().items():
|
|
119
|
+
console.print(f" {pname} engines: {', '.join(engines)}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@models_app.command("download")
|
|
123
|
+
def models_download(
|
|
124
|
+
plugin: str | None = typer.Argument(None),
|
|
125
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
126
|
+
backend: str | None = typer.Option(None, "--backend", "-b", help="Engine only (default: plugin default; 'all' = every engine)."),
|
|
127
|
+
with_optional: bool = typer.Option(False, "--with-optional", help="Include optional files (ASR, alt quants)."),
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Download weights with resume + sha256 verification."""
|
|
130
|
+
from aioli_contract import (
|
|
131
|
+
ModelFile,
|
|
132
|
+
check_space,
|
|
133
|
+
default_models_dir,
|
|
134
|
+
download,
|
|
135
|
+
human_gb,
|
|
136
|
+
verify,
|
|
137
|
+
)
|
|
138
|
+
from rich.progress import Progress
|
|
139
|
+
|
|
140
|
+
models_dir = default_models_dir()
|
|
141
|
+
wanted = [(pn, ModelFile(**raw)) for pn, raw in _wanted(plugin, backend)]
|
|
142
|
+
if not with_optional:
|
|
143
|
+
skipped = [e.rel for _, e in wanted if e.optional]
|
|
144
|
+
wanted = [(pn, e) for pn, e in wanted if not e.optional]
|
|
145
|
+
for rel in skipped:
|
|
146
|
+
console.print(f"⏭️ optional skipped: {rel} (use --with-optional)")
|
|
147
|
+
missing = [(pn, e) for pn, e in wanted if verify(e, models_dir) != "ok"]
|
|
148
|
+
if not missing:
|
|
149
|
+
console.print("✅ all models present + verified")
|
|
150
|
+
return
|
|
151
|
+
total = sum(e.size for _, e in missing)
|
|
152
|
+
fits, _needed, free = check_space([e for _, e in missing], models_dir)
|
|
153
|
+
console.print(f"⬇️ {len(missing)} file(s), {human_gb(total)} → {models_dir}")
|
|
154
|
+
if free:
|
|
155
|
+
console.print(f" disk free: {human_gb(free)}")
|
|
156
|
+
if not fits:
|
|
157
|
+
raise typer.Abort(f"❌ not enough disk space for {human_gb(total)}")
|
|
158
|
+
notes = {registry._plugins[pn].gated_note for pn, _ in missing}
|
|
159
|
+
for note in sorted(n for n in notes if n):
|
|
160
|
+
console.print(f"🔑 {note}")
|
|
161
|
+
if not yes and not typer.confirm("Download now?"):
|
|
162
|
+
raise typer.Abort()
|
|
163
|
+
|
|
164
|
+
class _Bar:
|
|
165
|
+
def __init__(self, progress: Progress) -> None:
|
|
166
|
+
self.progress = progress
|
|
167
|
+
self.tasks: dict[str, int] = {}
|
|
168
|
+
|
|
169
|
+
def update(self, path, done_bytes: int, total_bytes: int) -> None:
|
|
170
|
+
key = str(path)
|
|
171
|
+
task = self.tasks.get(key)
|
|
172
|
+
if task is None:
|
|
173
|
+
task = self.progress.add_task(path.name, total=total_bytes or 1)
|
|
174
|
+
self.tasks[key] = task
|
|
175
|
+
self.progress.update(task, completed=done_bytes, total=total_bytes or 1)
|
|
176
|
+
|
|
177
|
+
with Progress() as progress:
|
|
178
|
+
bar = _Bar(progress)
|
|
179
|
+
for _pn, entry in missing:
|
|
180
|
+
try:
|
|
181
|
+
download(entry, models_dir, progress=bar)
|
|
182
|
+
except (IOError, ValueError) as exc:
|
|
183
|
+
console.print(f"❌ {entry.rel}: {exc}")
|
|
184
|
+
raise typer.Abort() from exc
|
|
185
|
+
console.print("✅ all models present + verified")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@models_app.command("verify")
|
|
189
|
+
def models_verify(plugin: str | None = typer.Argument(None)) -> None:
|
|
190
|
+
"""Re-hash local files against the manifest."""
|
|
191
|
+
from aioli_contract import ModelFile, default_models_dir, verify
|
|
192
|
+
|
|
193
|
+
models_dir = default_models_dir()
|
|
194
|
+
bad = 0
|
|
195
|
+
for pname, raw in _wanted(plugin):
|
|
196
|
+
entry = ModelFile(**raw)
|
|
197
|
+
status = verify(entry, models_dir)
|
|
198
|
+
icon = {"ok": "✅", "missing": "⬇️", "corrupt": "⚠️", "unverified": "❔"}[status]
|
|
199
|
+
console.print(f"{icon} [{pname}] {entry.rel}: {status}")
|
|
200
|
+
if status == "corrupt":
|
|
201
|
+
bad += 1
|
|
202
|
+
if bad:
|
|
203
|
+
raise typer.Exit(code=1)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def main() -> None:
|
|
207
|
+
from aioli_cli import _plugins
|
|
208
|
+
|
|
209
|
+
_plugins.load_all()
|
|
210
|
+
registry.attach(app)
|
|
211
|
+
app()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__":
|
|
215
|
+
main()
|
aioli_cli/registry.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Plugin registry. Each modality registers a Typer app + metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from aioli_contract.doctor import Check
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Plugin:
|
|
15
|
+
name: str
|
|
16
|
+
description: str
|
|
17
|
+
app: typer.Typer = field(default_factory=typer.Typer)
|
|
18
|
+
checks: Callable[[], list[Check]] | None = None
|
|
19
|
+
models: Callable[[], list[dict]] | None = None
|
|
20
|
+
gated_note: str = ""
|
|
21
|
+
default_backend: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Registry:
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._plugins: dict[str, Plugin] = {}
|
|
27
|
+
|
|
28
|
+
def register(self, plugin: Plugin) -> None:
|
|
29
|
+
if plugin.name in self._plugins:
|
|
30
|
+
raise ValueError(f"plugin already registered: {plugin.name}")
|
|
31
|
+
self._plugins[plugin.name] = plugin
|
|
32
|
+
|
|
33
|
+
def names(self) -> list[str]:
|
|
34
|
+
return sorted(self._plugins)
|
|
35
|
+
|
|
36
|
+
def all(self) -> list[Plugin]:
|
|
37
|
+
return [self._plugins[name] for name in self.names()]
|
|
38
|
+
|
|
39
|
+
def attach(self, app: typer.Typer) -> None:
|
|
40
|
+
for name in self.names():
|
|
41
|
+
plugin = self._plugins[name]
|
|
42
|
+
app.add_typer(plugin.app, name=name, help=plugin.description)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
registry = Registry()
|
aioli_cli/setup.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""`aioli setup`: pretty first-run wizard (probe → advise → download)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.prompt import Confirm, Prompt
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
LEAGUE_LINES = {
|
|
14
|
+
"patata": "🥔 Sin NVIDIA a la vista: tiraremos de CPU (voice) y poco más.",
|
|
15
|
+
"modesta": "🛵 GPU modesta: motores ligeros y sin prisas.",
|
|
16
|
+
"capaz": "💪 GPU capaz: el set default corre bien.",
|
|
17
|
+
"bicha": "🔥 Menuda bicha: todo el set default sobrado.",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load():
|
|
22
|
+
from aioli_cli import _plugins # noqa: PLC0415
|
|
23
|
+
from aioli_cli.registry import registry # noqa: PLC0415
|
|
24
|
+
|
|
25
|
+
_plugins.load_all()
|
|
26
|
+
return registry
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def machine_panel(machine) -> Panel:
|
|
30
|
+
lines = []
|
|
31
|
+
if machine.gpus:
|
|
32
|
+
for g in machine.gpus:
|
|
33
|
+
lines.append(f"🎮 {g.name} — {g.total_gb:.0f} GB ({g.free_gb:.0f} libres)")
|
|
34
|
+
else:
|
|
35
|
+
lines.append("🎮 sin GPU NVIDIA")
|
|
36
|
+
lines.append(f"🧠 RAM: {machine.ram_gb:.0f} GB 💾 disco libre: {machine.disk_free_gb:.0f} GB")
|
|
37
|
+
lines.append("")
|
|
38
|
+
lines.append(LEAGUE_LINES[machine.league])
|
|
39
|
+
return Panel("\n".join(lines), title="🧄 aioli setup — tu equipo", border_style="yellow")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def verdict_table(registry, machine) -> Table:
|
|
43
|
+
from aioli_contract.advise import judge
|
|
44
|
+
|
|
45
|
+
table = Table(title="¿Qué corre aquí?")
|
|
46
|
+
table.add_column("plugin")
|
|
47
|
+
table.add_column("motor")
|
|
48
|
+
table.add_column("")
|
|
49
|
+
table.add_column("veredicto")
|
|
50
|
+
for p in registry.all():
|
|
51
|
+
if p.models is None:
|
|
52
|
+
continue
|
|
53
|
+
engines = sorted({e.get("backend", "?") for e in p.models()})
|
|
54
|
+
for engine in engines:
|
|
55
|
+
v = judge(p.name, engine, machine)
|
|
56
|
+
table.add_row(p.name, engine, v.icon, v.headline + (f"\n[yellow]{v.detail}[/yellow]" if v.detail else ""))
|
|
57
|
+
return table
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_setup(yes: bool = False, plugins: str | None = None) -> None:
|
|
61
|
+
from aioli_contract import ModelFile, default_models_dir, download, human_gb, verify
|
|
62
|
+
from aioli_contract.advise import judge, read_machine
|
|
63
|
+
from rich.progress import Progress
|
|
64
|
+
|
|
65
|
+
registry = _load()
|
|
66
|
+
models_dir = default_models_dir()
|
|
67
|
+
machine = read_machine(models_dir)
|
|
68
|
+
|
|
69
|
+
console.print()
|
|
70
|
+
console.print(machine_panel(machine))
|
|
71
|
+
console.print()
|
|
72
|
+
console.print(verdict_table(registry, machine))
|
|
73
|
+
console.print()
|
|
74
|
+
|
|
75
|
+
# Candidate set: default backend of each plugin that fits (✅/⚠️).
|
|
76
|
+
candidates: list[tuple[str, ModelFile]] = []
|
|
77
|
+
for p in registry.all():
|
|
78
|
+
if p.models is None:
|
|
79
|
+
continue
|
|
80
|
+
for raw in p.models():
|
|
81
|
+
entry = ModelFile(**raw)
|
|
82
|
+
if entry.backend != (p.default_backend or entry.backend):
|
|
83
|
+
continue
|
|
84
|
+
if entry.optional:
|
|
85
|
+
continue
|
|
86
|
+
v = judge(p.name, entry.backend, machine)
|
|
87
|
+
if v.fits:
|
|
88
|
+
candidates.append((p.name, entry))
|
|
89
|
+
|
|
90
|
+
by_plugin: dict[str, list[ModelFile]] = {}
|
|
91
|
+
for pname, entry in candidates:
|
|
92
|
+
by_plugin.setdefault(pname, []).append(entry)
|
|
93
|
+
|
|
94
|
+
if plugins:
|
|
95
|
+
chosen = [p.strip() for p in plugins.split(",") if p.strip()]
|
|
96
|
+
elif yes:
|
|
97
|
+
chosen = sorted(by_plugin)
|
|
98
|
+
else:
|
|
99
|
+
options = ", ".join(
|
|
100
|
+
f"{pn} ({human_gb(sum(e.size for e in es))})" for pn, es in sorted(by_plugin.items())
|
|
101
|
+
)
|
|
102
|
+
answer = Prompt.ask(
|
|
103
|
+
f"¿Qué instalo? [{options}]",
|
|
104
|
+
default=",".join(sorted(by_plugin)),
|
|
105
|
+
)
|
|
106
|
+
chosen = [p.strip() for p in answer.split(",") if p.strip()]
|
|
107
|
+
|
|
108
|
+
wanted = [(pn, e) for pn, e in candidates if pn in chosen]
|
|
109
|
+
missing = [(pn, e) for pn, e in wanted if verify(e, models_dir) != "ok"]
|
|
110
|
+
if not missing:
|
|
111
|
+
console.print(Panel("✅ Todo lo elegido ya está descargado y verificado.", border_style="green"))
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
total = sum(e.size for _, e in missing)
|
|
115
|
+
console.print(f"⬇️ {len(missing)} ficheros, {human_gb(total)} → {models_dir}")
|
|
116
|
+
notes = {registry._plugins[pn].gated_note for pn, _ in missing}
|
|
117
|
+
for note in sorted(n for n in notes if n):
|
|
118
|
+
console.print(f"🔑 {note}")
|
|
119
|
+
if not yes and not Confirm.ask("¿Descargo ahora?", default=True):
|
|
120
|
+
raise typer.Abort()
|
|
121
|
+
|
|
122
|
+
class _Bar:
|
|
123
|
+
def __init__(self, progress: Progress) -> None:
|
|
124
|
+
self.progress = progress
|
|
125
|
+
self.tasks: dict[str, int] = {}
|
|
126
|
+
|
|
127
|
+
def update(self, path, done_bytes: int, total_bytes: int) -> None:
|
|
128
|
+
key = str(path)
|
|
129
|
+
task = self.tasks.get(key)
|
|
130
|
+
if task is None:
|
|
131
|
+
task = self.progress.add_task(path.name, total=total_bytes or 1)
|
|
132
|
+
self.tasks[key] = task
|
|
133
|
+
self.progress.update(task, completed=done_bytes, total=total_bytes or 1)
|
|
134
|
+
|
|
135
|
+
with Progress() as progress:
|
|
136
|
+
for _pn, entry in missing:
|
|
137
|
+
try:
|
|
138
|
+
download(entry, models_dir, progress=_Bar(progress))
|
|
139
|
+
except (IOError, ValueError) as exc:
|
|
140
|
+
console.print(f"❌ {entry.rel}: {exc}")
|
|
141
|
+
raise typer.Abort() from exc
|
|
142
|
+
|
|
143
|
+
console.print(
|
|
144
|
+
Panel(
|
|
145
|
+
"✅ Listo. Prueba:\n"
|
|
146
|
+
" aioli doctor\n"
|
|
147
|
+
' aioli image generate "un faro en tormenta" -o faro.png',
|
|
148
|
+
title="🧄 a generar",
|
|
149
|
+
border_style="green",
|
|
150
|
+
)
|
|
151
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aioli-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: aioli — local-first AI media suite CLI
|
|
5
|
+
Author: aioli contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ai,generative,image,local-first,music,tts,video
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Multimedia
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: aioli-contract
|
|
14
|
+
Requires-Dist: rich>=13
|
|
15
|
+
Requires-Dist: typer>=0.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# aioli (see root README)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
aioli_cli/_plugins.py,sha256=e6PbLZ5ZC8hwYO-aSWjGNqXmkJaLfRte-KJdU5rFVAA,3734
|
|
2
|
+
aioli_cli/main.py,sha256=wl3DlcT1CWetRlMBCGnRsLrTYda7XTHxrUEudvOJt5I,7639
|
|
3
|
+
aioli_cli/registry.py,sha256=iCx4TPJBjnkwqWh6c67igoA5Vx_0vyYGgnnBmuOohto,1215
|
|
4
|
+
aioli_cli/setup.py,sha256=QXgHHzdNMo2C_w9wB55MkndEoLn2CyXT2X6zTG6oTyk,5347
|
|
5
|
+
aioli_cli-0.1.0.dist-info/METADATA,sha256=qIaGh4bj8U_zsOtt9LKWHHQmBXCEPRfVjjjvet8iUcU,550
|
|
6
|
+
aioli_cli-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
7
|
+
aioli_cli-0.1.0.dist-info/entry_points.txt,sha256=XMf3mcJqGsedAQELaeg97uvrMU9LEoGeksbIzXZVpf0,46
|
|
8
|
+
aioli_cli-0.1.0.dist-info/RECORD,,
|