package ml import ( "context" "fmt" "os" "os/exec" "strings" "time" ) // SdcliBinary describe el binario sd resuelto: su path absoluto, version detectada // y como fue localizado. type SdcliBinary struct { Path string `json:"path"` Version string `json:"version,omitempty"` Source string `json:"source"` // "config" | "path" } // SdcliResolveBinary localiza el binario sd / sd-cli de stable-diffusion.cpp. // // Orden de busqueda: // 1. Si hint != "" y el archivo existe y es ejecutable: usar como "config". // 2. exec.LookPath("sd"): primer candidato en PATH. // 3. exec.LookPath("sd-cli"): segundo candidato en PATH. // 4. Error descriptivo si no se encuentra ninguno. // // Tras localizar el binario, intenta obtener la version ejecutando // ` --version` con timeout de 3 segundos. Si el comando falla // o no produce output, Version queda vacia (no es error fatal). func SdcliResolveBinary(hint string) (SdcliBinary, error) { var binPath string var source string if hint != "" { info, err := os.Stat(hint) if err != nil { return SdcliBinary{}, fmt.Errorf("sdcli hint %q: %w", hint, err) } if info.Mode()&0o111 == 0 { return SdcliBinary{}, fmt.Errorf("sdcli hint %q: file exists but is not executable", hint) } binPath = hint source = "config" } if binPath == "" { if p, err := exec.LookPath("sd"); err == nil { binPath = p source = "path" } } if binPath == "" { if p, err := exec.LookPath("sd-cli"); err == nil { binPath = p source = "path" } } if binPath == "" { return SdcliBinary{}, fmt.Errorf( "sd binary not found in PATH (hint: install from leejet/stable-diffusion.cpp)", ) } version := sdcliProbeVersion(binPath) return SdcliBinary{ Path: binPath, Version: version, Source: source, }, nil } // sdcliProbeVersion ejecuta ` --version` con timeout 3s y retorna // la primera linea de la salida. Retorna "" si el comando falla o no // produce output; no propaga el error (version es best-effort). func sdcliProbeVersion(binPath string) string { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() out, err := exec.CommandContext(ctx, binPath, "--version").Output() if err != nil { return "" } line := strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0]) return line }