mirror of
https://github.com/mudler/LocalAI.git
synced 2024-12-19 20:57:54 +00:00
feat: Galleries UI (#2104)
* WIP: add models to webui Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Register routes Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: don't cache models Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * small fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: fixup multiple installs (strings.Clone) Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
parent
bd507678be
commit
0d8bf91699
@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
[![tests](https://github.com/go-skynet/LocalAI/actions/workflows/test.yml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/test.yml)[![Build and Release](https://github.com/go-skynet/LocalAI/actions/workflows/release.yaml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/release.yaml)[![build container images](https://github.com/go-skynet/LocalAI/actions/workflows/image.yml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/image.yml)[![Bump dependencies](https://github.com/go-skynet/LocalAI/actions/workflows/bump_deps.yaml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/bump_deps.yaml)[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/localai)](https://artifacthub.io/packages/search?repo=localai)
|
[![tests](https://github.com/go-skynet/LocalAI/actions/workflows/test.yml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/test.yml)[![Build and Release](https://github.com/go-skynet/LocalAI/actions/workflows/release.yaml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/release.yaml)[![build container images](https://github.com/go-skynet/LocalAI/actions/workflows/image.yml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/image.yml)[![Bump dependencies](https://github.com/go-skynet/LocalAI/actions/workflows/bump_deps.yaml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/bump_deps.yaml)[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/localai)](https://artifacthub.io/packages/search?repo=localai)
|
||||||
|
|
||||||
**LocalAI** is the free, Open Source OpenAI alternative. LocalAI act as a drop-in replacement REST API that’s compatible with OpenAI (Elevenlabs, Anthropic... ) API specifications for local AI inferencing. It allows you to run LLMs, generate images, audio (and not only) locally or on-prem with consumer grade hardware, supporting multiple model families. Does not require GPU.
|
**LocalAI** is the free, Open Source OpenAI alternative. LocalAI act as a drop-in replacement REST API that’s compatible with OpenAI (Elevenlabs, Anthropic... ) API specifications for local AI inferencing. It allows you to run LLMs, generate images, audio (and not only) locally or on-prem with consumer grade hardware, supporting multiple model families. Does not require GPU. It is created and maintained by [Ettore Di Giacinto](https://github.com/mudler).
|
||||||
|
|
||||||
## 🔥🔥 Hot topics / Roadmap
|
## 🔥🔥 Hot topics / Roadmap
|
||||||
|
|
||||||
|
@ -512,7 +512,7 @@ func (cl *BackendConfigLoader) Preload(modelPath string) error {
|
|||||||
for i, config := range cl.configs {
|
for i, config := range cl.configs {
|
||||||
|
|
||||||
// Download files and verify their SHA
|
// Download files and verify their SHA
|
||||||
for _, file := range config.DownloadFiles {
|
for i, file := range config.DownloadFiles {
|
||||||
log.Debug().Msgf("Checking %q exists and matches SHA", file.Filename)
|
log.Debug().Msgf("Checking %q exists and matches SHA", file.Filename)
|
||||||
|
|
||||||
if err := utils.VerifyPath(file.Filename, modelPath); err != nil {
|
if err := utils.VerifyPath(file.Filename, modelPath); err != nil {
|
||||||
@ -521,7 +521,7 @@ func (cl *BackendConfigLoader) Preload(modelPath string) error {
|
|||||||
// Create file path
|
// Create file path
|
||||||
filePath := filepath.Join(modelPath, file.Filename)
|
filePath := filepath.Join(modelPath, file.Filename)
|
||||||
|
|
||||||
if err := downloader.DownloadFile(file.URI, filePath, file.SHA256, status); err != nil {
|
if err := downloader.DownloadFile(file.URI, filePath, file.SHA256, i, len(config.DownloadFiles), status); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -535,7 +535,7 @@ func (cl *BackendConfigLoader) Preload(modelPath string) error {
|
|||||||
|
|
||||||
// check if file exists
|
// check if file exists
|
||||||
if _, err := os.Stat(filepath.Join(modelPath, md5Name)); errors.Is(err, os.ErrNotExist) {
|
if _, err := os.Stat(filepath.Join(modelPath, md5Name)); errors.Is(err, os.ErrNotExist) {
|
||||||
err := downloader.DownloadFile(modelURL, filepath.Join(modelPath, md5Name), "", status)
|
err := downloader.DownloadFile(modelURL, filepath.Join(modelPath, md5Name), "", 0, 0, status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -186,10 +186,14 @@ func App(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *confi
|
|||||||
utils.LoadConfig(appConfig.ConfigsDir, openai.AssistantsConfigFile, &openai.Assistants)
|
utils.LoadConfig(appConfig.ConfigsDir, openai.AssistantsConfigFile, &openai.Assistants)
|
||||||
utils.LoadConfig(appConfig.ConfigsDir, openai.AssistantsFileConfigFile, &openai.AssistantFiles)
|
utils.LoadConfig(appConfig.ConfigsDir, openai.AssistantsFileConfigFile, &openai.AssistantFiles)
|
||||||
|
|
||||||
|
galleryService := services.NewGalleryService(appConfig.ModelPath)
|
||||||
|
galleryService.Start(appConfig.Context, cl)
|
||||||
|
|
||||||
routes.RegisterElevenLabsRoutes(app, cl, ml, appConfig, auth)
|
routes.RegisterElevenLabsRoutes(app, cl, ml, appConfig, auth)
|
||||||
routes.RegisterLocalAIRoutes(app, cl, ml, appConfig, auth)
|
routes.RegisterLocalAIRoutes(app, cl, ml, appConfig, galleryService, auth)
|
||||||
routes.RegisterOpenAIRoutes(app, cl, ml, appConfig, auth)
|
routes.RegisterOpenAIRoutes(app, cl, ml, appConfig, auth)
|
||||||
routes.RegisterPagesRoutes(app, cl, ml, appConfig, auth)
|
routes.RegisterPagesRoutes(app, cl, ml, appConfig, auth)
|
||||||
|
routes.RegisterUIRoutes(app, cl, ml, appConfig, galleryService, auth)
|
||||||
|
|
||||||
// Define a custom 404 handler
|
// Define a custom 404 handler
|
||||||
// Note: keep this at the bottom!
|
// Note: keep this at the bottom!
|
||||||
|
171
core/http/elements/gallery.go
Normal file
171
core/http/elements/gallery.go
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
package elements
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/chasefleming/elem-go"
|
||||||
|
"github.com/chasefleming/elem-go/attrs"
|
||||||
|
"github.com/go-skynet/LocalAI/pkg/gallery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DoneProgress(uid string) string {
|
||||||
|
return elem.Div(
|
||||||
|
attrs.Props{},
|
||||||
|
elem.H3(
|
||||||
|
attrs.Props{
|
||||||
|
"role": "status",
|
||||||
|
"id": "pblabel",
|
||||||
|
"tabindex": "-1",
|
||||||
|
"autofocus": "",
|
||||||
|
},
|
||||||
|
elem.Text("Installation completed"),
|
||||||
|
),
|
||||||
|
).Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorProgress(err string) string {
|
||||||
|
return elem.Div(
|
||||||
|
attrs.Props{},
|
||||||
|
elem.H3(
|
||||||
|
attrs.Props{
|
||||||
|
"role": "status",
|
||||||
|
"id": "pblabel",
|
||||||
|
"tabindex": "-1",
|
||||||
|
"autofocus": "",
|
||||||
|
},
|
||||||
|
elem.Text("Error"+err),
|
||||||
|
),
|
||||||
|
).Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProgressBar(progress string) string {
|
||||||
|
return elem.Div(attrs.Props{
|
||||||
|
"class": "progress",
|
||||||
|
"role": "progressbar",
|
||||||
|
"aria-valuemin": "0",
|
||||||
|
"aria-valuemax": "100",
|
||||||
|
"aria-valuenow": "0",
|
||||||
|
"aria-labelledby": "pblabel",
|
||||||
|
},
|
||||||
|
elem.Div(attrs.Props{
|
||||||
|
"id": "pb",
|
||||||
|
"class": "progress-bar",
|
||||||
|
"style": "width:" + progress + "%",
|
||||||
|
}),
|
||||||
|
).Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartProgressBar(uid, progress string) string {
|
||||||
|
if progress == "" {
|
||||||
|
progress = "0"
|
||||||
|
}
|
||||||
|
return elem.Div(attrs.Props{
|
||||||
|
"hx-trigger": "done",
|
||||||
|
"hx-get": "/browse/job/" + uid,
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
"hx-target": "this",
|
||||||
|
},
|
||||||
|
elem.H3(
|
||||||
|
attrs.Props{
|
||||||
|
"role": "status",
|
||||||
|
"id": "pblabel",
|
||||||
|
"tabindex": "-1",
|
||||||
|
"autofocus": "",
|
||||||
|
},
|
||||||
|
elem.Text("Installing"),
|
||||||
|
// This is a simple example of how to use the HTMLX library to create a progress bar that updates every 600ms.
|
||||||
|
elem.Div(attrs.Props{
|
||||||
|
"hx-get": "/browse/job/progress/" + uid,
|
||||||
|
"hx-trigger": "every 600ms",
|
||||||
|
"hx-target": "this",
|
||||||
|
"hx-swap": "innerHTML",
|
||||||
|
},
|
||||||
|
elem.Raw(ProgressBar(progress)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListModels(models []*gallery.GalleryModel) string {
|
||||||
|
modelsElements := []elem.Node{}
|
||||||
|
span := func(s string) elem.Node {
|
||||||
|
return elem.Span(
|
||||||
|
attrs.Props{
|
||||||
|
"class": "float-right inline-block bg-green-500 text-white py-1 px-3 rounded-full text-xs",
|
||||||
|
},
|
||||||
|
elem.Text(s),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
installButton := func(m *gallery.GalleryModel) elem.Node {
|
||||||
|
return elem.Button(
|
||||||
|
attrs.Props{
|
||||||
|
"class": "float-right inline-block rounded bg-primary px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong",
|
||||||
|
// post the Model ID as param
|
||||||
|
"hx-post": "/browse/install/model/" + fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name),
|
||||||
|
},
|
||||||
|
elem.Text("Install"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptionDiv := func(m *gallery.GalleryModel) elem.Node {
|
||||||
|
|
||||||
|
return elem.Div(
|
||||||
|
attrs.Props{
|
||||||
|
"class": "p-6",
|
||||||
|
},
|
||||||
|
elem.H5(
|
||||||
|
attrs.Props{
|
||||||
|
"class": "mb-2 text-xl font-medium leading-tight",
|
||||||
|
},
|
||||||
|
elem.Text(m.Name),
|
||||||
|
),
|
||||||
|
elem.P(
|
||||||
|
attrs.Props{
|
||||||
|
"class": "mb-4 text-base",
|
||||||
|
},
|
||||||
|
elem.Text(m.Description),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
actionDiv := func(m *gallery.GalleryModel) elem.Node {
|
||||||
|
return elem.Div(
|
||||||
|
attrs.Props{
|
||||||
|
"class": "px-6 pt-4 pb-2",
|
||||||
|
},
|
||||||
|
elem.Span(
|
||||||
|
attrs.Props{
|
||||||
|
"class": "inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2",
|
||||||
|
},
|
||||||
|
elem.Text("Repository: "+m.Gallery.Name),
|
||||||
|
),
|
||||||
|
elem.If(m.Installed, span("Installed"), installButton(m)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range models {
|
||||||
|
modelsElements = append(modelsElements,
|
||||||
|
elem.Div(
|
||||||
|
attrs.Props{
|
||||||
|
"class": "me-4 mb-2 block rounded-lg bg-white shadow-secondary-1 dark:bg-gray-800 dark:bg-surface-dark dark:text-white text-surface p-2",
|
||||||
|
},
|
||||||
|
elem.Div(
|
||||||
|
attrs.Props{
|
||||||
|
"class": "p-6",
|
||||||
|
},
|
||||||
|
descriptionDiv(m),
|
||||||
|
actionDiv(m),
|
||||||
|
// elem.If(m.Installed, span("Installed"), installButton(m)),
|
||||||
|
|
||||||
|
// elem.If(m.Installed, span("Installed"), span("Not Installed")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper := elem.Div(attrs.Props{
|
||||||
|
"class": "dark grid grid-cols-1 grid-rows-1 md:grid-cols-2 ",
|
||||||
|
}, modelsElements...)
|
||||||
|
|
||||||
|
return wrapper.Render()
|
||||||
|
}
|
@ -3,12 +3,16 @@ package localai
|
|||||||
import (
|
import (
|
||||||
"github.com/go-skynet/LocalAI/core/config"
|
"github.com/go-skynet/LocalAI/core/config"
|
||||||
"github.com/go-skynet/LocalAI/internal"
|
"github.com/go-skynet/LocalAI/internal"
|
||||||
|
"github.com/go-skynet/LocalAI/pkg/model"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WelcomeEndpoint(appConfig *config.ApplicationConfig,
|
func WelcomeEndpoint(appConfig *config.ApplicationConfig,
|
||||||
models []string, backendConfigs []config.BackendConfig) func(*fiber.Ctx) error {
|
cl *config.BackendConfigLoader, ml *model.ModelLoader) func(*fiber.Ctx) error {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
|
models, _ := ml.ListModels()
|
||||||
|
backendConfigs := cl.GetAllBackendConfigs()
|
||||||
|
|
||||||
summary := fiber.Map{
|
summary := fiber.Map{
|
||||||
"Title": "LocalAI API - " + internal.PrintableVersion(),
|
"Title": "LocalAI API - " + internal.PrintableVersion(),
|
||||||
"Version": internal.PrintableVersion(),
|
"Version": internal.PrintableVersion(),
|
||||||
|
@ -14,13 +14,12 @@ func RegisterLocalAIRoutes(app *fiber.App,
|
|||||||
cl *config.BackendConfigLoader,
|
cl *config.BackendConfigLoader,
|
||||||
ml *model.ModelLoader,
|
ml *model.ModelLoader,
|
||||||
appConfig *config.ApplicationConfig,
|
appConfig *config.ApplicationConfig,
|
||||||
|
galleryService *services.GalleryService,
|
||||||
auth func(*fiber.Ctx) error) {
|
auth func(*fiber.Ctx) error) {
|
||||||
|
|
||||||
app.Get("/swagger/*", swagger.HandlerDefault) // default
|
app.Get("/swagger/*", swagger.HandlerDefault) // default
|
||||||
|
|
||||||
// LocalAI API endpoints
|
// LocalAI API endpoints
|
||||||
galleryService := services.NewGalleryService(appConfig.ModelPath)
|
|
||||||
galleryService.Start(appConfig.Context, cl)
|
|
||||||
|
|
||||||
modelGalleryEndpointService := localai.CreateModelGalleryEndpointService(appConfig.Galleries, appConfig.ModelPath, galleryService)
|
modelGalleryEndpointService := localai.CreateModelGalleryEndpointService(appConfig.Galleries, appConfig.ModelPath, galleryService)
|
||||||
app.Post("/models/apply", auth, modelGalleryEndpointService.ApplyModelGalleryEndpoint())
|
app.Post("/models/apply", auth, modelGalleryEndpointService.ApplyModelGalleryEndpoint())
|
||||||
|
107
core/http/routes/ui.go
Normal file
107
core/http/routes/ui.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-skynet/LocalAI/core/config"
|
||||||
|
"github.com/go-skynet/LocalAI/core/http/elements"
|
||||||
|
"github.com/go-skynet/LocalAI/core/services"
|
||||||
|
"github.com/go-skynet/LocalAI/pkg/gallery"
|
||||||
|
"github.com/go-skynet/LocalAI/pkg/model"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterUIRoutes(app *fiber.App,
|
||||||
|
cl *config.BackendConfigLoader,
|
||||||
|
ml *model.ModelLoader,
|
||||||
|
appConfig *config.ApplicationConfig,
|
||||||
|
galleryService *services.GalleryService,
|
||||||
|
auth func(*fiber.Ctx) error) {
|
||||||
|
|
||||||
|
// Show the Models page
|
||||||
|
app.Get("/browse", auth, func(c *fiber.Ctx) error {
|
||||||
|
models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
|
||||||
|
|
||||||
|
summary := fiber.Map{
|
||||||
|
"Title": "LocalAI API - Models",
|
||||||
|
"Models": template.HTML(elements.ListModels(models)),
|
||||||
|
// "ApplicationConfig": appConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render index
|
||||||
|
return c.Render("views/models", summary)
|
||||||
|
})
|
||||||
|
|
||||||
|
// HTMX: return the model details
|
||||||
|
// https://htmx.org/examples/active-search/
|
||||||
|
app.Post("/browse/search/models", auth, func(c *fiber.Ctx) error {
|
||||||
|
form := struct {
|
||||||
|
Search string `form:"search"`
|
||||||
|
}{}
|
||||||
|
if err := c.BodyParser(&form); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
|
||||||
|
|
||||||
|
filteredModels := []*gallery.GalleryModel{}
|
||||||
|
for _, m := range models {
|
||||||
|
if strings.Contains(m.Name, form.Search) {
|
||||||
|
filteredModels = append(filteredModels, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendString(elements.ListModels(filteredModels))
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://htmx.org/examples/progress-bar/
|
||||||
|
app.Post("/browse/install/model/:id", auth, func(c *fiber.Ctx) error {
|
||||||
|
galleryID := strings.Clone(c.Params("id")) // strings.Clone is required!
|
||||||
|
|
||||||
|
id, err := uuid.NewUUID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := id.String()
|
||||||
|
|
||||||
|
op := gallery.GalleryOp{
|
||||||
|
Id: uid,
|
||||||
|
GalleryName: galleryID,
|
||||||
|
Galleries: appConfig.Galleries,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
galleryService.C <- op
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c.SendString(elements.StartProgressBar(uid, "0"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://htmx.org/examples/progress-bar/
|
||||||
|
app.Get("/browse/job/progress/:uid", auth, func(c *fiber.Ctx) error {
|
||||||
|
jobUID := c.Params("uid")
|
||||||
|
|
||||||
|
status := galleryService.GetStatus(jobUID)
|
||||||
|
if status == nil {
|
||||||
|
//fmt.Errorf("could not find any status for ID")
|
||||||
|
return c.SendString(elements.ProgressBar("0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Progress == 100 {
|
||||||
|
c.Set("HX-Trigger", "done")
|
||||||
|
return c.SendString(elements.ProgressBar("100"))
|
||||||
|
}
|
||||||
|
if status.Error != nil {
|
||||||
|
return c.SendString(elements.ErrorProgress(status.Error.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendString(elements.ProgressBar(fmt.Sprint(status.Progress)))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/browse/job/:uid", auth, func(c *fiber.Ctx) error {
|
||||||
|
return c.SendString(elements.DoneProgress(c.Params("uid")))
|
||||||
|
})
|
||||||
|
}
|
@ -13,11 +13,7 @@ func RegisterPagesRoutes(app *fiber.App,
|
|||||||
appConfig *config.ApplicationConfig,
|
appConfig *config.ApplicationConfig,
|
||||||
auth func(*fiber.Ctx) error) {
|
auth func(*fiber.Ctx) error) {
|
||||||
|
|
||||||
models, _ := ml.ListModels()
|
|
||||||
backendConfigs := cl.GetAllBackendConfigs()
|
|
||||||
|
|
||||||
if !appConfig.DisableWelcomePage {
|
if !appConfig.DisableWelcomePage {
|
||||||
app.Get("/", auth, localai.WelcomeEndpoint(appConfig, models, backendConfigs))
|
app.Get("/", auth, localai.WelcomeEndpoint(appConfig, cl, ml))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
40
core/http/views/models.html
Normal file
40
core/http/views/models.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
|
<body class="bg-gray-900 text-gray-200">
|
||||||
|
<div class="flex flex-col min-h-screen">
|
||||||
|
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
|
<div class="container mx-auto px-4 flex-grow">
|
||||||
|
<div class="header text-center py-12">
|
||||||
|
<h1 class="text-5xl font-bold text-gray-100">Welcome to <i>your</i> LocalAI instance!</h1>
|
||||||
|
<div class="mt-6">
|
||||||
|
<!-- Logo can be uncommented and updated with a valid URL -->
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-lg">The FOSS alternative to OpenAI, Claude, ...</p>
|
||||||
|
<a href="https://localai.io" target="_blank" class="mt-4 inline-block bg-blue-500 text-white py-2 px-4 rounded-lg shadow transition duration-300 ease-in-out hover:bg-blue-700 hover:shadow-lg">
|
||||||
|
<i class="fas fa-book-reader pr-2"></i>Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="models mt-12">
|
||||||
|
<h2 class="text-center text-3xl font-semibold text-gray-100">Available models from repositories</h2>
|
||||||
|
|
||||||
|
<span class="htmx-indicator loader"></span>
|
||||||
|
<input class="form-control appearance-none block w-full px-3 py-2 text-base font-normal text-gray-300 pb-2 mb-5 bg-gray-800 bg-clip-padding border border-solid border-gray-600 rounded transition ease-in-out m-0 focus:text-gray-300 focus:bg-gray-900 focus:border-blue-500 focus:outline-none" type="search"
|
||||||
|
name="search" placeholder="Begin Typing To Search models..."
|
||||||
|
hx-post="/browse/search/models"
|
||||||
|
hx-trigger="input changed delay:500ms, search"
|
||||||
|
hx-target="#search-results"
|
||||||
|
hx-indicator=".htmx-indicator">
|
||||||
|
|
||||||
|
<div id="search-results">{{.Models}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -3,11 +3,76 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900&display=swap"
|
||||||
|
rel="stylesheet" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/tw-elements/css/tw-elements.min.css" />
|
||||||
|
<script src="https://cdn.tailwindcss.com/3.3.0"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["Roboto", "sans-serif"],
|
||||||
|
body: ["Roboto", "sans-serif"],
|
||||||
|
mono: ["ui-monospace", "monospace"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
corePlugins: {
|
||||||
|
preflight: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.12" integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2" crossorigin="anonymous"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
/* Loader (https://cssloaders.github.io/) */
|
||||||
|
.loader {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: block;
|
||||||
|
margin:15px auto;
|
||||||
|
position: relative;
|
||||||
|
color: #FFF;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: animloader 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animloader {
|
||||||
|
0% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||||
|
25% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 2px; }
|
||||||
|
50% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 2px, -38px 0 0 -2px; }
|
||||||
|
75% { box-shadow: 14px 0 0 2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||||
|
100% { box-shadow: 14px 0 0 -2px, 38px 0 0 2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
height: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
float: left;
|
||||||
|
width: 0%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #337ab7;
|
||||||
|
-webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
|
||||||
|
-webkit-transition: width .6s ease;
|
||||||
|
-o-transition: width .6s ease;
|
||||||
|
transition: width .6s ease;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
@ -9,6 +9,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<a href="/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-home pr-2"></i>Home</a>
|
<a href="/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-home pr-2"></i>Home</a>
|
||||||
<a href="https://localai.io" class="text-gray-400 hover:text-white px-3 py-2 rounded" target="_blank" ><i class="fas fa-book-reader pr-2"></i> Documentation</a>
|
<a href="https://localai.io" class="text-gray-400 hover:text-white px-3 py-2 rounded" target="_blank" ><i class="fas fa-book-reader pr-2"></i> Documentation</a>
|
||||||
|
<a href="/browse/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-brain pr-2"></i> Models</a>
|
||||||
<a href="/swagger/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-code pr-2"></i> API</a>
|
<a href="/swagger/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-code pr-2"></i> API</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,7 +56,7 @@ icon = "info"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
**LocalAI** is the free, Open Source OpenAI alternative. LocalAI act as a drop-in replacement REST API that's compatible with OpenAI API specifications for local inferencing. It allows you to run LLMs, generate images, audio (and not only) locally or on-prem with consumer grade hardware, supporting multiple model families and architectures. Does not require GPU. It is maintained by [mudler](https://github.com/mudler).
|
**LocalAI** is the free, Open Source OpenAI alternative. LocalAI act as a drop-in replacement REST API that's compatible with OpenAI API specifications for local inferencing. It allows you to run LLMs, generate images, audio (and not only) locally or on-prem with consumer grade hardware, supporting multiple model families and architectures. Does not require GPU. It is created and maintained by [Ettore Di Giacinto](https://github.com/mudler).
|
||||||
|
|
||||||
|
|
||||||
## Start LocalAI
|
## Start LocalAI
|
||||||
|
5
go.mod
5
go.mod
@ -1,6 +1,8 @@
|
|||||||
module github.com/go-skynet/LocalAI
|
module github.com/go-skynet/LocalAI
|
||||||
|
|
||||||
go 1.21
|
go 1.21.1
|
||||||
|
|
||||||
|
toolchain go1.22.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/M0Rf30/go-tiny-dream v0.0.0-20231128165230-772a9c0d9aaf
|
github.com/M0Rf30/go-tiny-dream v0.0.0-20231128165230-772a9c0d9aaf
|
||||||
@ -71,6 +73,7 @@ require (
|
|||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/chasefleming/elem-go v0.25.0 // indirect
|
||||||
github.com/containerd/continuity v0.3.0 // indirect
|
github.com/containerd/continuity v0.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.8.1 // indirect
|
github.com/dlclark/regexp2 v1.8.1 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -37,6 +37,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
|
|||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
|
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
|
||||||
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
|
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
|
||||||
|
github.com/chasefleming/elem-go v0.25.0 h1:LYzr1auk39Bh3bdKloArOFV7sOBnOfSOKxsg58eWL0Q=
|
||||||
|
github.com/chasefleming/elem-go v0.25.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
2
main.go
2
main.go
@ -72,7 +72,7 @@ Version: ${version}
|
|||||||
kong.Vars{
|
kong.Vars{
|
||||||
"basepath": kong.ExpandPath("."),
|
"basepath": kong.ExpandPath("."),
|
||||||
"remoteLibraryURL": "https://raw.githubusercontent.com/mudler/LocalAI/master/embedded/model_library.yaml",
|
"remoteLibraryURL": "https://raw.githubusercontent.com/mudler/LocalAI/master/embedded/model_library.yaml",
|
||||||
"galleries": `[{"name":"localai", "url":"github:mudler/LocalAI/gallery/index.yaml"}]`,
|
"galleries": `[{"name":"localai", "url":"github:mudler/LocalAI/gallery/index.yaml@master"}]`,
|
||||||
"version": internal.PrintableVersion(),
|
"version": internal.PrintableVersion(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -5,6 +5,8 @@ import "hash"
|
|||||||
type progressWriter struct {
|
type progressWriter struct {
|
||||||
fileName string
|
fileName string
|
||||||
total int64
|
total int64
|
||||||
|
fileNo int
|
||||||
|
totalFiles int
|
||||||
written int64
|
written int64
|
||||||
downloadStatus func(string, string, string, float64)
|
downloadStatus func(string, string, string, float64)
|
||||||
hash hash.Hash
|
hash hash.Hash
|
||||||
@ -16,6 +18,17 @@ func (pw *progressWriter) Write(p []byte) (n int, err error) {
|
|||||||
|
|
||||||
if pw.total > 0 {
|
if pw.total > 0 {
|
||||||
percentage := float64(pw.written) / float64(pw.total) * 100
|
percentage := float64(pw.written) / float64(pw.total) * 100
|
||||||
|
if pw.totalFiles > 1 {
|
||||||
|
// This is a multi-file download
|
||||||
|
// so we need to adjust the percentage
|
||||||
|
// to reflect the progress of the whole download
|
||||||
|
// This is the file pw.fileNo of pw.totalFiles files. We assume that
|
||||||
|
// the files before successfully downloaded.
|
||||||
|
percentage = percentage / float64(pw.totalFiles)
|
||||||
|
if pw.fileNo > 1 {
|
||||||
|
percentage += float64(pw.fileNo-1) * 100 / float64(pw.totalFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
//log.Debug().Msgf("Downloading %s: %s/%s (%.2f%%)", pw.fileName, formatBytes(pw.written), formatBytes(pw.total), percentage)
|
//log.Debug().Msgf("Downloading %s: %s/%s (%.2f%%)", pw.fileName, formatBytes(pw.written), formatBytes(pw.total), percentage)
|
||||||
pw.downloadStatus(pw.fileName, formatBytes(pw.written), formatBytes(pw.total), percentage)
|
pw.downloadStatus(pw.fileName, formatBytes(pw.written), formatBytes(pw.total), percentage)
|
||||||
} else {
|
} else {
|
||||||
|
@ -136,7 +136,7 @@ func removePartialFile(tmpFilePath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DownloadFile(url string, filePath, sha string, downloadStatus func(string, string, string, float64)) error {
|
func DownloadFile(url string, filePath, sha string, fileN, total int, downloadStatus func(string, string, string, float64)) error {
|
||||||
url = ConvertURL(url)
|
url = ConvertURL(url)
|
||||||
// Check if the file already exists
|
// Check if the file already exists
|
||||||
_, err := os.Stat(filePath)
|
_, err := os.Stat(filePath)
|
||||||
@ -209,6 +209,8 @@ func DownloadFile(url string, filePath, sha string, downloadStatus func(string,
|
|||||||
fileName: tmpFilePath,
|
fileName: tmpFilePath,
|
||||||
total: resp.ContentLength,
|
total: resp.ContentLength,
|
||||||
hash: sha256.New(),
|
hash: sha256.New(),
|
||||||
|
fileNo: fileN,
|
||||||
|
totalFiles: total,
|
||||||
downloadStatus: downloadStatus,
|
downloadStatus: downloadStatus,
|
||||||
}
|
}
|
||||||
_, err = io.Copy(io.MultiWriter(outFile, progress), resp.Body)
|
_, err = io.Copy(io.MultiWriter(outFile, progress), resp.Body)
|
||||||
|
@ -102,7 +102,7 @@ func InstallModel(basePath, nameOverride string, config *Config, configOverrides
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Download files and verify their SHA
|
// Download files and verify their SHA
|
||||||
for _, file := range config.Files {
|
for i, file := range config.Files {
|
||||||
log.Debug().Msgf("Checking %q exists and matches SHA", file.Filename)
|
log.Debug().Msgf("Checking %q exists and matches SHA", file.Filename)
|
||||||
|
|
||||||
if err := utils.VerifyPath(file.Filename, basePath); err != nil {
|
if err := utils.VerifyPath(file.Filename, basePath); err != nil {
|
||||||
@ -111,7 +111,7 @@ func InstallModel(basePath, nameOverride string, config *Config, configOverrides
|
|||||||
// Create file path
|
// Create file path
|
||||||
filePath := filepath.Join(basePath, file.Filename)
|
filePath := filepath.Join(basePath, file.Filename)
|
||||||
|
|
||||||
if err := downloader.DownloadFile(file.URI, filePath, file.SHA256, downloadStatus); err != nil {
|
if err := downloader.DownloadFile(file.URI, filePath, file.SHA256, i, len(config.Files), downloadStatus); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package gallery
|
package gallery
|
||||||
|
|
||||||
type GalleryOp struct {
|
type GalleryOp struct {
|
||||||
Req GalleryModel
|
|
||||||
Id string
|
Id string
|
||||||
Galleries []Gallery
|
|
||||||
GalleryName string
|
GalleryName string
|
||||||
ConfigURL string
|
ConfigURL string
|
||||||
|
|
||||||
|
Req GalleryModel
|
||||||
|
Galleries []Gallery
|
||||||
}
|
}
|
||||||
|
|
||||||
type GalleryOpStatus struct {
|
type GalleryOpStatus struct {
|
||||||
|
@ -54,7 +54,7 @@ func PreloadModelsConfigurations(modelLibraryURL string, modelPath string, model
|
|||||||
// check if file exists
|
// check if file exists
|
||||||
if _, err := os.Stat(filepath.Join(modelPath, md5Name)); errors.Is(err, os.ErrNotExist) {
|
if _, err := os.Stat(filepath.Join(modelPath, md5Name)); errors.Is(err, os.ErrNotExist) {
|
||||||
modelDefinitionFilePath := filepath.Join(modelPath, md5Name) + ".yaml"
|
modelDefinitionFilePath := filepath.Join(modelPath, md5Name) + ".yaml"
|
||||||
err := downloader.DownloadFile(url, modelDefinitionFilePath, "", func(fileName, current, total string, percent float64) {
|
err := downloader.DownloadFile(url, modelDefinitionFilePath, "", 0, 0, func(fileName, current, total string, percent float64) {
|
||||||
utils.DisplayDownloadFunction(fileName, current, total, percent)
|
utils.DisplayDownloadFunction(fileName, current, total, percent)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user