mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
* feat(stablediffusion-ggml): run MiniMax-H3 video with reference conditioning Bump stable-diffusion.cpp to 2988060 and follow its tiling rename (tile_size_x/y -> tile_size_w/h, rel_size_x/y -> rel_size_w/h). GenerateVideo now passes params["ref_image"] and the request audio to the MiniMax-H3 Ref2VA inputs (ref_images, ref_audios). The audio must be a WAV file. params["ref_video"] is refused, since the proto cannot carry a frame sequence yet. stable-diffusion.cpp cannot tell the FL2VA and REF2VA DiTs apart, and a reference input on FL2VA renders a wrong video without an error. The backend now reads the video_partition option, or the model file name, and fails early on that mix. Keyframes plus references are refused too. Add gallery entries for the FL2VA and REF2VA Q4_K_M sets. They do not enable any bf16 dequantisation. Assisted-by: Claude:claude-sonnet-5-5 go vet gofmt * feat(stablediffusion-ggml): take several reference images, videos and audio clips Phase 1 passed one reference image and one reference audio clip to stable-diffusion.cpp and refused ref_video. Add repeated reference fields to GenerateVideoRequest (ref_images, ref_videos, ref_audios) so the MiniMax-H3 Ref2VA path can use as many references as upstream accepts. A reference video is a directory of frames with an fps and an optional paired audio clip. The /video endpoint stages the new inputs through the same path as start_image, end_image and audio. Frames of one reference video are written to a private directory under zero-padded names so the backend can read them in order. The input count and frame count per request are bounded. The backend merges the new fields with params.ref_image, params.ref_video and audio, which keep working. The FL2VA partition refuses any reference input, REF2VA accepts all of them, and keyframes stay exclusive with references. Reference audio is still WAV only. Assisted-by: Claude:claude-sonnet-5-5 [Bash] * feat(gallery): offer MiniMax-H3 by partition, DiT layout and format Name each MiniMax-H3 entry after its partition (fl2va or ref2va), DiT layout (unpruned or pruned) and format. Add the vllm.cpp builds that its model page lists: pruned Q8_0 (renders), pruned Q6_K, Q5_0, Q4_K, Q3_K and Q2_K (load only), and the pruned FL2VA NVFP4 file (loads only). The earlier names minimax-h3-fl2va-q4 and minimax-h3-ref2va-q4 stay as aliases. The two stable-diffusion.cpp entries become minimax-h3-fl2va-unpruned-q4-ggml and minimax-h3-ref2va-pruned-q4-ggml, and the latter now lists video input. Fix two bad sources. The stable-diffusion.cpp entries took the VAEs from a path that does not exist in the GGUF repository; they now use the Comfy-Org copies. The vllm.cpp entries paired the fp16 video VAE weights with a config from a different directory; they now use the official weights and config from FL2VA/video_vae/source/. Drop video_dequant_bf16 from every vllm.cpp entry. Expanding a quantised DiT to bf16 is the suspected cause of a host out-of-memory error, so the quantised weights stay resident. The model page records its renders with the expansion on, and each description says so. Assisted-by: Claude:claude-sonnet-5-5 [Bash] * feat(gallery): add pruned FL2VA Q4 MiniMax-H3 for sd.cpp Add minimax-h3-fl2va-pruned-q4-ggml, the 11.4 GB pruned FL2VA counterpart of the existing pruned REF2VA entry. Assisted-by: Claude Code:claude-sonnet-5-5 [rc] --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
150 lines
4.4 KiB
Go
150 lines
4.4 KiB
Go
package backend
|
|
|
|
import (
|
|
"maps"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/trace"
|
|
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
|
model "github.com/mudler/LocalAI/pkg/model"
|
|
)
|
|
|
|
// VideoGenerationOptions is the backend-neutral request passed to video generators.
|
|
// Media fields contain staged local paths by the time they reach this layer.
|
|
// VideoReferenceOptions is a reference video whose frames were staged into one
|
|
// directory, named so that lexicographic order is frame order.
|
|
type VideoReferenceOptions struct {
|
|
FramesDir string
|
|
FPS int32
|
|
Audio string
|
|
}
|
|
|
|
type VideoGenerationOptions struct {
|
|
Height int32
|
|
Width int32
|
|
Prompt string
|
|
NegativePrompt string
|
|
StartImage string
|
|
EndImage string
|
|
Audio string
|
|
RefImages []string
|
|
RefVideos []VideoReferenceOptions
|
|
RefAudios []string
|
|
Destination string
|
|
NumFrames int32
|
|
FPS int32
|
|
Seed int32
|
|
CFGScale float32
|
|
Step int32
|
|
Params map[string]string
|
|
}
|
|
|
|
func refVideosToProto(in []VideoReferenceOptions) []*proto.VideoReference {
|
|
out := make([]*proto.VideoReference, 0, len(in))
|
|
for _, r := range in {
|
|
out = append(out, &proto.VideoReference{FramesDir: r.FramesDir, Fps: r.FPS, Audio: r.Audio})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func VideoGeneration(options VideoGenerationOptions, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig) (func() error, error) {
|
|
opts := ModelOptions(modelConfig, appConfig)
|
|
inferenceModel, err := loader.Load(opts...)
|
|
if err != nil {
|
|
recordModelLoadFailure(appConfig, modelConfig.Name, modelConfig.Backend, err, nil)
|
|
return nil, err
|
|
}
|
|
|
|
fn := func() error {
|
|
_, err := inferenceModel.GenerateVideo(
|
|
appConfig.Context,
|
|
&proto.GenerateVideoRequest{
|
|
// See the ModelIdentity note in image.go: this is the value
|
|
// ModelOptions passed to LoadModel a few lines above.
|
|
ModelIdentity: modelConfig.Model,
|
|
Height: options.Height,
|
|
Width: options.Width,
|
|
Prompt: options.Prompt,
|
|
NegativePrompt: options.NegativePrompt,
|
|
StartImage: options.StartImage,
|
|
EndImage: options.EndImage,
|
|
Audio: options.Audio,
|
|
RefImages: slices.Clone(options.RefImages),
|
|
RefVideos: refVideosToProto(options.RefVideos),
|
|
RefAudios: slices.Clone(options.RefAudios),
|
|
NumFrames: options.NumFrames,
|
|
Fps: options.FPS,
|
|
Seed: options.Seed,
|
|
CfgScale: options.CFGScale,
|
|
Step: options.Step,
|
|
Dst: options.Destination,
|
|
Params: maps.Clone(options.Params),
|
|
},
|
|
)
|
|
return err
|
|
}
|
|
|
|
if appConfig.EnableTracing {
|
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems, appConfig.TracingMaxBodyBytes)
|
|
|
|
traceData := map[string]any{
|
|
"prompt": options.Prompt,
|
|
"negative_prompt": options.NegativePrompt,
|
|
"height": options.Height,
|
|
"width": options.Width,
|
|
"num_frames": options.NumFrames,
|
|
"fps": options.FPS,
|
|
"seed": options.Seed,
|
|
"cfg_scale": options.CFGScale,
|
|
"step": options.Step,
|
|
"has_start_image": options.StartImage != "",
|
|
"has_end_image": options.EndImage != "",
|
|
"has_audio": options.Audio != "",
|
|
"ref_images": len(options.RefImages),
|
|
"ref_videos": len(options.RefVideos),
|
|
"ref_audios": len(options.RefAudios),
|
|
}
|
|
|
|
originalFn := fn
|
|
fn = func() error {
|
|
startTime := time.Now()
|
|
traceID := trace.BeginBackendTrace(trace.BackendTrace{Timestamp: startTime, Type: trace.BackendTraceVideoGeneration, ModelName: modelConfig.Name, Backend: modelConfig.Backend, Summary: trace.TruncateString(options.Prompt, 200)})
|
|
defer trace.CancelBackendTrace(traceID)
|
|
err := originalFn()
|
|
duration := time.Since(startTime)
|
|
|
|
errStr := ""
|
|
if err != nil {
|
|
errStr = err.Error()
|
|
}
|
|
|
|
trace.RecordBackendTrace(trace.BackendTrace{
|
|
ID: traceID,
|
|
Timestamp: startTime,
|
|
Duration: duration,
|
|
Type: trace.BackendTraceVideoGeneration,
|
|
ModelName: modelConfig.Name,
|
|
Backend: modelConfig.Backend,
|
|
Summary: trace.TruncateString(options.Prompt, 200),
|
|
Error: errStr,
|
|
Data: traceData,
|
|
})
|
|
|
|
return err
|
|
}
|
|
}
|
|
originalFn := fn
|
|
fn = func() error {
|
|
release, err := AcquireGlobalBackendSlot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
return originalFn()
|
|
}
|
|
|
|
return fn, nil
|
|
}
|