aioli-cli 0.1.40__tar.gz → 0.1.41__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 (116) hide show
  1. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/PKG-INFO +1 -1
  2. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/pyproject.toml +1 -1
  3. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/cli.py +5 -0
  4. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/launcher.py +93 -6
  5. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/ui.py +7 -0
  6. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/.gitignore +0 -0
  7. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/LICENSE +0 -0
  8. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/README.md +0 -0
  9. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_cli/_plugins.py +0 -0
  10. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_cli/fetch_ui.py +0 -0
  11. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_cli/main.py +0 -0
  12. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_cli/readiness.py +0 -0
  13. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_cli/registry.py +0 -0
  14. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_cli/setup.py +0 -0
  15. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_cli/status.py +0 -0
  16. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_contract/__init__.py +0 -0
  17. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_contract/advise.py +0 -0
  18. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_contract/config.py +0 -0
  19. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_contract/doctor.py +0 -0
  20. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_contract/fetch.py +0 -0
  21. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_contract/probe.py +0 -0
  22. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_contract/readiness.py +0 -0
  23. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_contract/ui.py +0 -0
  24. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/__init__.py +0 -0
  25. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/companions.py +0 -0
  26. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/config.py +0 -0
  27. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/data/models.toml +0 -0
  28. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/doctor.py +0 -0
  29. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/gpu.py +0 -0
  30. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/models.py +0 -0
  31. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/models_data.py +0 -0
  32. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/plugin.py +0 -0
  33. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/presets.py +0 -0
  34. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_image/stub.py +0 -0
  35. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_master/cli.py +0 -0
  36. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_master/doctor.py +0 -0
  37. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_master/ops.py +0 -0
  38. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_master/plugin.py +0 -0
  39. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/__init__.py +0 -0
  40. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/backends/__init__.py +0 -0
  41. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/backends/acestep.py +0 -0
  42. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/backends/base.py +0 -0
  43. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/backends/llm_caption.py +0 -0
  44. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/backends/minimax_comfy.py +0 -0
  45. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/backends/process.py +0 -0
  46. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/backends/stub.py +0 -0
  47. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/backends/yue2.py +0 -0
  48. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/cli_commands/__init__.py +0 -0
  49. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/cli_commands/backend_cmd.py +0 -0
  50. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/cli_commands/generate_cmd.py +0 -0
  51. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/cli_commands/inspire_cmd.py +0 -0
  52. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/cli_commands/models_cmd.py +0 -0
  53. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/client.py +0 -0
  54. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/config.py +0 -0
  55. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/config_data/default.toml +0 -0
  56. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/core/__init__.py +0 -0
  57. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/core/formats.py +0 -0
  58. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/core/generate.py +0 -0
  59. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/core/jobs.py +0 -0
  60. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/core/layout.py +0 -0
  61. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/doctor.py +0 -0
  62. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/generator.py +0 -0
  63. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/history.py +0 -0
  64. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/jsonstore.py +0 -0
  65. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/models.py +0 -0
  66. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/models_data.py +0 -0
  67. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/paths.py +0 -0
  68. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/plugin.py +0 -0
  69. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/presets.py +0 -0
  70. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/prompts.py +0 -0
  71. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/server.py +0 -0
  72. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/transport/__init__.py +0 -0
  73. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/transport/client.py +0 -0
  74. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_music/tui.py +0 -0
  75. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_podcast/cast.py +0 -0
  76. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_podcast/cli.py +0 -0
  77. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_podcast/doctor.py +0 -0
  78. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_podcast/episode.py +0 -0
  79. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_podcast/feed.py +0 -0
  80. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_podcast/plugin.py +0 -0
  81. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_podcast/script.py +0 -0
  82. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_video/cli.py +0 -0
  83. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_video/doctor.py +0 -0
  84. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_video/models_data.py +0 -0
  85. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_video/paths.py +0 -0
  86. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_video/plugin.py +0 -0
  87. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_video/stub.py +0 -0
  88. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_video/wgp.py +0 -0
  89. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_video/worker.py +0 -0
  90. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/__init__.py +0 -0
  91. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/cli/doctor.py +0 -0
  92. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/cli/engines_cmd.py +0 -0
  93. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/cli/generate.py +0 -0
  94. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/cli/models_cmd.py +0 -0
  95. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/cli/profiles.py +0 -0
  96. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/config/__init__.py +0 -0
  97. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/config/settings.py +0 -0
  98. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/core/__init__.py +0 -0
  99. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/core/generation.py +0 -0
  100. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/core/model_manager.py +0 -0
  101. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/core/profiles.py +0 -0
  102. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/core/registry.py +0 -0
  103. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/core/types.py +0 -0
  104. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/devices.py +0 -0
  105. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/doctor.py +0 -0
  106. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/engines/__init__.py +0 -0
  107. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/engines/chatterbox.py +0 -0
  108. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/engines/kokoro.py +0 -0
  109. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/engines/moss.py +0 -0
  110. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/engines/piper.py +0 -0
  111. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/engines/qwen3.py +0 -0
  112. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/engines/stub.py +0 -0
  113. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/engines/voxcpm.py +0 -0
  114. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/engines/xtts.py +0 -0
  115. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/models_data.py +0 -0
  116. {aioli_cli-0.1.40 → aioli_cli-0.1.41}/src/aioli_voice/plugin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: aioli-cli
3
- Version: 0.1.40
3
+ Version: 0.1.41
4
4
  Summary: aioli — local-first AI media suite CLI (images, video, music, voice, podcast)
5
5
  Author: aioli contributors
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioli-cli"
3
- version = "0.1.40"
3
+ version = "0.1.41"
4
4
  description = "aioli — local-first AI media suite CLI (images, video, music, voice, podcast)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -159,6 +159,9 @@ def generate(
159
159
  stub_mode: bool = typer.Option(
160
160
  False, "--stub", help="Write a placeholder PNG (offline tests, no GPU)."
161
161
  ),
162
+ verbose: bool = typer.Option(
163
+ False, "--verbose", help="Muestra los logs de sd-cli en vivo (por defecto se capturan)."
164
+ ),
162
165
  models_dir: Path | None = typer.Option(None, "--models-dir"),
163
166
  ) -> None:
164
167
  """Generate an image (or video with --video-frames > 1)."""
@@ -229,6 +232,7 @@ def generate(
229
232
  gpu=gpu,
230
233
  dry_run=dry_run,
231
234
  picker=False,
235
+ verbose=verbose,
232
236
  )
233
237
  if vae_tile_size is not None and not vae_tiling:
234
238
  typer.echo("ERROR: --vae-tile-size requires --vae-tiling.", err=True)
@@ -283,6 +287,7 @@ def generate(
283
287
  gpu=gpu,
284
288
  dry_run=dry_run,
285
289
  picker=model is None,
290
+ verbose=verbose,
286
291
  )
287
292
 
288
293
 
@@ -6,6 +6,7 @@ import os
6
6
  import shlex
7
7
  import subprocess
8
8
  import sys
9
+ import time
9
10
  from dataclasses import replace
10
11
  from pathlib import Path
11
12
 
@@ -327,6 +328,62 @@ def _default_output_path(model: ModelInfo, video: bool = False) -> Path:
327
328
  return (out_dir / f"{stem}_{ts}{ext}").resolve()
328
329
 
329
330
 
331
+ _KNOWN_OUTPUT_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".webm", ".mp4")
332
+
333
+
334
+ def _resolve_actual_output(requested: Path) -> Path | None:
335
+ """Resolve the real artefact sd-cli left on disk.
336
+
337
+ sd-cli appends an image extension when `-o` has no suffix
338
+ (`-o superheroe` -> `superheroe.png`), so the requested path alone
339
+ false-negatives. If `requested` exists and is non-empty it wins;
340
+ otherwise probe `<stem><known-ext>` in the destination dir and CWD.
341
+ Returns None when nothing valid is found.
342
+ """
343
+ try:
344
+ if requested.is_file() and requested.stat().st_size > 0:
345
+ return requested
346
+ except OSError:
347
+ pass
348
+ stem = requested.stem if requested.suffix else requested.name
349
+ if not stem:
350
+ return None
351
+ ordered_dirs: list[Path] = []
352
+ seen: set[Path] = set()
353
+ for d in (requested.parent, Path.cwd()):
354
+ try:
355
+ key = d.resolve()
356
+ except OSError:
357
+ key = d
358
+ if key not in seen:
359
+ seen.add(key)
360
+ ordered_dirs.append(d)
361
+ for d in ordered_dirs:
362
+ for ext in _KNOWN_OUTPUT_EXTS:
363
+ cand = d / f"{stem}{ext}"
364
+ try:
365
+ if cand.is_file() and cand.stat().st_size > 0:
366
+ return cand
367
+ except OSError:
368
+ continue
369
+ return None
370
+
371
+
372
+ def _log_tail_lines(exc: BaseException, limit: int = 5) -> list[str]:
373
+ """Last non-blank log lines from a failed capture (stdout + stderr)."""
374
+ lines: list[str] = []
375
+ for chunk in (getattr(exc, "stdout", None), getattr(exc, "stderr", None)):
376
+ if not chunk:
377
+ continue
378
+ if isinstance(chunk, bytes):
379
+ try:
380
+ chunk = chunk.decode(errors="replace")
381
+ except Exception:
382
+ chunk = str(chunk)
383
+ lines.extend(str(chunk).splitlines())
384
+ return [ln for ln in lines if ln.strip()][-limit:]
385
+
386
+
330
387
  def _image_dimensions(path: Path) -> tuple[int, int] | None:
331
388
  """Read the pixel size (width, height) of a PNG/JPEG/WebP without deps.
332
389
 
@@ -541,6 +598,7 @@ def launch_gen(
541
598
  gpu: str = "auto",
542
599
  dry_run: bool = False,
543
600
  picker: bool = True,
601
+ verbose: bool = False,
544
602
  ) -> None:
545
603
  """Generate an image (or video with `video_frames > 1`)."""
546
604
  sd_cli = require_sd_cli()
@@ -724,12 +782,33 @@ def launch_gen(
724
782
  f"{final_steps} steps {final_method} · GPU {gpu} → [bold]{final_out.name}[/]"
725
783
  )
726
784
  console.print("[dim] cargando modelo (la primera vez tarda)…[/]\n")
785
+ start = time.monotonic()
727
786
  try:
728
- subprocess.run(shlex.split(cmd), check=True, env=run_env)
787
+ if verbose:
788
+ subprocess.run(shlex.split(cmd), check=True, env=run_env)
789
+ else:
790
+ status_msg = (
791
+ f"🎨 generando… {final_width}×{final_height} · "
792
+ f"{final_steps} pasos — el detalle con --verbose"
793
+ )
794
+ with console.status(status_msg, spinner="dots"):
795
+ subprocess.run(
796
+ shlex.split(cmd), check=True, env=run_env, capture_output=True, text=True
797
+ )
798
+ elapsed = time.monotonic() - start
729
799
  # D17: exit 0 no es éxito si no hay artefacto. Fichero ausente o
730
- # vacío = error con código propio (3), nunca "✓ saved".
800
+ # vacío = error con código propio (3), nunca "✓ saved". sd-cli
801
+ # añade la extensión cuando `-o` va sin sufijo (`-o superheroe` ->
802
+ # `superheroe.png`): resolver la salida real antes de fallar.
803
+ real_out = _resolve_actual_output(final_out)
804
+ if real_out is None:
805
+ console.print(
806
+ f"[red]❌ sd-cli terminó pero no dejó imagen válida en {final_out}[/]\n"
807
+ "[dim] (ausente o 0 bytes: disco lleno, OOM silenciado o bug del runtime)[/]"
808
+ )
809
+ sys.exit(3)
731
810
  try:
732
- size = final_out.stat().st_size
811
+ size = real_out.stat().st_size
733
812
  except OSError:
734
813
  size = -1
735
814
  if size <= 0:
@@ -739,17 +818,25 @@ def launch_gen(
739
818
  )
740
819
  sys.exit(3)
741
820
  size_mb = size / 1024 / 1024
742
- console.print(f"\n[green]✓ saved[/] [bold]{final_out}[/] ({size_mb:.1f} MB)")
821
+ console.print(f"\n[green]✓ saved[/] [bold]{real_out}[/] ({size_mb:.1f} MB, {elapsed:.1f}s)")
743
822
  # Show absolute path again so the user can find it from any CWD.
744
- console.print(f" [dim]→ {final_out.resolve()}[/]")
823
+ console.print(f" [dim]→ {real_out.resolve()}[/]")
745
824
  except subprocess.CalledProcessError as e:
746
825
  console.print(f"[red]sd-cli exited with code {e.returncode}[/]")
826
+ for line in _log_tail_lines(e):
827
+ console.print(f"[dim] │ {line.strip()}[/]")
747
828
  if e.returncode == -4:
748
829
  console.print(
749
830
  "[dim] código -4 = SIGILL: binario con instrucciones que tu CPU no tiene. "
750
831
  "Actualiza el runtime: aioli models download image -b <motor> --force[/]"
751
832
  )
752
- sys.exit(e.returncode)
833
+ console.print(
834
+ "[yellow]⚠ este modelo puede estar mal descargado o no soportado "
835
+ "por tu versión del motor; prueba otro modelo "
836
+ "(`aioli image generate` y elige otro) o reintenta con "
837
+ "--verbose para el detalle[/]"
838
+ )
839
+ sys.exit(1)
753
840
  except KeyboardInterrupt:
754
841
  console.print("\n[dim] cancelled.[/]")
755
842
  sys.exit(130)
@@ -407,6 +407,13 @@ def run_generate_wizard(models: list[ModelInfo], config: dict) -> dict | None:
407
407
  console.print("[yellow]Sin prompt no hay imagen.[/]")
408
408
  return None
409
409
  prompt = prompt.strip()
410
+ if len(prompt) >= 2 and (
411
+ (prompt[0] == '"' and prompt[-1] == '"') or (prompt[0] == "'" and prompt[-1] == "'")
412
+ ):
413
+ prompt = prompt[1:-1].strip()
414
+ if not prompt:
415
+ console.print("[yellow]Sin prompt no hay imagen.[/]")
416
+ return None
410
417
 
411
418
  model = pick_model(models, config)
412
419
  if model is None:
File without changes
File without changes
File without changes