feat(ml): auto-commit con 14 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 01:22:02 +02:00
parent 88b5b27dc0
commit aec5d82011
14 changed files with 1302 additions and 0 deletions
@@ -0,0 +1,76 @@
---
name: sdcpp_python_generate
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: impure
signature: "def sdcpp_python_generate(sd: Any, cfg: GenerationConfig) -> ImageGenResult"
description: "Genera una imagen con un StableDiffusion (stable-diffusion-cpp-python) usando GenerationConfig como contrato. Mapea sampler, mide duracion y retorna ImageGenResult con meta del backend."
tags: [ml, stable-diffusion, sdcpp, inference, backend, generate, txt2img]
uses_functions: [sdcpp_python_load_py_ml]
uses_types: [generation_config_py_ml, image_gen_result_py_ml]
returns: [image_gen_result_py_ml]
returns_optional: false
error_type: "error_go_core"
imports: [stable_diffusion_cpp, PIL]
params:
- name: sd
desc: "Instancia StableDiffusion cargada via sdcpp_python_load. Debe tener metodo generate_image()."
- name: cfg
desc: "Contrato de parametros de generacion. cfg.sampler debe ser uno de: euler | euler_a | dpm++2m | dpm++2m_v2 | heun | dpm2 | lcm."
output: "ImageGenResult con image=PIL.Image (primera del batch), meta con backend/model/sampler/seed/wtype, duration_ms medido, vram_peak_mb=None."
tested: true
tests:
- "generate retorna ImageGenResult valido"
- "duration_ms mayor que cero"
- "meta backend es sdcpp_python"
test_file_path: "python/functions/ml/tests/test_sdcpp_python_backend.py"
file_path: "python/functions/ml/sdcpp_python_generate.py"
---
## Ejemplo
```python
import sys
sys.path.insert(0, "python/functions/ml")
from model_ref import ModelRef
from generation_config import GenerationConfig
from sdcpp_python_load import sdcpp_python_load
from sdcpp_python_generate import sdcpp_python_generate
model = ModelRef(
name="sd-turbo",
model_type="sd15",
quantization="fp16",
path="/home/lucas/vaults/imagegen_models/diffusers/sd-turbo/sd_turbo.safetensors",
)
sd = sdcpp_python_load(model)
cfg = GenerationConfig(
prompt="a red cat sitting on a wooden table",
seed=42,
steps=4,
cfg_scale=1.0,
sampler="euler_a",
width=512,
height=512,
model=model,
)
result = sdcpp_python_generate(sd, cfg)
result.image.save("/tmp/output.png")
print(f"Generado en {result.duration_ms}ms, meta={result.meta}")
```
## Notas
- El sampler mapping canonico: euler->euler, euler_a->euler_a, dpm++2m->dpmpp2m,
dpm++2m_v2->dpmpp2mv2, heun->heun, dpm2->dpm2, lcm->lcm.
- API usada: `StableDiffusion.generate_image()` (binding 0.4.7+). Versiones anteriores
exponían `txt_to_img()` — actualizar el package si se encuentra ese error.
- `vram_peak_mb` siempre None: stable-diffusion-cpp-python no expone medicion de VRAM.
- `clip_skip`: -1 le dice al backend que use el valor por defecto del modelo (equivale a
no especificarlo). Si cfg.clip_skip es None, se pasa -1.
- El campo `wtype` en meta se extrae via `getattr(sd, 'wtype', 'unknown')` ya que el
binding no garantiza el atributo en todas las versiones.
---
@@ -0,0 +1,103 @@
"""sdcpp_python_generate — genera una imagen con stable-diffusion-cpp-python a partir de GenerationConfig."""
from __future__ import annotations
import sys
import os
import time
from typing import Any
sys.path.insert(0, os.path.dirname(__file__))
from generation_config import GenerationConfig
from image_gen_result import ImageGenResult
# Mapa de sampler del registry al nombre que espera stable-diffusion-cpp-python
_SAMPLER_MAP: dict[str, str] = {
"euler": "euler",
"euler_a": "euler_a",
"dpm++2m": "dpmpp2m",
"dpm++2m_v2": "dpmpp2mv2",
"heun": "heun",
"dpm2": "dpm2",
"lcm": "lcm",
}
def sdcpp_python_generate(sd: Any, cfg: GenerationConfig) -> ImageGenResult:
"""Genera una imagen con un objeto StableDiffusion usando GenerationConfig como contrato.
Mapea los campos del GenerationConfig canonico a los parametros de
StableDiffusion.generate_image(). Mide la duracion total de la llamada.
Retorna un ImageGenResult con la primera imagen del batch, metadata del backend
y duracion en milisegundos. VRAM no se mide (None).
Args:
sd: Instancia StableDiffusion cargada via sdcpp_python_load.
cfg: Contrato de parametros de generacion. Todos los campos son leidos.
cfg.sampler debe ser uno de los valores del SamplerName del registry.
Returns:
ImageGenResult con image=PIL.Image, meta con backend/modelo/sampler/seed/wtype,
duration_ms medido via time.perf_counter(), vram_peak_mb=None.
Raises:
KeyError: Si cfg.sampler no tiene correspondencia en _SAMPLER_MAP.
ImportError: Si stable_diffusion_cpp no esta instalado.
RuntimeError: Si generate_image retorna lista vacia o None.
"""
try:
from stable_diffusion_cpp import StableDiffusion # noqa: F401 — verifica disponibilidad
except ImportError as exc:
raise ImportError(
"sdcpp_python_generate requiere stable-diffusion-cpp-python. "
"Instalar con: pip install stable-diffusion-cpp-python"
) from exc
sample_method = _SAMPLER_MAP.get(cfg.sampler)
if sample_method is None:
raise KeyError(
f"Sampler '{cfg.sampler}' no tiene correspondencia en sdcpp_python_generate. "
f"Valores soportados: {list(_SAMPLER_MAP.keys())}"
)
# wtype del objeto sd (para metadata)
wtype = getattr(sd, "wtype", "unknown")
t0 = time.perf_counter()
images = sd.generate_image(
prompt=cfg.prompt,
negative_prompt=cfg.negative_prompt or "",
cfg_scale=cfg.cfg_scale,
sample_method=sample_method,
sample_steps=cfg.steps,
seed=cfg.seed,
width=cfg.width,
height=cfg.height,
clip_skip=cfg.clip_skip if cfg.clip_skip is not None else -1,
batch_count=1,
)
duration_ms = int((time.perf_counter() - t0) * 1000)
if not images:
raise RuntimeError(
"sdcpp_python_generate: generate_image retorno lista vacia o None."
)
meta: dict[str, Any] = {
"backend": "sdcpp_python",
"model": cfg.model.name,
"sampler": cfg.sampler,
"actual_steps": cfg.steps,
"seed": cfg.seed,
"wtype": wtype,
}
return ImageGenResult(
image=images[0],
meta=meta,
duration_ms=duration_ms,
vram_peak_mb=None,
)
+61
View File
@@ -0,0 +1,61 @@
---
name: sdcpp_python_load
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: impure
signature: "def sdcpp_python_load(model: ModelRef, n_threads: int = -1, wtype: str = 'default', rng_type: str = 'cuda') -> Any"
description: "Carga un StableDiffusion via stable-diffusion-cpp-python con cache global por (model_key, wtype, n_threads). Segunda llamada con mismos params retorna instancia cacheada sin recargar disco."
tags: [ml, stable-diffusion, sdcpp, inference, backend, cache, load]
uses_functions: []
uses_types: [model_ref_py_ml]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [stable_diffusion_cpp]
params:
- name: model
desc: "Referencia al modelo. model.path se usa si presente; si no, model.name como ruta local o HuggingFace hub."
- name: n_threads
desc: "Numero de hilos CPU para inferencia. -1 usa todos los disponibles."
- name: wtype
desc: "Tipo de pesos en memoria: 'default' | 'f32' | 'f16' | 'q8_0' | 'q5_1' | 'q5_0' | 'q4_1' | 'q4_0'. 'default' respeta el tipo original del checkpoint."
- name: rng_type
desc: "Generador de aleatorios: 'std_default' | 'cuda'. 'cuda' produce resultados compatibles con la implementacion CUDA incluso en CPU."
output: "Instancia StableDiffusion (stable_diffusion_cpp.StableDiffusion) lista para llamar a generate_image()."
tested: true
tests:
- "load retorna objeto StableDiffusion"
- "segunda llamada retorna instancia cacheada"
test_file_path: "python/functions/ml/tests/test_sdcpp_python_backend.py"
file_path: "python/functions/ml/sdcpp_python_load.py"
---
## Ejemplo
```python
import sys
sys.path.insert(0, "python/functions/ml")
from model_ref import ModelRef
from sdcpp_python_load import sdcpp_python_load
model = ModelRef(
name="sd-turbo",
model_type="sd15",
quantization="fp16",
path="/home/lucas/vaults/imagegen_models/diffusers/sd-turbo/sd_turbo.safetensors",
)
sd = sdcpp_python_load(model, n_threads=-1, wtype="default")
# sd listo para sd.generate_image(...)
```
## Notas
- El cache evita recargas de disco en bucles de generacion con el mismo modelo.
- `wtype="default"` respeta el tipo de cuantizacion del checkpoint; util para safetensors mixtos.
- `rng_type="cuda"` produce seeds compatibles con la implementacion GPU aunque se corra en CPU.
- Para limpiar el cache en tests: `sdcpp_python_load._clear_sd_cache()`.
- Compilacion sin CUDA: `CMAKE_ARGS="-DSD_CUDA=OFF" pip install stable-diffusion-cpp-python`.
- El binding 0.4.7 usa `generate_image()` (no `txt_to_img` que era la API de versiones anteriores).
---
+86
View File
@@ -0,0 +1,86 @@
"""sdcpp_python_load — carga un StableDiffusion (stable-diffusion-cpp-python) con cache global."""
from __future__ import annotations
import sys
import os
from typing import Any
sys.path.insert(0, os.path.dirname(__file__))
from model_ref import ModelRef
# Cache global: (model_key, wtype, n_threads) -> StableDiffusion object
_SD_CACHE: dict[tuple[str, str, int], Any] = {}
def _get_model_key(model: ModelRef) -> str:
"""Retorna la clave de cache para un ModelRef."""
return model.path if model.path else model.name
def sdcpp_python_load(
model: ModelRef,
n_threads: int = -1,
wtype: str = "default",
rng_type: str = "cuda",
) -> Any:
"""Carga un StableDiffusion via stable-diffusion-cpp-python con cache global.
Instancia StableDiffusion con el checkpoint indicado por model.path (o model.name
si path es None). Los objetos se cachean en memoria por (model_key, wtype, n_threads)
— una segunda llamada con los mismos parametros retorna la instancia cacheada sin
recargar el modelo del disco.
Args:
model: Referencia al modelo. model.path se usa si esta presente;
si no, model.name se pasa como model_path (ruta local o hub).
n_threads: Numero de hilos de CPU para inferencia. -1 usa todos los disponibles.
wtype: Tipo de pesos / cuantizacion en memoria.
Valores: "default" | "f32" | "f16" | "q8_0" | "q5_1" | "q5_0" | "q4_1" | "q4_0".
"default" respeta el tipo original del checkpoint.
rng_type: Tipo de generador de numeros aleatorios.
Valores: "std_default" | "cuda".
"cuda" produce resultados compatibles con la implementacion CUDA
incluso en CPU.
Returns:
Instancia StableDiffusion lista para llamar a generate_image().
Raises:
ImportError: Si stable_diffusion_cpp no esta instalado.
Instalar con: pip install stable-diffusion-cpp-python
OSError: Si el path del modelo no existe o es invalido.
"""
try:
from stable_diffusion_cpp import StableDiffusion
except ImportError as exc:
raise ImportError(
"sdcpp_python_load requiere stable-diffusion-cpp-python. "
"Instalar con: pip install stable-diffusion-cpp-python\n"
"Para compilar sin CUDA: "
"CMAKE_ARGS='-DSD_CUDA=OFF' pip install stable-diffusion-cpp-python"
) from exc
model_key = _get_model_key(model)
cache_key = (model_key, wtype, n_threads)
if cache_key in _SD_CACHE:
return _SD_CACHE[cache_key]
load_path = model.path if model.path else model.name
sd = StableDiffusion(
model_path=load_path,
wtype=wtype,
n_threads=n_threads,
rng_type=rng_type,
)
_SD_CACHE[cache_key] = sd
return sd
def _clear_sd_cache() -> None:
"""Limpia el cache global de instancias StableDiffusion (uso interno y tests)."""
_SD_CACHE.clear()
@@ -0,0 +1,167 @@
"""Tests para el backend sdcpp_python: sdcpp_python_load y sdcpp_python_generate."""
from __future__ import annotations
import sys
import os
import pytest
# Ajustar path para importar desde python/functions/ml/
_ML_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
sys.path.insert(0, _ML_PATH)
# Importacion lazy — salta todos los tests si el package no esta instalado.
pytest.importorskip(
"stable_diffusion_cpp",
reason="stable_diffusion_cpp no instalado — skip tests sdcpp_python backend",
)
# El paquete usa modulos hermanos sin prefijo (model_ref, generation_config...).
# Para evitar el double-import problem, mapeamos los aliases antes de importar
# las funciones bajo test.
import ml.model_ref as _mref_module
import ml.generation_config as _gcfg_module
import ml.image_gen_result as _igr_module
for _alias, _mod in [
("model_ref", _mref_module),
("generation_config", _gcfg_module),
("image_gen_result", _igr_module),
]:
if _alias not in sys.modules:
sys.modules[_alias] = _mod # type: ignore[assignment]
from ml.model_ref import ModelRef
from ml.generation_config import GenerationConfig
from ml.image_gen_result import ImageGenResult
from ml.sdcpp_python_load import sdcpp_python_load, _clear_sd_cache
from ml.sdcpp_python_generate import sdcpp_python_generate
# ---------------------------------------------------------------------------
# Constantes
# ---------------------------------------------------------------------------
SD_TURBO_SAFETENSORS = (
"/home/lucas/vaults/imagegen_models/diffusers/sd-turbo/sd_turbo.safetensors"
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def sd_turbo_model() -> ModelRef:
"""ModelRef apuntando al safetensors de SD Turbo en local."""
if not os.path.isfile(SD_TURBO_SAFETENSORS):
pytest.skip(
f"Modelo SD Turbo no encontrado en {SD_TURBO_SAFETENSORS}"
)
return ModelRef(
name="sd-turbo",
model_type="sd15",
quantization="fp16",
path=SD_TURBO_SAFETENSORS,
)
@pytest.fixture(scope="session")
def loaded_sd(sd_turbo_model: ModelRef):
"""StableDiffusion cargado una sola vez para toda la sesion de tests."""
_clear_sd_cache()
sd = sdcpp_python_load(sd_turbo_model, n_threads=-1, wtype="default")
yield sd
_clear_sd_cache()
@pytest.fixture(scope="session")
def sd_turbo_cfg(sd_turbo_model: ModelRef) -> GenerationConfig:
"""GenerationConfig para SD Turbo: 512x512, 4 steps, euler_a, seed=42."""
return GenerationConfig(
prompt="a simple red apple on a white table",
negative_prompt=None,
seed=42,
steps=4,
cfg_scale=1.0,
sampler="euler_a",
width=512,
height=512,
model=sd_turbo_model,
loras=[],
)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_load_retorna_objeto(loaded_sd) -> None:
"""load retorna objeto StableDiffusion"""
from stable_diffusion_cpp import StableDiffusion
assert isinstance(loaded_sd, StableDiffusion), (
f"Se esperaba StableDiffusion, se obtuvo {type(loaded_sd)}"
)
def test_load_caches(sd_turbo_model: ModelRef, loaded_sd) -> None:
"""segunda llamada retorna instancia cacheada"""
import time
t0 = time.perf_counter()
sd2 = sdcpp_python_load(sd_turbo_model, n_threads=-1, wtype="default")
elapsed = time.perf_counter() - t0
assert sd2 is loaded_sd, "Segunda llamada debe retornar la misma instancia cacheada"
assert elapsed < 0.5, (
f"Segunda llamada tardo {elapsed:.3f}s — deberia ser inmediata (cache hit)"
)
def test_generate_retorna_image_gen_result(
loaded_sd, sd_turbo_cfg: GenerationConfig
) -> None:
"""generate retorna ImageGenResult valido"""
result = sdcpp_python_generate(loaded_sd, sd_turbo_cfg)
assert isinstance(result, ImageGenResult), (
f"Se esperaba ImageGenResult, se obtuvo {type(result)}"
)
assert result.image is not None, "result.image no debe ser None"
def test_duration_ms_mayor_que_cero(
loaded_sd, sd_turbo_cfg: GenerationConfig
) -> None:
"""duration_ms mayor que cero"""
result = sdcpp_python_generate(loaded_sd, sd_turbo_cfg)
assert result.duration_ms > 0, (
f"duration_ms debe ser > 0, se obtuvo {result.duration_ms}"
)
def test_meta_backend_es_sdcpp_python(
loaded_sd, sd_turbo_cfg: GenerationConfig
) -> None:
"""meta backend es sdcpp_python"""
result = sdcpp_python_generate(loaded_sd, sd_turbo_cfg)
assert result.meta.get("backend") == "sdcpp_python", (
f"meta['backend'] debe ser 'sdcpp_python', se obtuvo {result.meta.get('backend')!r}"
)
assert result.meta.get("model") == sd_turbo_cfg.model.name
assert result.meta.get("sampler") == sd_turbo_cfg.sampler
assert result.meta.get("seed") == sd_turbo_cfg.seed
def test_vram_peak_mb_es_none(
loaded_sd, sd_turbo_cfg: GenerationConfig
) -> None:
"""vram_peak_mb es None — sdcpp no expone medicion de VRAM"""
result = sdcpp_python_generate(loaded_sd, sd_turbo_cfg)
assert result.vram_peak_mb is None, (
f"vram_peak_mb debe ser None, se obtuvo {result.vram_peak_mb}"
)