mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
* feat(images): add upscale model capabilities * fix(config): keep SD GGML upscale opt-in * feat(stablediffusion): add lazy ESRGAN upscale bridge Keep native image ownership in C++ and serialize context creation, inference and teardown. Use upscale_scale in backend options as the explicit upscale-only marker because ModelOptions does not carry usecases. Assisted-by: Codex:gpt-5 * fix(stablediffusion): validate ESRGAN settings * fix(upscale): forward typed settings and contain resolved model paths Typed settings replace legacy options without duplicates. Resolve model and trusted root symlinks before caching the model path, rejecting missing files and escapes. Assisted-by: Codex:gpt-5 * fix(upscale): validate scale and clean failed HTTP outputs Require an explicit scale bounded to 1-16, check backend results, and keep destination cleanup owned by the allocating endpoint. Cover validation, result failures, cleanup, and successful URL responses. Assisted-by: Codex:gpt-5 * feat(studio): add inline image upscale workflow Assisted-by: OpenAI:gpt-5 * test(studio): serve upscale capability fixtures * fix(stablediffusion): default ESRGAN tiling to full frame * feat(gallery): add RealESRGAN anime upscaler * fix(stablediffusion): allow full-frame ESRGAN tiles * fix(routes): select explicit upscale models Give image upscaling dedicated usecase-filtered middleware and keep ordinary image models out of the endpoint. Mark and verify the legacy Diffusers x4 gallery model as an explicit upscaler. Assisted-by: OpenAI:gpt-5 [shell] * refactor(upscale): keep settings in backend options * test(config): keep upscale options backend-specific * fix: keep upscale routing metadata out of backend options --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
149 lines
4.3 KiB
Go
149 lines
4.3 KiB
Go
package openai
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/mudler/xlog"
|
|
|
|
"github.com/mudler/LocalAI/core/backend"
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/http/middleware"
|
|
"github.com/mudler/LocalAI/core/schema"
|
|
model "github.com/mudler/LocalAI/pkg/model"
|
|
)
|
|
|
|
// maxUpscaleScale limits output area amplification to 256x and fits the int32
|
|
// protobuf field. The backend still enforces the model's exact native scale.
|
|
const maxUpscaleScale = 16
|
|
|
|
// UpscaleEndpoint handles POST /v1/images/upscale
|
|
//
|
|
// @Summary Image upscaling
|
|
// @Description Upscale an image using a specified model (e.g. stable-diffusion-x4-upscaler). Accepts multipart/form-data.
|
|
// @Tags images
|
|
// @Accept multipart/form-data
|
|
// @Produce application/json
|
|
// @Param model formData string true "Upscaler model identifier (e.g. stable-diffusion-x4-upscaler)"
|
|
// @Param image formData file true "Input image file"
|
|
// @Param scale formData int true "Upscale factor: integer from 1 to 16; must match the model native scale"
|
|
// @Success 200 {object} schema.OpenAIResponse
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /v1/images/upscale [post]
|
|
func UpscaleEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
modelName := c.FormValue("model")
|
|
scaleStr := c.FormValue("scale")
|
|
|
|
if modelName == "" {
|
|
xlog.Error("Upscale Endpoint - missing model")
|
|
return echo.NewHTTPError(http.StatusBadRequest, "missing model")
|
|
}
|
|
|
|
if scaleStr == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "missing scale")
|
|
}
|
|
scale, err := strconv.Atoi(scaleStr)
|
|
if err != nil || scale < 1 || scale > maxUpscaleScale {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "scale must be an integer from 1 to 16")
|
|
}
|
|
|
|
// Read uploaded image
|
|
imageFile, err := c.FormFile("image")
|
|
if err != nil {
|
|
xlog.Error("Upscale Endpoint - missing image file", "error", err)
|
|
return echo.NewHTTPError(http.StatusBadRequest, "missing image file")
|
|
}
|
|
|
|
imgSrc, err := imageFile.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer imgSrc.Close()
|
|
imgBytes, err := io.ReadAll(imgSrc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get model config from middleware context
|
|
cfg, ok := c.Get(middleware.CONTEXT_LOCALS_KEY_MODEL_CONFIG).(*config.ModelConfig)
|
|
if !ok || cfg == nil {
|
|
xlog.Error("Upscale Endpoint - model config not found in context")
|
|
return echo.ErrBadRequest
|
|
}
|
|
|
|
tmpDir := filepath.Join(appConfig.GeneratedContentDir, "images")
|
|
if err := os.MkdirAll(tmpDir, 0750); err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to prepare storage")
|
|
}
|
|
|
|
// Write input image to a temp file
|
|
srcTmp, err := os.CreateTemp(tmpDir, "upscale_src_")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := srcTmp.Write(imgBytes); err != nil {
|
|
_ = srcTmp.Close()
|
|
_ = os.Remove(srcTmp.Name())
|
|
return err
|
|
}
|
|
if err := srcTmp.Close(); err != nil {
|
|
xlog.Warn("Upscale Endpoint - failed to close src temp file", "error", err)
|
|
}
|
|
srcPath := srcTmp.Name()
|
|
defer os.Remove(srcPath)
|
|
|
|
// Prepare output file path
|
|
id := uuid.New().String()
|
|
dstPath := filepath.Join(tmpDir, fmt.Sprintf("upscale_%s.png", id))
|
|
|
|
// Only this endpoint owns the generated destination; the generic backend
|
|
// must not delete arbitrary caller-supplied paths on failure.
|
|
completed := false
|
|
defer func() {
|
|
if !completed {
|
|
_ = os.Remove(dstPath)
|
|
}
|
|
}()
|
|
|
|
fn, err := backend.ImageUpscaleFunc(c.Request().Context(), srcPath, dstPath, scale, ml, *cfg, appConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := fn(); err != nil {
|
|
return err
|
|
}
|
|
|
|
baseURL := middleware.BaseURL(c)
|
|
imgURL, err := url.JoinPath(baseURL, "generated-images", filepath.Base(dstPath))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
created := int(time.Now().Unix())
|
|
resp := &schema.OpenAIResponse{
|
|
ID: id,
|
|
Created: created,
|
|
Data: []schema.Item{{URL: imgURL}},
|
|
Usage: &schema.OpenAIUsage{
|
|
InputTokensDetails: &schema.InputTokensDetails{},
|
|
},
|
|
}
|
|
|
|
if err := c.JSON(http.StatusOK, resp); err != nil {
|
|
return err
|
|
}
|
|
completed = true
|
|
return nil
|
|
}
|
|
}
|