Merge master into the distributed test branch

Assisted-by: Codex:GPT-6
This commit is contained in:
localai-org-maint-bot committed 2026-09-07 16:08:13 +00:00
commit d397393022
18 files changed
+378 -42

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@
# recipe is a make target (not a prepare.sh) so 'make purge && make' is a clean
# rebuild and so the bump bot can see the pin.
AUDIO_CPP_VERSION?=c41ffefb44c16345c0ac57a0030f4cd3b5fbc4b8
AUDIO_CPP_VERSION?=f6277c1695a83cf388a8282c1c1a8757cf626f18
AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+4 -3
View File
@@ -84,9 +84,10 @@ elseif(DS4_GPU STREQUAL "cpu")
set(DS4_OBJS "${DS4_DIR}/ds4_cpu.o")
endif()
# Upstream splits distributed inference, tensor-parallel transport, the SSD
# expert cache, and layer placement into GPU-agnostic translation units. Link
# them regardless of DS4_GPU.
# Upstream splits image preprocessing, distributed inference, tensor-parallel
# transport, the SSD expert cache, and layer placement into GPU-agnostic
# translation units. Link them regardless of DS4_GPU.
list(APPEND DS4_OBJS "${DS4_DIR}/ds4_image.o")
list(APPEND DS4_OBJS "${DS4_DIR}/ds4_distributed.o")
list(APPEND DS4_OBJS "${DS4_DIR}/ds4_tp.o")
list(APPEND DS4_OBJS "${DS4_DIR}/ds4_ssd.o")
+11 -10
View File
@@ -1,10 +1,10 @@
# ds4 backend Makefile.
#
# Upstream pin lives below as DS4_VERSION?=8db89fe083ae4d17c9a2428ccd29803d3ae8f577
# Upstream pin lives below as DS4_VERSION?=9ab705347c1775e7599ede7eb81a6255ec7dccb5
# (.github/bump_deps.sh) can find and update it - matches the
# llama-cpp / ik-llama-cpp / turboquant convention.
DS4_VERSION?=8db89fe083ae4d17c9a2428ccd29803d3ae8f577
DS4_VERSION?=9ab705347c1775e7599ede7eb81a6255ec7dccb5
DS4_REPO?=https://github.com/antirez/ds4
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
@@ -79,21 +79,22 @@ else
endif
endif
# Upstream splits distributed inference, tensor-parallel transport, the SSD
# expert cache, and layer placement into GPU-agnostic translation units. They
# are shared by every GPU mode, so append them unconditionally below.
# Upstream splits image preprocessing, distributed inference, tensor-parallel
# transport, the SSD expert cache, and layer placement into GPU-agnostic
# translation units. They are shared by every GPU mode, so append them
# unconditionally below.
ifeq ($(BUILD_TYPE),cublas)
CMAKE_ARGS += -DDS4_GPU=cuda
DS4_OBJ_TARGET := ds4.o ds4_cuda.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o \
DS4_OBJ_TARGET := ds4.o ds4_image.o ds4_cuda.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o \
cuda/mmq/ds4_ggml_stubs.o cuda/mmq/ds4_mmq.o cuda/mmq/ds4_mmq_d2r.o \
cuda/mmq/quantize.o cuda/mmq/mmid.o cuda/mmq/mmvq.o cuda/mmq/ds4_repack.o
else ifeq ($(UNAME_S),Darwin)
CMAKE_ARGS += -DDS4_GPU=metal
DS4_OBJ_TARGET := ds4.o ds4_metal.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
DS4_OBJ_TARGET := ds4.o ds4_image.o ds4_metal.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
else
# CPU reference path (Linux only - macOS CPU path is broken by VM bug per ds4 README).
CMAKE_ARGS += -DDS4_GPU=cpu
DS4_OBJ_TARGET := ds4_cpu.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
DS4_OBJ_TARGET := ds4_cpu.o ds4_image.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
endif
ifneq ($(NATIVE),true)
@@ -120,9 +121,9 @@ ds4/ds4.o: ds4
ifeq ($(BUILD_TYPE),cublas)
+$(MAKE) -C ds4 $(DS4_ARCH_MAKEVARS) $(DS4_OBJ_TARGET)
else ifeq ($(UNAME_S),Darwin)
+$(MAKE) -C ds4 ds4.o ds4_metal.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
+$(MAKE) -C ds4 ds4.o ds4_image.o ds4_metal.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
else
+$(MAKE) -C ds4 ds4_cpu.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
+$(MAKE) -C ds4 ds4_cpu.o ds4_image.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o
endif
grpc-server: ds4/ds4.o
+1 -1
View File
@@ -1,5 +1,5 @@
LLAMA_VERSION?=74a7c897f049c17e7080423aa2111776eff6ebbf
LLAMA_VERSION?=465e49b9cea78a68b9c244ffb48d0ee24a82873d
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
CMAKE_ARGS?=
+1 -1
View File
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# stablediffusion.cpp (ggml)
STABLEDIFFUSION_GGML_REPO?=https://github.com/leejet/stable-diffusion.cpp
STABLEDIFFUSION_GGML_VERSION?=6b3edaaf32cc19e5bb2d819c788bd557eddc8eba
STABLEDIFFUSION_GGML_VERSION?=d8fb10c02977c8ca999f3fb4e02df9ecf10f7ba6
CMAKE_ARGS+=-DGGML_MAX_NAME=128
+6 -7
View File
@@ -401,7 +401,6 @@ int load_model(const char *model, char *model_path, char* options[], int threads
const char *params_backend_arg = "";
const char *rpc_servers_arg = "";
const char *max_vram_arg = "";
bool stream_layers = false;
int n_threads = threads;
enum sd_type_t wtype = SD_TYPE_COUNT;
@@ -510,7 +509,10 @@ int load_model(const char *model, char *model_path, char* options[], int threads
if (!strcmp(optname, "params_backend")) params_backend_arg = strdup(optval);
if (!strcmp(optname, "rpc_servers")) rpc_servers_arg = strdup(optval);
if (!strcmp(optname, "max_vram")) max_vram_arg = strdup(optval);
if (!strcmp(optname, "stream_layers")) stream_layers = (strcmp(optval, "true") == 0 || strcmp(optval, "1") == 0);
if (!strcmp(optname, "stream_layers")) {
// Retained as a no-op for existing configurations. Upstream now
// selects segmented weight streaming automatically.
}
// vae_decode_only is still accepted for backwards compatibility with
// existing gallery configs, but upstream dropped the option (the model
@@ -650,11 +652,9 @@ int load_model(const char *model, char *model_path, char* options[], int threads
ctx_params.rpc_servers = env_rpc_servers;
}
}
// max_vram: GiB budget or per-backend spec for graph-cut segmented param
// offload ("0" = disabled, "-1" = auto). stream_layers only has effect when
// max_vram is set.
// max_vram is an optional GiB budget or per-backend spec for automatic
// graph-cut execution. A zero value uses the live free-VRAM budget.
if (strlen(max_vram_arg) > 0) ctx_params.max_vram = max_vram_arg;
ctx_params.stream_layers = stream_layers;
ctx_params.diffusion_flash_attn = diffusion_flash_attn;
ctx_params.tae_preview_only = tae_preview_only;
ctx_params.diffusion_conv_direct = diffusion_conv_direct;
@@ -1438,4 +1438,3 @@ int unload() {
free_sd_ctx(sd_c);
return 0;
}
+20 -10
View File
@@ -1,6 +1,7 @@
package localai
import (
"errors"
"net/http"
"github.com/labstack/echo/v4"
@@ -33,22 +34,31 @@ func FaceRegisterEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, a
return echo.NewHTTPError(http.StatusBadRequest, "name is required")
}
img, err := decodeImageInput(input.Img)
if err != nil {
return err
if (input.Img == "") == (len(input.Embedding) == 0) {
return echo.NewHTTPError(http.StatusBadRequest, "provide exactly one of img or embedding")
}
xlog.Debug("FaceRegister", "model", cfg.Name, "name", input.Name)
embedding, err := backend.FaceEmbed(c.Request().Context(), img, ml, appConfig, *cfg)
if err != nil {
return mapBackendError(err)
embedding := input.Embedding
if len(embedding) == 0 {
img, err := decodeImageInput(input.Img)
if err != nil {
return err
}
xlog.Debug("FaceRegister", "model", cfg.Name, "name", input.Name)
embedding, err = backend.FaceEmbed(c.Request().Context(), img, ml, appConfig, *cfg)
if err != nil {
return mapBackendError(err)
}
}
stored, err := registry.Register(c.Request().Context(), embedding, facerecognition.Metadata{
Name: input.Name,
Labels: input.Labels,
Name: input.Name,
RegisteredAt: input.RegisteredAt,
Labels: input.Labels,
})
if err != nil {
if errors.Is(err, facerecognition.ErrInvalidEmbedding) || errors.Is(err, facerecognition.ErrDimensionMismatch) {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return err
}
return c.JSON(http.StatusOK, schema.FaceRegisterResponse{
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
package localai_test
import (
"context"
"net/http"
"net/http/httptest"
"time"
"github.com/labstack/echo/v4"
"github.com/mudler/LocalAI/core/config"
. "github.com/mudler/LocalAI/core/http/endpoints/localai"
"github.com/mudler/LocalAI/core/http/middleware"
"github.com/mudler/LocalAI/core/schema"
"github.com/mudler/LocalAI/core/services/facerecognition"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
type registrationRecorder struct {
facerecognition.Registry
vector []float32
meta facerecognition.Metadata
err error
}
func (r *registrationRecorder) Register(_ context.Context, v []float32, m facerecognition.Metadata) (facerecognition.Metadata, error) {
r.vector = v
r.meta = m
m.ID = "saved-id"
return m, r.err
}
var _ = Describe("Face registration replay", func() {
var reg *registrationRecorder
call := func(in schema.FaceRegisterRequest) (*httptest.ResponseRecorder, error) {
e := echo.New()
rec := httptest.NewRecorder()
c := e.NewContext(httptest.NewRequest(http.MethodPost, "/v1/face/register", nil), rec)
c.Set(middleware.CONTEXT_LOCALS_KEY_LOCALAI_REQUEST, &in)
c.Set(middleware.CONTEXT_LOCALS_KEY_MODEL_CONFIG, &config.ModelConfig{})
// No model loader: replay must not call the embedding backend.
err := FaceRegisterEndpoint(nil, nil, nil, reg)(c)
return rec, err
}
BeforeEach(func() { reg = &registrationRecorder{} })
It("accepts the saved vector and timestamp without running inference", func() {
at := time.Now().UTC()
in := schema.FaceRegisterRequest{Name: "Alice", Embedding: []float32{1, 0}, RegisteredAt: at, Labels: map[string]string{"client_id": "alice"}}
in.Model = "faces"
rec, err := call(in)
Expect(err).NotTo(HaveOccurred())
Expect(rec.Code).To(Equal(http.StatusOK))
Expect(reg.vector).To(Equal(in.Embedding))
Expect(reg.meta.RegisteredAt).To(Equal(at))
Expect(reg.meta.Labels).To(Equal(in.Labels))
Expect(rec.Body.String()).To(ContainSubstring("saved-id"))
})
It("rejects ambiguous and missing inputs before inference", func() {
for _, in := range []schema.FaceRegisterRequest{
{Name: "Alice"},
{Name: "Alice", Img: "image", Embedding: []float32{1, 0}},
} {
in.Model = "faces"
_, err := call(in)
Expect(err).To(HaveOccurred())
Expect(err.(*echo.HTTPError).Code).To(Equal(http.StatusBadRequest))
Expect(reg.vector).To(BeNil())
}
})
It("reports invalid vectors as a client error", func() {
reg.err = facerecognition.ErrInvalidEmbedding
in := schema.FaceRegisterRequest{Name: "Alice", Embedding: []float32{0, 0}}
in.Model = "faces"
_, err := call(in)
Expect(err).To(HaveOccurred())
Expect(err.(*echo.HTTPError).Code).To(Equal(http.StatusBadRequest))
})
})
+6 -4
View File
@@ -353,10 +353,12 @@ type FaceEmbedResponse struct {
// FaceRegisterRequest enrolls a face into the 1:N recognition store.
type FaceRegisterRequest struct {
BasicModelRequest
Img string `json:"img"`
Name string `json:"name"`
Labels map[string]string `json:"labels,omitempty"`
Store string `json:"store,omitempty"` // vector store model; empty = local-store default
RegisteredAt time.Time `json:"registered_at,omitempty"` // original enrollment time when replaying a saved embedding
Embedding []float32 `json:"embedding,omitempty"`
Img string `json:"img"`
Name string `json:"name"`
Labels map[string]string `json:"labels,omitempty"`
Store string `json:"store,omitempty"` // vector store model; empty = local-store default
}
type FaceRegisterResponse struct {
@@ -56,5 +56,6 @@ type Match struct {
var (
ErrNotFound = errors.New("facerecognition: id not found")
ErrEmptyEmbedding = errors.New("facerecognition: embedding is empty")
ErrInvalidEmbedding = errors.New("facerecognition: embedding must be finite and nonzero")
ErrDimensionMismatch = errors.New("facerecognition: embedding dimension mismatch")
)
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
package facerecognition
import (
"context"
"encoding/json"
"math"
"sync"
"testing"
"time"
grpc "github.com/mudler/LocalAI/pkg/grpc"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
ggrpc "google.golang.org/grpc"
)
func TestEnrollmentReplay(t *testing.T) { RegisterFailHandler(Fail); RunSpecs(t, "Enrollment replay") }
type replayStore struct {
grpc.Backend
mu sync.Mutex
entries map[string][]byte
}
func (s *replayStore) StoresSet(_ context.Context, in *pb.StoresSetOptions, _ ...ggrpc.CallOption) (*pb.Result, error) {
s.mu.Lock()
defer s.mu.Unlock()
for i, k := range in.Keys {
b, _ := json.Marshal(k.Floats)
s.entries[string(b)] = append([]byte(nil), in.Values[i].Bytes...)
}
return &pb.Result{Success: true}, nil
}
var _ = Describe("Enrollment replay", func() {
It("keeps the identity across registry instances and a cleared store", func(ctx SpecContext) {
storage := &replayStore{entries: map[string][]byte{}}
newRegistry := func() Registry {
return NewStoreRegistry(func(context.Context, string) (grpc.Backend, error) { return storage, nil }, "faces", 0)
}
vector := []float32{1, 0, 0, 0}
meta := Metadata{Name: "Alice", RegisteredAt: time.Now().UTC()}
first, err := newRegistry().Register(ctx, vector, meta)
Expect(err).NotTo(HaveOccurred())
again, err := newRegistry().Register(ctx, vector, meta)
Expect(err).NotTo(HaveOccurred())
Expect(again).To(Equal(first))
Expect(storage.entries).To(HaveLen(1))
storage.entries = map[string][]byte{}
restored, err := newRegistry().Register(ctx, vector, meta)
Expect(err).NotTo(HaveOccurred())
Expect(restored).To(Equal(first))
Expect(storage.entries).To(HaveLen(1))
})
It("rejects zero and non-finite embeddings before writing", func(ctx SpecContext) {
for _, v := range [][]float32{{0, 0}, {float32(math.NaN()), 1}, {float32(math.Inf(1)), 1}} {
storage := &replayStore{entries: map[string][]byte{}}
reg := NewStoreRegistry(func(context.Context, string) (grpc.Backend, error) { return storage, nil }, "faces", 0)
_, err := reg.Register(ctx, v, Metadata{Name: "Alice"})
Expect(err).To(HaveOccurred())
Expect(storage.entries).To(BeEmpty())
}
})
})
@@ -2,8 +2,10 @@ package facerecognition
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"math"
"sort"
"sync"
"time"
@@ -57,13 +59,32 @@ func (r *storeRegistry) Register(ctx context.Context, embedding []float32, meta
if r.dim != 0 && len(embedding) != r.dim {
return Metadata{}, fmt.Errorf("%w: expected %d, got %d", ErrDimensionMismatch, r.dim, len(embedding))
}
var norm float64
key := make([]byte, 4*len(embedding))
for i, value := range embedding {
if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
return Metadata{}, ErrInvalidEmbedding
}
norm += float64(value) * float64(value)
// The store treats negative and positive zero as the same key.
if value == 0 {
value = 0
}
binary.LittleEndian.PutUint32(key[i*4:], math.Float32bits(value))
}
if norm == 0 {
return Metadata{}, ErrInvalidEmbedding
}
backend, err := r.resolve(ctx, r.storeName)
if err != nil {
return Metadata{}, fmt.Errorf("facerecognition: resolve store: %w", err)
}
meta.ID = uuid.NewString()
// The vector store upserts by the exact embedding. Derive the ID from the
// same key so replaying a saved vector preserves identity across replicas
// and after the in-memory store restarts.
meta.ID = uuid.NewSHA1(uuid.NewSHA1(uuid.NameSpaceOID, []byte(r.storeName)), key).String()
if meta.RegisteredAt.IsZero() {
meta.RegisteredAt = time.Now().UTC()
}
@@ -16,6 +16,7 @@ import (
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
"github.com/mudler/xlog"
ggrpc "google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)
// FileStagingClient wraps a grpc.Backend to transparently handle file transfer
@@ -346,6 +347,19 @@ func (f *FileStagingClient) SoundGeneration(ctx context.Context, in *pb.SoundGen
return result, nil
}
func (f *FileStagingClient) SoundDetection(ctx context.Context, in *pb.SoundDetectionRequest, opts ...ggrpc.CallOption) (*pb.SoundDetectionResponse, error) {
if in.Src != "" && isFilePath(in.Src) {
backendPath, _, err := f.stageInputFile(ctx, requestID(), in.Src, "inputs")
if err != nil {
return nil, fmt.Errorf("staging audio for sound detection: %w", err)
}
// Keep the frontend path available if the caller retries on another node.
in = proto.Clone(in).(*pb.SoundDetectionRequest)
in.Src = backendPath
}
return f.Backend.SoundDetection(ctx, in, opts...)
}
func (f *FileStagingClient) AudioTranscription(ctx context.Context, in *pb.TranscriptRequest, opts ...ggrpc.CallOption) (*pb.TranscriptResult, error) {
reqID := requestID()
@@ -0,0 +1,94 @@
package nodes
import (
"context"
"errors"
grpc "github.com/mudler/LocalAI/pkg/grpc"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
ggrpc "google.golang.org/grpc"
)
type soundStagingBackend struct {
grpc.Backend
request *pb.SoundDetectionRequest
err error
}
func (b *soundStagingBackend) SoundDetection(_ context.Context, in *pb.SoundDetectionRequest, _ ...ggrpc.CallOption) (*pb.SoundDetectionResponse, error) {
b.request = in
return &pb.SoundDetectionResponse{}, b.err
}
type soundStagingFailure struct{ FileStager }
func (s *soundStagingFailure) EnsureRemote(context.Context, string, string, string) (string, error) {
return "", errors.New("upload failed")
}
type soundRouteFactory struct{ client grpc.Backend }
func (f *soundRouteFactory) NewClient(string, bool) grpc.Backend { return f.client }
var _ = Describe("FileStagingClient sound detection", func() {
It("stages sound audio through the client returned by SmartRouter.Route", func(ctx SpecContext) {
node := &BackendNode{ID: "worker-1", Name: "worker", Address: "10.0.0.1:50051"}
reg := &fakeModelRouter{
findAndLockNode: node,
findAndLockNM: &NodeModel{NodeID: node.ID, ModelName: "ced", Address: "10.0.0.1:9001"},
}
backend := &soundStagingBackend{Backend: &stubBackend{healthResult: true}}
stager := &fakeFileStager{}
router := NewSmartRouter(reg, SmartRouterOptions{
ClientFactory: &soundRouteFactory{client: backend},
FileStager: stager,
Unloader: &fakeUnloader{},
})
result, err := router.Route(ctx, "ced", "ced.gguf", "ced", "", nil, false)
Expect(err).NotTo(HaveOccurred())
Expect(result).NotTo(BeNil())
defer result.Release()
request := &pb.SoundDetectionRequest{Src: "/tmp/realtime-sound-window-test.wav", ModelIdentity: "ced.gguf"}
_, err = result.Client.SoundDetection(ctx, request)
Expect(err).NotTo(HaveOccurred())
Expect(stager.ensureCalls).To(HaveLen(1))
Expect(stager.ensureCalls[0].localPath).To(Equal(request.Src))
Expect(backend.request.Src).To(Equal("/remote/" + stager.ensureCalls[0].key))
Expect(request.Src).To(Equal("/tmp/realtime-sound-window-test.wav"))
})
It("stages audio on the worker without changing the caller's request", func(ctx SpecContext) {
backend := &soundStagingBackend{}
stager := &fakeFileStager{}
client := NewFileStagingClient(backend, stager, "worker-1")
request := &pb.SoundDetectionRequest{Src: "/tmp/realtime-sound-window-test.wav", ModelIdentity: "ced.gguf", TopK: 5, Threshold: 0.25}
_, err := client.SoundDetection(ctx, request)
Expect(err).NotTo(HaveOccurred())
Expect(stager.ensureCalls).To(HaveLen(1))
Expect(stager.ensureCalls[0].nodeID).To(Equal("worker-1"))
Expect(stager.ensureCalls[0].localPath).To(Equal(request.Src))
Expect(backend.request.Src).To(Equal("/remote/" + stager.ensureCalls[0].key))
Expect(backend.request.ModelIdentity).To(Equal(request.ModelIdentity))
Expect(backend.request.TopK).To(Equal(request.TopK))
Expect(backend.request.Threshold).To(Equal(request.Threshold))
Expect(request.Src).To(Equal("/tmp/realtime-sound-window-test.wav"))
})
It("does not call the backend when staging fails", func(ctx SpecContext) {
backend := &soundStagingBackend{}
client := NewFileStagingClient(backend, &soundStagingFailure{}, "worker-1")
_, err := client.SoundDetection(ctx, &pb.SoundDetectionRequest{Src: "/tmp/clip.wav"})
Expect(err).To(MatchError(ContainSubstring("upload failed")))
Expect(backend.request).To(BeNil())
})
It("passes through requests without a file and preserves backend errors", func(ctx SpecContext) {
failure := errors.New("classifier failed")
backend := &soundStagingBackend{err: failure}
stager := &fakeFileStager{}
client := NewFileStagingClient(backend, stager, "worker-1")
_, err := client.SoundDetection(ctx, &pb.SoundDetectionRequest{})
Expect(err).To(MatchError(failure))
Expect(stager.ensureCalls).To(BeEmpty())
})
})
@@ -11,6 +11,10 @@ LocalAI exposes this through the `/v1/audio/classification` endpoint, modelled a
Because classification is exposed as a regular OpenAI-style endpoint, any HTTP client works - there is no Python dependency on the consumer side.
In distributed mode, LocalAI stages uploaded audio and realtime sound-detection
windows on the selected worker before classification. The API server and worker
do not need a shared temporary directory.
## Endpoint
```
+35 -2
View File
@@ -73,9 +73,9 @@ Detect faces and analyze demographics (buffalo entries populate
age / gender; YuNet + SFace returns regions only):
```bash
curl -sX POST http://localhost:8080/v1/face/detect \
curl -sX POST http://localhost:8080/v1/detection \
-H "Content-Type: application/json" \
-d '{"model": "face-detect-buffalo-l", "img": "https://example.com/group.jpg"}'
-d '{"model": "face-detect-buffalo-l", "image": "https://example.com/group.jpg"}'
curl -sX POST http://localhost:8080/v1/face/analyze \
-H "Content-Type: application/json" \
@@ -141,6 +141,39 @@ Response:
}
```
## Restore enrollments after a restart
The default identity store is in memory. Clients can keep an enrollment record
and replay it with `POST /v1/face/register` after a restart. Extract the embedding
once with `/v1/face/embed`, then save the exact returned vector, model, name,
labels, and enrollment timestamp. Submit `embedding` instead of `img`:
```json
{
"model": "insightface-opencv",
"name": "Alice",
"embedding": [0.12, -0.04, 0.31],
"registered_at": "2026-09-07T12:00:00Z",
"labels": {"client_id": "alice"}
}
```
The vector above is abbreviated; send the complete embedding from the same
recognizer model. Provide exactly one of `img` or `embedding`. Vectors must be
finite and nonzero. `registered_at` is optional and defaults to the current time;
replay the original timestamp to preserve it.
The store upserts by exact vector. Registration now derives a stable ID from
that vector and the store namespace, so retries and replay after a restart
return the same ID without adding duplicate entries. Replaying updates the name
and labels. Images can produce slightly different embeddings across runs; keep
the original vector instead of embedding the photo again on each retry.
This does not make the server store persistent. Clients must retain and restore
the records themselves. With independent stores behind a load balancer, replay
into each store or use a shared store. Do not mix different recognizer models in
one store. IDs from older versions change on their first registration replay.
## 1:N identification workflow (register → identify → forget)
This is the primary "face recognition" flow. Under the hood it uses
+2 -2
View File
@@ -79,8 +79,8 @@ When a model does not fit entirely in VRAM, the following `options:` control whe
|--------|---------|-------------|
| `backend` | `backend:clip=cpu,vae=cuda0,diffusion=vulkan0` | Runtime (compute) backend assignment per component. Use `cpu` to place a component's compute on the CPU. Component keys include `te` (text encoder / CLIP), `vae`, `diffusion`, `controlnet`. |
| `params_backend` | `params_backend:diffusion=disk,clip=cpu` | Where parameters (weights) are stored. Supports `cpu`, `disk` (mmap weights from disk to save RAM/VRAM), or per-component specs. |
| `max_vram` | `max_vram:8` or `max_vram:-1` | VRAM budget (in GiB) for graph-cut segmented parameter offload. `0` disables it, `-1` auto-selects (free VRAM minus ~1 GiB). Also accepts per-backend budgets. |
| `stream_layers` | `stream_layers:true` | Enable residency + prefetch streaming on top of `max_vram` (no effect unless `max_vram` is set). |
| `max_vram` | `max_vram:8` or `max_vram:-1` | Optional per-device VRAM budget (in GiB) for managed weights and automatic graph-cut execution. `0` uses live free VRAM without an explicit cap; a negative value reserves that many GiB of free VRAM. Also accepts per-backend budgets. |
| `stream_layers` | `stream_layers:true` | Deprecated compatibility option. Segmented weight streaming is now selected automatically, so this value is ignored. |
| `rpc_servers` | `rpc_servers:localhost:50052,192.168.1.3:50052` | Comma-separated list of `host:port` RPC servers to offload compute to. |
| `pulid_weights_path` | `pulid_weights_path:pulid.safetensors` | Path to PuLID-Flux weights for identity injection. |
+9
View File
@@ -18,6 +18,15 @@ Feel free to open up a Pull request (by clicking at the "Edit page" below) to ge
- [Helm chart](https://github.com/go-skynet/helm-charts) - Deploy LocalAI on Kubernetes
- [GitHub Actions](https://github.com/marketplace/actions/start-localai) - Use LocalAI in CI/CD workflows
### Distribution Packages
Community-maintained packages of LocalAI. Package versions and
availability may lag upstream releases.
- [Homebrew](https://formulae.brew.sh/formula/localai) - `brew install localai`
- [ALT Sisyphus](https://packages.altlinux.org/en/sisyphus/srpms/localai/) - `localai` package
- [Gentoo overlay](https://git.ipnmod.org/packages/local-ai-overlay) - `sci-ml/local-ai` and per-backend packages, overlay `local-ai` in Gentoo's repository index
### Web UIs
- [localai-admin](https://github.com/Jirubizu/localai-admin)