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.
Files changed (109) hide show
  1. aioli_cli-0.1.2/.gitignore +10 -0
  2. aioli_cli-0.1.2/LICENSE +39 -0
  3. aioli_cli-0.1.2/PKG-INFO +93 -0
  4. aioli_cli-0.1.2/README.md +64 -0
  5. aioli_cli-0.1.2/pyproject.toml +114 -0
  6. aioli_cli-0.1.2/src/aioli_cli/_plugins.py +148 -0
  7. aioli_cli-0.1.2/src/aioli_cli/main.py +215 -0
  8. aioli_cli-0.1.2/src/aioli_cli/registry.py +45 -0
  9. aioli_cli-0.1.2/src/aioli_cli/setup.py +151 -0
  10. aioli_cli-0.1.2/src/aioli_contract/__init__.py +33 -0
  11. aioli_cli-0.1.2/src/aioli_contract/advise.py +169 -0
  12. aioli_cli-0.1.2/src/aioli_contract/config.py +49 -0
  13. aioli_cli-0.1.2/src/aioli_contract/doctor.py +51 -0
  14. aioli_cli-0.1.2/src/aioli_contract/fetch.py +176 -0
  15. aioli_cli-0.1.2/src/aioli_contract/probe.py +127 -0
  16. aioli_cli-0.1.2/src/aioli_image/__init__.py +3 -0
  17. aioli_cli-0.1.2/src/aioli_image/cli.py +259 -0
  18. aioli_cli-0.1.2/src/aioli_image/companions.py +232 -0
  19. aioli_cli-0.1.2/src/aioli_image/config.py +189 -0
  20. aioli_cli-0.1.2/src/aioli_image/data/models.toml +342 -0
  21. aioli_cli-0.1.2/src/aioli_image/doctor.py +48 -0
  22. aioli_cli-0.1.2/src/aioli_image/gpu.py +140 -0
  23. aioli_cli-0.1.2/src/aioli_image/launcher.py +723 -0
  24. aioli_cli-0.1.2/src/aioli_image/models.py +449 -0
  25. aioli_cli-0.1.2/src/aioli_image/models_data.py +55 -0
  26. aioli_cli-0.1.2/src/aioli_image/plugin.py +13 -0
  27. aioli_cli-0.1.2/src/aioli_image/presets.py +196 -0
  28. aioli_cli-0.1.2/src/aioli_image/stub.py +48 -0
  29. aioli_cli-0.1.2/src/aioli_image/ui.py +200 -0
  30. aioli_cli-0.1.2/src/aioli_master/cli.py +107 -0
  31. aioli_cli-0.1.2/src/aioli_master/doctor.py +23 -0
  32. aioli_cli-0.1.2/src/aioli_master/ops.py +154 -0
  33. aioli_cli-0.1.2/src/aioli_master/plugin.py +8 -0
  34. aioli_cli-0.1.2/src/aioli_music/__init__.py +3 -0
  35. aioli_cli-0.1.2/src/aioli_music/backends/__init__.py +100 -0
  36. aioli_cli-0.1.2/src/aioli_music/backends/acestep.py +92 -0
  37. aioli_cli-0.1.2/src/aioli_music/backends/base.py +151 -0
  38. aioli_cli-0.1.2/src/aioli_music/backends/llm_caption.py +172 -0
  39. aioli_cli-0.1.2/src/aioli_music/backends/minimax_comfy.py +506 -0
  40. aioli_cli-0.1.2/src/aioli_music/backends/process.py +170 -0
  41. aioli_cli-0.1.2/src/aioli_music/backends/stub.py +93 -0
  42. aioli_cli-0.1.2/src/aioli_music/backends/yue2.py +271 -0
  43. aioli_cli-0.1.2/src/aioli_music/cli_commands/__init__.py +58 -0
  44. aioli_cli-0.1.2/src/aioli_music/cli_commands/backend_cmd.py +158 -0
  45. aioli_cli-0.1.2/src/aioli_music/cli_commands/generate_cmd.py +262 -0
  46. aioli_cli-0.1.2/src/aioli_music/cli_commands/inspire_cmd.py +73 -0
  47. aioli_cli-0.1.2/src/aioli_music/cli_commands/models_cmd.py +171 -0
  48. aioli_cli-0.1.2/src/aioli_music/client.py +30 -0
  49. aioli_cli-0.1.2/src/aioli_music/config.py +395 -0
  50. aioli_cli-0.1.2/src/aioli_music/core/__init__.py +28 -0
  51. aioli_cli-0.1.2/src/aioli_music/core/formats.py +117 -0
  52. aioli_cli-0.1.2/src/aioli_music/core/generate.py +441 -0
  53. aioli_cli-0.1.2/src/aioli_music/core/jobs.py +266 -0
  54. aioli_cli-0.1.2/src/aioli_music/core/layout.py +103 -0
  55. aioli_cli-0.1.2/src/aioli_music/doctor.py +58 -0
  56. aioli_cli-0.1.2/src/aioli_music/generator.py +119 -0
  57. aioli_cli-0.1.2/src/aioli_music/history.py +140 -0
  58. aioli_cli-0.1.2/src/aioli_music/jsonstore.py +45 -0
  59. aioli_cli-0.1.2/src/aioli_music/models.py +381 -0
  60. aioli_cli-0.1.2/src/aioli_music/models_data.py +100 -0
  61. aioli_cli-0.1.2/src/aioli_music/paths.py +183 -0
  62. aioli_cli-0.1.2/src/aioli_music/plugin.py +23 -0
  63. aioli_cli-0.1.2/src/aioli_music/presets.py +183 -0
  64. aioli_cli-0.1.2/src/aioli_music/prompts.py +112 -0
  65. aioli_cli-0.1.2/src/aioli_music/server.py +394 -0
  66. aioli_cli-0.1.2/src/aioli_music/transport/__init__.py +30 -0
  67. aioli_cli-0.1.2/src/aioli_music/transport/client.py +510 -0
  68. aioli_cli-0.1.2/src/aioli_music/tui.py +171 -0
  69. aioli_cli-0.1.2/src/aioli_podcast/cast.py +83 -0
  70. aioli_cli-0.1.2/src/aioli_podcast/cli.py +54 -0
  71. aioli_cli-0.1.2/src/aioli_podcast/doctor.py +22 -0
  72. aioli_cli-0.1.2/src/aioli_podcast/episode.py +197 -0
  73. aioli_cli-0.1.2/src/aioli_podcast/plugin.py +8 -0
  74. aioli_cli-0.1.2/src/aioli_podcast/script.py +97 -0
  75. aioli_cli-0.1.2/src/aioli_video/cli.py +146 -0
  76. aioli_cli-0.1.2/src/aioli_video/doctor.py +23 -0
  77. aioli_cli-0.1.2/src/aioli_video/models_data.py +50 -0
  78. aioli_cli-0.1.2/src/aioli_video/paths.py +28 -0
  79. aioli_cli-0.1.2/src/aioli_video/plugin.py +8 -0
  80. aioli_cli-0.1.2/src/aioli_video/stub.py +30 -0
  81. aioli_cli-0.1.2/src/aioli_video/wgp.py +136 -0
  82. aioli_cli-0.1.2/src/aioli_video/worker.py +35 -0
  83. aioli_cli-0.1.2/src/aioli_voice/__init__.py +0 -0
  84. aioli_cli-0.1.2/src/aioli_voice/cli/doctor.py +260 -0
  85. aioli_cli-0.1.2/src/aioli_voice/cli/engines_cmd.py +37 -0
  86. aioli_cli-0.1.2/src/aioli_voice/cli/generate.py +129 -0
  87. aioli_cli-0.1.2/src/aioli_voice/cli/models_cmd.py +73 -0
  88. aioli_cli-0.1.2/src/aioli_voice/cli/profiles.py +122 -0
  89. aioli_cli-0.1.2/src/aioli_voice/config/__init__.py +0 -0
  90. aioli_cli-0.1.2/src/aioli_voice/config/settings.py +84 -0
  91. aioli_cli-0.1.2/src/aioli_voice/core/__init__.py +0 -0
  92. aioli_cli-0.1.2/src/aioli_voice/core/generation.py +229 -0
  93. aioli_cli-0.1.2/src/aioli_voice/core/model_manager.py +88 -0
  94. aioli_cli-0.1.2/src/aioli_voice/core/profiles.py +162 -0
  95. aioli_cli-0.1.2/src/aioli_voice/core/registry.py +65 -0
  96. aioli_cli-0.1.2/src/aioli_voice/core/types.py +59 -0
  97. aioli_cli-0.1.2/src/aioli_voice/devices.py +118 -0
  98. aioli_cli-0.1.2/src/aioli_voice/doctor.py +17 -0
  99. aioli_cli-0.1.2/src/aioli_voice/engines/__init__.py +155 -0
  100. aioli_cli-0.1.2/src/aioli_voice/engines/chatterbox.py +151 -0
  101. aioli_cli-0.1.2/src/aioli_voice/engines/kokoro.py +97 -0
  102. aioli_cli-0.1.2/src/aioli_voice/engines/moss.py +198 -0
  103. aioli_cli-0.1.2/src/aioli_voice/engines/piper.py +100 -0
  104. aioli_cli-0.1.2/src/aioli_voice/engines/qwen3.py +198 -0
  105. aioli_cli-0.1.2/src/aioli_voice/engines/stub.py +62 -0
  106. aioli_cli-0.1.2/src/aioli_voice/engines/voxcpm.py +169 -0
  107. aioli_cli-0.1.2/src/aioli_voice/engines/xtts.py +138 -0
  108. aioli_cli-0.1.2/src/aioli_voice/models_data.py +64 -0
  109. aioli_cli-0.1.2/src/aioli_voice/plugin.py +24 -0
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+
5
+ # web
6
+ web/node_modules/
7
+ web/dist/
8
+ web/.astro/
9
+
10
+ dist/
@@ -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".
@@ -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 &nbsp;•&nbsp; 🎵 Music &nbsp;•&nbsp; 🖼️ Image &nbsp;•&nbsp; 🎬 Video &nbsp;•&nbsp; 🎧 Post &nbsp;•&nbsp; 🎙️📻 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 &nbsp;•&nbsp; 🎵 Music &nbsp;•&nbsp; 🖼️ Image &nbsp;•&nbsp; 🎬 Video &nbsp;•&nbsp; 🎧 Post &nbsp;•&nbsp; 🎙️📻 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()