aioli-cli 0.1.2__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.2/.gitignore +10 -0
- aioli_cli-0.1.2/LICENSE +39 -0
- aioli_cli-0.1.2/PKG-INFO +93 -0
- aioli_cli-0.1.2/README.md +64 -0
- aioli_cli-0.1.2/pyproject.toml +114 -0
- aioli_cli-0.1.2/src/aioli_cli/_plugins.py +148 -0
- aioli_cli-0.1.2/src/aioli_cli/main.py +215 -0
- aioli_cli-0.1.2/src/aioli_cli/registry.py +45 -0
- aioli_cli-0.1.2/src/aioli_cli/setup.py +151 -0
- aioli_cli-0.1.2/src/aioli_contract/__init__.py +33 -0
- aioli_cli-0.1.2/src/aioli_contract/advise.py +169 -0
- aioli_cli-0.1.2/src/aioli_contract/config.py +49 -0
- aioli_cli-0.1.2/src/aioli_contract/doctor.py +51 -0
- aioli_cli-0.1.2/src/aioli_contract/fetch.py +176 -0
- aioli_cli-0.1.2/src/aioli_contract/probe.py +127 -0
- aioli_cli-0.1.2/src/aioli_image/__init__.py +3 -0
- aioli_cli-0.1.2/src/aioli_image/cli.py +259 -0
- aioli_cli-0.1.2/src/aioli_image/companions.py +232 -0
- aioli_cli-0.1.2/src/aioli_image/config.py +189 -0
- aioli_cli-0.1.2/src/aioli_image/data/models.toml +342 -0
- aioli_cli-0.1.2/src/aioli_image/doctor.py +48 -0
- aioli_cli-0.1.2/src/aioli_image/gpu.py +140 -0
- aioli_cli-0.1.2/src/aioli_image/launcher.py +723 -0
- aioli_cli-0.1.2/src/aioli_image/models.py +449 -0
- aioli_cli-0.1.2/src/aioli_image/models_data.py +55 -0
- aioli_cli-0.1.2/src/aioli_image/plugin.py +13 -0
- aioli_cli-0.1.2/src/aioli_image/presets.py +196 -0
- aioli_cli-0.1.2/src/aioli_image/stub.py +48 -0
- aioli_cli-0.1.2/src/aioli_image/ui.py +200 -0
- aioli_cli-0.1.2/src/aioli_master/cli.py +107 -0
- aioli_cli-0.1.2/src/aioli_master/doctor.py +23 -0
- aioli_cli-0.1.2/src/aioli_master/ops.py +154 -0
- aioli_cli-0.1.2/src/aioli_master/plugin.py +8 -0
- aioli_cli-0.1.2/src/aioli_music/__init__.py +3 -0
- aioli_cli-0.1.2/src/aioli_music/backends/__init__.py +100 -0
- aioli_cli-0.1.2/src/aioli_music/backends/acestep.py +92 -0
- aioli_cli-0.1.2/src/aioli_music/backends/base.py +151 -0
- aioli_cli-0.1.2/src/aioli_music/backends/llm_caption.py +172 -0
- aioli_cli-0.1.2/src/aioli_music/backends/minimax_comfy.py +506 -0
- aioli_cli-0.1.2/src/aioli_music/backends/process.py +170 -0
- aioli_cli-0.1.2/src/aioli_music/backends/stub.py +93 -0
- aioli_cli-0.1.2/src/aioli_music/backends/yue2.py +271 -0
- aioli_cli-0.1.2/src/aioli_music/cli_commands/__init__.py +58 -0
- aioli_cli-0.1.2/src/aioli_music/cli_commands/backend_cmd.py +158 -0
- aioli_cli-0.1.2/src/aioli_music/cli_commands/generate_cmd.py +262 -0
- aioli_cli-0.1.2/src/aioli_music/cli_commands/inspire_cmd.py +73 -0
- aioli_cli-0.1.2/src/aioli_music/cli_commands/models_cmd.py +171 -0
- aioli_cli-0.1.2/src/aioli_music/client.py +30 -0
- aioli_cli-0.1.2/src/aioli_music/config.py +395 -0
- aioli_cli-0.1.2/src/aioli_music/core/__init__.py +28 -0
- aioli_cli-0.1.2/src/aioli_music/core/formats.py +117 -0
- aioli_cli-0.1.2/src/aioli_music/core/generate.py +441 -0
- aioli_cli-0.1.2/src/aioli_music/core/jobs.py +266 -0
- aioli_cli-0.1.2/src/aioli_music/core/layout.py +103 -0
- aioli_cli-0.1.2/src/aioli_music/doctor.py +58 -0
- aioli_cli-0.1.2/src/aioli_music/generator.py +119 -0
- aioli_cli-0.1.2/src/aioli_music/history.py +140 -0
- aioli_cli-0.1.2/src/aioli_music/jsonstore.py +45 -0
- aioli_cli-0.1.2/src/aioli_music/models.py +381 -0
- aioli_cli-0.1.2/src/aioli_music/models_data.py +100 -0
- aioli_cli-0.1.2/src/aioli_music/paths.py +183 -0
- aioli_cli-0.1.2/src/aioli_music/plugin.py +23 -0
- aioli_cli-0.1.2/src/aioli_music/presets.py +183 -0
- aioli_cli-0.1.2/src/aioli_music/prompts.py +112 -0
- aioli_cli-0.1.2/src/aioli_music/server.py +394 -0
- aioli_cli-0.1.2/src/aioli_music/transport/__init__.py +30 -0
- aioli_cli-0.1.2/src/aioli_music/transport/client.py +510 -0
- aioli_cli-0.1.2/src/aioli_music/tui.py +171 -0
- aioli_cli-0.1.2/src/aioli_podcast/cast.py +83 -0
- aioli_cli-0.1.2/src/aioli_podcast/cli.py +54 -0
- aioli_cli-0.1.2/src/aioli_podcast/doctor.py +22 -0
- aioli_cli-0.1.2/src/aioli_podcast/episode.py +197 -0
- aioli_cli-0.1.2/src/aioli_podcast/plugin.py +8 -0
- aioli_cli-0.1.2/src/aioli_podcast/script.py +97 -0
- aioli_cli-0.1.2/src/aioli_video/cli.py +146 -0
- aioli_cli-0.1.2/src/aioli_video/doctor.py +23 -0
- aioli_cli-0.1.2/src/aioli_video/models_data.py +50 -0
- aioli_cli-0.1.2/src/aioli_video/paths.py +28 -0
- aioli_cli-0.1.2/src/aioli_video/plugin.py +8 -0
- aioli_cli-0.1.2/src/aioli_video/stub.py +30 -0
- aioli_cli-0.1.2/src/aioli_video/wgp.py +136 -0
- aioli_cli-0.1.2/src/aioli_video/worker.py +35 -0
- aioli_cli-0.1.2/src/aioli_voice/__init__.py +0 -0
- aioli_cli-0.1.2/src/aioli_voice/cli/doctor.py +260 -0
- aioli_cli-0.1.2/src/aioli_voice/cli/engines_cmd.py +37 -0
- aioli_cli-0.1.2/src/aioli_voice/cli/generate.py +129 -0
- aioli_cli-0.1.2/src/aioli_voice/cli/models_cmd.py +73 -0
- aioli_cli-0.1.2/src/aioli_voice/cli/profiles.py +122 -0
- aioli_cli-0.1.2/src/aioli_voice/config/__init__.py +0 -0
- aioli_cli-0.1.2/src/aioli_voice/config/settings.py +84 -0
- aioli_cli-0.1.2/src/aioli_voice/core/__init__.py +0 -0
- aioli_cli-0.1.2/src/aioli_voice/core/generation.py +229 -0
- aioli_cli-0.1.2/src/aioli_voice/core/model_manager.py +88 -0
- aioli_cli-0.1.2/src/aioli_voice/core/profiles.py +162 -0
- aioli_cli-0.1.2/src/aioli_voice/core/registry.py +65 -0
- aioli_cli-0.1.2/src/aioli_voice/core/types.py +59 -0
- aioli_cli-0.1.2/src/aioli_voice/devices.py +118 -0
- aioli_cli-0.1.2/src/aioli_voice/doctor.py +17 -0
- aioli_cli-0.1.2/src/aioli_voice/engines/__init__.py +155 -0
- aioli_cli-0.1.2/src/aioli_voice/engines/chatterbox.py +151 -0
- aioli_cli-0.1.2/src/aioli_voice/engines/kokoro.py +97 -0
- aioli_cli-0.1.2/src/aioli_voice/engines/moss.py +198 -0
- aioli_cli-0.1.2/src/aioli_voice/engines/piper.py +100 -0
- aioli_cli-0.1.2/src/aioli_voice/engines/qwen3.py +198 -0
- aioli_cli-0.1.2/src/aioli_voice/engines/stub.py +62 -0
- aioli_cli-0.1.2/src/aioli_voice/engines/voxcpm.py +169 -0
- aioli_cli-0.1.2/src/aioli_voice/engines/xtts.py +138 -0
- aioli_cli-0.1.2/src/aioli_voice/models_data.py +64 -0
- aioli_cli-0.1.2/src/aioli_voice/plugin.py +24 -0
aioli_cli-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aioli contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Third-party notes (not covered by the MIT above)
|
|
26
|
+
|
|
27
|
+
aioli downloads WEIGHTS, it does not ship them. Each weight file keeps its
|
|
28
|
+
own license, shown before download (`GATED_NOTE` in every `models_data.py`):
|
|
29
|
+
|
|
30
|
+
- Flux / LTX-2.5 weights: gated HuggingFace repos — accept the upstream
|
|
31
|
+
license on the web before downloading.
|
|
32
|
+
- Piper voices: no additional license imposed upstream (end-user judgement).
|
|
33
|
+
- Chatterbox / XTTS engines: check upstream terms (non-commercial clauses
|
|
34
|
+
may apply) before monetized use.
|
|
35
|
+
|
|
36
|
+
External tools aioli talks to (never bundled) keep their licenses:
|
|
37
|
+
Wan2GP, ComfyUI (GPL-3.0), acestep.cpp, Blender. The `aioli™` name and the
|
|
38
|
+
logo/mascot in `assets/` are not covered by the MIT grant — ask before
|
|
39
|
+
selling anything called "aioli".
|
aioli_cli-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aioli-cli
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: aioli — local-first AI media suite CLI (images, video, music, voice, podcast)
|
|
5
|
+
Author: aioli contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: ai,generative,image,local-first,music,tts,video
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Multimedia
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Requires-Dist: httpx>=0.27
|
|
15
|
+
Requires-Dist: huggingface-hub>=0.30
|
|
16
|
+
Requires-Dist: inquirerpy>=0.3.4
|
|
17
|
+
Requires-Dist: kokoro-onnx>=0.4
|
|
18
|
+
Requires-Dist: numpy>=1.26
|
|
19
|
+
Requires-Dist: pydantic-settings>=2.3
|
|
20
|
+
Requires-Dist: pydantic>=2.7
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Requires-Dist: rich>=13.0
|
|
23
|
+
Requires-Dist: soundfile>=0.12
|
|
24
|
+
Requires-Dist: typer>=0.12
|
|
25
|
+
Provides-Extra: music-storage
|
|
26
|
+
Requires-Dist: boto3>=1.34; extra == 'music-storage'
|
|
27
|
+
Provides-Extra: voice-piper
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="assets/logo-transparent.png" width="220" alt="aioli logo — robot garlic" />
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<h1 align="center">🧄 aioli</h1>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<em>Local-first AI media suite. One CLI, many plugins — everything on your machine, no cloud required. 🌙</em>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
🎙️ Voice • 🎵 Music • 🖼️ Image • 🎬 Video • 🎧 Post • 🎙️📻 Podcast
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## ✨ Quickstart
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -LsSf aioli.es/install.sh | sh # 🧄 dummy-proof: equipo → consejo → modelos
|
|
50
|
+
# …o paso a paso:
|
|
51
|
+
uv tool install aioli-cli # 📦 un paquete; extras: aioli-cli[voice-qwen3,full]
|
|
52
|
+
aioli setup # 🧭 asesora según tu GPU y descarga pesos
|
|
53
|
+
aioli doctor # 🩺 preflight: CUDA, ffmpeg, models, VRAM
|
|
54
|
+
aioli models download image music # ⬇️ weights with resume + sha256 (HF_TOKEN for gated repos)
|
|
55
|
+
aioli voice generate "..." --profile vega # 🎙️ clone a voice
|
|
56
|
+
aioli music generate "rumba alegre" --backend yue2 # 🎵 compose a song
|
|
57
|
+
aioli podcast episode "la historia del café" --minutes 5 # 📻 full episode
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
> **Desde el repo (dev):** `uv tool install -e .` desde la raíz (un solo
|
|
61
|
+
> paquete editable con todo dentro). Extras: `uv tool install -e ".[full]"`.
|
|
62
|
+
|
|
63
|
+
## 🗂️ Layout
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
aioli/
|
|
67
|
+
apps/aioli/src/aioli_cli/ # 🧄 CLI (Typer + registry + setup + models)
|
|
68
|
+
libs/contract/src/ # 📜 contrato: config, doctor, probe, advise, fetch
|
|
69
|
+
plugins/*/src/ # 🔌 un módulo por modalidad (mismo dist, entry points para terceros)
|
|
70
|
+
docs/catalog.md # 🗺️ inventory of everything (legacy + new)
|
|
71
|
+
assets/ # 🎨 logo + mascot variants
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 📚 Docs
|
|
75
|
+
|
|
76
|
+
| Doc | Para quién |
|
|
77
|
+
|---|---|
|
|
78
|
+
| [docs/INSTALL.md](docs/INSTALL.md) | 🧑💻 instalar desde cero (requisitos, HF_TOKEN, problemas comunes) |
|
|
79
|
+
| [docs/USAGE.md](docs/USAGE.md) | 🎨 recetas validadas por plugin (comandos que funcionan) |
|
|
80
|
+
| [docs/adr/](docs/adr/) | 🧠 por qué se decidió cada cosa (4 ADRs) |
|
|
81
|
+
| [docs/catalog.md](docs/catalog.md) | 🗺️ inventario vivo: portado vs legacy + deuda |
|
|
82
|
+
|
|
83
|
+
## 📜 Contract (every plugin obeys it)
|
|
84
|
+
|
|
85
|
+
- ▶️ `aioli <plugin> generate ...` — produce media
|
|
86
|
+
- ⬇️ `aioli <plugin> models download` — fetch weights with checksum
|
|
87
|
+
- 🩺 `aioli doctor` — preflight: CUDA, ffmpeg, models, VRAM
|
|
88
|
+
- ⚙️ Config in `~/.config/aioli/config.toml`, env overrides (`AIOLI_MODELS_DIR`, …)
|
|
89
|
+
- 🚫 No absolute paths, no hardcoded GPUs, no cloud credentials required.
|
|
90
|
+
|
|
91
|
+
## 🗺️ Status
|
|
92
|
+
|
|
93
|
+
See [docs/catalog.md](docs/catalog.md) for the living inventory: what's ported, what's legacy, and the tech debt. 🧾
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo-transparent.png" width="220" alt="aioli logo — robot garlic" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">🧄 aioli</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<em>Local-first AI media suite. One CLI, many plugins — everything on your machine, no cloud required. 🌙</em>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
🎙️ Voice • 🎵 Music • 🖼️ Image • 🎬 Video • 🎧 Post • 🎙️📻 Podcast
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## ✨ Quickstart
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
curl -LsSf aioli.es/install.sh | sh # 🧄 dummy-proof: equipo → consejo → modelos
|
|
21
|
+
# …o paso a paso:
|
|
22
|
+
uv tool install aioli-cli # 📦 un paquete; extras: aioli-cli[voice-qwen3,full]
|
|
23
|
+
aioli setup # 🧭 asesora según tu GPU y descarga pesos
|
|
24
|
+
aioli doctor # 🩺 preflight: CUDA, ffmpeg, models, VRAM
|
|
25
|
+
aioli models download image music # ⬇️ weights with resume + sha256 (HF_TOKEN for gated repos)
|
|
26
|
+
aioli voice generate "..." --profile vega # 🎙️ clone a voice
|
|
27
|
+
aioli music generate "rumba alegre" --backend yue2 # 🎵 compose a song
|
|
28
|
+
aioli podcast episode "la historia del café" --minutes 5 # 📻 full episode
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> **Desde el repo (dev):** `uv tool install -e .` desde la raíz (un solo
|
|
32
|
+
> paquete editable con todo dentro). Extras: `uv tool install -e ".[full]"`.
|
|
33
|
+
|
|
34
|
+
## 🗂️ Layout
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
aioli/
|
|
38
|
+
apps/aioli/src/aioli_cli/ # 🧄 CLI (Typer + registry + setup + models)
|
|
39
|
+
libs/contract/src/ # 📜 contrato: config, doctor, probe, advise, fetch
|
|
40
|
+
plugins/*/src/ # 🔌 un módulo por modalidad (mismo dist, entry points para terceros)
|
|
41
|
+
docs/catalog.md # 🗺️ inventory of everything (legacy + new)
|
|
42
|
+
assets/ # 🎨 logo + mascot variants
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 📚 Docs
|
|
46
|
+
|
|
47
|
+
| Doc | Para quién |
|
|
48
|
+
|---|---|
|
|
49
|
+
| [docs/INSTALL.md](docs/INSTALL.md) | 🧑💻 instalar desde cero (requisitos, HF_TOKEN, problemas comunes) |
|
|
50
|
+
| [docs/USAGE.md](docs/USAGE.md) | 🎨 recetas validadas por plugin (comandos que funcionan) |
|
|
51
|
+
| [docs/adr/](docs/adr/) | 🧠 por qué se decidió cada cosa (4 ADRs) |
|
|
52
|
+
| [docs/catalog.md](docs/catalog.md) | 🗺️ inventario vivo: portado vs legacy + deuda |
|
|
53
|
+
|
|
54
|
+
## 📜 Contract (every plugin obeys it)
|
|
55
|
+
|
|
56
|
+
- ▶️ `aioli <plugin> generate ...` — produce media
|
|
57
|
+
- ⬇️ `aioli <plugin> models download` — fetch weights with checksum
|
|
58
|
+
- 🩺 `aioli doctor` — preflight: CUDA, ffmpeg, models, VRAM
|
|
59
|
+
- ⚙️ Config in `~/.config/aioli/config.toml`, env overrides (`AIOLI_MODELS_DIR`, …)
|
|
60
|
+
- 🚫 No absolute paths, no hardcoded GPUs, no cloud credentials required.
|
|
61
|
+
|
|
62
|
+
## 🗺️ Status
|
|
63
|
+
|
|
64
|
+
See [docs/catalog.md](docs/catalog.md) for the living inventory: what's ported, what's legacy, and the tech debt. 🧾
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aioli-cli"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "aioli — local-first AI media suite CLI (images, video, music, voice, podcast)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "aioli contributors" }]
|
|
9
|
+
keywords = ["ai", "generative", "local-first", "tts", "image", "video", "music"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Operating System :: POSIX :: Linux",
|
|
14
|
+
"Topic :: Multimedia",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
# base: CLI + contrato + todos los plugins (el codigo pesa KB; los pesos
|
|
18
|
+
# se descargan con `aioli setup`). Motores pesados van en extras.
|
|
19
|
+
"typer>=0.12",
|
|
20
|
+
"rich>=13.0",
|
|
21
|
+
"InquirerPy>=0.3.4",
|
|
22
|
+
"httpx>=0.27",
|
|
23
|
+
"huggingface-hub>=0.30",
|
|
24
|
+
"pydantic>=2.7",
|
|
25
|
+
"pydantic-settings>=2.3",
|
|
26
|
+
"pyyaml>=6.0",
|
|
27
|
+
"numpy>=1.26",
|
|
28
|
+
"soundfile>=0.12",
|
|
29
|
+
"kokoro-onnx>=0.4",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
# Solo extras COMPATIBLES entre si: el lock del workspace los resuelve todos
|
|
35
|
+
# juntos. Los motores torch (qwen3/chatterbox/voxcpm/xtts/moss) se instalan a
|
|
36
|
+
# mano — ver plugins/voice/README.md (son mundos de dependencias incompatibles
|
|
37
|
+
# entre si: moss clava transformers==4.57.1, qwen-tts exige ==4.57.3).
|
|
38
|
+
voice-piper = [] # binario externo (`uv tool install piper-tts`), sin deps python
|
|
39
|
+
music-storage = ["boto3>=1.34"]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
aioli = "aioli_cli.main:main"
|
|
43
|
+
|
|
44
|
+
# Third-party plugins: register a `Plugin` (or zero-arg callable returning
|
|
45
|
+
# one, see aioli_cli.registry.Plugin) under this group in YOUR package:
|
|
46
|
+
# [project.entry-points."aioli.plugins"]
|
|
47
|
+
# comic = "aioli_comic.plugin:comic_plugin"
|
|
48
|
+
[project.entry-points."aioli.plugins"]
|
|
49
|
+
# (builtin plugins load directly from this same dist; the group is for externals)
|
|
50
|
+
|
|
51
|
+
[tool.uv.workspace]
|
|
52
|
+
members = ["libs/*", "plugins/*"]
|
|
53
|
+
|
|
54
|
+
[tool.uv]
|
|
55
|
+
override-dependencies = [
|
|
56
|
+
"torch>=2.7,<3",
|
|
57
|
+
"torchaudio>=2.7,<3",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[[tool.uv.index]]
|
|
61
|
+
name = "pytorch-cu128"
|
|
62
|
+
url = "https://download.pytorch.org/whl/cu128"
|
|
63
|
+
explicit = true
|
|
64
|
+
|
|
65
|
+
[tool.uv.sources]
|
|
66
|
+
torch = { index = "pytorch-cu128" }
|
|
67
|
+
torchaudio = { index = "pytorch-cu128" }
|
|
68
|
+
torchcodec = { index = "pytorch-cu128" }
|
|
69
|
+
moss-tts-nano = { git = "https://github.com/OpenMOSS/MOSS-TTS-Nano", rev = "cc7bdf19c7639c0870dab22045a33b442760f6be" }
|
|
70
|
+
|
|
71
|
+
[build-system]
|
|
72
|
+
requires = ["hatchling"]
|
|
73
|
+
build-backend = "hatchling.build"
|
|
74
|
+
|
|
75
|
+
# Single dist: todo el codigo (CLI + contrato + plugins) en un wheel.
|
|
76
|
+
[tool.hatch.build.targets.wheel]
|
|
77
|
+
packages = [
|
|
78
|
+
"src/aioli_cli",
|
|
79
|
+
"src/aioli_contract",
|
|
80
|
+
"src/aioli_voice",
|
|
81
|
+
"src/aioli_image",
|
|
82
|
+
"src/aioli_video",
|
|
83
|
+
"src/aioli_music",
|
|
84
|
+
"src/aioli_podcast",
|
|
85
|
+
"src/aioli_master",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
[tool.hatch.build.targets.sdist]
|
|
89
|
+
# only-include PRESERVA rutas (packages las aplana y rompe el wheel-from-sdist).
|
|
90
|
+
only-include = [
|
|
91
|
+
"src",
|
|
92
|
+
"pyproject.toml",
|
|
93
|
+
"README.md",
|
|
94
|
+
"LICENSE",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.pytest.ini_options]
|
|
98
|
+
pythonpath = ["src"]
|
|
99
|
+
testpaths = [
|
|
100
|
+
"libs/contract/tests",
|
|
101
|
+
"plugins/voice/tests",
|
|
102
|
+
"plugins/image/tests",
|
|
103
|
+
"plugins/video/tests",
|
|
104
|
+
"plugins/music/tests",
|
|
105
|
+
"plugins/podcast/tests",
|
|
106
|
+
"plugins/master/tests",
|
|
107
|
+
]
|
|
108
|
+
addopts = "-q"
|
|
109
|
+
|
|
110
|
+
[dependency-groups]
|
|
111
|
+
dev = [
|
|
112
|
+
"pytest>=8.0",
|
|
113
|
+
"pillow>=10.0", # solo tests (stubs de imagen); nunca en dependencies
|
|
114
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
ENTRY_POINT_GROUP = "aioli.plugins"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_entry_points() -> None:
|
|
16
|
+
"""Third-party plugins via `[project.entry-points."aioli.plugins"]`.
|
|
17
|
+
|
|
18
|
+
Each entry point must resolve to a `registry.Plugin` instance or to a
|
|
19
|
+
zero-arg callable returning one (so externals never touch our release).
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
from importlib.metadata import entry_points
|
|
23
|
+
except ImportError: # pragma: no cover — py3.12 always has it
|
|
24
|
+
return
|
|
25
|
+
try:
|
|
26
|
+
eps = entry_points(group=ENTRY_POINT_GROUP)
|
|
27
|
+
except Exception:
|
|
28
|
+
return
|
|
29
|
+
for ep in eps:
|
|
30
|
+
if ep.name in registry._plugins:
|
|
31
|
+
continue
|
|
32
|
+
try:
|
|
33
|
+
obj = ep.load()
|
|
34
|
+
plugin = obj() if callable(obj) and not isinstance(obj, Plugin) else obj
|
|
35
|
+
if isinstance(plugin, Plugin):
|
|
36
|
+
registry.register(plugin)
|
|
37
|
+
except Exception:
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_all() -> None:
|
|
42
|
+
"""Import plugin packages and register their Typer apps + checks."""
|
|
43
|
+
global _LOADED
|
|
44
|
+
if _LOADED:
|
|
45
|
+
return
|
|
46
|
+
_LOADED = True
|
|
47
|
+
try:
|
|
48
|
+
from aioli_voice.models_data import DEFAULT_BACKEND as VOICE_DEFAULT
|
|
49
|
+
from aioli_voice.models_data import MODELS as VOICE_MODELS
|
|
50
|
+
from aioli_voice.plugin import DESCRIPTION, voice_app, voice_checks
|
|
51
|
+
|
|
52
|
+
registry.register(
|
|
53
|
+
Plugin(
|
|
54
|
+
name="voice",
|
|
55
|
+
description=DESCRIPTION,
|
|
56
|
+
app=voice_app,
|
|
57
|
+
checks=voice_checks,
|
|
58
|
+
models=lambda: VOICE_MODELS,
|
|
59
|
+
default_backend=VOICE_DEFAULT,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
except ImportError:
|
|
63
|
+
pass
|
|
64
|
+
try:
|
|
65
|
+
from aioli_image.models_data import GATED_NOTE as IMAGE_GATED
|
|
66
|
+
from aioli_image.models_data import DEFAULT_BACKEND as IMAGE_DEFAULT
|
|
67
|
+
from aioli_image.models_data import MODELS as IMAGE_MODELS
|
|
68
|
+
from aioli_image.plugin import DESCRIPTION, image_app, image_checks
|
|
69
|
+
|
|
70
|
+
registry.register(
|
|
71
|
+
Plugin(
|
|
72
|
+
name="image",
|
|
73
|
+
description=DESCRIPTION,
|
|
74
|
+
app=image_app,
|
|
75
|
+
checks=image_checks,
|
|
76
|
+
models=lambda: IMAGE_MODELS,
|
|
77
|
+
default_backend=IMAGE_DEFAULT,
|
|
78
|
+
gated_note=IMAGE_GATED,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
except ImportError:
|
|
82
|
+
pass
|
|
83
|
+
try:
|
|
84
|
+
from aioli_video.models_data import GATED_NOTE as VIDEO_GATED
|
|
85
|
+
from aioli_video.models_data import DEFAULT_BACKEND as VIDEO_DEFAULT
|
|
86
|
+
from aioli_video.models_data import MODELS as VIDEO_MODELS
|
|
87
|
+
from aioli_video.plugin import DESCRIPTION, video_app, video_checks
|
|
88
|
+
|
|
89
|
+
registry.register(
|
|
90
|
+
Plugin(
|
|
91
|
+
name="video",
|
|
92
|
+
description=DESCRIPTION,
|
|
93
|
+
app=video_app,
|
|
94
|
+
checks=video_checks,
|
|
95
|
+
models=lambda: VIDEO_MODELS,
|
|
96
|
+
default_backend=VIDEO_DEFAULT,
|
|
97
|
+
gated_note=VIDEO_GATED,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
except ImportError:
|
|
101
|
+
pass
|
|
102
|
+
try:
|
|
103
|
+
from aioli_podcast.plugin import DESCRIPTION, podcast_app, podcast_checks
|
|
104
|
+
|
|
105
|
+
registry.register(
|
|
106
|
+
Plugin(
|
|
107
|
+
name="podcast",
|
|
108
|
+
description=DESCRIPTION,
|
|
109
|
+
app=podcast_app,
|
|
110
|
+
checks=podcast_checks,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
except ImportError:
|
|
114
|
+
pass
|
|
115
|
+
try:
|
|
116
|
+
from aioli_master.plugin import DESCRIPTION, master_app, master_checks
|
|
117
|
+
|
|
118
|
+
registry.register(
|
|
119
|
+
Plugin(
|
|
120
|
+
name="master",
|
|
121
|
+
description=DESCRIPTION,
|
|
122
|
+
app=master_app,
|
|
123
|
+
checks=master_checks,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
except ImportError:
|
|
127
|
+
pass
|
|
128
|
+
try:
|
|
129
|
+
from aioli_music.models_data import GATED_NOTE as MUSIC_GATED
|
|
130
|
+
from aioli_music.models_data import DEFAULT_BACKEND as MUSIC_DEFAULT
|
|
131
|
+
from aioli_music.models_data import MODELS as MUSIC_MODELS
|
|
132
|
+
from aioli_music.plugin import DESCRIPTION, music_app, music_checks
|
|
133
|
+
|
|
134
|
+
registry.register(
|
|
135
|
+
Plugin(
|
|
136
|
+
name="music",
|
|
137
|
+
description=DESCRIPTION,
|
|
138
|
+
app=music_app,
|
|
139
|
+
checks=music_checks,
|
|
140
|
+
models=lambda: MUSIC_MODELS,
|
|
141
|
+
default_backend=MUSIC_DEFAULT,
|
|
142
|
+
gated_note=MUSIC_GATED,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
except ImportError:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
_load_entry_points()
|
|
@@ -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()
|