From 882556d4db728a6af3129f6369b54a4fbb0c17f3 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Thu, 13 Jun 2024 00:47:16 +0200 Subject: [PATCH] feat(gallery): show available models in website, allow `local-ai models install` to install from galleries (#2555) * WIP Signed-off-by: Ettore Di Giacinto * gen a static page instead (we force DNS redirects to it) Signed-off-by: Ettore Di Giacinto * feat(gallery): install models from CLI, unify install Signed-off-by: Ettore Di Giacinto * Uniform graphic of model page Signed-off-by: Ettore Di Giacinto * Makefile: update targets Signed-off-by: Ettore Di Giacinto * Slightly enhance gallery view Signed-off-by: Ettore Di Giacinto --------- Signed-off-by: Ettore Di Giacinto --- .github/ci/modelslist.go | 291 +++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + Makefile | 20 ++- core/cli/models.go | 58 +++++--- pkg/gallery/gallery.go | 59 ++++---- pkg/gallery/request.go | 9 ++ 6 files changed, 383 insertions(+), 55 deletions(-) create mode 100644 .github/ci/modelslist.go diff --git a/.github/ci/modelslist.go b/.github/ci/modelslist.go new file mode 100644 index 00000000..36413068 --- /dev/null +++ b/.github/ci/modelslist.go @@ -0,0 +1,291 @@ +package main + +import ( + "fmt" + "html/template" + "io/ioutil" + "os" + + "gopkg.in/yaml.v3" +) + +var modelPageTemplate string = ` + + + + + + LocalAI models + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ +
+

+ LocalAI model gallery list


+ +

+ + 🖼️ Available {{.AvailableModels}} models repositories + +

+ +

+ Refer to Model gallery for more information on how to use the models with LocalAI. + + You can install models with the CLI command local-ai models install . or by using the WebUI. +

+ + +
+ {{ range $_, $model := .Models }} +
+
+ {{ $icon := "https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg" }} + {{ if $model.Icon }} + {{ $icon = $model.Icon }} + {{ end }} +
+ {{$model.Name}} +
+
+
{{$model.Name}}
+ + +

{{ $model.Description }}

+ +
+
+ + + + + + + + +
+
+
+ {{ end }} + +
+
+
+ + + +
+ + + + +` + +type GalleryModel struct { + Name string `json:"name" yaml:"name"` + URLs []string `json:"urls" yaml:"urls"` + Icon string `json:"icon" yaml:"icon"` + Description string `json:"description" yaml:"description"` +} + +func main() { + // read the YAML file which contains the models + + f, err := ioutil.ReadFile(os.Args[1]) + if err != nil { + fmt.Println("Error reading file:", err) + return + } + + models := []*GalleryModel{} + err = yaml.Unmarshal(f, &models) + if err != nil { + // write to stderr + os.Stderr.WriteString("Error unmarshaling YAML: " + err.Error() + "\n") + return + } + + // render the template + data := struct { + Models []*GalleryModel + AvailableModels int + }{ + Models: models, + AvailableModels: len(models), + } + tmpl := template.Must(template.New("modelPage").Parse(modelPageTemplate)) + + err = tmpl.Execute(os.Stdout, data) + if err != nil { + fmt.Println("Error executing template:", err) + return + } +} diff --git a/.gitignore b/.gitignore index a67a71c4..096689c5 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ backend-assets/* !backend-assets/.keep prepare /ggml-metal.metal +docs/static/gallery.html # Protobuf generated files *.pb.go diff --git a/Makefile b/Makefile index 54a340a2..717a048b 100644 --- a/Makefile +++ b/Makefile @@ -836,4 +836,22 @@ swagger: .PHONY: gen-assets gen-assets: - $(GOCMD) run core/dependencies_manager/manager.go embedded/webui_static.yaml core/http/static/assets \ No newline at end of file + $(GOCMD) run core/dependencies_manager/manager.go embedded/webui_static.yaml core/http/static/assets + +## Documentation +docs/layouts/_default: + mkdir -p docs/layouts/_default + +docs/static/gallery.html: docs/layouts/_default + $(GOCMD) run ./.github/ci/modelslist.go ./gallery/index.yaml > docs/static/gallery.html + +docs/public: docs/layouts/_default docs/static/gallery.html + cd docs && hugo --minify + +docs-clean: + rm -rf docs/public + rm -rf docs/static/gallery.html + +.PHONY: docs +docs: docs/static/gallery.html + cd docs && hugo serve \ No newline at end of file diff --git a/core/cli/models.go b/core/cli/models.go index 96b7e4e7..d5138585 100644 --- a/core/cli/models.go +++ b/core/cli/models.go @@ -12,7 +12,7 @@ import ( ) type ModelsCMDFlags struct { - Galleries string `env:"LOCALAI_GALLERIES,GALLERIES" help:"JSON list of galleries" group:"models"` + Galleries string `env:"LOCALAI_GALLERIES,GALLERIES" help:"JSON list of galleries" group:"models" default:"${galleries}"` ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"` } @@ -52,29 +52,43 @@ func (ml *ModelsList) Run(ctx *cliContext.Context) error { } func (mi *ModelsInstall) Run(ctx *cliContext.Context) error { - modelName := mi.ModelArgs[0] + for _, modelName := range mi.ModelArgs { - var galleries []gallery.Gallery - if err := json.Unmarshal([]byte(mi.Galleries), &galleries); err != nil { - log.Error().Err(err).Msg("unable to load galleries") - } - - progressBar := progressbar.NewOptions( - 1000, - progressbar.OptionSetDescription(fmt.Sprintf("downloading model %s", modelName)), - progressbar.OptionShowBytes(false), - progressbar.OptionClearOnFinish(), - ) - progressCallback := func(fileName string, current string, total string, percentage float64) { - v := int(percentage * 10) - err := progressBar.Set(v) - if err != nil { - log.Error().Err(err).Str("filename", fileName).Int("value", v).Msg("error while updating progress bar") + var galleries []gallery.Gallery + if err := json.Unmarshal([]byte(mi.Galleries), &galleries); err != nil { + log.Error().Err(err).Msg("unable to load galleries") + } + + progressBar := progressbar.NewOptions( + 1000, + progressbar.OptionSetDescription(fmt.Sprintf("downloading model %s", modelName)), + progressbar.OptionShowBytes(false), + progressbar.OptionClearOnFinish(), + ) + progressCallback := func(fileName string, current string, total string, percentage float64) { + v := int(percentage * 10) + err := progressBar.Set(v) + if err != nil { + log.Error().Err(err).Str("filename", fileName).Int("value", v).Msg("error while updating progress bar") + } + } + + models, err := gallery.AvailableGalleryModels(galleries, mi.ModelsPath) + if err != nil { + return err + } + + model := gallery.FindModel(models, modelName, mi.ModelsPath) + if model == nil { + log.Error().Str("model", modelName).Msg("model not found") + return err + } + + log.Info().Str("model", modelName).Str("license", model.License).Msg("installing model") + err = gallery.InstallModelFromGalleryByName(galleries, modelName, mi.ModelsPath, gallery.GalleryModel{}, progressCallback) + if err != nil { + return err } - } - err := gallery.InstallModelFromGallery(galleries, modelName, mi.ModelsPath, gallery.GalleryModel{}, progressCallback) - if err != nil { - return err } return nil } diff --git a/pkg/gallery/gallery.go b/pkg/gallery/gallery.go index 0e9daa79..a18404f5 100644 --- a/pkg/gallery/gallery.go +++ b/pkg/gallery/gallery.go @@ -20,6 +20,7 @@ type Gallery struct { // Installs a model from the gallery (galleryname@modelname) func InstallModelFromGallery(galleries []Gallery, name string, basePath string, req GalleryModel, downloadStatus func(string, string, string, float64)) error { + applyModel := func(model *GalleryModel) error { name = strings.ReplaceAll(name, string(os.PathSeparator), "__") @@ -78,50 +79,44 @@ func InstallModelFromGallery(galleries []Gallery, name string, basePath string, return err } - model, err := FindGallery(models, name) - if err != nil { - var err2 error - model, err2 = FindGallery(models, strings.ToLower(name)) - if err2 != nil { - return err - } + model := FindModel(models, name, basePath) + if model == nil { + return fmt.Errorf("no model found with name %q", name) } return applyModel(model) } -func FindGallery(models []*GalleryModel, name string) (*GalleryModel, error) { - // os.PathSeparator is not allowed in model names. Replace them with "__" to avoid conflicts with file paths. +func FindModel(models []*GalleryModel, name string, basePath string) *GalleryModel { + var model *GalleryModel name = strings.ReplaceAll(name, string(os.PathSeparator), "__") - for _, model := range models { - if name == fmt.Sprintf("%s@%s", model.Gallery.Name, model.Name) { - return model, nil + if !strings.Contains(name, "@") { + for _, m := range models { + if strings.EqualFold(m.Name, name) { + model = m + break + } + } + + if model == nil { + return nil + } + } else { + for _, m := range models { + if strings.EqualFold(name, fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)) { + model = m + break + } } } - return nil, fmt.Errorf("no gallery found with name %q", name) + + return model } -// InstallModelFromGalleryByName loads a model from the gallery by specifying only the name (first match wins) +// InstallModelFromGalleryByName is planned for deprecation func InstallModelFromGalleryByName(galleries []Gallery, name string, basePath string, req GalleryModel, downloadStatus func(string, string, string, float64)) error { - models, err := AvailableGalleryModels(galleries, basePath) - if err != nil { - return err - } - - name = strings.ReplaceAll(name, string(os.PathSeparator), "__") - var model *GalleryModel - for _, m := range models { - if name == m.Name || m.Name == strings.ToLower(name) { - model = m - } - } - - if model == nil { - return fmt.Errorf("no model found with name %q", name) - } - - return InstallModelFromGallery(galleries, fmt.Sprintf("%s@%s", model.Gallery.Name, model.Name), basePath, req, downloadStatus) + return InstallModelFromGallery(galleries, name, basePath, req, downloadStatus) } // List available models diff --git a/pkg/gallery/request.go b/pkg/gallery/request.go index b6efc2ff..61a25912 100644 --- a/pkg/gallery/request.go +++ b/pkg/gallery/request.go @@ -47,3 +47,12 @@ func (gm GalleryModels) Search(term string) GalleryModels { } return filteredModels } + +func (gm GalleryModels) FindByName(name string) *GalleryModel { + for _, m := range gm { + if strings.EqualFold(m.Name, name) { + return m + } + } + return nil +}