aioli-cli 0.1.43__tar.gz → 0.1.45__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.43 → aioli_cli-0.1.45}/PKG-INFO +1 -1
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/pyproject.toml +1 -1
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_cli/main.py +9 -4
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_contract/ui.py +115 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/cli.py +72 -17
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/companions.py +22 -14
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/config.py +131 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/data/models.toml +26 -11
- aioli_cli-0.1.45/src/aioli_image/doctor.py +150 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/gpu.py +5 -2
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/launcher.py +284 -35
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/models.py +11 -5
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/models_data.py +84 -0
- aioli_cli-0.1.45/src/aioli_image/progress.py +397 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/ui.py +49 -14
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_master/cli.py +59 -2
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/cli_commands/backend_cmd.py +54 -1
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/cli_commands/generate_cmd.py +134 -5
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/generator.py +12 -3
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/tui.py +125 -3
- aioli_cli-0.1.45/src/aioli_podcast/cli.py +303 -0
- aioli_cli-0.1.45/src/aioli_video/__init__.py +3 -0
- aioli_cli-0.1.45/src/aioli_video/cli.py +731 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/cli/doctor.py +1 -3
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/cli/generate.py +216 -4
- aioli_cli-0.1.43/src/aioli_image/doctor.py +0 -59
- aioli_cli-0.1.43/src/aioli_podcast/cli.py +0 -153
- aioli_cli-0.1.43/src/aioli_video/cli.py +0 -292
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/.gitignore +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/LICENSE +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/README.md +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_cli/_plugins.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_cli/fetch_ui.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_cli/readiness.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_cli/registry.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_cli/setup.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_cli/status.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_contract/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_contract/advise.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_contract/config.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_contract/doctor.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_contract/fetch.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_contract/probe.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_contract/readiness.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/plugin.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/presets.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_image/stub.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_master/doctor.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_master/ops.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_master/plugin.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/backends/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/backends/acestep.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/backends/base.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/backends/llm_caption.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/backends/minimax_comfy.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/backends/process.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/backends/stub.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/backends/yue2.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/cli_commands/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/cli_commands/inspire_cmd.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/cli_commands/models_cmd.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/client.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/config.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/config_data/default.toml +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/core/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/core/formats.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/core/generate.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/core/jobs.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/core/layout.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/doctor.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/history.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/jsonstore.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/models.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/models_data.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/paths.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/plugin.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/presets.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/prompts.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/server.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/transport/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_music/transport/client.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_podcast/cast.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_podcast/doctor.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_podcast/episode.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_podcast/feed.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_podcast/plugin.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_podcast/script.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_video/doctor.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_video/models_data.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_video/paths.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_video/plugin.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_video/stub.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_video/wgp.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_video/worker.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/cli/engines_cmd.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/cli/models_cmd.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/cli/profiles.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/config/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/config/settings.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/core/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/core/generation.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/core/model_manager.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/core/profiles.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/core/registry.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/core/types.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/devices.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/doctor.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/engines/__init__.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/engines/chatterbox.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/engines/kokoro.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/engines/moss.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/engines/piper.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/engines/qwen3.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/engines/stub.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/engines/voxcpm.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/engines/xtts.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/models_data.py +0 -0
- {aioli_cli-0.1.43 → aioli_cli-0.1.45}/src/aioli_voice/plugin.py +0 -0
|
@@ -6,6 +6,7 @@ from typing import Any
|
|
|
6
6
|
import typer
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
9
10
|
|
|
10
11
|
from aioli_cli.registry import registry
|
|
11
12
|
from aioli_contract import AioliConfig, run_checks
|
|
@@ -67,8 +68,12 @@ def doctor() -> None:
|
|
|
67
68
|
table.add_column("status")
|
|
68
69
|
table.add_column("detail")
|
|
69
70
|
ok = True
|
|
71
|
+
# Text(): las celdas de detalle son datos literales (los paths pueden
|
|
72
|
+
# traer '[') — nunca markup. Sin esto, la lista de raíces `[p1, p2]`
|
|
73
|
+
# se interpreta como tag de cierre y `aioli doctor` crashea con
|
|
74
|
+
# MarkupError (verify round 3, issue 1).
|
|
70
75
|
for check in run_checks():
|
|
71
|
-
table.add_row(check.name, "✅" if check.ok else "❌", check.hint)
|
|
76
|
+
table.add_row(check.name, "✅" if check.ok else "❌", Text(check.hint))
|
|
72
77
|
ok = ok and check.ok
|
|
73
78
|
for plugin in registry.all():
|
|
74
79
|
if plugin.checks is None:
|
|
@@ -77,7 +82,7 @@ def doctor() -> None:
|
|
|
77
82
|
table.add_row(
|
|
78
83
|
f"{plugin.name}/{check.name}",
|
|
79
84
|
"✅" if check.ok else "❌",
|
|
80
|
-
check.hint,
|
|
85
|
+
Text(check.hint),
|
|
81
86
|
)
|
|
82
87
|
ok = ok and check.ok
|
|
83
88
|
try:
|
|
@@ -124,12 +129,12 @@ def doctor() -> None:
|
|
|
124
129
|
table.add_row(
|
|
125
130
|
f"readiness/{check.name}",
|
|
126
131
|
"✅" if check.ok else "❌",
|
|
127
|
-
check.hint,
|
|
132
|
+
Text(check.hint),
|
|
128
133
|
)
|
|
129
134
|
ok = ok and check.ok
|
|
130
135
|
except (ImportError, OSError, RuntimeError):
|
|
131
136
|
pass
|
|
132
|
-
table.add_row("models_dir", "📁", str(cfg.models_dir))
|
|
137
|
+
table.add_row("models_dir", "📁", Text(str(cfg.models_dir)))
|
|
133
138
|
table.add_row("plugins", "🔌", ", ".join(registry.names()) or "(none yet)")
|
|
134
139
|
console.print(table)
|
|
135
140
|
if not ok:
|
|
@@ -34,11 +34,16 @@ __all__ = [
|
|
|
34
34
|
"AIOLI_GREEN",
|
|
35
35
|
"AIOLI_MARK",
|
|
36
36
|
"PROMPT_STYLE",
|
|
37
|
+
"QUALITY_PRESETS",
|
|
38
|
+
"apply_quality_preset",
|
|
39
|
+
"ask_quality_preset",
|
|
37
40
|
"confirm",
|
|
38
41
|
"header",
|
|
39
42
|
"is_interactive",
|
|
43
|
+
"measured_eta_hint",
|
|
40
44
|
"number",
|
|
41
45
|
"plain",
|
|
46
|
+
"quality_preset_steps",
|
|
42
47
|
"select",
|
|
43
48
|
"style",
|
|
44
49
|
"summary_panel",
|
|
@@ -274,3 +279,113 @@ def confirm(
|
|
|
274
279
|
amark=amark,
|
|
275
280
|
**kwargs,
|
|
276
281
|
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ── Presets de calidad rapido|calidad (un solo sitio) ─────────────────────
|
|
285
|
+
# `--preset` en image/video generate y pregunta opcional en sus wizards.
|
|
286
|
+
# El preset SOLO toca los pasos; el CFG queda estándar ("rapido = menos
|
|
287
|
+
# steps/CFG estándar; calidad = más steps"). Flags explícitos mandan siempre
|
|
288
|
+
# sobre el preset (ver `apply_quality_preset`).
|
|
289
|
+
|
|
290
|
+
QUALITY_PRESETS: tuple[str, str] = ("rapido", "calidad")
|
|
291
|
+
|
|
292
|
+
IMAGE_QUALITY_STEPS: dict[str, int] = {"rapido": 8, "calidad": 30}
|
|
293
|
+
VIDEO_QUALITY_STEPS: dict[str, int] = {"rapido": 4, "calidad": 16}
|
|
294
|
+
|
|
295
|
+
_QUALITY_STEPS_BY_DOMAIN: dict[str, dict[str, int]] = {
|
|
296
|
+
"image": IMAGE_QUALITY_STEPS,
|
|
297
|
+
"video": VIDEO_QUALITY_STEPS,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def quality_preset_steps(preset: str | None, domain: str) -> int | None:
|
|
302
|
+
"""Pasos que impone un preset de calidad, o None sin preset.
|
|
303
|
+
|
|
304
|
+
`ValueError` con preset o dominio desconocidos: el llamador lo convierte
|
|
305
|
+
en error accionable (exit 2), nunca en traceback.
|
|
306
|
+
"""
|
|
307
|
+
if preset is None:
|
|
308
|
+
return None
|
|
309
|
+
try:
|
|
310
|
+
table = _QUALITY_STEPS_BY_DOMAIN[domain]
|
|
311
|
+
except KeyError:
|
|
312
|
+
raise ValueError(f"dominio desconocido {domain!r}") from None
|
|
313
|
+
try:
|
|
314
|
+
return table[preset]
|
|
315
|
+
except KeyError:
|
|
316
|
+
raise ValueError(
|
|
317
|
+
f"preset desconocido {preset!r} (usa {'|'.join(QUALITY_PRESETS)})"
|
|
318
|
+
) from None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def apply_quality_preset(preset: str | None, steps: int | None, domain: str) -> int | None:
|
|
322
|
+
"""Resuelve pasos con precedencia flags > preset > defaults.
|
|
323
|
+
|
|
324
|
+
Un `--steps` explícito manda siempre; el preset solo rellena cuando no
|
|
325
|
+
hay paso explícito; sin preset se devuelve None (cada plugin aplica su
|
|
326
|
+
default). Valida el preset aunque haya paso explícito: un typo no pasa
|
|
327
|
+
en silencio.
|
|
328
|
+
"""
|
|
329
|
+
preset_steps = quality_preset_steps(preset, domain)
|
|
330
|
+
if steps is not None:
|
|
331
|
+
return steps
|
|
332
|
+
return preset_steps
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def ask_quality_preset() -> str | None:
|
|
336
|
+
"""Pregunta opcional de preset para los wizards (misma copia en todos).
|
|
337
|
+
|
|
338
|
+
Devuelve None (estándar, el del modelo), "rapido" o "calidad".
|
|
339
|
+
Quien llama gatea TTY antes: esto bloquea en stdin.
|
|
340
|
+
"""
|
|
341
|
+
return select(
|
|
342
|
+
"Calidad",
|
|
343
|
+
[
|
|
344
|
+
{"name": "Estándar (la del modelo)", "value": None},
|
|
345
|
+
{"name": "Rápida (menos pasos)", "value": "rapido"},
|
|
346
|
+
{"name": "Calidad (más pasos)", "value": "calidad"},
|
|
347
|
+
],
|
|
348
|
+
instruction="rapido = menos pasos · calidad = más pasos (↑↓ elegir · Ctrl-C cancelar)",
|
|
349
|
+
qmark="⚡",
|
|
350
|
+
amark="⚡",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def measured_eta_hint(rel_path: str) -> str | None:
|
|
355
|
+
"""Estimación honesta para el resumen del wizard, o None sin dato.
|
|
356
|
+
|
|
357
|
+
Busca el modelo en el probe store (`aioli image probe` guarda `secs`
|
|
358
|
+
medidos en ESTE equipo): si hay medición devuelve "~Ns (medido aquí)".
|
|
359
|
+
Sin medición devuelve None y el llamador OMITE la fila: jamás se inventa
|
|
360
|
+
un tiempo estimado (regla H3.1).
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
from aioli_contract.probe import load_store # noqa: PLC0415
|
|
364
|
+
except Exception: # noqa: BLE001 — sin probe no hay ETA, nunca crash
|
|
365
|
+
return None
|
|
366
|
+
try:
|
|
367
|
+
store = load_store()
|
|
368
|
+
except Exception: # noqa: BLE001 — store corrupto: sin ETA, nunca crash
|
|
369
|
+
return None
|
|
370
|
+
best: float | None = None
|
|
371
|
+
try:
|
|
372
|
+
for _hw, plugins in store.items():
|
|
373
|
+
if not isinstance(plugins, dict):
|
|
374
|
+
continue
|
|
375
|
+
for _plugin, models in plugins.items():
|
|
376
|
+
if not isinstance(models, dict):
|
|
377
|
+
continue
|
|
378
|
+
entry = models.get(rel_path)
|
|
379
|
+
if not isinstance(entry, dict):
|
|
380
|
+
continue
|
|
381
|
+
for mode in entry.values():
|
|
382
|
+
if not isinstance(mode, dict):
|
|
383
|
+
continue
|
|
384
|
+
secs = mode.get("secs")
|
|
385
|
+
if isinstance(secs, (int, float)) and secs > 0:
|
|
386
|
+
best = secs if best is None else min(best, secs)
|
|
387
|
+
except Exception: # noqa: BLE001 — forma inesperada: sin ETA
|
|
388
|
+
return None
|
|
389
|
+
if best is None:
|
|
390
|
+
return None
|
|
391
|
+
return f"~{best:.0f}s (medido aquí)"
|
|
@@ -11,18 +11,20 @@ from pathlib import Path
|
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
13
|
|
|
14
|
-
from aioli_contract.ui import is_interactive
|
|
14
|
+
from aioli_contract.ui import apply_quality_preset, is_interactive
|
|
15
15
|
|
|
16
16
|
from . import stub
|
|
17
17
|
from .companions import CompanionError
|
|
18
18
|
from .config import (
|
|
19
|
+
composed_scan_roots,
|
|
19
20
|
ensure_user_config,
|
|
20
21
|
load_config_safe,
|
|
21
22
|
load_state,
|
|
22
|
-
|
|
23
|
+
toml_scan_roots,
|
|
23
24
|
)
|
|
24
25
|
from .launcher import (
|
|
25
26
|
_companion_skip_note,
|
|
27
|
+
_scan_for_launch,
|
|
26
28
|
build_probe_trials,
|
|
27
29
|
cmd_doctor,
|
|
28
30
|
launch_convert,
|
|
@@ -32,7 +34,7 @@ from .launcher import (
|
|
|
32
34
|
launch_serve,
|
|
33
35
|
)
|
|
34
36
|
from .launcher import _scan_with_spinner as _scan_merged
|
|
35
|
-
from .models import find_model_by_name
|
|
37
|
+
from .models import ModelInfo, find_model_by_name
|
|
36
38
|
from .ui import console, models_table, print_banner, show_model_info
|
|
37
39
|
|
|
38
40
|
app = typer.Typer(
|
|
@@ -40,10 +42,40 @@ app = typer.Typer(
|
|
|
40
42
|
)
|
|
41
43
|
|
|
42
44
|
|
|
45
|
+
# `_toml_scan_roots` movió a `config.toml_scan_roots` (F1): el doctor la
|
|
46
|
+
# reúsa via `composed_scan_roots` para ver las mismas raíces que el CLI.
|
|
47
|
+
# Alias por compat con llamadores/tests existentes.
|
|
48
|
+
_toml_scan_roots = toml_scan_roots
|
|
49
|
+
|
|
50
|
+
|
|
43
51
|
def _resolve_context(models_dir: Path | None) -> tuple[list[Path], dict]:
|
|
44
|
-
"""Seed user config, resolve models search dirs + merged config.
|
|
52
|
+
"""Seed user config, resolve models search dirs + merged config.
|
|
53
|
+
|
|
54
|
+
Roots = `composed_scan_roots`: `models_search_dirs` (explicit/--models-dir
|
|
55
|
+
> env > fresh + legacy) plus any `[paths]` roots declared in models.toml —
|
|
56
|
+
la misma composición que usa el doctor por defecto (F1); ver
|
|
57
|
+
`config.toml_scan_roots` para el rationale add-vs-replace (AC6/AC8).
|
|
58
|
+
"""
|
|
45
59
|
ensure_user_config()
|
|
46
|
-
|
|
60
|
+
config = load_config_safe()
|
|
61
|
+
return composed_scan_roots(models_dir, config), config
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _scan_or_exit(mds: list[Path], config: dict, model_name: str | None = None) -> list[ModelInfo]:
|
|
65
|
+
"""Scan models, failing with exit 3 ONLY when the selected model is broken.
|
|
66
|
+
|
|
67
|
+
Unrelated broken models degrade to warning+skip (same `skip_broken`
|
|
68
|
+
pattern as `list`): `info -m X` / `generate -m X` / `probe -m X` fail
|
|
69
|
+
only when X itself is the broken one; model-less flows (`model_name`
|
|
70
|
+
`None`: wizard, all-probe) never abort over other models. The
|
|
71
|
+
`CompanionError` for the selected model is shown cleanly (exit 3)
|
|
72
|
+
instead of leaking a raw traceback.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
return _scan_for_launch(mds, config, model_name)
|
|
76
|
+
except CompanionError as e: # safety net: _scan_for_launch already exits 3
|
|
77
|
+
console.print(f"[red]{e}[/]")
|
|
78
|
+
raise typer.Exit(3) from None
|
|
47
79
|
|
|
48
80
|
|
|
49
81
|
def availability_lines(machine: object | None = None) -> list[str]:
|
|
@@ -84,8 +116,17 @@ def list_models(
|
|
|
84
116
|
) -> None:
|
|
85
117
|
"""List discovered diffusion models."""
|
|
86
118
|
mds, config = _resolve_context(models_dir)
|
|
87
|
-
|
|
119
|
+
# skip_broken: es una lista, no un lanzamiento. Un modelo con el companion
|
|
120
|
+
# declarado ausente se avisa y se omite; los demás siguen visibles.
|
|
121
|
+
# category="image": el wizard/lista/probe sin -m solo ofrece image (AC1/AC2).
|
|
122
|
+
wired = _scan_merged(mds, config, skip_broken=True, category="image")
|
|
88
123
|
if not wired:
|
|
124
|
+
try:
|
|
125
|
+
_scan_merged(mds, config, category="image")
|
|
126
|
+
except CompanionError as e:
|
|
127
|
+
console.print(f"[yellow]⚠ {e}[/]")
|
|
128
|
+
console.print("[red]All models skipped: broken companions.[/]")
|
|
129
|
+
raise typer.Exit(1) from None
|
|
89
130
|
console.print(f"[red]No models found in {', '.join(str(d) for d in mds)}[/]")
|
|
90
131
|
raise typer.Exit(1)
|
|
91
132
|
families = {m.family for m in wired}
|
|
@@ -103,7 +144,7 @@ def model_info(
|
|
|
103
144
|
) -> None:
|
|
104
145
|
"""Show details for one model."""
|
|
105
146
|
mds, config = _resolve_context(models_dir)
|
|
106
|
-
wired =
|
|
147
|
+
wired = _scan_or_exit(mds, config, model)
|
|
107
148
|
m = find_model_by_name(model, wired)
|
|
108
149
|
if m is None:
|
|
109
150
|
console.print(f"[red]Model not found: {model}[/]")
|
|
@@ -126,6 +167,11 @@ def generate(
|
|
|
126
167
|
height: int | None = typer.Option(None, "-H", "--height"),
|
|
127
168
|
steps: int | None = typer.Option(None, "--steps"),
|
|
128
169
|
cfg: float | None = typer.Option(None, "--cfg", help="CFG scale."),
|
|
170
|
+
preset: str | None = typer.Option(
|
|
171
|
+
None,
|
|
172
|
+
"--preset",
|
|
173
|
+
help="rapido|calidad: ajusta los pasos (CFG estándar); --steps manda.",
|
|
174
|
+
),
|
|
129
175
|
seed: int | None = typer.Option(None, "--seed"),
|
|
130
176
|
sampling_method: str | None = typer.Option(None, "--sampling-method"),
|
|
131
177
|
scheduler: str | None = typer.Option(None, "--scheduler"),
|
|
@@ -167,6 +213,14 @@ def generate(
|
|
|
167
213
|
models_dir: Path | None = typer.Option(None, "--models-dir"),
|
|
168
214
|
) -> None:
|
|
169
215
|
"""Generate an image (or video with --video-frames > 1)."""
|
|
216
|
+
# isinstance: llamada directa (tests) deja el OptionInfo sin resolver;
|
|
217
|
+
# el CLI siempre entrega str|None.
|
|
218
|
+
if isinstance(preset, str):
|
|
219
|
+
try:
|
|
220
|
+
apply_quality_preset(preset, None, "image")
|
|
221
|
+
except ValueError as exc:
|
|
222
|
+
console.print(f"[red]{exc}[/]")
|
|
223
|
+
raise typer.Exit(2) from None
|
|
170
224
|
mds, config = _resolve_context(models_dir)
|
|
171
225
|
if prompt is None:
|
|
172
226
|
# Sin prompt: wizard si hay TTY, error accionable si no (nunca colgar CI).
|
|
@@ -177,7 +231,7 @@ def generate(
|
|
|
177
231
|
)
|
|
178
232
|
raise typer.Exit(2)
|
|
179
233
|
with console.status(" [dim]scanning models…[/]", spinner="dots"):
|
|
180
|
-
wired =
|
|
234
|
+
wired = _scan_or_exit(mds, config)
|
|
181
235
|
if not wired:
|
|
182
236
|
console.print(f"[red]No models found in {', '.join(str(d) for d in mds)}[/]")
|
|
183
237
|
raise typer.Exit(1)
|
|
@@ -195,11 +249,15 @@ def generate(
|
|
|
195
249
|
height = wiz["height"]
|
|
196
250
|
if output is None and wiz["output"]:
|
|
197
251
|
output = Path(wiz["output"])
|
|
252
|
+
wiz_preset = preset if preset is not None else wiz.get("preset")
|
|
253
|
+
eff_steps = apply_quality_preset(wiz_preset, steps, "image")
|
|
198
254
|
parts: list[str] = [f'aioli image generate "{prompt}"']
|
|
199
255
|
if model:
|
|
200
256
|
parts.append(f"-m {model}")
|
|
201
257
|
if width is not None and height is not None:
|
|
202
258
|
parts += [f"-W {width}", f"-H {height}"]
|
|
259
|
+
if wiz_preset is not None:
|
|
260
|
+
parts.append(f"--preset {wiz_preset}")
|
|
203
261
|
if output is not None:
|
|
204
262
|
parts.append(f"-o {output}")
|
|
205
263
|
console.print(f"[dim]$ {' '.join(parts)}[/]")
|
|
@@ -215,7 +273,7 @@ def generate(
|
|
|
215
273
|
out=output,
|
|
216
274
|
width=width,
|
|
217
275
|
height=height,
|
|
218
|
-
steps=
|
|
276
|
+
steps=eff_steps,
|
|
219
277
|
cfg=cfg,
|
|
220
278
|
seed=seed,
|
|
221
279
|
sampling_method=sampling_method,
|
|
@@ -253,7 +311,7 @@ def generate(
|
|
|
253
311
|
from aioli_contract.probe import hardware_fingerprint, lookup
|
|
254
312
|
|
|
255
313
|
with console.status(" [dim]scanning models…[/]", spinner="dots"):
|
|
256
|
-
known_models =
|
|
314
|
+
known_models = _scan_or_exit(mds, config, model)
|
|
257
315
|
m = find_model_by_name(model, known_models)
|
|
258
316
|
if m is not None:
|
|
259
317
|
rec = lookup("image", m.rel_path, hardware_fingerprint())
|
|
@@ -270,7 +328,7 @@ def generate(
|
|
|
270
328
|
out=output,
|
|
271
329
|
width=width,
|
|
272
330
|
height=height,
|
|
273
|
-
steps=steps,
|
|
331
|
+
steps=apply_quality_preset(preset, steps, "image"),
|
|
274
332
|
cfg=cfg,
|
|
275
333
|
seed=seed,
|
|
276
334
|
sampling_method=sampling_method,
|
|
@@ -378,7 +436,7 @@ def probe(
|
|
|
378
436
|
sd_cli = require_sd_cli()
|
|
379
437
|
hw = hardware_fingerprint()
|
|
380
438
|
console.print(f"[dim]hardware: {hw}[/]")
|
|
381
|
-
models =
|
|
439
|
+
models = _scan_or_exit(mds, config, model)
|
|
382
440
|
if model:
|
|
383
441
|
m = find_model_by_name(model, models)
|
|
384
442
|
models = [m] if m else []
|
|
@@ -391,14 +449,11 @@ def probe(
|
|
|
391
449
|
mroot = Path(m.root) if m.root else mds[0]
|
|
392
450
|
try:
|
|
393
451
|
trials = build_probe_trials(
|
|
394
|
-
m, models_dir=mroot, config=config, sd_cli=sd_cli, modes=modes
|
|
452
|
+
m, models_dir=mroot, config=config, sd_cli=sd_cli, modes=modes, search_dirs=mds
|
|
395
453
|
)
|
|
396
454
|
except CompanionError as exc:
|
|
397
455
|
# Modelo que no se puede cablear: se reporta y se sigue con los
|
|
398
|
-
# demás (
|
|
399
|
-
# Ojo: build_probe_trials resuelve los dirs de config, no las
|
|
400
|
-
# raíces de `--models-dir`, así que puede fallar aunque el scan
|
|
401
|
-
# haya pasado.
|
|
456
|
+
# demás (defensa en profundidad; el scan ya filtra los rotos).
|
|
402
457
|
console.print(f" [red]⚠ {m.name}: {_companion_skip_note(exc)}[/]")
|
|
403
458
|
continue
|
|
404
459
|
for t in trials:
|
|
@@ -321,11 +321,12 @@ def find_companions(
|
|
|
321
321
|
# substring collisions (e.g. "ae.safetensors" is a substring of
|
|
322
322
|
# "z_image_vae.safetensors").
|
|
323
323
|
if role == "llm" and model.preferred_llm:
|
|
324
|
+
hint_in = model.preferred_llm.lower()
|
|
324
325
|
preferred = [
|
|
325
326
|
m
|
|
326
327
|
for m in matches
|
|
327
|
-
if
|
|
328
|
-
and not _would_be_substring_collision(
|
|
328
|
+
if hint_in in m.name.lower()
|
|
329
|
+
and not _would_be_substring_collision(hint_in, m.name.lower())
|
|
329
330
|
]
|
|
330
331
|
if not preferred:
|
|
331
332
|
# The hint is not in this dir: look in the other configured
|
|
@@ -413,19 +414,26 @@ def _preferred_llm_elsewhere(
|
|
|
413
414
|
if tdir in seen or not tdir.is_dir():
|
|
414
415
|
continue
|
|
415
416
|
seen.add(tdir)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
cand_group = _candidate_group(f.name)
|
|
426
|
-
if cand_group and cand_group != model_group:
|
|
417
|
+
try:
|
|
418
|
+
entries = list(tdir.iterdir())
|
|
419
|
+
except OSError:
|
|
420
|
+
continue
|
|
421
|
+
for f in entries:
|
|
422
|
+
try:
|
|
423
|
+
if not f.is_file() or f.suffix.lower() != ".gguf":
|
|
424
|
+
continue
|
|
425
|
+
if f.name == model.name or not pattern.search(f.name):
|
|
427
426
|
continue
|
|
428
|
-
|
|
427
|
+
low = f.name.lower()
|
|
428
|
+
if hint not in low or _would_be_substring_collision(hint, low):
|
|
429
|
+
continue
|
|
430
|
+
if model_group:
|
|
431
|
+
cand_group = _candidate_group(f.name)
|
|
432
|
+
if cand_group and cand_group != model_group:
|
|
433
|
+
continue
|
|
434
|
+
size_gb = f.stat().st_size / (1024**3)
|
|
435
|
+
except OSError:
|
|
436
|
+
continue
|
|
429
437
|
out.append(Companion(role="llm", flag="--llm", path=f, approx_gb=size_gb))
|
|
430
438
|
if not out:
|
|
431
439
|
roots = ", ".join(str(r) for r in [own_dir, *searched])
|
|
@@ -14,6 +14,7 @@ import json
|
|
|
14
14
|
import os
|
|
15
15
|
import shutil
|
|
16
16
|
import tomllib
|
|
17
|
+
import warnings
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any
|
|
19
20
|
|
|
@@ -92,6 +93,136 @@ def models_search_dirs(explicit: Path | None = None) -> list[Path]:
|
|
|
92
93
|
return dirs or [legacy.resolve()]
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
def _category_subdir_or_flat(base: Path, cat: str) -> Path:
|
|
97
|
+
"""`<base>/<cat>` si existe y no esta vacio; si no, `<base>` plano (E7)."""
|
|
98
|
+
from .models_data import CATEGORIES
|
|
99
|
+
|
|
100
|
+
if base.name in CATEGORIES:
|
|
101
|
+
return base
|
|
102
|
+
cand = base / cat
|
|
103
|
+
try:
|
|
104
|
+
if cand.is_dir() and any(cand.iterdir()):
|
|
105
|
+
return cand.resolve()
|
|
106
|
+
except OSError:
|
|
107
|
+
pass
|
|
108
|
+
return base
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resolve_category_dirs(
|
|
112
|
+
cat: str, explicit_root: Path | str | None = None, config: dict | None = None
|
|
113
|
+
) -> list[Path]:
|
|
114
|
+
"""Raices de scan para una categoria (`image`/`video`, extensible).
|
|
115
|
+
|
|
116
|
+
Precedencia (AC4): `[paths.<cat>]` > `--models-dir`/`IMAGE|AIOLI_MODELS_DIR`
|
|
117
|
+
como root+`/<cat>` > fresh XDG+legacy `~/ai/models`+`/<cat>`. `<root>/<cat>`
|
|
118
|
+
ausente o vacio implica scan plano de `<root>` (legacy flat + E7). `[]`
|
|
119
|
+
deshabilita la categoria sin fallback (E2). Un `explicit_root` que ya
|
|
120
|
+
termina en `/image|/video` es base final (E1). Entradas `[paths]` relativas
|
|
121
|
+
se resuelven contra CWD (E3, se recomienda absoluto). Categorias
|
|
122
|
+
desconocidas en `[paths]` se ignoran con warning (E6).
|
|
123
|
+
"""
|
|
124
|
+
from .models_data import CATEGORIES
|
|
125
|
+
|
|
126
|
+
cfg = config if config is not None else load_config_safe()
|
|
127
|
+
raw_paths = cfg.get("paths", {}) or {}
|
|
128
|
+
if not isinstance(raw_paths, dict):
|
|
129
|
+
raw_paths = {}
|
|
130
|
+
unknown = [k for k in raw_paths if k not in CATEGORIES]
|
|
131
|
+
if unknown:
|
|
132
|
+
warnings.warn(
|
|
133
|
+
f"Unknown categor{'y' if len(unknown) == 1 else 'ies'} in [paths]: "
|
|
134
|
+
+ ", ".join(sorted(unknown))
|
|
135
|
+
+ " (ignored)",
|
|
136
|
+
UserWarning,
|
|
137
|
+
stacklevel=2,
|
|
138
|
+
)
|
|
139
|
+
if cat in raw_paths:
|
|
140
|
+
entries = raw_paths[cat]
|
|
141
|
+
if isinstance(entries, (str, os.PathLike)):
|
|
142
|
+
entries = [entries]
|
|
143
|
+
return [Path(e).expanduser().resolve() for e in entries]
|
|
144
|
+
if explicit_root is not None:
|
|
145
|
+
base = Path(explicit_root).expanduser().resolve()
|
|
146
|
+
if base.name in CATEGORIES:
|
|
147
|
+
return [base]
|
|
148
|
+
return [_category_subdir_or_flat(base, cat)]
|
|
149
|
+
for var in ("IMAGE_MODELS_DIR", "AIOLI_MODELS_DIR"):
|
|
150
|
+
override = os.environ.get(var, "").strip()
|
|
151
|
+
if override:
|
|
152
|
+
base = Path(override).expanduser().resolve()
|
|
153
|
+
if base.name in CATEGORIES:
|
|
154
|
+
return [base]
|
|
155
|
+
return [_category_subdir_or_flat(base, cat)]
|
|
156
|
+
bases: list[Path] = []
|
|
157
|
+
fresh = _xdg_data_home() / "aioli" / "models"
|
|
158
|
+
legacy = Path.home() / "ai" / "models"
|
|
159
|
+
if fresh.is_dir():
|
|
160
|
+
bases.append(fresh.resolve())
|
|
161
|
+
try:
|
|
162
|
+
legacy_resolved = legacy.resolve()
|
|
163
|
+
except OSError:
|
|
164
|
+
legacy_resolved = legacy
|
|
165
|
+
if legacy.is_dir() and legacy_resolved not in bases:
|
|
166
|
+
bases.append(legacy_resolved)
|
|
167
|
+
if not bases:
|
|
168
|
+
bases = [legacy_resolved]
|
|
169
|
+
out: list[Path] = []
|
|
170
|
+
for base in bases:
|
|
171
|
+
d = _category_subdir_or_flat(base, cat)
|
|
172
|
+
if d not in out:
|
|
173
|
+
out.append(d)
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def toml_scan_roots(models_dir: Path | None, config: dict) -> list[Path]:
|
|
178
|
+
"""Extra scan roots contributed by `[paths]` in models.toml (AC3/AC4).
|
|
179
|
+
|
|
180
|
+
The CLI scan is cross-category ON PURPOSE (AC6: `-m <video>` must
|
|
181
|
+
resolve), so TOML entries are ADDED to the base `models_search_dirs`
|
|
182
|
+
roots and the engine's category filter keeps governing model-less
|
|
183
|
+
flows (wizard/list/probe). Replacing the base roots with
|
|
184
|
+
`resolve_category_dirs("image")` output would hide every video model
|
|
185
|
+
from `-m <video>` whenever `[paths].image` is declared — hence add.
|
|
186
|
+
|
|
187
|
+
Only TOML-declared roots are appended: the env/fresh/legacy fallback
|
|
188
|
+
branches of `resolve_category_dirs` already are (or are parents of)
|
|
189
|
+
the base roots, and adding `<root>/<cat>` would scan models twice
|
|
190
|
+
under different `rel_path` (`m.gguf` vs `image/m.gguf`), defeating
|
|
191
|
+
the rel_path dedupe. Without `[paths]` the result is byte-identical
|
|
192
|
+
to `models_search_dirs` (AC8). `[paths].<cat> = []` adds nothing
|
|
193
|
+
here: E2 disable is enforced by `resolve_category_dirs` at unit
|
|
194
|
+
level (documented divergence, see design.md E2).
|
|
195
|
+
"""
|
|
196
|
+
paths = config.get("paths")
|
|
197
|
+
if not isinstance(paths, dict) or not paths:
|
|
198
|
+
return []
|
|
199
|
+
from .models_data import CATEGORIES # noqa: PLC0415
|
|
200
|
+
|
|
201
|
+
extra: list[Path] = []
|
|
202
|
+
for cat in CATEGORIES:
|
|
203
|
+
if cat not in paths:
|
|
204
|
+
continue
|
|
205
|
+
# Full config on purpose: a declared cat returns exactly its TOML
|
|
206
|
+
# entries (precedence AC4 inside resolve_category_dirs) and unknown
|
|
207
|
+
# `[paths]` keys still raise the E6 warning at the call site.
|
|
208
|
+
for root in resolve_category_dirs(cat, models_dir, config):
|
|
209
|
+
if root not in extra:
|
|
210
|
+
extra.append(root)
|
|
211
|
+
return extra
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def composed_scan_roots(models_dir: Path | None = None, config: dict | None = None) -> list[Path]:
|
|
215
|
+
"""Raíces de scan canónicas: `models_search_dirs` + extra `[paths]` TOML.
|
|
216
|
+
|
|
217
|
+
Composición compartida (F1): la usa `_resolve_context` (CLI) y
|
|
218
|
+
`category_checks` (doctor) como default, para que ambos vean las mismas
|
|
219
|
+
raíces — un `[paths].video` custom aparece también en el doctor.
|
|
220
|
+
`config=None` carga la config de usuario.
|
|
221
|
+
"""
|
|
222
|
+
cfg = load_config_safe() if config is None else config
|
|
223
|
+
return models_search_dirs(models_dir) + toml_scan_roots(models_dir, cfg)
|
|
224
|
+
|
|
225
|
+
|
|
95
226
|
def resolve_output_dir() -> Path:
|
|
96
227
|
"""Output directory for generated images.
|
|
97
228
|
|
|
@@ -323,9 +323,19 @@ preferred_llm = "gemma-3-12b"
|
|
|
323
323
|
preferred_vae = "ltx-2.3-22b-dev_video_vae"
|
|
324
324
|
encoder_mode = "balanced-v2"
|
|
325
325
|
|
|
326
|
-
#
|
|
327
|
-
# 11.
|
|
328
|
-
#
|
|
326
|
+
# LTX-2.5-Distilled (22B) — video nativo, vía sd-cli vid_gen. Pesa 14.6 GiB
|
|
327
|
+
# el Q4_K_M y 11.8 GiB el Q3_K_S (16.0 / 13.2 GiB contando su VAE de 1.4 GiB),
|
|
328
|
+
# así que los dos llevan tier `high` — igual que los 22B de ltx-2.3: `_compute_
|
|
329
|
+
# encoder_mode` responde `balanced-v2` (diffusión en CPU) en auto/cuda0/cuda1 en
|
|
330
|
+
# esta doble tarjeta, o sea que NINGUNO queda residente en VRAM. El tier NO lo
|
|
331
|
+
# valida el probe de esta máquina: las 6 combinaciones abortan en ~1.1 s por
|
|
332
|
+
# validación de metadatos del runtime (falta el VAE variante conv y el Gemma4 en
|
|
333
|
+
# GGUF → docs/WEIGHTS.md "Pendientes"), antes de asignar VRAM. Se re-probea al
|
|
334
|
+
# entrar esos pesos.
|
|
335
|
+
# `no_llm`: el único companion utilizable del dir es el VAE; el
|
|
336
|
+
# `gemma4-...-comfy-int8-convrot.safetensors` es un proyector CLIP de ComfyUI,
|
|
337
|
+
# no un LLM de sd.cpp, así que NO se declara preferred_llm (degradación con
|
|
338
|
+
# VAE-only, nunca crash).
|
|
329
339
|
[models."video/minimax_h3_ref2va_pruned-Q4_K_M.gguf"]
|
|
330
340
|
family = "minimax"
|
|
331
341
|
vram_tier = "high"
|
|
@@ -346,12 +356,15 @@ encoder_mode = "balanced-v2"
|
|
|
346
356
|
# Stack: difusion Q4_K_M 4.19 GB + VAE 2.1 (NO intercambiable con el de 1.x) +
|
|
347
357
|
# Qwen3-VL-8B-Instruct GGUF (~5 GB) + su mmproj para editar.
|
|
348
358
|
# - dims multiplos de 32 y euler: handoff V8.4.
|
|
349
|
-
# - cfg 6.0
|
|
350
|
-
#
|
|
351
|
-
#
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
#
|
|
359
|
+
# - cfg 6.0, euler, dims %32 y "flow schedule segun resolucion" (sin
|
|
360
|
+
# --flow-shift) FIJADOS por docs/qwen_image_2.1.md de sd.cpp master, leida
|
|
361
|
+
# 2026-09-21 (los checkouts locales del engine son Aug-30 / 2026-09-17 y aun
|
|
362
|
+
# no la traen; 1.x y Edit usan cfg 2.5 + --flow-shift 3, NO vale aqui). Lo
|
|
363
|
+
# que la doc no fija sigue provisional: steps (20) y encoder_mode.
|
|
364
|
+
# - encoder_mode sin curar: el probe no pasa de la validacion de metadatos con
|
|
365
|
+
# el runtime vigente (runtimes-v4 / sd-cli de298c2), asi que no hay dato
|
|
366
|
+
# observational. Tamano real en disco: 3.9 difusor + 4.7 LLM + 1.1 mmproj +
|
|
367
|
+
# 0.6 VAE = 10.3 GB -> "balanced" o "gpu" siguen siendo la hipotesis.
|
|
355
368
|
# `wants_llm_vision = true` es lo que deja adjuntar el mmproj con --llm_vision
|
|
356
369
|
# (companions.py): sin flag, un mmproj en la carpeta no se toca nunca.
|
|
357
370
|
[models."image/Qwen-Image-2.1-Q4_K_M.gguf"]
|
|
@@ -387,7 +400,8 @@ family = "ltx"
|
|
|
387
400
|
experimental = true
|
|
388
401
|
requires_runtime = "post-2026-09-20"
|
|
389
402
|
vram_tier = "high"
|
|
390
|
-
preferred_llm = "
|
|
403
|
+
preferred_llm = ""
|
|
404
|
+
no_llm = true # hasta que exista el gemma4-...-Q8_0.gguf convertido
|
|
391
405
|
preferred_vae = "ltx-2.5-video-vae-conv"
|
|
392
406
|
preferred_audio_vae = "ltx-2.5-audio-vae"
|
|
393
407
|
deny_roles = ["embeddings_connectors"]
|
|
@@ -400,7 +414,8 @@ family = "ltx"
|
|
|
400
414
|
experimental = true
|
|
401
415
|
requires_runtime = "post-2026-09-20"
|
|
402
416
|
vram_tier = "high"
|
|
403
|
-
preferred_llm = "
|
|
417
|
+
preferred_llm = ""
|
|
418
|
+
no_llm = true # hasta que exista el gemma4-...-Q8_0.gguf convertido
|
|
404
419
|
preferred_vae = "ltx-2.5-video-vae-conv"
|
|
405
420
|
preferred_audio_vae = "ltx-2.5-audio-vae"
|
|
406
421
|
deny_roles = ["embeddings_connectors"]
|