mirror of
https://github.com/mudler/LocalAI.git
synced 2026-02-03 03:02:38 -05:00
Compare commits
52 Commits
feat/trans
...
v3.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
034b9b691b | ||
|
|
ba52822e5c | ||
|
|
eb30f6c090 | ||
|
|
caba098959 | ||
|
|
3c75ea1e0e | ||
|
|
c5f911812f | ||
|
|
d82922786a | ||
|
|
d9e9bb4c0e | ||
|
|
657027bec6 | ||
|
|
2f5635308d | ||
|
|
63b5338dbd | ||
|
|
3150174962 | ||
|
|
4330fdce33 | ||
|
|
fef8583144 | ||
|
|
d4d6a56a4f | ||
|
|
2900a601a0 | ||
|
|
43e0437db6 | ||
|
|
976c159fdb | ||
|
|
969922ffec | ||
|
|
739573e41b | ||
|
|
dbdf2908ad | ||
|
|
317f8641dc | ||
|
|
54ff70e451 | ||
|
|
723f01c87e | ||
|
|
79a41a5e07 | ||
|
|
d0b6aa3f7d | ||
|
|
ad99399c6e | ||
|
|
e6ebfd3ba1 | ||
|
|
ead00a28b9 | ||
|
|
9621edb4c5 | ||
|
|
7ce92f0646 | ||
|
|
6a4ab3c1e0 | ||
|
|
83b85494c1 | ||
|
|
df6a80b38d | ||
|
|
21faa4114b | ||
|
|
e35ad56602 | ||
|
|
3be8b2d8e1 | ||
|
|
900745bb4d | ||
|
|
15a7fc7e9a | ||
|
|
03dddec538 | ||
|
|
3d34386712 | ||
|
|
1b3f66018b | ||
|
|
4381e892b8 | ||
|
|
3c3f477854 | ||
|
|
f8a8cf3e95 | ||
|
|
0fc88b3cdf | ||
|
|
4993df81c3 | ||
|
|
599bc88c6c | ||
|
|
1a0d06f3db | ||
|
|
5e1a8b3621 | ||
|
|
960e51e527 | ||
|
|
195aa22e77 |
@@ -6,6 +6,10 @@ models
|
||||
backends
|
||||
examples/chatbot-ui/models
|
||||
backend/go/image/stablediffusion-ggml/build/
|
||||
backend/go/*/build
|
||||
backend/go/*/.cache
|
||||
backend/go/*/sources
|
||||
backend/go/*/package
|
||||
examples/rwkv/models
|
||||
examples/**/models
|
||||
Dockerfile*
|
||||
|
||||
89
.github/workflows/backend.yml
vendored
89
.github/workflows/backend.yml
vendored
@@ -2,7 +2,6 @@
|
||||
name: 'build backend container images'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -64,18 +63,6 @@ jobs:
|
||||
backend: "llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.llama-cpp"
|
||||
context: "./"
|
||||
- build-type: ''
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
platforms: 'linux/amd64'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-cpu-transformers'
|
||||
runs-on: 'ubuntu-latest'
|
||||
base-image: "ubuntu:22.04"
|
||||
skip-drivers: 'true'
|
||||
backend: "transformers"
|
||||
dockerfile: "./backend/Dockerfile.python"
|
||||
context: "./backend"
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "11"
|
||||
cuda-minor-version: "7"
|
||||
@@ -243,7 +230,7 @@ jobs:
|
||||
runs-on: 'ubuntu-latest'
|
||||
base-image: "ubuntu:22.04"
|
||||
skip-drivers: 'false'
|
||||
backend: "diffusers"
|
||||
backend: "diffusers"
|
||||
dockerfile: "./backend/Dockerfile.python"
|
||||
context: "./backend"
|
||||
# CUDA 12 additional backends
|
||||
@@ -970,54 +957,38 @@ jobs:
|
||||
backend: "kitten-tts"
|
||||
dockerfile: "./backend/Dockerfile.python"
|
||||
context: "./backend"
|
||||
transformers-darwin:
|
||||
backend-jobs-darwin:
|
||||
uses: ./.github/workflows/backend_build_darwin.yml
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- backend: "diffusers"
|
||||
tag-suffix: "-metal-darwin-arm64-diffusers"
|
||||
build-type: "mps"
|
||||
- backend: "mlx"
|
||||
tag-suffix: "-metal-darwin-arm64-mlx"
|
||||
build-type: "mps"
|
||||
- backend: "mlx-vlm"
|
||||
tag-suffix: "-metal-darwin-arm64-mlx-vlm"
|
||||
build-type: "mps"
|
||||
- backend: "mlx-audio"
|
||||
tag-suffix: "-metal-darwin-arm64-mlx-audio"
|
||||
build-type: "mps"
|
||||
- backend: "stablediffusion-ggml"
|
||||
tag-suffix: "-metal-darwin-arm64-stablediffusion-ggml"
|
||||
build-type: "metal"
|
||||
lang: "go"
|
||||
- backend: "whisper"
|
||||
tag-suffix: "-metal-darwin-arm64-whisper"
|
||||
build-type: "metal"
|
||||
lang: "go"
|
||||
with:
|
||||
backend: "transformers"
|
||||
build-type: "mps"
|
||||
backend: ${{ matrix.backend }}
|
||||
build-type: ${{ matrix.build-type }}
|
||||
go-version: "1.24.x"
|
||||
tag-suffix: "-metal-darwin-arm64-transformers"
|
||||
use-pip: true
|
||||
runs-on: "macOS-14"
|
||||
secrets:
|
||||
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
dockerPassword: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
|
||||
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
|
||||
diffusers-darwin:
|
||||
uses: ./.github/workflows/backend_build_darwin.yml
|
||||
with:
|
||||
backend: "diffusers"
|
||||
build-type: "mps"
|
||||
go-version: "1.24.x"
|
||||
tag-suffix: "-metal-darwin-arm64-diffusers"
|
||||
use-pip: true
|
||||
runs-on: "macOS-14"
|
||||
secrets:
|
||||
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
dockerPassword: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
|
||||
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
|
||||
mlx-darwin:
|
||||
uses: ./.github/workflows/backend_build_darwin.yml
|
||||
with:
|
||||
backend: "mlx"
|
||||
build-type: "mps"
|
||||
go-version: "1.24.x"
|
||||
tag-suffix: "-metal-darwin-arm64-mlx"
|
||||
runs-on: "macOS-14"
|
||||
secrets:
|
||||
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
dockerPassword: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
|
||||
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
|
||||
mlx-vlm-darwin:
|
||||
uses: ./.github/workflows/backend_build_darwin.yml
|
||||
with:
|
||||
backend: "mlx-vlm"
|
||||
build-type: "mps"
|
||||
go-version: "1.24.x"
|
||||
tag-suffix: "-metal-darwin-arm64-mlx-vlm"
|
||||
tag-suffix: ${{ matrix.tag-suffix }}
|
||||
lang: ${{ matrix.lang || 'python' }}
|
||||
use-pip: ${{ matrix.backend == 'diffusers' }}
|
||||
runs-on: "macOS-14"
|
||||
secrets:
|
||||
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
||||
30
.github/workflows/backend_build_darwin.yml
vendored
30
.github/workflows/backend_build_darwin.yml
vendored
@@ -16,6 +16,10 @@ on:
|
||||
description: 'Use pip to install dependencies'
|
||||
default: false
|
||||
type: boolean
|
||||
lang:
|
||||
description: 'Programming language (e.g. go)'
|
||||
default: 'python'
|
||||
type: string
|
||||
go-version:
|
||||
description: 'Go version to use'
|
||||
default: '1.24.x'
|
||||
@@ -49,26 +53,26 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
|
||||
- name: Setup Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
|
||||
|
||||
# You can test your matrix by printing the current Go version
|
||||
- name: Display Go version
|
||||
run: go version
|
||||
|
||||
|
||||
- name: Dependencies
|
||||
run: |
|
||||
brew install protobuf grpc make protoc-gen-go protoc-gen-go-grpc libomp llvm
|
||||
|
||||
|
||||
- name: Build ${{ inputs.backend }}-darwin
|
||||
run: |
|
||||
make protogen-go
|
||||
BACKEND=${{ inputs.backend }} BUILD_TYPE=${{ inputs.build-type }} USE_PIP=${{ inputs.use-pip }} make build-darwin-python-backend
|
||||
|
||||
BACKEND=${{ inputs.backend }} BUILD_TYPE=${{ inputs.build-type }} USE_PIP=${{ inputs.use-pip }} make build-darwin-${{ inputs.lang }}-backend
|
||||
|
||||
- name: Upload ${{ inputs.backend }}.tar
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -85,20 +89,20 @@ jobs:
|
||||
with:
|
||||
name: ${{ inputs.backend }}-tar
|
||||
path: .
|
||||
|
||||
|
||||
- name: Install crane
|
||||
run: |
|
||||
curl -L https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar -xz
|
||||
sudo mv crane /usr/local/bin/
|
||||
|
||||
|
||||
- name: Log in to DockerHub
|
||||
run: |
|
||||
echo "${{ secrets.dockerPassword }}" | crane auth login docker.io -u "${{ secrets.dockerUsername }}" --password-stdin
|
||||
|
||||
|
||||
- name: Log in to quay.io
|
||||
run: |
|
||||
echo "${{ secrets.quayPassword }}" | crane auth login quay.io -u "${{ secrets.quayUsername }}" --password-stdin
|
||||
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -112,7 +116,7 @@ jobs:
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=${{ inputs.tag-suffix }},onlatest=true
|
||||
|
||||
|
||||
- name: Docker meta
|
||||
id: quaymeta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -126,13 +130,13 @@ jobs:
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=${{ inputs.tag-suffix }},onlatest=true
|
||||
|
||||
|
||||
- name: Push Docker image (DockerHub)
|
||||
run: |
|
||||
for tag in $(echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n'); do
|
||||
crane push ${{ inputs.backend }}.tar $tag
|
||||
done
|
||||
|
||||
|
||||
- name: Push Docker image (Quay)
|
||||
run: |
|
||||
for tag in $(echo "${{ steps.quaymeta.outputs.tags }}" | tr ',' '\n'); do
|
||||
|
||||
20
.github/workflows/backend_pr.yml
vendored
20
.github/workflows/backend_pr.yml
vendored
@@ -12,7 +12,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
matrix-darwin: ${{ steps.set-matrix.outputs.matrix-darwin }}
|
||||
has-backends: ${{ steps.set-matrix.outputs.has-backends }}
|
||||
has-backends-darwin: ${{ steps.set-matrix.outputs.has-backends-darwin }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
@@ -56,3 +58,21 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
|
||||
backend-jobs-darwin:
|
||||
needs: generate-matrix
|
||||
uses: ./.github/workflows/backend_build_darwin.yml
|
||||
if: needs.generate-matrix.outputs.has-backends-darwin == 'true'
|
||||
with:
|
||||
backend: ${{ matrix.backend }}
|
||||
build-type: ${{ matrix.build-type }}
|
||||
go-version: "1.24.x"
|
||||
tag-suffix: ${{ matrix.tag-suffix }}
|
||||
lang: ${{ matrix.lang || 'python' }}
|
||||
use-pip: ${{ matrix.backend == 'diffusers' }}
|
||||
runs-on: "macOS-14"
|
||||
secrets:
|
||||
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
|
||||
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix-darwin) }}
|
||||
|
||||
44
.github/workflows/build-test.yaml
vendored
44
.github/workflows/build-test.yaml
vendored
@@ -21,3 +21,47 @@ jobs:
|
||||
- name: Run GoReleaser
|
||||
run: |
|
||||
make dev-dist
|
||||
launcher-build-darwin:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23
|
||||
- name: Build launcher for macOS ARM64
|
||||
run: |
|
||||
make build-launcher-darwin
|
||||
ls -liah dist
|
||||
- name: Upload macOS launcher artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: launcher-macos
|
||||
path: dist/
|
||||
retention-days: 30
|
||||
|
||||
launcher-build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23
|
||||
- name: Build launcher for Linux
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install golang gcc libgl1-mesa-dev xorg-dev libxkbcommon-dev
|
||||
make build-launcher-linux
|
||||
- name: Upload Linux launcher artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: launcher-linux
|
||||
path: local-ai-launcher-linux.tar.xz
|
||||
retention-days: 30
|
||||
40
.github/workflows/release.yaml
vendored
40
.github/workflows/release.yaml
vendored
@@ -23,4 +23,42 @@ jobs:
|
||||
version: v2.11.0
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
launcher-build-darwin:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23
|
||||
- name: Build launcher for macOS ARM64
|
||||
run: |
|
||||
make build-launcher-darwin
|
||||
- name: Upload DMG to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./dist/LocalAI-Launcher.dmg
|
||||
launcher-build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23
|
||||
- name: Build launcher for Linux
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install golang gcc libgl1-mesa-dev xorg-dev libxkbcommon-dev
|
||||
make build-launcher-linux
|
||||
- name: Upload Linux launcher artifacts
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./local-ai-launcher-linux.tar.xz
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,7 +24,7 @@ go-bert
|
||||
|
||||
# LocalAI build binary
|
||||
LocalAI
|
||||
local-ai
|
||||
/local-ai
|
||||
# prevent above rules from omitting the helm chart
|
||||
!charts/*
|
||||
# prevent above rules from omitting the api/localai folder
|
||||
|
||||
@@ -8,7 +8,7 @@ source:
|
||||
enabled: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Tag }}-source'
|
||||
builds:
|
||||
-
|
||||
- main: ./cmd/local-ai
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
|
||||
@@ -100,6 +100,10 @@ RUN if [ "${BUILD_TYPE}" = "hipblas" ] && [ "${SKIP_DRIVERS}" = "false" ]; then
|
||||
ldconfig \
|
||||
; fi
|
||||
|
||||
RUN if [ "${BUILD_TYPE}" = "hipblas" ]; then \
|
||||
ln -s /opt/rocm-**/lib/llvm/lib/libomp.so /usr/lib/libomp.so \
|
||||
; fi
|
||||
|
||||
RUN expr "${BUILD_TYPE}" = intel && echo "intel" > /run/localai/capability || echo "not intel"
|
||||
|
||||
# Cuda
|
||||
|
||||
40
Makefile
40
Makefile
@@ -2,6 +2,7 @@ GOCMD=go
|
||||
GOTEST=$(GOCMD) test
|
||||
GOVET=$(GOCMD) vet
|
||||
BINARY_NAME=local-ai
|
||||
LAUNCHER_BINARY_NAME=local-ai-launcher
|
||||
|
||||
GORELEASER?=
|
||||
|
||||
@@ -90,7 +91,17 @@ build: protogen-go install-go-tools ## Build the project
|
||||
$(info ${GREEN}I LD_FLAGS: ${YELLOW}$(LD_FLAGS)${RESET})
|
||||
$(info ${GREEN}I UPX: ${YELLOW}$(UPX)${RESET})
|
||||
rm -rf $(BINARY_NAME) || true
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS)" $(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o $(BINARY_NAME) ./
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS)" $(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o $(BINARY_NAME) ./cmd/local-ai
|
||||
|
||||
build-launcher: ## Build the launcher application
|
||||
$(info ${GREEN}I local-ai launcher build info:${RESET})
|
||||
$(info ${GREEN}I BUILD_TYPE: ${YELLOW}$(BUILD_TYPE)${RESET})
|
||||
$(info ${GREEN}I GO_TAGS: ${YELLOW}$(GO_TAGS)${RESET})
|
||||
$(info ${GREEN}I LD_FLAGS: ${YELLOW}$(LD_FLAGS)${RESET})
|
||||
rm -rf $(LAUNCHER_BINARY_NAME) || true
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS)" $(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o $(LAUNCHER_BINARY_NAME) ./cmd/launcher
|
||||
|
||||
build-all: build build-launcher ## Build both server and launcher
|
||||
|
||||
dev-dist:
|
||||
$(GORELEASER) build --snapshot --clean
|
||||
@@ -365,6 +376,9 @@ backends/llama-cpp-darwin: build
|
||||
build-darwin-python-backend: build
|
||||
bash ./scripts/build/python-darwin.sh
|
||||
|
||||
build-darwin-go-backend: build
|
||||
bash ./scripts/build/golang-darwin.sh
|
||||
|
||||
backends/mlx:
|
||||
BACKEND=mlx $(MAKE) build-darwin-python-backend
|
||||
./local-ai backends install "ocifile://$(abspath ./backend-images/mlx.tar)"
|
||||
@@ -377,6 +391,14 @@ backends/mlx-vlm:
|
||||
BACKEND=mlx-vlm $(MAKE) build-darwin-python-backend
|
||||
./local-ai backends install "ocifile://$(abspath ./backend-images/mlx-vlm.tar)"
|
||||
|
||||
backends/mlx-audio:
|
||||
BACKEND=mlx-audio $(MAKE) build-darwin-python-backend
|
||||
./local-ai backends install "ocifile://$(abspath ./backend-images/mlx-audio.tar)"
|
||||
|
||||
backends/stablediffusion-ggml-darwin:
|
||||
BACKEND=stablediffusion-ggml BUILD_TYPE=metal $(MAKE) build-darwin-go-backend
|
||||
./local-ai backends install "ocifile://$(abspath ./backend-images/stablediffusion-ggml.tar)"
|
||||
|
||||
backend-images:
|
||||
mkdir -p backend-images
|
||||
|
||||
@@ -507,3 +529,19 @@ docs-clean:
|
||||
.PHONY: docs
|
||||
docs: docs/static/gallery.html
|
||||
cd docs && hugo serve
|
||||
|
||||
########################################################
|
||||
## Platform-specific builds
|
||||
########################################################
|
||||
|
||||
## fyne cross-platform build
|
||||
build-launcher-darwin: build-launcher
|
||||
go run github.com/tiagomelo/macos-dmg-creator/cmd/createdmg@latest \
|
||||
--appName "LocalAI" \
|
||||
--appBinaryPath "$(LAUNCHER_BINARY_NAME)" \
|
||||
--bundleIdentifier "com.localai.launcher" \
|
||||
--iconPath "core/http/static/logo.png" \
|
||||
--outputDir "dist/"
|
||||
|
||||
build-launcher-linux:
|
||||
cd cmd/launcher && go run fyne.io/tools/cmd/fyne@latest package -os linux -icon ../../core/http/static/logo.png --executable $(LAUNCHER_BINARY_NAME)-linux && mv launcher.tar.xz ../../$(LAUNCHER_BINARY_NAME)-linux.tar.xz
|
||||
|
||||
57
README.md
57
README.md
@@ -233,6 +233,60 @@ Roadmap items: [List of issues](https://github.com/mudler/LocalAI/issues?q=is%3A
|
||||
- 🔊 Voice activity detection (Silero-VAD support)
|
||||
- 🌍 Integrated WebUI!
|
||||
|
||||
## 🧩 Supported Backends & Acceleration
|
||||
|
||||
LocalAI supports a comprehensive range of AI backends with multiple acceleration options:
|
||||
|
||||
### Text Generation & Language Models
|
||||
| Backend | Description | Acceleration Support |
|
||||
|---------|-------------|---------------------|
|
||||
| **llama.cpp** | LLM inference in C/C++ | CUDA 11/12, ROCm, Intel SYCL, Vulkan, Metal, CPU |
|
||||
| **vLLM** | Fast LLM inference with PagedAttention | CUDA 12, ROCm, Intel |
|
||||
| **transformers** | HuggingFace transformers framework | CUDA 11/12, ROCm, Intel, CPU |
|
||||
| **exllama2** | GPTQ inference library | CUDA 12 |
|
||||
| **MLX** | Apple Silicon LLM inference | Metal (M1/M2/M3+) |
|
||||
| **MLX-VLM** | Apple Silicon Vision-Language Models | Metal (M1/M2/M3+) |
|
||||
|
||||
### Audio & Speech Processing
|
||||
| Backend | Description | Acceleration Support |
|
||||
|---------|-------------|---------------------|
|
||||
| **whisper.cpp** | OpenAI Whisper in C/C++ | CUDA 12, ROCm, Intel SYCL, Vulkan, CPU |
|
||||
| **faster-whisper** | Fast Whisper with CTranslate2 | CUDA 12, ROCm, Intel, CPU |
|
||||
| **bark** | Text-to-audio generation | CUDA 12, ROCm, Intel |
|
||||
| **bark-cpp** | C++ implementation of Bark | CUDA, Metal, CPU |
|
||||
| **coqui** | Advanced TTS with 1100+ languages | CUDA 12, ROCm, Intel, CPU |
|
||||
| **kokoro** | Lightweight TTS model | CUDA 12, ROCm, Intel, CPU |
|
||||
| **chatterbox** | Production-grade TTS | CUDA 11/12, CPU |
|
||||
| **piper** | Fast neural TTS system | CPU |
|
||||
| **kitten-tts** | Kitten TTS models | CPU |
|
||||
| **silero-vad** | Voice Activity Detection | CPU |
|
||||
|
||||
### Image & Video Generation
|
||||
| Backend | Description | Acceleration Support |
|
||||
|---------|-------------|---------------------|
|
||||
| **stablediffusion.cpp** | Stable Diffusion in C/C++ | CUDA 12, Intel SYCL, Vulkan, CPU |
|
||||
| **diffusers** | HuggingFace diffusion models | CUDA 11/12, ROCm, Intel, Metal, CPU |
|
||||
|
||||
### Specialized AI Tasks
|
||||
| Backend | Description | Acceleration Support |
|
||||
|---------|-------------|---------------------|
|
||||
| **rfdetr** | Real-time object detection | CUDA 12, Intel, CPU |
|
||||
| **rerankers** | Document reranking API | CUDA 11/12, ROCm, Intel, CPU |
|
||||
| **local-store** | Vector database | CPU |
|
||||
| **huggingface** | HuggingFace API integration | API-based |
|
||||
|
||||
### Hardware Acceleration Matrix
|
||||
|
||||
| Acceleration Type | Supported Backends | Hardware Support |
|
||||
|-------------------|-------------------|------------------|
|
||||
| **NVIDIA CUDA 11** | llama.cpp, whisper, stablediffusion, diffusers, rerankers, bark, chatterbox | Nvidia hardware |
|
||||
| **NVIDIA CUDA 12** | All CUDA-compatible backends | Nvidia hardware |
|
||||
| **AMD ROCm** | llama.cpp, whisper, vllm, transformers, diffusers, rerankers, coqui, kokoro, bark | AMD Graphics |
|
||||
| **Intel oneAPI** | llama.cpp, whisper, stablediffusion, vllm, transformers, diffusers, rfdetr, rerankers, exllama2, coqui, kokoro, bark | Intel Arc, Intel iGPUs |
|
||||
| **Apple Metal** | llama.cpp, whisper, diffusers, MLX, MLX-VLM, bark-cpp | Apple M1/M2/M3+ |
|
||||
| **Vulkan** | llama.cpp, whisper, stablediffusion | Cross-platform GPUs |
|
||||
| **NVIDIA Jetson** | llama.cpp, whisper, stablediffusion, diffusers, rfdetr | ARM64 embedded AI |
|
||||
| **CPU Optimized** | All backends | AVX/AVX2/AVX512, quantization support |
|
||||
|
||||
### 🔗 Community and integrations
|
||||
|
||||
@@ -247,6 +301,9 @@ WebUIs:
|
||||
Model galleries
|
||||
- https://github.com/go-skynet/model-gallery
|
||||
|
||||
Voice:
|
||||
- https://github.com/richiejp/VoxInput
|
||||
|
||||
Other:
|
||||
- Helm chart https://github.com/go-skynet/helm-charts
|
||||
- VSCode extension https://github.com/badgooooor/localai-vscode-plugin
|
||||
|
||||
213
backend/README.md
Normal file
213
backend/README.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# LocalAI Backend Architecture
|
||||
|
||||
This directory contains the core backend infrastructure for LocalAI, including the gRPC protocol definition, multi-language Dockerfiles, and language-specific backend implementations.
|
||||
|
||||
## Overview
|
||||
|
||||
LocalAI uses a unified gRPC-based architecture that allows different programming languages to implement AI backends while maintaining consistent interfaces and capabilities. The backend system supports multiple hardware acceleration targets and provides a standardized way to integrate various AI models and frameworks.
|
||||
|
||||
## Architecture Components
|
||||
|
||||
### 1. Protocol Definition (`backend.proto`)
|
||||
|
||||
The `backend.proto` file defines the gRPC service interface that all backends must implement. This ensures consistency across different language implementations and provides a contract for communication between LocalAI core and backend services.
|
||||
|
||||
#### Core Services
|
||||
|
||||
- **Text Generation**: `Predict`, `PredictStream` for LLM inference
|
||||
- **Embeddings**: `Embedding` for text vectorization
|
||||
- **Image Generation**: `GenerateImage` for stable diffusion and image models
|
||||
- **Audio Processing**: `AudioTranscription`, `TTS`, `SoundGeneration`
|
||||
- **Video Generation**: `GenerateVideo` for video synthesis
|
||||
- **Object Detection**: `Detect` for computer vision tasks
|
||||
- **Vector Storage**: `StoresSet`, `StoresGet`, `StoresFind` for RAG operations
|
||||
- **Reranking**: `Rerank` for document relevance scoring
|
||||
- **Voice Activity Detection**: `VAD` for audio segmentation
|
||||
|
||||
#### Key Message Types
|
||||
|
||||
- **`PredictOptions`**: Comprehensive configuration for text generation
|
||||
- **`ModelOptions`**: Model loading and configuration parameters
|
||||
- **`Result`**: Standardized response format
|
||||
- **`StatusResponse`**: Backend health and memory usage information
|
||||
|
||||
### 2. Multi-Language Dockerfiles
|
||||
|
||||
The backend system provides language-specific Dockerfiles that handle the build environment and dependencies for different programming languages:
|
||||
|
||||
- `Dockerfile.python`
|
||||
- `Dockerfile.golang`
|
||||
- `Dockerfile.llama-cpp`
|
||||
|
||||
### 3. Language-Specific Implementations
|
||||
|
||||
#### Python Backends (`python/`)
|
||||
- **transformers**: Hugging Face Transformers framework
|
||||
- **vllm**: High-performance LLM inference
|
||||
- **mlx**: Apple Silicon optimization
|
||||
- **diffusers**: Stable Diffusion models
|
||||
- **Audio**: bark, coqui, faster-whisper, kitten-tts
|
||||
- **Vision**: mlx-vlm, rfdetr
|
||||
- **Specialized**: rerankers, chatterbox, kokoro
|
||||
|
||||
#### Go Backends (`go/`)
|
||||
- **whisper**: OpenAI Whisper speech recognition in Go with GGML cpp backend (whisper.cpp)
|
||||
- **stablediffusion-ggml**: Stable Diffusion in Go with GGML Cpp backend
|
||||
- **huggingface**: Hugging Face model integration
|
||||
- **piper**: Text-to-speech synthesis Golang with C bindings using rhaspy/piper
|
||||
- **bark-cpp**: Bark TTS models Golang with Cpp bindings
|
||||
- **local-store**: Vector storage backend
|
||||
|
||||
#### C++ Backends (`cpp/`)
|
||||
- **llama-cpp**: Llama.cpp integration
|
||||
- **grpc**: GRPC utilities and helpers
|
||||
|
||||
## Hardware Acceleration Support
|
||||
|
||||
### CUDA (NVIDIA)
|
||||
- **Versions**: CUDA 11.x, 12.x
|
||||
- **Features**: cuBLAS, cuDNN, TensorRT optimization
|
||||
- **Targets**: x86_64, ARM64 (Jetson)
|
||||
|
||||
### ROCm (AMD)
|
||||
- **Features**: HIP, rocBLAS, MIOpen
|
||||
- **Targets**: AMD GPUs with ROCm support
|
||||
|
||||
### Intel
|
||||
- **Features**: oneAPI, Intel Extension for PyTorch
|
||||
- **Targets**: Intel GPUs, XPUs, CPUs
|
||||
|
||||
### Vulkan
|
||||
- **Features**: Cross-platform GPU acceleration
|
||||
- **Targets**: Windows, Linux, Android, macOS
|
||||
|
||||
### Apple Silicon
|
||||
- **Features**: MLX framework, Metal Performance Shaders
|
||||
- **Targets**: M1/M2/M3 Macs
|
||||
|
||||
## Backend Registry (`index.yaml`)
|
||||
|
||||
The `index.yaml` file serves as a central registry for all available backends, providing:
|
||||
|
||||
- **Metadata**: Name, description, license, icons
|
||||
- **Capabilities**: Hardware targets and optimization profiles
|
||||
- **Tags**: Categorization for discovery
|
||||
- **URLs**: Source code and documentation links
|
||||
|
||||
## Building Backends
|
||||
|
||||
### Prerequisites
|
||||
- Docker with multi-architecture support
|
||||
- Appropriate hardware drivers (CUDA, ROCm, etc.)
|
||||
- Build tools (make, cmake, compilers)
|
||||
|
||||
### Build Commands
|
||||
|
||||
Example of build commands with Docker
|
||||
|
||||
```bash
|
||||
# Build Python backend
|
||||
docker build -f backend/Dockerfile.python \
|
||||
--build-arg BACKEND=transformers \
|
||||
--build-arg BUILD_TYPE=cublas12 \
|
||||
--build-arg CUDA_MAJOR_VERSION=12 \
|
||||
--build-arg CUDA_MINOR_VERSION=0 \
|
||||
-t localai-backend-transformers .
|
||||
|
||||
# Build Go backend
|
||||
docker build -f backend/Dockerfile.golang \
|
||||
--build-arg BACKEND=whisper \
|
||||
--build-arg BUILD_TYPE=cpu \
|
||||
-t localai-backend-whisper .
|
||||
|
||||
# Build C++ backend
|
||||
docker build -f backend/Dockerfile.llama-cpp \
|
||||
--build-arg BACKEND=llama-cpp \
|
||||
--build-arg BUILD_TYPE=cublas12 \
|
||||
-t localai-backend-llama-cpp .
|
||||
```
|
||||
|
||||
For ARM64/Mac builds, docker can't be used, and the makefile in the respective backend has to be used.
|
||||
|
||||
### Build Types
|
||||
|
||||
- **`cpu`**: CPU-only optimization
|
||||
- **`cublas11`**: CUDA 11.x with cuBLAS
|
||||
- **`cublas12`**: CUDA 12.x with cuBLAS
|
||||
- **`hipblas`**: ROCm with rocBLAS
|
||||
- **`intel`**: Intel oneAPI optimization
|
||||
- **`vulkan`**: Vulkan-based acceleration
|
||||
- **`metal`**: Apple Metal optimization
|
||||
|
||||
## Backend Development
|
||||
|
||||
### Creating a New Backend
|
||||
|
||||
1. **Choose Language**: Select Python, Go, or C++ based on requirements
|
||||
2. **Implement Interface**: Implement the gRPC service defined in `backend.proto`
|
||||
3. **Add Dependencies**: Create appropriate requirements files
|
||||
4. **Configure Build**: Set up Dockerfile and build scripts
|
||||
5. **Register Backend**: Add entry to `index.yaml`
|
||||
6. **Test Integration**: Verify gRPC communication and functionality
|
||||
|
||||
### Backend Structure
|
||||
|
||||
```
|
||||
backend-name/
|
||||
├── backend.py/go/cpp # Main implementation
|
||||
├── requirements.txt # Dependencies
|
||||
├── Dockerfile # Build configuration
|
||||
├── install.sh # Installation script
|
||||
├── run.sh # Execution script
|
||||
├── test.sh # Test script
|
||||
└── README.md # Backend documentation
|
||||
```
|
||||
|
||||
### Required gRPC Methods
|
||||
|
||||
At minimum, backends must implement:
|
||||
- `Health()` - Service health check
|
||||
- `LoadModel()` - Model loading and initialization
|
||||
- `Predict()` - Main inference endpoint
|
||||
- `Status()` - Backend status and metrics
|
||||
|
||||
## Integration with LocalAI Core
|
||||
|
||||
Backends communicate with LocalAI core through gRPC:
|
||||
|
||||
1. **Service Discovery**: Core discovers available backends
|
||||
2. **Model Loading**: Core requests model loading via `LoadModel`
|
||||
3. **Inference**: Core sends requests via `Predict` or specialized endpoints
|
||||
4. **Streaming**: Core handles streaming responses for real-time generation
|
||||
5. **Monitoring**: Core tracks backend health and performance
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Memory Management
|
||||
- **Model Caching**: Efficient model loading and caching
|
||||
- **Batch Processing**: Optimize for multiple concurrent requests
|
||||
- **Memory Pinning**: GPU memory optimization for CUDA/ROCm
|
||||
|
||||
### Hardware Utilization
|
||||
- **Multi-GPU**: Support for tensor parallelism
|
||||
- **Mixed Precision**: FP16/BF16 for memory efficiency
|
||||
- **Kernel Fusion**: Optimized CUDA/ROCm kernels
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **GRPC Connection**: Verify backend service is running and accessible
|
||||
2. **Model Loading**: Check model paths and dependencies
|
||||
3. **Hardware Detection**: Ensure appropriate drivers and libraries
|
||||
4. **Memory Issues**: Monitor GPU memory usage and model sizes
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to the backend system:
|
||||
|
||||
1. **Follow Protocol**: Implement the exact gRPC interface
|
||||
2. **Add Tests**: Include comprehensive test coverage
|
||||
3. **Document**: Provide clear usage examples
|
||||
4. **Optimize**: Consider performance and resource usage
|
||||
5. **Validate**: Test across different hardware targets
|
||||
@@ -242,7 +242,7 @@ message ModelOptions {
|
||||
|
||||
string Type = 49;
|
||||
|
||||
bool FlashAttention = 56;
|
||||
string FlashAttention = 56;
|
||||
bool NoKVOffload = 57;
|
||||
|
||||
string ModelPath = 59;
|
||||
@@ -312,15 +312,17 @@ message GenerateImageRequest {
|
||||
|
||||
message GenerateVideoRequest {
|
||||
string prompt = 1;
|
||||
string start_image = 2; // Path or base64 encoded image for the start frame
|
||||
string end_image = 3; // Path or base64 encoded image for the end frame
|
||||
int32 width = 4;
|
||||
int32 height = 5;
|
||||
int32 num_frames = 6; // Number of frames to generate
|
||||
int32 fps = 7; // Frames per second
|
||||
int32 seed = 8;
|
||||
float cfg_scale = 9; // Classifier-free guidance scale
|
||||
string dst = 10; // Output path for the generated video
|
||||
string negative_prompt = 2; // Negative prompt for video generation
|
||||
string start_image = 3; // Path or base64 encoded image for the start frame
|
||||
string end_image = 4; // Path or base64 encoded image for the end frame
|
||||
int32 width = 5;
|
||||
int32 height = 6;
|
||||
int32 num_frames = 7; // Number of frames to generate
|
||||
int32 fps = 8; // Frames per second
|
||||
int32 seed = 9;
|
||||
float cfg_scale = 10; // Classifier-free guidance scale
|
||||
int32 step = 11; // Number of inference steps
|
||||
string dst = 12; // Output path for the generated video
|
||||
}
|
||||
|
||||
message TTSRequest {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
LLAMA_VERSION?=710dfc465a68f7443b87d9f792cffba00ed739fe
|
||||
LLAMA_VERSION?=3de008208b9b8a33f49f979097a99b4d59e6e521
|
||||
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
|
||||
|
||||
CMAKE_ARGS?=
|
||||
|
||||
@@ -304,7 +304,15 @@ static void params_parse(const backend::ModelOptions* request,
|
||||
}
|
||||
params.use_mlock = request->mlock();
|
||||
params.use_mmap = request->mmap();
|
||||
params.flash_attn = request->flashattention();
|
||||
|
||||
if (request->flashattention() == "on" || request->flashattention() == "enabled") {
|
||||
params.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_ENABLED;
|
||||
} else if (request->flashattention() == "off" || request->flashattention() == "disabled") {
|
||||
params.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_DISABLED;
|
||||
} else if (request->flashattention() == "auto") {
|
||||
params.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_AUTO;
|
||||
}
|
||||
|
||||
params.no_kv_offload = request->nokvoffload();
|
||||
params.ctx_shift = false; // We control context-shifting in any case (and we disable it as it could just lead to infinite loops)
|
||||
|
||||
|
||||
4
backend/go/stablediffusion-ggml/.gitignore
vendored
Normal file
4
backend/go/stablediffusion-ggml/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
package/
|
||||
sources/
|
||||
libgosd.so
|
||||
stablediffusion-ggml
|
||||
@@ -5,7 +5,11 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
add_subdirectory(./sources/stablediffusion-ggml.cpp)
|
||||
|
||||
add_library(gosd MODULE gosd.cpp)
|
||||
target_link_libraries(gosd PRIVATE stable-diffusion ggml stdc++fs)
|
||||
target_link_libraries(gosd PRIVATE stable-diffusion ggml)
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0)
|
||||
target_link_libraries(gosd PRIVATE stdc++fs)
|
||||
endif()
|
||||
|
||||
target_include_directories(gosd PUBLIC
|
||||
stable-diffusion.cpp
|
||||
|
||||
@@ -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?=5900ef6605c6fbf7934239f795c13c97bc993853
|
||||
STABLEDIFFUSION_GGML_VERSION?=4c6475f9176bf99271ccf5a2817b30a490b83db0
|
||||
|
||||
CMAKE_ARGS+=-DGGML_MAX_NAME=128
|
||||
|
||||
@@ -29,8 +29,6 @@ else ifeq ($(BUILD_TYPE),clblas)
|
||||
# If it's hipblas we do have also to set CC=/opt/rocm/llvm/bin/clang CXX=/opt/rocm/llvm/bin/clang++
|
||||
else ifeq ($(BUILD_TYPE),hipblas)
|
||||
CMAKE_ARGS+=-DSD_HIPBLAS=ON -DGGML_HIPBLAS=ON
|
||||
# If it's OSX, DO NOT embed the metal library - -DGGML_METAL_EMBED_LIBRARY=ON requires further investigation
|
||||
# But if it's OSX without metal, disable it here
|
||||
else ifeq ($(BUILD_TYPE),vulkan)
|
||||
CMAKE_ARGS+=-DSD_VULKAN=ON -DGGML_VULKAN=ON
|
||||
else ifeq ($(OS),Darwin)
|
||||
@@ -74,10 +72,10 @@ libgosd.so: sources/stablediffusion-ggml.cpp CMakeLists.txt gosd.cpp gosd.h
|
||||
stablediffusion-ggml: main.go gosd.go libgosd.so
|
||||
CGO_ENABLED=0 $(GOCMD) build -tags "$(GO_TAGS)" -o stablediffusion-ggml ./
|
||||
|
||||
package:
|
||||
package: stablediffusion-ggml
|
||||
bash package.sh
|
||||
|
||||
build: stablediffusion-ggml package
|
||||
build: package
|
||||
|
||||
clean:
|
||||
rm -rf libgosd.o build stablediffusion-ggml
|
||||
rm -rf libgosd.so build stablediffusion-ggml package sources
|
||||
|
||||
@@ -10,9 +10,9 @@ CURDIR=$(dirname "$(realpath $0)")
|
||||
# Create lib directory
|
||||
mkdir -p $CURDIR/package/lib
|
||||
|
||||
cp -avrf $CURDIR/libgosd.so $CURDIR/package/
|
||||
cp -avrf $CURDIR/stablediffusion-ggml $CURDIR/package/
|
||||
cp -rfv $CURDIR/run.sh $CURDIR/package/
|
||||
cp -avf $CURDIR/libgosd.so $CURDIR/package/
|
||||
cp -avf $CURDIR/stablediffusion-ggml $CURDIR/package/
|
||||
cp -fv $CURDIR/run.sh $CURDIR/package/
|
||||
|
||||
# Detect architecture and copy appropriate libraries
|
||||
if [ -f "/lib64/ld-linux-x86-64.so.2" ]; then
|
||||
@@ -43,6 +43,8 @@ elif [ -f "/lib/ld-linux-aarch64.so.1" ]; then
|
||||
cp -arfLv /lib/aarch64-linux-gnu/libdl.so.2 $CURDIR/package/lib/libdl.so.2
|
||||
cp -arfLv /lib/aarch64-linux-gnu/librt.so.1 $CURDIR/package/lib/librt.so.1
|
||||
cp -arfLv /lib/aarch64-linux-gnu/libpthread.so.0 $CURDIR/package/lib/libpthread.so.0
|
||||
elif [ $(uname -s) = "Darwin" ]; then
|
||||
echo "Detected Darwin"
|
||||
else
|
||||
echo "Error: Could not detect architecture"
|
||||
exit 1
|
||||
|
||||
7
backend/go/whisper/.gitignore
vendored
Normal file
7
backend/go/whisper/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
.cache/
|
||||
sources/
|
||||
build/
|
||||
package/
|
||||
whisper
|
||||
libgowhisper.so
|
||||
|
||||
16
backend/go/whisper/CMakeLists.txt
Normal file
16
backend/go/whisper/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
cmake_minimum_required(VERSION 3.12)
|
||||
project(gowhisper LANGUAGES C CXX)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
add_subdirectory(./sources/whisper.cpp)
|
||||
|
||||
add_library(gowhisper MODULE gowhisper.cpp)
|
||||
target_link_libraries(gowhisper PRIVATE whisper ggml)
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0)
|
||||
target_link_libraries(gosd PRIVATE stdc++fs)
|
||||
endif()
|
||||
|
||||
set_property(TARGET gowhisper PROPERTY CXX_STANDARD 17)
|
||||
set_target_properties(gowhisper PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
@@ -1,110 +1,53 @@
|
||||
GOCMD=go
|
||||
CMAKE_ARGS?=
|
||||
BUILD_TYPE?=
|
||||
NATIVE?=false
|
||||
|
||||
BUILD_TYPE?=
|
||||
CMAKE_ARGS?=
|
||||
GOCMD?=go
|
||||
GO_TAGS?=
|
||||
JOBS?=$(shell nproc --ignore=1)
|
||||
|
||||
# whisper.cpp version
|
||||
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
|
||||
WHISPER_CPP_VERSION?=fc45bb86251f774ef817e89878bb4c2636c8a58f
|
||||
WHISPER_CPP_VERSION?=7745fcf32846006128f16de429cfe1677c963b30
|
||||
|
||||
export WHISPER_CMAKE_ARGS?=-DBUILD_SHARED_LIBS=OFF
|
||||
export WHISPER_DIR=$(abspath ./sources/whisper.cpp)
|
||||
export WHISPER_INCLUDE_PATH=$(WHISPER_DIR)/include:$(WHISPER_DIR)/ggml/include
|
||||
export WHISPER_LIBRARY_PATH=$(WHISPER_DIR)/build/src/:$(WHISPER_DIR)/build/ggml/src
|
||||
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
|
||||
|
||||
CGO_LDFLAGS_WHISPER?=
|
||||
CGO_LDFLAGS_WHISPER+=-lggml
|
||||
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF -DLLAMA_CURL=OFF
|
||||
CUDA_LIBPATH?=/usr/local/cuda/lib64/
|
||||
|
||||
ONEAPI_VERSION?=2025.2
|
||||
|
||||
# IF native is false, we add -DGGML_NATIVE=OFF to CMAKE_ARGS
|
||||
ifeq ($(NATIVE),false)
|
||||
CMAKE_ARGS+=-DGGML_NATIVE=OFF
|
||||
WHISPER_CMAKE_ARGS+=-DGGML_NATIVE=OFF
|
||||
endif
|
||||
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
ifeq ($(NATIVE),false)
|
||||
CMAKE_ARGS+=-DGGML_NATIVE=OFF
|
||||
endif
|
||||
# If build type is cublas, then we set -DGGML_CUDA=ON to CMAKE_ARGS automatically
|
||||
|
||||
ifeq ($(BUILD_TYPE),cublas)
|
||||
CGO_LDFLAGS+=-lcublas -lcudart -L$(CUDA_LIBPATH) -L$(CUDA_LIBPATH)/stubs/ -lcuda
|
||||
CMAKE_ARGS+=-DGGML_CUDA=ON
|
||||
CGO_LDFLAGS_WHISPER+=-lcufft -lggml-cuda
|
||||
export WHISPER_LIBRARY_PATH:=$(WHISPER_LIBRARY_PATH):$(WHISPER_DIR)/build/ggml/src/ggml-cuda/
|
||||
# If build type is openblas then we set -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS
|
||||
# to CMAKE_ARGS automatically
|
||||
else ifeq ($(BUILD_TYPE),openblas)
|
||||
CMAKE_ARGS+=-DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS
|
||||
# If build type is clblas (openCL) we set -DGGML_CLBLAST=ON -DCLBlast_DIR=/some/path
|
||||
else ifeq ($(BUILD_TYPE),clblas)
|
||||
CMAKE_ARGS+=-DGGML_CLBLAST=ON -DCLBlast_DIR=/some/path
|
||||
# If it's hipblas we do have also to set CC=/opt/rocm/llvm/bin/clang CXX=/opt/rocm/llvm/bin/clang++
|
||||
else ifeq ($(BUILD_TYPE),hipblas)
|
||||
ROCM_HOME ?= /opt/rocm
|
||||
ROCM_PATH ?= /opt/rocm
|
||||
LD_LIBRARY_PATH ?= /opt/rocm/lib:/opt/rocm/llvm/lib
|
||||
export STABLE_BUILD_TYPE=
|
||||
export CXX=$(ROCM_HOME)/llvm/bin/clang++
|
||||
export CC=$(ROCM_HOME)/llvm/bin/clang
|
||||
# GPU_TARGETS ?= gfx803,gfx900,gfx906,gfx908,gfx90a,gfx942,gfx1010,gfx1030,gfx1032,gfx1100,gfx1101,gfx1102
|
||||
# AMDGPU_TARGETS ?= "$(GPU_TARGETS)"
|
||||
CMAKE_ARGS+=-DGGML_HIP=ON
|
||||
CGO_LDFLAGS += -O3 --rtlib=compiler-rt -unwindlib=libgcc -lhipblas -lrocblas --hip-link -L${ROCM_HOME}/lib/llvm/lib -L$(CURRENT_MAKEFILE_DIR)/sources/whisper.cpp/build/ggml/src/ggml-hip/ -lggml-hip
|
||||
# CMAKE_ARGS+=-DGGML_HIP=ON -DAMDGPU_TARGETS="$(AMDGPU_TARGETS)" -DGPU_TARGETS="$(GPU_TARGETS)"
|
||||
CMAKE_ARGS+=-DGGML_HIPBLAS=ON
|
||||
else ifeq ($(BUILD_TYPE),vulkan)
|
||||
CMAKE_ARGS+=-DGGML_VULKAN=1
|
||||
CGO_LDFLAGS_WHISPER+=-lggml-vulkan -lvulkan
|
||||
export WHISPER_LIBRARY_PATH:=$(WHISPER_LIBRARY_PATH):$(WHISPER_DIR)/build/ggml/src/ggml-vulkan/
|
||||
CMAKE_ARGS+=-DGGML_VULKAN=ON
|
||||
else ifeq ($(OS),Darwin)
|
||||
ifeq ($(BUILD_TYPE),)
|
||||
BUILD_TYPE=metal
|
||||
endif
|
||||
ifneq ($(BUILD_TYPE),metal)
|
||||
CMAKE_ARGS+=-DGGML_METAL=OFF
|
||||
CGO_LDFLAGS_WHISPER+=-lggml-blas
|
||||
export WHISPER_LIBRARY_PATH:=$(WHISPER_LIBRARY_PATH):$(WHISPER_DIR)/build/ggml/src/ggml-blas
|
||||
else
|
||||
CMAKE_ARGS+=-DGGML_METAL=ON
|
||||
CMAKE_ARGS+=-DGGML_METAL_EMBED_LIBRARY=ON
|
||||
CMAKE_ARGS+=-DGGML_METAL_USE_BF16=ON
|
||||
CMAKE_ARGS+=-DGGML_OPENMP=OFF
|
||||
CMAKE_ARGS+=-DWHISPER_BUILD_EXAMPLES=OFF
|
||||
CMAKE_ARGS+=-DWHISPER_BUILD_TESTS=OFF
|
||||
CMAKE_ARGS+=-DWHISPER_BUILD_SERVER=OFF
|
||||
CGO_LDFLAGS += -framework Accelerate
|
||||
CGO_LDFLAGS_WHISPER+=-lggml-metal -lggml-blas
|
||||
export WHISPER_LIBRARY_PATH:=$(WHISPER_LIBRARY_PATH):$(WHISPER_DIR)/build/ggml/src/ggml-metal/:$(WHISPER_DIR)/build/ggml/src/ggml-blas
|
||||
endif
|
||||
TARGET+=--target ggml-metal
|
||||
endif
|
||||
|
||||
ifneq (,$(findstring sycl,$(BUILD_TYPE)))
|
||||
export CC=icx
|
||||
export CXX=icpx
|
||||
CGO_LDFLAGS_WHISPER += -fsycl -L${DNNLROOT}/lib -rpath ${ONEAPI_ROOT}/${ONEAPI_VERSION}/lib -ldnnl ${MKLROOT}/lib/intel64/libmkl_sycl.a -fiopenmp -fopenmp-targets=spir64 -lOpenCL -lggml-sycl
|
||||
CGO_LDFLAGS_WHISPER += $(shell pkg-config --libs mkl-static-lp64-gomp)
|
||||
CGO_CXXFLAGS_WHISPER += -fiopenmp -fopenmp-targets=spir64
|
||||
CGO_CXXFLAGS_WHISPER += $(shell pkg-config --cflags mkl-static-lp64-gomp )
|
||||
export WHISPER_LIBRARY_PATH:=$(WHISPER_LIBRARY_PATH):$(WHISPER_DIR)/build/ggml/src/ggml-sycl/
|
||||
CMAKE_ARGS+=-DGGML_SYCL=ON \
|
||||
-DCMAKE_C_COMPILER=icx \
|
||||
-DCMAKE_CXX_COMPILER=icpx \
|
||||
-DCMAKE_CXX_FLAGS="-fsycl"
|
||||
endif
|
||||
|
||||
ifeq ($(BUILD_TYPE),sycl_f16)
|
||||
CMAKE_ARGS+=-DGGML_SYCL_F16=ON
|
||||
CMAKE_ARGS+=-DGGML_SYCL=ON \
|
||||
-DCMAKE_C_COMPILER=icx \
|
||||
-DCMAKE_CXX_COMPILER=icpx \
|
||||
-DGGML_SYCL_F16=ON
|
||||
endif
|
||||
|
||||
ifneq ($(OS),Darwin)
|
||||
CGO_LDFLAGS_WHISPER+=-lgomp
|
||||
ifeq ($(BUILD_TYPE),sycl_f32)
|
||||
CMAKE_ARGS+=-DGGML_SYCL=ON \
|
||||
-DCMAKE_C_COMPILER=icx \
|
||||
-DCMAKE_CXX_COMPILER=icpx
|
||||
endif
|
||||
|
||||
## whisper
|
||||
sources/whisper.cpp:
|
||||
mkdir -p sources/whisper.cpp
|
||||
cd sources/whisper.cpp && \
|
||||
@@ -114,18 +57,21 @@ sources/whisper.cpp:
|
||||
git checkout $(WHISPER_CPP_VERSION) && \
|
||||
git submodule update --init --recursive --depth 1 --single-branch
|
||||
|
||||
sources/whisper.cpp/build/src/libwhisper.a: sources/whisper.cpp
|
||||
cd sources/whisper.cpp && cmake $(CMAKE_ARGS) $(WHISPER_CMAKE_ARGS) . -B ./build
|
||||
cd sources/whisper.cpp/build && cmake --build . --config Release
|
||||
libgowhisper.so: sources/whisper.cpp CMakeLists.txt gowhisper.cpp gowhisper.h
|
||||
mkdir -p build && \
|
||||
cd build && \
|
||||
cmake .. $(CMAKE_ARGS) && \
|
||||
cmake --build . --config Release -j$(JOBS) && \
|
||||
cd .. && \
|
||||
mv build/libgowhisper.so ./
|
||||
|
||||
whisper: sources/whisper.cpp sources/whisper.cpp/build/src/libwhisper.a
|
||||
$(GOCMD) mod edit -replace github.com/ggerganov/whisper.cpp=$(CURDIR)/sources/whisper.cpp
|
||||
$(GOCMD) mod edit -replace github.com/ggerganov/whisper.cpp/bindings/go=$(CURDIR)/sources/whisper.cpp/bindings/go
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS) $(CGO_LDFLAGS_WHISPER)" C_INCLUDE_PATH="${WHISPER_INCLUDE_PATH}" LIBRARY_PATH="${WHISPER_LIBRARY_PATH}" LD_LIBRARY_PATH="${WHISPER_LIBRARY_PATH}" \
|
||||
CGO_CXXFLAGS="$(CGO_CXXFLAGS_WHISPER)" \
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o whisper ./
|
||||
whisper: main.go gowhisper.go libgowhisper.so
|
||||
CGO_ENABLED=0 $(GOCMD) build -tags "$(GO_TAGS)" -o whisper ./
|
||||
|
||||
package:
|
||||
package: whisper
|
||||
bash package.sh
|
||||
|
||||
build: whisper package
|
||||
build: package
|
||||
|
||||
clean:
|
||||
rm -rf libgowhisper.o build whisper
|
||||
|
||||
146
backend/go/whisper/gowhisper.cpp
Normal file
146
backend/go/whisper/gowhisper.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "gowhisper.h"
|
||||
#include "ggml-backend.h"
|
||||
#include "whisper.h"
|
||||
#include <vector>
|
||||
|
||||
static struct whisper_vad_context *vctx;
|
||||
static struct whisper_context *ctx;
|
||||
static std::vector<float> flat_segs;
|
||||
|
||||
static void ggml_log_cb(enum ggml_log_level level, const char* log, void* data) {
|
||||
const char* level_str;
|
||||
|
||||
if (!log) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (level) {
|
||||
case GGML_LOG_LEVEL_DEBUG:
|
||||
level_str = "DEBUG";
|
||||
break;
|
||||
case GGML_LOG_LEVEL_INFO:
|
||||
level_str = "INFO";
|
||||
break;
|
||||
case GGML_LOG_LEVEL_WARN:
|
||||
level_str = "WARN";
|
||||
break;
|
||||
case GGML_LOG_LEVEL_ERROR:
|
||||
level_str = "ERROR";
|
||||
break;
|
||||
default: /* Potential future-proofing */
|
||||
level_str = "?????";
|
||||
break;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[%-5s] ", level_str);
|
||||
fputs(log, stderr);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
int load_model(const char *const model_path) {
|
||||
whisper_log_set(ggml_log_cb, nullptr);
|
||||
ggml_backend_load_all();
|
||||
|
||||
struct whisper_context_params cparams = whisper_context_default_params();
|
||||
|
||||
ctx = whisper_init_from_file_with_params(model_path, cparams);
|
||||
if (ctx == nullptr) {
|
||||
fprintf(stderr, "error: Also failed to init model as transcriber\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int load_model_vad(const char *const model_path) {
|
||||
whisper_log_set(ggml_log_cb, nullptr);
|
||||
ggml_backend_load_all();
|
||||
|
||||
struct whisper_vad_context_params vcparams =
|
||||
whisper_vad_default_context_params();
|
||||
|
||||
// XXX: Overridden to false in upstream due to performance?
|
||||
// vcparams.use_gpu = true;
|
||||
|
||||
vctx = whisper_vad_init_from_file_with_params(model_path, vcparams);
|
||||
if (vctx == nullptr) {
|
||||
fprintf(stderr, "error: Failed to init model as VAD\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vad(float pcmf32[], size_t pcmf32_len, float **segs_out,
|
||||
size_t *segs_out_len) {
|
||||
if (!whisper_vad_detect_speech(vctx, pcmf32, pcmf32_len)) {
|
||||
fprintf(stderr, "error: failed to detect speech\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct whisper_vad_params params = whisper_vad_default_params();
|
||||
struct whisper_vad_segments *segs =
|
||||
whisper_vad_segments_from_probs(vctx, params);
|
||||
size_t segn = whisper_vad_segments_n_segments(segs);
|
||||
|
||||
// fprintf(stderr, "Got segments %zd\n", segn);
|
||||
|
||||
flat_segs.clear();
|
||||
|
||||
for (int i = 0; i < segn; i++) {
|
||||
flat_segs.push_back(whisper_vad_segments_get_segment_t0(segs, i));
|
||||
flat_segs.push_back(whisper_vad_segments_get_segment_t1(segs, i));
|
||||
}
|
||||
|
||||
// fprintf(stderr, "setting out variables: %p=%p -> %p, %p=%zx -> %zx\n",
|
||||
// segs_out, *segs_out, flat_segs.data(), segs_out_len, *segs_out_len,
|
||||
// flat_segs.size());
|
||||
*segs_out = flat_segs.data();
|
||||
*segs_out_len = flat_segs.size();
|
||||
|
||||
// fprintf(stderr, "freeing segs\n");
|
||||
whisper_vad_free_segments(segs);
|
||||
|
||||
// fprintf(stderr, "returning\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int transcribe(uint32_t threads, char *lang, bool translate, float pcmf32[],
|
||||
size_t pcmf32_len, size_t *segs_out_len) {
|
||||
whisper_full_params wparams =
|
||||
whisper_full_default_params(WHISPER_SAMPLING_GREEDY);
|
||||
|
||||
wparams.n_threads = threads;
|
||||
if (*lang != '\0')
|
||||
wparams.language = lang;
|
||||
else {
|
||||
wparams.language = nullptr;
|
||||
}
|
||||
|
||||
wparams.translate = translate;
|
||||
wparams.debug_mode = true;
|
||||
wparams.print_progress = true;
|
||||
|
||||
if (whisper_full(ctx, wparams, pcmf32, pcmf32_len)) {
|
||||
fprintf(stderr, "error: transcription failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
*segs_out_len = whisper_full_n_segments(ctx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *get_segment_text(int i) {
|
||||
return whisper_full_get_segment_text(ctx, i);
|
||||
}
|
||||
|
||||
int64_t get_segment_t0(int i) { return whisper_full_get_segment_t0(ctx, i); }
|
||||
|
||||
int64_t get_segment_t1(int i) { return whisper_full_get_segment_t1(ctx, i); }
|
||||
|
||||
int n_tokens(int i) { return whisper_full_n_tokens(ctx, i); }
|
||||
|
||||
int32_t get_token_id(int i, int j) {
|
||||
return whisper_full_get_token_id(ctx, i, j);
|
||||
}
|
||||
156
backend/go/whisper/gowhisper.go
Normal file
156
backend/go/whisper/gowhisper.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-audio/wav"
|
||||
"github.com/mudler/LocalAI/pkg/grpc/base"
|
||||
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
CppLoadModel func(modelPath string) int
|
||||
CppLoadModelVAD func(modelPath string) int
|
||||
CppVAD func(pcmf32 []float32, pcmf32Size uintptr, segsOut unsafe.Pointer, segsOutLen unsafe.Pointer) int
|
||||
CppTranscribe func(threads uint32, lang string, translate bool, pcmf32 []float32, pcmf32Len uintptr, segsOutLen unsafe.Pointer) int
|
||||
CppGetSegmentText func(i int) string
|
||||
CppGetSegmentStart func(i int) int64
|
||||
CppGetSegmentEnd func(i int) int64
|
||||
CppNTokens func(i int) int
|
||||
CppGetTokenID func(i int, j int) int
|
||||
)
|
||||
|
||||
type Whisper struct {
|
||||
base.SingleThread
|
||||
}
|
||||
|
||||
func (w *Whisper) Load(opts *pb.ModelOptions) error {
|
||||
vadOnly := false
|
||||
|
||||
for _, oo := range opts.Options {
|
||||
if oo == "vad_only" {
|
||||
vadOnly = true
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Unrecognized option: %v\n", oo)
|
||||
}
|
||||
}
|
||||
|
||||
if vadOnly {
|
||||
if ret := CppLoadModelVAD(opts.ModelFile); ret != 0 {
|
||||
return fmt.Errorf("Failed to load Whisper VAD model")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if ret := CppLoadModel(opts.ModelFile); ret != 0 {
|
||||
return fmt.Errorf("Failed to load Whisper transcription model")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Whisper) VAD(req *pb.VADRequest) (pb.VADResponse, error) {
|
||||
audio := req.Audio
|
||||
// We expect 0xdeadbeef to be overwritten and if we see it in a stack trace we know it wasn't
|
||||
segsPtr, segsLen := uintptr(0xdeadbeef), uintptr(0xdeadbeef)
|
||||
segsPtrPtr, segsLenPtr := unsafe.Pointer(&segsPtr), unsafe.Pointer(&segsLen)
|
||||
|
||||
if ret := CppVAD(audio, uintptr(len(audio)), segsPtrPtr, segsLenPtr); ret != 0 {
|
||||
return pb.VADResponse{}, fmt.Errorf("Failed VAD")
|
||||
}
|
||||
|
||||
// Happens when CPP vector has not had any elements pushed to it
|
||||
if segsPtr == 0 {
|
||||
return pb.VADResponse{
|
||||
Segments: []*pb.VADSegment{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// unsafeptr warning is caused by segsPtr being on the stack and therefor being subject to stack copying AFAICT
|
||||
// however the stack shouldn't have grown between setting segsPtr and now, also the memory pointed to is allocated by C++
|
||||
segs := unsafe.Slice((*float32)(unsafe.Pointer(segsPtr)), segsLen)
|
||||
|
||||
vadSegments := []*pb.VADSegment{}
|
||||
for i := range len(segs) >> 1 {
|
||||
s := segs[2*i] / 100
|
||||
t := segs[2*i+1] / 100
|
||||
vadSegments = append(vadSegments, &pb.VADSegment{
|
||||
Start: s,
|
||||
End: t,
|
||||
})
|
||||
}
|
||||
|
||||
return pb.VADResponse{
|
||||
Segments: vadSegments,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Whisper) AudioTranscription(opts *pb.TranscriptRequest) (pb.TranscriptResult, error) {
|
||||
dir, err := os.MkdirTemp("", "whisper")
|
||||
if err != nil {
|
||||
return pb.TranscriptResult{}, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
convertedPath := filepath.Join(dir, "converted.wav")
|
||||
|
||||
if err := utils.AudioToWav(opts.Dst, convertedPath); err != nil {
|
||||
return pb.TranscriptResult{}, err
|
||||
}
|
||||
|
||||
// Open samples
|
||||
fh, err := os.Open(convertedPath)
|
||||
if err != nil {
|
||||
return pb.TranscriptResult{}, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
// Read samples
|
||||
d := wav.NewDecoder(fh)
|
||||
buf, err := d.FullPCMBuffer()
|
||||
if err != nil {
|
||||
return pb.TranscriptResult{}, err
|
||||
}
|
||||
|
||||
data := buf.AsFloat32Buffer().Data
|
||||
segsLen := uintptr(0xdeadbeef)
|
||||
segsLenPtr := unsafe.Pointer(&segsLen)
|
||||
|
||||
if ret := CppTranscribe(opts.Threads, opts.Language, opts.Translate, data, uintptr(len(data)), segsLenPtr); ret != 0 {
|
||||
return pb.TranscriptResult{}, fmt.Errorf("Failed Transcribe")
|
||||
}
|
||||
|
||||
segments := []*pb.TranscriptSegment{}
|
||||
text := ""
|
||||
for i := range int(segsLen) {
|
||||
s := CppGetSegmentStart(i)
|
||||
t := CppGetSegmentEnd(i)
|
||||
txt := strings.Clone(CppGetSegmentText(i))
|
||||
tokens := make([]int32, CppNTokens(i))
|
||||
|
||||
for j := range tokens {
|
||||
tokens[j] = int32(CppGetTokenID(i, j))
|
||||
}
|
||||
segment := &pb.TranscriptSegment{
|
||||
Id: int32(i),
|
||||
Text: txt,
|
||||
Start: s, End: t,
|
||||
Tokens: tokens,
|
||||
}
|
||||
|
||||
segments = append(segments, segment)
|
||||
|
||||
text += " " + strings.TrimSpace(txt)
|
||||
}
|
||||
|
||||
return pb.TranscriptResult{
|
||||
Segments: segments,
|
||||
Text: strings.TrimSpace(text),
|
||||
}, nil
|
||||
}
|
||||
16
backend/go/whisper/gowhisper.h
Normal file
16
backend/go/whisper/gowhisper.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
extern "C" {
|
||||
int load_model(const char *const model_path);
|
||||
int load_model_vad(const char *const model_path);
|
||||
int vad(float pcmf32[], size_t pcmf32_size, float **segs_out,
|
||||
size_t *segs_out_len);
|
||||
int transcribe(uint32_t threads, char *lang, bool translate, float pcmf32[],
|
||||
size_t pcmf32_len, size_t *segs_out_len);
|
||||
const char *get_segment_text(int i);
|
||||
int64_t get_segment_t0(int i);
|
||||
int64_t get_segment_t1(int i);
|
||||
int n_tokens(int i);
|
||||
int32_t get_token_id(int i, int j);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package main
|
||||
|
||||
// Note: this is started internally by LocalAI and a server is allocated for each model
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/ebitengine/purego"
|
||||
grpc "github.com/mudler/LocalAI/pkg/grpc"
|
||||
)
|
||||
|
||||
@@ -12,7 +12,33 @@ var (
|
||||
addr = flag.String("addr", "localhost:50051", "the address to connect to")
|
||||
)
|
||||
|
||||
type LibFuncs struct {
|
||||
FuncPtr any
|
||||
Name string
|
||||
}
|
||||
|
||||
func main() {
|
||||
gosd, err := purego.Dlopen("./libgowhisper.so", purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
libFuncs := []LibFuncs{
|
||||
{&CppLoadModel, "load_model"},
|
||||
{&CppLoadModelVAD, "load_model_vad"},
|
||||
{&CppVAD, "vad"},
|
||||
{&CppTranscribe, "transcribe"},
|
||||
{&CppGetSegmentText, "get_segment_text"},
|
||||
{&CppGetSegmentStart, "get_segment_t0"},
|
||||
{&CppGetSegmentEnd, "get_segment_t1"},
|
||||
{&CppNTokens, "n_tokens"},
|
||||
{&CppGetTokenID, "get_token_id"},
|
||||
}
|
||||
|
||||
for _, lf := range libFuncs {
|
||||
purego.RegisterLibFunc(lf.FuncPtr, gosd, lf.Name)
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if err := grpc.StartServer(*addr, &Whisper{}); err != nil {
|
||||
|
||||
@@ -10,8 +10,8 @@ CURDIR=$(dirname "$(realpath $0)")
|
||||
# Create lib directory
|
||||
mkdir -p $CURDIR/package/lib
|
||||
|
||||
cp -avrf $CURDIR/whisper $CURDIR/package/
|
||||
cp -rfv $CURDIR/run.sh $CURDIR/package/
|
||||
cp -avf $CURDIR/whisper $CURDIR/libgowhisper.so $CURDIR/package/
|
||||
cp -fv $CURDIR/run.sh $CURDIR/package/
|
||||
|
||||
# Detect architecture and copy appropriate libraries
|
||||
if [ -f "/lib64/ld-linux-x86-64.so.2" ]; then
|
||||
@@ -42,11 +42,13 @@ elif [ -f "/lib/ld-linux-aarch64.so.1" ]; then
|
||||
cp -arfLv /lib/aarch64-linux-gnu/libdl.so.2 $CURDIR/package/lib/libdl.so.2
|
||||
cp -arfLv /lib/aarch64-linux-gnu/librt.so.1 $CURDIR/package/lib/librt.so.1
|
||||
cp -arfLv /lib/aarch64-linux-gnu/libpthread.so.0 $CURDIR/package/lib/libpthread.so.0
|
||||
elif [ $(uname -s) = "Darwin" ]; then
|
||||
echo "Detected Darwin"
|
||||
else
|
||||
echo "Error: Could not detect architecture"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Packaging completed successfully"
|
||||
echo "Packaging completed successfully"
|
||||
ls -liah $CURDIR/package/
|
||||
ls -liah $CURDIR/package/lib/
|
||||
ls -liah $CURDIR/package/lib/
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
package main
|
||||
|
||||
// This is a wrapper to statisfy the GRPC service interface
|
||||
// It is meant to be used by the main executable that is the server for the specific backend type (falcon, gpt3, etc)
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ggerganov/whisper.cpp/bindings/go/pkg/whisper"
|
||||
"github.com/go-audio/wav"
|
||||
"github.com/mudler/LocalAI/pkg/grpc/base"
|
||||
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
)
|
||||
|
||||
type Whisper struct {
|
||||
base.SingleThread
|
||||
whisper whisper.Model
|
||||
}
|
||||
|
||||
func (sd *Whisper) Load(opts *pb.ModelOptions) error {
|
||||
// Note: the Model here is a path to a directory containing the model files
|
||||
w, err := whisper.New(opts.ModelFile)
|
||||
sd.whisper = w
|
||||
return err
|
||||
}
|
||||
|
||||
func (sd *Whisper) AudioTranscription(opts *pb.TranscriptRequest) (pb.TranscriptResult, error) {
|
||||
|
||||
dir, err := os.MkdirTemp("", "whisper")
|
||||
if err != nil {
|
||||
return pb.TranscriptResult{}, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
convertedPath := filepath.Join(dir, "converted.wav")
|
||||
|
||||
if err := utils.AudioToWav(opts.Dst, convertedPath); err != nil {
|
||||
return pb.TranscriptResult{}, err
|
||||
}
|
||||
|
||||
// Open samples
|
||||
fh, err := os.Open(convertedPath)
|
||||
if err != nil {
|
||||
return pb.TranscriptResult{}, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
// Read samples
|
||||
d := wav.NewDecoder(fh)
|
||||
buf, err := d.FullPCMBuffer()
|
||||
if err != nil {
|
||||
return pb.TranscriptResult{}, err
|
||||
}
|
||||
|
||||
data := buf.AsFloat32Buffer().Data
|
||||
|
||||
// Process samples
|
||||
context, err := sd.whisper.NewContext()
|
||||
if err != nil {
|
||||
return pb.TranscriptResult{}, err
|
||||
|
||||
}
|
||||
|
||||
context.SetThreads(uint(opts.Threads))
|
||||
|
||||
if opts.Language != "" {
|
||||
context.SetLanguage(opts.Language)
|
||||
} else {
|
||||
context.SetLanguage("auto")
|
||||
}
|
||||
|
||||
if opts.Translate {
|
||||
context.SetTranslate(true)
|
||||
}
|
||||
|
||||
if err := context.Process(data, nil, nil, nil); err != nil {
|
||||
return pb.TranscriptResult{}, err
|
||||
}
|
||||
|
||||
segments := []*pb.TranscriptSegment{}
|
||||
text := ""
|
||||
for {
|
||||
s, err := context.NextSegment()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var tokens []int32
|
||||
for _, t := range s.Tokens {
|
||||
tokens = append(tokens, int32(t.Id))
|
||||
}
|
||||
|
||||
segment := &pb.TranscriptSegment{Id: int32(s.Num), Text: s.Text, Start: int64(s.Start), End: int64(s.End), Tokens: tokens}
|
||||
segments = append(segments, segment)
|
||||
|
||||
text += s.Text
|
||||
}
|
||||
|
||||
return pb.TranscriptResult{
|
||||
Segments: segments,
|
||||
Text: text,
|
||||
}, nil
|
||||
|
||||
}
|
||||
@@ -45,6 +45,7 @@
|
||||
default: "cpu-whisper"
|
||||
nvidia: "cuda12-whisper"
|
||||
intel: "intel-sycl-f16-whisper"
|
||||
metal: "metal-whisper"
|
||||
amd: "rocm-whisper"
|
||||
vulkan: "vulkan-whisper"
|
||||
nvidia-l4t: "nvidia-l4t-arm64-whisper"
|
||||
@@ -71,7 +72,7 @@
|
||||
# amd: "rocm-stablediffusion-ggml"
|
||||
vulkan: "vulkan-stablediffusion-ggml"
|
||||
nvidia-l4t: "nvidia-l4t-arm64-stablediffusion-ggml"
|
||||
# metal: "metal-stablediffusion-ggml"
|
||||
metal: "metal-stablediffusion-ggml"
|
||||
# darwin-x86: "darwin-x86-stablediffusion-ggml"
|
||||
- &rfdetr
|
||||
name: "rfdetr"
|
||||
@@ -147,7 +148,7 @@
|
||||
uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-mlx-vlm"
|
||||
icon: https://avatars.githubusercontent.com/u/102832242?s=200&v=4
|
||||
urls:
|
||||
- https://github.com/ml-explore/mlx-vlm
|
||||
- https://github.com/Blaizzy/mlx-vlm
|
||||
mirrors:
|
||||
- localai/localai-backends:latest-metal-darwin-arm64-mlx-vlm
|
||||
license: MIT
|
||||
@@ -159,6 +160,23 @@
|
||||
- vision-language
|
||||
- LLM
|
||||
- MLX
|
||||
- &mlx-audio
|
||||
name: "mlx-audio"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-mlx-audio"
|
||||
icon: https://avatars.githubusercontent.com/u/102832242?s=200&v=4
|
||||
urls:
|
||||
- https://github.com/Blaizzy/mlx-audio
|
||||
mirrors:
|
||||
- localai/localai-backends:latest-metal-darwin-arm64-mlx-audio
|
||||
license: MIT
|
||||
description: |
|
||||
Run Audio Models with MLX
|
||||
tags:
|
||||
- audio-to-text
|
||||
- audio-generation
|
||||
- text-to-audio
|
||||
- LLM
|
||||
- MLX
|
||||
- &rerankers
|
||||
name: "rerankers"
|
||||
alias: "rerankers"
|
||||
@@ -183,8 +201,6 @@
|
||||
nvidia: "cuda12-transformers"
|
||||
intel: "intel-transformers"
|
||||
amd: "rocm-transformers"
|
||||
metal: "metal-transformers"
|
||||
default: "cpu-transformers"
|
||||
- &diffusers
|
||||
name: "diffusers"
|
||||
icon: https://raw.githubusercontent.com/huggingface/diffusers/main/docs/source/en/imgs/diffusers_library.jpg
|
||||
@@ -417,6 +433,11 @@
|
||||
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-mlx-vlm"
|
||||
mirrors:
|
||||
- localai/localai-backends:master-metal-darwin-arm64-mlx-vlm
|
||||
- !!merge <<: *mlx-audio
|
||||
name: "mlx-audio-development"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-mlx-audio"
|
||||
mirrors:
|
||||
- localai/localai-backends:master-metal-darwin-arm64-mlx-audio
|
||||
- !!merge <<: *kitten-tts
|
||||
name: "kitten-tts-development"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:master-kitten-tts"
|
||||
@@ -559,6 +580,16 @@
|
||||
uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-whisper"
|
||||
mirrors:
|
||||
- localai/localai-backends:latest-cpu-whisper
|
||||
- !!merge <<: *whispercpp
|
||||
name: "metal-whisper"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-whisper"
|
||||
mirrors:
|
||||
- localai/localai-backends:latest-metal-darwin-arm64-whisper
|
||||
- !!merge <<: *whispercpp
|
||||
name: "metal-whisper-development"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-whisper"
|
||||
mirrors:
|
||||
- localai/localai-backends:master-metal-darwin-arm64-whisper
|
||||
- !!merge <<: *whispercpp
|
||||
name: "cpu-whisper-development"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:master-cpu-whisper"
|
||||
@@ -645,6 +676,16 @@
|
||||
uri: "quay.io/go-skynet/local-ai-backends:master-cpu-stablediffusion-ggml"
|
||||
mirrors:
|
||||
- localai/localai-backends:master-cpu-stablediffusion-ggml
|
||||
- !!merge <<: *stablediffusionggml
|
||||
name: "metal-stablediffusion-ggml"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-stablediffusion-ggml"
|
||||
mirrors:
|
||||
- localai/localai-backends:latest-metal-darwin-arm64-stablediffusion-ggml
|
||||
- !!merge <<: *stablediffusionggml
|
||||
name: "metal-stablediffusion-ggml-development"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-stablediffusion-ggml"
|
||||
mirrors:
|
||||
- localai/localai-backends:master-metal-darwin-arm64-stablediffusion-ggml
|
||||
- !!merge <<: *stablediffusionggml
|
||||
name: "vulkan-stablediffusion-ggml"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-vulkan-stablediffusion-ggml"
|
||||
@@ -853,28 +894,6 @@
|
||||
nvidia: "cuda12-transformers-development"
|
||||
intel: "intel-transformers-development"
|
||||
amd: "rocm-transformers-development"
|
||||
default: "cpu-transformers-development"
|
||||
metal: "metal-transformers-development"
|
||||
- !!merge <<: *transformers
|
||||
name: "cpu-transformers"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-transformers"
|
||||
mirrors:
|
||||
- localai/localai-backends:latest-cpu-transformers
|
||||
- !!merge <<: *transformers
|
||||
name: "cpu-transformers-development"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:master-cpu-transformers"
|
||||
mirrors:
|
||||
- localai/localai-backends:master-cpu-transformers
|
||||
- !!merge <<: *transformers
|
||||
name: "metal-transformers"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-transformers"
|
||||
mirrors:
|
||||
- localai/localai-backends:latest-metal-darwin-arm64-transformers
|
||||
- !!merge <<: *transformers
|
||||
name: "metal-transformers-development"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-transformers"
|
||||
mirrors:
|
||||
- localai/localai-backends:master-metal-darwin-arm64-transformers
|
||||
- !!merge <<: *transformers
|
||||
name: "cuda12-transformers"
|
||||
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-12-transformers"
|
||||
|
||||
@@ -1,38 +1,190 @@
|
||||
# Common commands about conda environment
|
||||
# Python Backends for LocalAI
|
||||
|
||||
## Create a new empty conda environment
|
||||
This directory contains Python-based AI backends for LocalAI, providing support for various AI models and hardware acceleration targets.
|
||||
|
||||
```
|
||||
conda create --name <env-name> python=<your version> -y
|
||||
## Overview
|
||||
|
||||
conda create --name autogptq python=3.11 -y
|
||||
The Python backends use a unified build system based on `libbackend.sh` that provides:
|
||||
- **Automatic virtual environment management** with support for both `uv` and `pip`
|
||||
- **Hardware-specific dependency installation** (CPU, CUDA, Intel, MLX, etc.)
|
||||
- **Portable Python support** for standalone deployments
|
||||
- **Consistent backend execution** across different environments
|
||||
|
||||
## Available Backends
|
||||
|
||||
### Core AI Models
|
||||
- **transformers** - Hugging Face Transformers framework (PyTorch-based)
|
||||
- **vllm** - High-performance LLM inference engine
|
||||
- **mlx** - Apple Silicon optimized ML framework
|
||||
- **exllama2** - ExLlama2 quantized models
|
||||
|
||||
### Audio & Speech
|
||||
- **bark** - Text-to-speech synthesis
|
||||
- **coqui** - Coqui TTS models
|
||||
- **faster-whisper** - Fast Whisper speech recognition
|
||||
- **kitten-tts** - Lightweight TTS
|
||||
- **mlx-audio** - Apple Silicon audio processing
|
||||
- **chatterbox** - TTS model
|
||||
- **kokoro** - TTS models
|
||||
|
||||
### Computer Vision
|
||||
- **diffusers** - Stable Diffusion and image generation
|
||||
- **mlx-vlm** - Vision-language models for Apple Silicon
|
||||
- **rfdetr** - Object detection models
|
||||
|
||||
### Specialized
|
||||
|
||||
- **rerankers** - Text reranking models
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.10+ (default: 3.10.18)
|
||||
- `uv` package manager (recommended) or `pip`
|
||||
- Appropriate hardware drivers for your target (CUDA, Intel, etc.)
|
||||
|
||||
### Installation
|
||||
|
||||
Each backend can be installed individually:
|
||||
|
||||
```bash
|
||||
# Navigate to a specific backend
|
||||
cd backend/python/transformers
|
||||
|
||||
# Install dependencies
|
||||
make transformers
|
||||
# or
|
||||
bash install.sh
|
||||
|
||||
# Run the backend
|
||||
make run
|
||||
# or
|
||||
bash run.sh
|
||||
```
|
||||
|
||||
## To activate the environment
|
||||
### Using the Unified Build System
|
||||
|
||||
As of conda 4.4
|
||||
```
|
||||
conda activate autogptq
|
||||
The `libbackend.sh` script provides consistent commands across all backends:
|
||||
|
||||
```bash
|
||||
# Source the library in your backend script
|
||||
source $(dirname $0)/../common/libbackend.sh
|
||||
|
||||
# Install requirements (automatically handles hardware detection)
|
||||
installRequirements
|
||||
|
||||
# Start the backend server
|
||||
startBackend $@
|
||||
|
||||
# Run tests
|
||||
runUnittests
|
||||
```
|
||||
|
||||
The conda version older than 4.4
|
||||
## Hardware Targets
|
||||
|
||||
```
|
||||
source activate autogptq
|
||||
The build system automatically detects and configures for different hardware:
|
||||
|
||||
- **CPU** - Standard CPU-only builds
|
||||
- **CUDA** - NVIDIA GPU acceleration (supports CUDA 11/12)
|
||||
- **Intel** - Intel XPU/GPU optimization
|
||||
- **MLX** - Apple Silicon (M1/M2/M3) optimization
|
||||
- **HIP** - AMD GPU acceleration
|
||||
|
||||
### Target-Specific Requirements
|
||||
|
||||
Backends can specify hardware-specific dependencies:
|
||||
- `requirements.txt` - Base requirements
|
||||
- `requirements-cpu.txt` - CPU-specific packages
|
||||
- `requirements-cublas11.txt` - CUDA 11 packages
|
||||
- `requirements-cublas12.txt` - CUDA 12 packages
|
||||
- `requirements-intel.txt` - Intel-optimized packages
|
||||
- `requirements-mps.txt` - Apple Silicon packages
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `PYTHON_VERSION` - Python version (default: 3.10)
|
||||
- `PYTHON_PATCH` - Python patch version (default: 18)
|
||||
- `BUILD_TYPE` - Force specific build target
|
||||
- `USE_PIP` - Use pip instead of uv (default: false)
|
||||
- `PORTABLE_PYTHON` - Enable portable Python builds
|
||||
- `LIMIT_TARGETS` - Restrict backend to specific targets
|
||||
|
||||
### Example: CUDA 12 Only Backend
|
||||
|
||||
```bash
|
||||
# In your backend script
|
||||
LIMIT_TARGETS="cublas12"
|
||||
source $(dirname $0)/../common/libbackend.sh
|
||||
```
|
||||
|
||||
## Install the packages to your environment
|
||||
### Example: Intel-Optimized Backend
|
||||
|
||||
Sometimes you need to install the packages from the conda-forge channel
|
||||
|
||||
By using `conda`
|
||||
```
|
||||
conda install <your-package-name>
|
||||
|
||||
conda install -c conda-forge <your package-name>
|
||||
```bash
|
||||
# In your backend script
|
||||
LIMIT_TARGETS="intel"
|
||||
source $(dirname $0)/../common/libbackend.sh
|
||||
```
|
||||
|
||||
Or by using `pip`
|
||||
## Development
|
||||
|
||||
### Adding a New Backend
|
||||
|
||||
1. Create a new directory in `backend/python/`
|
||||
2. Copy the template structure from `common/template/`
|
||||
3. Implement your `backend.py` with the required gRPC interface
|
||||
4. Add appropriate requirements files for your target hardware
|
||||
5. Use `libbackend.sh` for consistent build and execution
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run backend tests
|
||||
make test
|
||||
# or
|
||||
bash test.sh
|
||||
```
|
||||
pip install <your-package-name>
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
make <backend-name>
|
||||
|
||||
# Clean build artifacts
|
||||
make clean
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Each backend follows a consistent structure:
|
||||
```
|
||||
backend-name/
|
||||
├── backend.py # Main backend implementation
|
||||
├── requirements.txt # Base dependencies
|
||||
├── requirements-*.txt # Hardware-specific dependencies
|
||||
├── install.sh # Installation script
|
||||
├── run.sh # Execution script
|
||||
├── test.sh # Test script
|
||||
├── Makefile # Build targets
|
||||
└── test.py # Unit tests
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Missing dependencies**: Ensure all requirements files are properly configured
|
||||
2. **Hardware detection**: Check that `BUILD_TYPE` matches your system
|
||||
3. **Python version**: Verify Python 3.10+ is available
|
||||
4. **Virtual environment**: Use `ensureVenv` to create/activate environments
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new backends or modifying existing ones:
|
||||
1. Follow the established directory structure
|
||||
2. Use `libbackend.sh` for consistent behavior
|
||||
3. Include appropriate requirements files for all target hardware
|
||||
4. Add comprehensive tests
|
||||
5. Update this README if adding new backend types
|
||||
|
||||
@@ -286,7 +286,8 @@ _makeVenvPortable() {
|
||||
function ensureVenv() {
|
||||
local interpreter=""
|
||||
|
||||
if [ "x${PORTABLE_PYTHON}" == "xtrue" ]; then
|
||||
if [ "x${PORTABLE_PYTHON}" == "xtrue" ] || [ -e "$(_portable_python)" ]; then
|
||||
echo "Using portable Python"
|
||||
ensurePortablePython
|
||||
interpreter="$(_portable_python)"
|
||||
else
|
||||
@@ -384,6 +385,11 @@ function installRequirements() {
|
||||
requirementFiles+=("${EDIR}/requirements-${BUILD_PROFILE}-after.txt")
|
||||
fi
|
||||
|
||||
# This is needed to build wheels that e.g. depends on Python.h
|
||||
if [ "x${PORTABLE_PYTHON}" == "xtrue" ]; then
|
||||
export C_INCLUDE_PATH="${C_INCLUDE_PATH:-}:$(_portable_dir)/include/python${PYTHON_VERSION}"
|
||||
fi
|
||||
|
||||
for reqFile in ${requirementFiles[@]}; do
|
||||
if [ -f "${reqFile}" ]; then
|
||||
echo "starting requirements install for ${reqFile}"
|
||||
|
||||
@@ -18,7 +18,7 @@ import backend_pb2_grpc
|
||||
import grpc
|
||||
|
||||
from diffusers import SanaPipeline, StableDiffusion3Pipeline, StableDiffusionXLPipeline, StableDiffusionDepth2ImgPipeline, DPMSolverMultistepScheduler, StableDiffusionPipeline, DiffusionPipeline, \
|
||||
EulerAncestralDiscreteScheduler, FluxPipeline, FluxTransformer2DModel, QwenImageEditPipeline
|
||||
EulerAncestralDiscreteScheduler, FluxPipeline, FluxTransformer2DModel, QwenImageEditPipeline, AutoencoderKLWan, WanPipeline, WanImageToVideoPipeline
|
||||
from diffusers import StableDiffusionImg2ImgPipeline, AutoPipelineForText2Image, ControlNetModel, StableVideoDiffusionPipeline, Lumina2Text2ImgPipeline
|
||||
from diffusers.pipelines.stable_diffusion import safety_checker
|
||||
from diffusers.utils import load_image, export_to_video
|
||||
@@ -72,13 +72,6 @@ def is_float(s):
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def is_int(s):
|
||||
try:
|
||||
int(s)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# The scheduler list mapping was taken from here: https://github.com/neggles/animatediff-cli/blob/6f336f5f4b5e38e85d7f06f1744ef42d0a45f2a7/src/animatediff/schedulers.py#L39
|
||||
# Credits to https://github.com/neggles
|
||||
# See https://github.com/huggingface/diffusers/issues/4167 for more details on sched mapping from A1111
|
||||
@@ -184,9 +177,10 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
key, value = opt.split(":")
|
||||
# if value is a number, convert it to the appropriate type
|
||||
if is_float(value):
|
||||
value = float(value)
|
||||
elif is_int(value):
|
||||
value = int(value)
|
||||
if value.is_integer():
|
||||
value = int(value)
|
||||
else:
|
||||
value = float(value)
|
||||
self.options[key] = value
|
||||
|
||||
# From options, extract if present "torch_dtype" and set it to the appropriate type
|
||||
@@ -334,6 +328,32 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
torch_dtype=torch.bfloat16)
|
||||
self.pipe.vae.to(torch.bfloat16)
|
||||
self.pipe.text_encoder.to(torch.bfloat16)
|
||||
elif request.PipelineType == "WanPipeline":
|
||||
# WAN2.2 pipeline requires special VAE handling
|
||||
vae = AutoencoderKLWan.from_pretrained(
|
||||
request.Model,
|
||||
subfolder="vae",
|
||||
torch_dtype=torch.float32
|
||||
)
|
||||
self.pipe = WanPipeline.from_pretrained(
|
||||
request.Model,
|
||||
vae=vae,
|
||||
torch_dtype=torchType
|
||||
)
|
||||
self.txt2vid = True # WAN2.2 is a text-to-video pipeline
|
||||
elif request.PipelineType == "WanImageToVideoPipeline":
|
||||
# WAN2.2 image-to-video pipeline
|
||||
vae = AutoencoderKLWan.from_pretrained(
|
||||
request.Model,
|
||||
subfolder="vae",
|
||||
torch_dtype=torch.float32
|
||||
)
|
||||
self.pipe = WanImageToVideoPipeline.from_pretrained(
|
||||
request.Model,
|
||||
vae=vae,
|
||||
torch_dtype=torchType
|
||||
)
|
||||
self.img2vid = True # WAN2.2 image-to-video pipeline
|
||||
|
||||
if CLIPSKIP and request.CLIPSkip != 0:
|
||||
self.clip_skip = request.CLIPSkip
|
||||
@@ -475,11 +495,24 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
"num_inference_steps": steps,
|
||||
}
|
||||
|
||||
if request.src != "" and not self.controlnet and not self.img2vid:
|
||||
image = Image.open(request.src)
|
||||
# Handle image source: prioritize RefImages over request.src
|
||||
image_src = None
|
||||
if hasattr(request, 'ref_images') and request.ref_images and len(request.ref_images) > 0:
|
||||
# Use the first reference image if available
|
||||
image_src = request.ref_images[0]
|
||||
print(f"Using reference image: {image_src}", file=sys.stderr)
|
||||
elif request.src != "":
|
||||
# Fall back to request.src if no ref_images
|
||||
image_src = request.src
|
||||
print(f"Using source image: {image_src}", file=sys.stderr)
|
||||
else:
|
||||
print("No image source provided", file=sys.stderr)
|
||||
|
||||
if image_src and not self.controlnet and not self.img2vid:
|
||||
image = Image.open(image_src)
|
||||
options["image"] = image
|
||||
elif self.controlnet and request.src:
|
||||
pose_image = load_image(request.src)
|
||||
elif self.controlnet and image_src:
|
||||
pose_image = load_image(image_src)
|
||||
options["image"] = pose_image
|
||||
|
||||
if CLIPSKIP and self.clip_skip != 0:
|
||||
@@ -521,7 +554,11 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
|
||||
if self.img2vid:
|
||||
# Load the conditioning image
|
||||
image = load_image(request.src)
|
||||
if image_src:
|
||||
image = load_image(image_src)
|
||||
else:
|
||||
# Fallback to request.src for img2vid if no ref_images
|
||||
image = load_image(request.src)
|
||||
image = image.resize((1024, 576))
|
||||
|
||||
generator = torch.manual_seed(request.seed)
|
||||
@@ -558,6 +595,96 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
|
||||
return backend_pb2.Result(message="Media generated", success=True)
|
||||
|
||||
def GenerateVideo(self, request, context):
|
||||
try:
|
||||
prompt = request.prompt
|
||||
if not prompt:
|
||||
return backend_pb2.Result(success=False, message="No prompt provided for video generation")
|
||||
|
||||
# Set default values from request or use defaults
|
||||
num_frames = request.num_frames if request.num_frames > 0 else 81
|
||||
fps = request.fps if request.fps > 0 else 16
|
||||
cfg_scale = request.cfg_scale if request.cfg_scale > 0 else 4.0
|
||||
num_inference_steps = request.step if request.step > 0 else 40
|
||||
|
||||
# Prepare generation parameters
|
||||
kwargs = {
|
||||
"prompt": prompt,
|
||||
"negative_prompt": request.negative_prompt if request.negative_prompt else "",
|
||||
"height": request.height if request.height > 0 else 720,
|
||||
"width": request.width if request.width > 0 else 1280,
|
||||
"num_frames": num_frames,
|
||||
"guidance_scale": cfg_scale,
|
||||
"num_inference_steps": num_inference_steps,
|
||||
}
|
||||
|
||||
# Add custom options from self.options (including guidance_scale_2 if specified)
|
||||
kwargs.update(self.options)
|
||||
|
||||
# Set seed if provided
|
||||
if request.seed > 0:
|
||||
kwargs["generator"] = torch.Generator(device=self.device).manual_seed(request.seed)
|
||||
|
||||
# Handle start and end images for video generation
|
||||
if request.start_image:
|
||||
kwargs["start_image"] = load_image(request.start_image)
|
||||
if request.end_image:
|
||||
kwargs["end_image"] = load_image(request.end_image)
|
||||
|
||||
print(f"Generating video with {kwargs=}", file=sys.stderr)
|
||||
|
||||
# Generate video frames based on pipeline type
|
||||
if self.PipelineType == "WanPipeline":
|
||||
# WAN2.2 text-to-video generation
|
||||
output = self.pipe(**kwargs)
|
||||
frames = output.frames[0] # WAN2.2 returns frames in this format
|
||||
elif self.PipelineType == "WanImageToVideoPipeline":
|
||||
# WAN2.2 image-to-video generation
|
||||
if request.start_image:
|
||||
# Load and resize the input image according to WAN2.2 requirements
|
||||
image = load_image(request.start_image)
|
||||
# Use request dimensions or defaults, but respect WAN2.2 constraints
|
||||
request_height = request.height if request.height > 0 else 480
|
||||
request_width = request.width if request.width > 0 else 832
|
||||
max_area = request_height * request_width
|
||||
aspect_ratio = image.height / image.width
|
||||
mod_value = self.pipe.vae_scale_factor_spatial * self.pipe.transformer.config.patch_size[1]
|
||||
height = round((max_area * aspect_ratio) ** 0.5 / mod_value) * mod_value
|
||||
width = round((max_area / aspect_ratio) ** 0.5 / mod_value) * mod_value
|
||||
image = image.resize((width, height))
|
||||
kwargs["image"] = image
|
||||
kwargs["height"] = height
|
||||
kwargs["width"] = width
|
||||
|
||||
output = self.pipe(**kwargs)
|
||||
frames = output.frames[0]
|
||||
elif self.img2vid:
|
||||
# Generic image-to-video generation
|
||||
if request.start_image:
|
||||
image = load_image(request.start_image)
|
||||
image = image.resize((request.width if request.width > 0 else 1024,
|
||||
request.height if request.height > 0 else 576))
|
||||
kwargs["image"] = image
|
||||
|
||||
output = self.pipe(**kwargs)
|
||||
frames = output.frames[0]
|
||||
elif self.txt2vid:
|
||||
# Generic text-to-video generation
|
||||
output = self.pipe(**kwargs)
|
||||
frames = output.frames[0]
|
||||
else:
|
||||
return backend_pb2.Result(success=False, message=f"Pipeline {self.PipelineType} does not support video generation")
|
||||
|
||||
# Export video
|
||||
export_to_video(frames, request.dst, fps=fps)
|
||||
|
||||
return backend_pb2.Result(message="Video generated successfully", success=True)
|
||||
|
||||
except Exception as err:
|
||||
print(f"Error generating video: {err}", file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
return backend_pb2.Result(success=False, message=f"Error generating video: {err}")
|
||||
|
||||
|
||||
def serve(address):
|
||||
server = grpc.server(futures.ThreadPoolExecutor(max_workers=MAX_WORKERS),
|
||||
|
||||
@@ -8,4 +8,5 @@ compel
|
||||
peft
|
||||
sentencepiece
|
||||
torch==2.7.1
|
||||
optimum-quanto
|
||||
optimum-quanto
|
||||
ftfy
|
||||
@@ -1,11 +1,12 @@
|
||||
--extra-index-url https://download.pytorch.org/whl/cu118
|
||||
torch==2.7.1+cu118
|
||||
torchvision==0.22.1+cu118
|
||||
git+https://github.com/huggingface/diffusers
|
||||
opencv-python
|
||||
transformers
|
||||
torchvision==0.22.1
|
||||
accelerate
|
||||
compel
|
||||
peft
|
||||
sentencepiece
|
||||
optimum-quanto
|
||||
torch==2.7.1
|
||||
optimum-quanto
|
||||
ftfy
|
||||
@@ -1,10 +1,12 @@
|
||||
torch==2.7.1
|
||||
torchvision==0.22.1
|
||||
--extra-index-url https://download.pytorch.org/whl/cu121
|
||||
git+https://github.com/huggingface/diffusers
|
||||
opencv-python
|
||||
transformers
|
||||
torchvision
|
||||
accelerate
|
||||
compel
|
||||
peft
|
||||
sentencepiece
|
||||
optimum-quanto
|
||||
torch
|
||||
ftfy
|
||||
optimum-quanto
|
||||
|
||||
@@ -8,4 +8,5 @@ accelerate
|
||||
compel
|
||||
peft
|
||||
sentencepiece
|
||||
optimum-quanto
|
||||
optimum-quanto
|
||||
ftfy
|
||||
@@ -12,4 +12,5 @@ accelerate
|
||||
compel
|
||||
peft
|
||||
sentencepiece
|
||||
optimum-quanto
|
||||
optimum-quanto
|
||||
ftfy
|
||||
@@ -8,4 +8,5 @@ peft
|
||||
optimum-quanto
|
||||
numpy<2
|
||||
sentencepiece
|
||||
torchvision
|
||||
torchvision
|
||||
ftfy
|
||||
@@ -7,4 +7,5 @@ accelerate
|
||||
compel
|
||||
peft
|
||||
sentencepiece
|
||||
optimum-quanto
|
||||
optimum-quanto
|
||||
ftfy
|
||||
23
backend/python/mlx-audio/Makefile
Normal file
23
backend/python/mlx-audio/Makefile
Normal file
@@ -0,0 +1,23 @@
|
||||
.PHONY: mlx-audio
|
||||
mlx-audio:
|
||||
bash install.sh
|
||||
|
||||
.PHONY: run
|
||||
run: mlx-audio
|
||||
@echo "Running mlx-audio..."
|
||||
bash run.sh
|
||||
@echo "mlx run."
|
||||
|
||||
.PHONY: test
|
||||
test: mlx-audio
|
||||
@echo "Testing mlx-audio..."
|
||||
bash test.sh
|
||||
@echo "mlx tested."
|
||||
|
||||
.PHONY: protogen-clean
|
||||
protogen-clean:
|
||||
$(RM) backend_pb2_grpc.py backend_pb2.py
|
||||
|
||||
.PHONY: clean
|
||||
clean: protogen-clean
|
||||
rm -rf venv __pycache__
|
||||
459
backend/python/mlx-audio/backend.py
Normal file
459
backend/python/mlx-audio/backend.py
Normal file
@@ -0,0 +1,459 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
from concurrent import futures
|
||||
import argparse
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import glob
|
||||
from typing import List
|
||||
import time
|
||||
import tempfile
|
||||
|
||||
import backend_pb2
|
||||
import backend_pb2_grpc
|
||||
|
||||
import grpc
|
||||
from mlx_audio.tts.utils import load_model
|
||||
import soundfile as sf
|
||||
import numpy as np
|
||||
import uuid
|
||||
|
||||
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
|
||||
|
||||
# If MAX_WORKERS are specified in the environment use it, otherwise default to 1
|
||||
MAX_WORKERS = int(os.environ.get('PYTHON_GRPC_MAX_WORKERS', '1'))
|
||||
|
||||
# Implement the BackendServicer class with the service methods
|
||||
class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
"""
|
||||
A gRPC servicer that implements the Backend service defined in backend.proto.
|
||||
This backend provides TTS (Text-to-Speech) functionality using MLX-Audio.
|
||||
"""
|
||||
|
||||
def _is_float(self, s):
|
||||
"""Check if a string can be converted to float."""
|
||||
try:
|
||||
float(s)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def Health(self, request, context):
|
||||
"""
|
||||
Returns a health check message.
|
||||
|
||||
Args:
|
||||
request: The health check request.
|
||||
context: The gRPC context.
|
||||
|
||||
Returns:
|
||||
backend_pb2.Reply: The health check reply.
|
||||
"""
|
||||
return backend_pb2.Reply(message=bytes("OK", 'utf-8'))
|
||||
|
||||
async def LoadModel(self, request, context):
|
||||
"""
|
||||
Loads a TTS model using MLX-Audio.
|
||||
|
||||
Args:
|
||||
request: The load model request.
|
||||
context: The gRPC context.
|
||||
|
||||
Returns:
|
||||
backend_pb2.Result: The load model result.
|
||||
"""
|
||||
try:
|
||||
print(f"Loading MLX-Audio TTS model: {request.Model}", file=sys.stderr)
|
||||
print(f"Request: {request}", file=sys.stderr)
|
||||
|
||||
# Parse options like in the kokoro backend
|
||||
options = request.Options
|
||||
self.options = {}
|
||||
|
||||
# The options are a list of strings in this form optname:optvalue
|
||||
# We store all the options in a dict for later use
|
||||
for opt in options:
|
||||
if ":" not in opt:
|
||||
continue
|
||||
key, value = opt.split(":", 1) # Split only on first colon to handle values with colons
|
||||
|
||||
# Convert numeric values to appropriate types
|
||||
if self._is_float(value):
|
||||
if float(value).is_integer():
|
||||
value = int(value)
|
||||
else:
|
||||
value = float(value)
|
||||
elif value.lower() in ["true", "false"]:
|
||||
value = value.lower() == "true"
|
||||
|
||||
self.options[key] = value
|
||||
|
||||
print(f"Options: {self.options}", file=sys.stderr)
|
||||
|
||||
# Load the model using MLX-Audio's load_model function
|
||||
try:
|
||||
self.tts_model = load_model(request.Model)
|
||||
self.model_path = request.Model
|
||||
print(f"TTS model loaded successfully from {request.Model}", file=sys.stderr)
|
||||
except Exception as model_err:
|
||||
print(f"Error loading TTS model: {model_err}", file=sys.stderr)
|
||||
return backend_pb2.Result(success=False, message=f"Failed to load model: {model_err}")
|
||||
|
||||
except Exception as err:
|
||||
print(f"Error loading MLX-Audio TTS model {err=}, {type(err)=}", file=sys.stderr)
|
||||
return backend_pb2.Result(success=False, message=f"Error loading MLX-Audio TTS model: {err}")
|
||||
|
||||
print("MLX-Audio TTS model loaded successfully", file=sys.stderr)
|
||||
return backend_pb2.Result(message="MLX-Audio TTS model loaded successfully", success=True)
|
||||
|
||||
def TTS(self, request, context):
|
||||
"""
|
||||
Generates TTS audio from text using MLX-Audio.
|
||||
|
||||
Args:
|
||||
request: A TTSRequest object containing text, model, destination, voice, and language.
|
||||
context: A grpc.ServicerContext object that provides information about the RPC.
|
||||
|
||||
Returns:
|
||||
A Result object indicating success or failure.
|
||||
"""
|
||||
try:
|
||||
# Check if model is loaded
|
||||
if not hasattr(self, 'tts_model') or self.tts_model is None:
|
||||
return backend_pb2.Result(success=False, message="TTS model not loaded. Please call LoadModel first.")
|
||||
|
||||
print(f"Generating TTS with MLX-Audio - text: {request.text[:50]}..., voice: {request.voice}, language: {request.language}", file=sys.stderr)
|
||||
|
||||
# Handle speed parameter based on model type
|
||||
speed_value = self._handle_speed_parameter(request, self.model_path)
|
||||
|
||||
# Map language names to codes if needed
|
||||
lang_code = self._map_language_code(request.language, request.voice)
|
||||
|
||||
# Prepare generation parameters
|
||||
gen_params = {
|
||||
"text": request.text,
|
||||
"speed": speed_value,
|
||||
"verbose": False,
|
||||
}
|
||||
|
||||
# Add model-specific parameters
|
||||
if request.voice and request.voice.strip():
|
||||
gen_params["voice"] = request.voice
|
||||
|
||||
# Check if model supports language codes (primarily Kokoro)
|
||||
if "kokoro" in self.model_path.lower():
|
||||
gen_params["lang_code"] = lang_code
|
||||
|
||||
# Add pitch and gender for Spark models
|
||||
if "spark" in self.model_path.lower():
|
||||
gen_params["pitch"] = 1.0 # Default to moderate
|
||||
gen_params["gender"] = "female" # Default to female
|
||||
|
||||
print(f"Generation parameters: {gen_params}", file=sys.stderr)
|
||||
|
||||
# Generate audio using the loaded model
|
||||
try:
|
||||
results = self.tts_model.generate(**gen_params)
|
||||
except Exception as gen_err:
|
||||
print(f"Error during TTS generation: {gen_err}", file=sys.stderr)
|
||||
return backend_pb2.Result(success=False, message=f"TTS generation failed: {gen_err}")
|
||||
|
||||
# Process the generated audio segments
|
||||
audio_arrays = []
|
||||
for segment in results:
|
||||
audio_arrays.append(segment.audio)
|
||||
|
||||
# If no segments, return error
|
||||
if not audio_arrays:
|
||||
print("No audio segments generated", file=sys.stderr)
|
||||
return backend_pb2.Result(success=False, message="No audio generated")
|
||||
|
||||
# Concatenate all segments
|
||||
cat_audio = np.concatenate(audio_arrays, axis=0)
|
||||
|
||||
# Generate output filename and path
|
||||
if request.dst:
|
||||
output_path = request.dst
|
||||
else:
|
||||
unique_id = str(uuid.uuid4())
|
||||
filename = f"tts_{unique_id}.wav"
|
||||
output_path = filename
|
||||
|
||||
# Write the audio as a WAV
|
||||
try:
|
||||
sf.write(output_path, cat_audio, 24000)
|
||||
print(f"Successfully wrote audio file to {output_path}", file=sys.stderr)
|
||||
|
||||
# Verify the file exists and has content
|
||||
if not os.path.exists(output_path):
|
||||
print(f"File was not created at {output_path}", file=sys.stderr)
|
||||
return backend_pb2.Result(success=False, message="Failed to create audio file")
|
||||
|
||||
file_size = os.path.getsize(output_path)
|
||||
if file_size == 0:
|
||||
print("File was created but is empty", file=sys.stderr)
|
||||
return backend_pb2.Result(success=False, message="Generated audio file is empty")
|
||||
|
||||
print(f"Audio file size: {file_size} bytes", file=sys.stderr)
|
||||
|
||||
except Exception as write_err:
|
||||
print(f"Error writing audio file: {write_err}", file=sys.stderr)
|
||||
return backend_pb2.Result(success=False, message=f"Failed to save audio: {write_err}")
|
||||
|
||||
return backend_pb2.Result(success=True, message=f"TTS audio generated successfully: {output_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in MLX-Audio TTS: {e}", file=sys.stderr)
|
||||
return backend_pb2.Result(success=False, message=f"TTS generation failed: {str(e)}")
|
||||
|
||||
async def Predict(self, request, context):
|
||||
"""
|
||||
Generates TTS audio based on the given prompt using MLX-Audio TTS.
|
||||
This is a fallback method for compatibility with the Predict endpoint.
|
||||
|
||||
Args:
|
||||
request: The predict request.
|
||||
context: The gRPC context.
|
||||
|
||||
Returns:
|
||||
backend_pb2.Reply: The predict result.
|
||||
"""
|
||||
try:
|
||||
# Check if model is loaded
|
||||
if not hasattr(self, 'tts_model') or self.tts_model is None:
|
||||
context.set_code(grpc.StatusCode.FAILED_PRECONDITION)
|
||||
context.set_details("TTS model not loaded. Please call LoadModel first.")
|
||||
return backend_pb2.Reply(message=bytes("", encoding='utf-8'))
|
||||
|
||||
# For TTS, we expect the prompt to contain the text to synthesize
|
||||
if not request.Prompt:
|
||||
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
|
||||
context.set_details("Prompt is required for TTS generation")
|
||||
return backend_pb2.Reply(message=bytes("", encoding='utf-8'))
|
||||
|
||||
# Handle speed parameter based on model type
|
||||
speed_value = self._handle_speed_parameter(request, self.model_path)
|
||||
|
||||
# Map language names to codes if needed
|
||||
lang_code = self._map_language_code(None, None) # Use defaults for Predict
|
||||
|
||||
# Prepare generation parameters
|
||||
gen_params = {
|
||||
"text": request.Prompt,
|
||||
"speed": speed_value,
|
||||
"verbose": False,
|
||||
}
|
||||
|
||||
# Add model-specific parameters
|
||||
if hasattr(self, 'options') and 'voice' in self.options:
|
||||
gen_params["voice"] = self.options['voice']
|
||||
|
||||
# Check if model supports language codes (primarily Kokoro)
|
||||
if "kokoro" in self.model_path.lower():
|
||||
gen_params["lang_code"] = lang_code
|
||||
|
||||
print(f"Generating TTS with MLX-Audio - text: {request.Prompt[:50]}..., params: {gen_params}", file=sys.stderr)
|
||||
|
||||
# Generate audio using the loaded model
|
||||
try:
|
||||
results = self.tts_model.generate(**gen_params)
|
||||
except Exception as gen_err:
|
||||
print(f"Error during TTS generation: {gen_err}", file=sys.stderr)
|
||||
context.set_code(grpc.StatusCode.INTERNAL)
|
||||
context.set_details(f"TTS generation failed: {gen_err}")
|
||||
return backend_pb2.Reply(message=bytes("", encoding='utf-8'))
|
||||
|
||||
# Process the generated audio segments
|
||||
audio_arrays = []
|
||||
for segment in results:
|
||||
audio_arrays.append(segment.audio)
|
||||
|
||||
# If no segments, return error
|
||||
if not audio_arrays:
|
||||
print("No audio segments generated", file=sys.stderr)
|
||||
return backend_pb2.Reply(message=bytes("No audio generated", encoding='utf-8'))
|
||||
|
||||
# Concatenate all segments
|
||||
cat_audio = np.concatenate(audio_arrays, axis=0)
|
||||
duration = len(cat_audio) / 24000 # Assuming 24kHz sample rate
|
||||
|
||||
# Return success message with audio information
|
||||
response = f"TTS audio generated successfully. Duration: {duration:.2f}s, Sample rate: 24000Hz"
|
||||
return backend_pb2.Reply(message=bytes(response, encoding='utf-8'))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in MLX-Audio TTS Predict: {e}", file=sys.stderr)
|
||||
context.set_code(grpc.StatusCode.INTERNAL)
|
||||
context.set_details(f"TTS generation failed: {str(e)}")
|
||||
return backend_pb2.Reply(message=bytes("", encoding='utf-8'))
|
||||
|
||||
def _handle_speed_parameter(self, request, model_path):
|
||||
"""
|
||||
Handle speed parameter based on model type.
|
||||
|
||||
Args:
|
||||
request: The TTSRequest object.
|
||||
model_path: The model path to determine model type.
|
||||
|
||||
Returns:
|
||||
float: The processed speed value.
|
||||
"""
|
||||
# Get speed from options if available
|
||||
speed = 1.0
|
||||
if hasattr(self, 'options') and 'speed' in self.options:
|
||||
speed = self.options['speed']
|
||||
|
||||
# Handle speed parameter based on model type
|
||||
if "spark" in model_path.lower():
|
||||
# Spark actually expects float values that map to speed descriptions
|
||||
speed_map = {
|
||||
"very_low": 0.0,
|
||||
"low": 0.5,
|
||||
"moderate": 1.0,
|
||||
"high": 1.5,
|
||||
"very_high": 2.0,
|
||||
}
|
||||
if isinstance(speed, str) and speed in speed_map:
|
||||
speed_value = speed_map[speed]
|
||||
else:
|
||||
# Try to use as float, default to 1.0 (moderate) if invalid
|
||||
try:
|
||||
speed_value = float(speed)
|
||||
if speed_value not in [0.0, 0.5, 1.0, 1.5, 2.0]:
|
||||
speed_value = 1.0 # Default to moderate
|
||||
except:
|
||||
speed_value = 1.0 # Default to moderate
|
||||
else:
|
||||
# Other models use float speed values
|
||||
try:
|
||||
speed_value = float(speed)
|
||||
if speed_value < 0.5 or speed_value > 2.0:
|
||||
speed_value = 1.0 # Default to 1.0 if out of range
|
||||
except ValueError:
|
||||
speed_value = 1.0 # Default to 1.0 if invalid
|
||||
|
||||
return speed_value
|
||||
|
||||
def _map_language_code(self, language, voice):
|
||||
"""
|
||||
Map language names to codes if needed.
|
||||
|
||||
Args:
|
||||
language: The language parameter from the request.
|
||||
voice: The voice parameter from the request.
|
||||
|
||||
Returns:
|
||||
str: The language code.
|
||||
"""
|
||||
if not language:
|
||||
# Default to voice[0] if not found
|
||||
return voice[0] if voice else "a"
|
||||
|
||||
# Map language names to codes if needed
|
||||
language_map = {
|
||||
"american_english": "a",
|
||||
"british_english": "b",
|
||||
"spanish": "e",
|
||||
"french": "f",
|
||||
"hindi": "h",
|
||||
"italian": "i",
|
||||
"portuguese": "p",
|
||||
"japanese": "j",
|
||||
"mandarin_chinese": "z",
|
||||
# Also accept direct language codes
|
||||
"a": "a", "b": "b", "e": "e", "f": "f", "h": "h", "i": "i", "p": "p", "j": "j", "z": "z",
|
||||
}
|
||||
|
||||
return language_map.get(language.lower(), language)
|
||||
|
||||
def _build_generation_params(self, request, default_speed=1.0):
|
||||
"""
|
||||
Build generation parameters from request attributes and options for MLX-Audio TTS.
|
||||
|
||||
Args:
|
||||
request: The gRPC request.
|
||||
default_speed: Default speed if not specified.
|
||||
|
||||
Returns:
|
||||
dict: Generation parameters for MLX-Audio
|
||||
"""
|
||||
# Initialize generation parameters for MLX-Audio TTS
|
||||
generation_params = {
|
||||
'speed': default_speed,
|
||||
'voice': 'af_heart', # Default voice
|
||||
'lang_code': 'a', # Default language code
|
||||
}
|
||||
|
||||
# Extract parameters from request attributes
|
||||
if hasattr(request, 'Temperature') and request.Temperature > 0:
|
||||
# Temperature could be mapped to speed variation
|
||||
generation_params['speed'] = 1.0 + (request.Temperature - 0.5) * 0.5
|
||||
|
||||
# Override with options if available
|
||||
if hasattr(self, 'options'):
|
||||
# Speed from options
|
||||
if 'speed' in self.options:
|
||||
generation_params['speed'] = self.options['speed']
|
||||
|
||||
# Voice from options
|
||||
if 'voice' in self.options:
|
||||
generation_params['voice'] = self.options['voice']
|
||||
|
||||
# Language code from options
|
||||
if 'lang_code' in self.options:
|
||||
generation_params['lang_code'] = self.options['lang_code']
|
||||
|
||||
# Model-specific parameters
|
||||
param_option_mapping = {
|
||||
'temp': 'speed',
|
||||
'temperature': 'speed',
|
||||
'top_p': 'speed', # Map top_p to speed variation
|
||||
}
|
||||
|
||||
for option_key, param_key in param_option_mapping.items():
|
||||
if option_key in self.options:
|
||||
if param_key == 'speed':
|
||||
# Ensure speed is within reasonable bounds
|
||||
speed_val = float(self.options[option_key])
|
||||
if 0.5 <= speed_val <= 2.0:
|
||||
generation_params[param_key] = speed_val
|
||||
|
||||
return generation_params
|
||||
|
||||
async def serve(address):
|
||||
# Start asyncio gRPC server
|
||||
server = grpc.aio.server(migration_thread_pool=futures.ThreadPoolExecutor(max_workers=MAX_WORKERS),
|
||||
options=[
|
||||
('grpc.max_message_length', 50 * 1024 * 1024), # 50MB
|
||||
('grpc.max_send_message_length', 50 * 1024 * 1024), # 50MB
|
||||
('grpc.max_receive_message_length', 50 * 1024 * 1024), # 50MB
|
||||
])
|
||||
# Add the servicer to the server
|
||||
backend_pb2_grpc.add_BackendServicer_to_server(BackendServicer(), server)
|
||||
# Bind the server to the address
|
||||
server.add_insecure_port(address)
|
||||
|
||||
# Gracefully shutdown the server on SIGTERM or SIGINT
|
||||
loop = asyncio.get_event_loop()
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
loop.add_signal_handler(
|
||||
sig, lambda: asyncio.ensure_future(server.stop(5))
|
||||
)
|
||||
|
||||
# Start the server
|
||||
await server.start()
|
||||
print("MLX-Audio TTS Server started. Listening on: " + address, file=sys.stderr)
|
||||
# Wait for the server to be terminated
|
||||
await server.wait_for_termination()
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Run the MLX-Audio TTS gRPC server.")
|
||||
parser.add_argument(
|
||||
"--addr", default="localhost:50051", help="The address to bind the server to."
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(serve(args.addr))
|
||||
14
backend/python/mlx-audio/install.sh
Executable file
14
backend/python/mlx-audio/install.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
USE_PIP=true
|
||||
|
||||
backend_dir=$(dirname $0)
|
||||
|
||||
if [ -d $backend_dir/common ]; then
|
||||
source $backend_dir/common/libbackend.sh
|
||||
else
|
||||
source $backend_dir/../common/libbackend.sh
|
||||
fi
|
||||
|
||||
installRequirements
|
||||
1
backend/python/mlx-audio/requirements-mps.txt
Normal file
1
backend/python/mlx-audio/requirements-mps.txt
Normal file
@@ -0,0 +1 @@
|
||||
git+https://github.com/Blaizzy/mlx-audio
|
||||
7
backend/python/mlx-audio/requirements.txt
Normal file
7
backend/python/mlx-audio/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
grpcio==1.71.0
|
||||
protobuf
|
||||
certifi
|
||||
setuptools
|
||||
mlx-audio
|
||||
soundfile
|
||||
numpy
|
||||
11
backend/python/mlx-audio/run.sh
Executable file
11
backend/python/mlx-audio/run.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
backend_dir=$(dirname $0)
|
||||
|
||||
if [ -d $backend_dir/common ]; then
|
||||
source $backend_dir/common/libbackend.sh
|
||||
else
|
||||
source $backend_dir/../common/libbackend.sh
|
||||
fi
|
||||
|
||||
startBackend $@
|
||||
142
backend/python/mlx-audio/test.py
Normal file
142
backend/python/mlx-audio/test.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import unittest
|
||||
import subprocess
|
||||
import time
|
||||
import backend_pb2
|
||||
import backend_pb2_grpc
|
||||
|
||||
import grpc
|
||||
|
||||
import unittest
|
||||
import subprocess
|
||||
import time
|
||||
import grpc
|
||||
import backend_pb2_grpc
|
||||
import backend_pb2
|
||||
|
||||
class TestBackendServicer(unittest.TestCase):
|
||||
"""
|
||||
TestBackendServicer is the class that tests the gRPC service.
|
||||
|
||||
This class contains methods to test the startup and shutdown of the gRPC service.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.service = subprocess.Popen(["python", "backend.py", "--addr", "localhost:50051"])
|
||||
time.sleep(10)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.service.terminate()
|
||||
self.service.wait()
|
||||
|
||||
def test_server_startup(self):
|
||||
try:
|
||||
self.setUp()
|
||||
with grpc.insecure_channel("localhost:50051") as channel:
|
||||
stub = backend_pb2_grpc.BackendStub(channel)
|
||||
response = stub.Health(backend_pb2.HealthMessage())
|
||||
self.assertEqual(response.message, b'OK')
|
||||
except Exception as err:
|
||||
print(err)
|
||||
self.fail("Server failed to start")
|
||||
finally:
|
||||
self.tearDown()
|
||||
def test_load_model(self):
|
||||
"""
|
||||
This method tests if the TTS model is loaded successfully
|
||||
"""
|
||||
try:
|
||||
self.setUp()
|
||||
with grpc.insecure_channel("localhost:50051") as channel:
|
||||
stub = backend_pb2_grpc.BackendStub(channel)
|
||||
response = stub.LoadModel(backend_pb2.ModelOptions(Model="mlx-community/Kokoro-82M-4bit"))
|
||||
self.assertTrue(response.success)
|
||||
self.assertEqual(response.message, "MLX-Audio TTS model loaded successfully")
|
||||
except Exception as err:
|
||||
print(err)
|
||||
self.fail("LoadModel service failed")
|
||||
finally:
|
||||
self.tearDown()
|
||||
|
||||
def test_tts_generation(self):
|
||||
"""
|
||||
This method tests if TTS audio is generated successfully
|
||||
"""
|
||||
try:
|
||||
self.setUp()
|
||||
with grpc.insecure_channel("localhost:50051") as channel:
|
||||
stub = backend_pb2_grpc.BackendStub(channel)
|
||||
response = stub.LoadModel(backend_pb2.ModelOptions(Model="mlx-community/Kokoro-82M-4bit"))
|
||||
self.assertTrue(response.success)
|
||||
|
||||
# Test TTS generation
|
||||
tts_req = backend_pb2.TTSRequest(
|
||||
text="Hello, this is a test of the MLX-Audio TTS system.",
|
||||
model="mlx-community/Kokoro-82M-4bit",
|
||||
voice="af_heart",
|
||||
language="a"
|
||||
)
|
||||
tts_resp = stub.TTS(tts_req)
|
||||
self.assertTrue(tts_resp.success)
|
||||
self.assertIn("TTS audio generated successfully", tts_resp.message)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
self.fail("TTS service failed")
|
||||
finally:
|
||||
self.tearDown()
|
||||
|
||||
def test_tts_with_options(self):
|
||||
"""
|
||||
This method tests if TTS works with various options and parameters
|
||||
"""
|
||||
try:
|
||||
self.setUp()
|
||||
with grpc.insecure_channel("localhost:50051") as channel:
|
||||
stub = backend_pb2_grpc.BackendStub(channel)
|
||||
response = stub.LoadModel(backend_pb2.ModelOptions(
|
||||
Model="mlx-community/Kokoro-82M-4bit",
|
||||
Options=["voice:af_soft", "speed:1.2", "lang_code:b"]
|
||||
))
|
||||
self.assertTrue(response.success)
|
||||
|
||||
# Test TTS generation with different voice and language
|
||||
tts_req = backend_pb2.TTSRequest(
|
||||
text="Hello, this is a test with British English accent.",
|
||||
model="mlx-community/Kokoro-82M-4bit",
|
||||
voice="af_soft",
|
||||
language="b"
|
||||
)
|
||||
tts_resp = stub.TTS(tts_req)
|
||||
self.assertTrue(tts_resp.success)
|
||||
self.assertIn("TTS audio generated successfully", tts_resp.message)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
self.fail("TTS with options service failed")
|
||||
finally:
|
||||
self.tearDown()
|
||||
|
||||
|
||||
def test_tts_multilingual(self):
|
||||
"""
|
||||
This method tests if TTS works with different languages
|
||||
"""
|
||||
try:
|
||||
self.setUp()
|
||||
with grpc.insecure_channel("localhost:50051") as channel:
|
||||
stub = backend_pb2_grpc.BackendStub(channel)
|
||||
response = stub.LoadModel(backend_pb2.ModelOptions(Model="mlx-community/Kokoro-82M-4bit"))
|
||||
self.assertTrue(response.success)
|
||||
|
||||
# Test Spanish TTS
|
||||
tts_req = backend_pb2.TTSRequest(
|
||||
text="Hola, esto es una prueba del sistema TTS MLX-Audio.",
|
||||
model="mlx-community/Kokoro-82M-4bit",
|
||||
voice="af_heart",
|
||||
language="e"
|
||||
)
|
||||
tts_resp = stub.TTS(tts_req)
|
||||
self.assertTrue(tts_resp.success)
|
||||
self.assertIn("TTS audio generated successfully", tts_resp.message)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
self.fail("Multilingual TTS service failed")
|
||||
finally:
|
||||
self.tearDown()
|
||||
12
backend/python/mlx-audio/test.sh
Executable file
12
backend/python/mlx-audio/test.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
backend_dir=$(dirname $0)
|
||||
|
||||
if [ -d $backend_dir/common ]; then
|
||||
source $backend_dir/common/libbackend.sh
|
||||
else
|
||||
source $backend_dir/../common/libbackend.sh
|
||||
fi
|
||||
|
||||
runUnittests
|
||||
@@ -40,14 +40,6 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def _is_int(self, s):
|
||||
"""Check if a string can be converted to int."""
|
||||
try:
|
||||
int(s)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def Health(self, request, context):
|
||||
"""
|
||||
Returns a health check message.
|
||||
@@ -89,9 +81,10 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
|
||||
# Convert numeric values to appropriate types
|
||||
if self._is_float(value):
|
||||
value = float(value)
|
||||
elif self._is_int(value):
|
||||
value = int(value)
|
||||
if float(value).is_integer():
|
||||
value = int(value)
|
||||
else:
|
||||
value = float(value)
|
||||
elif value.lower() in ["true", "false"]:
|
||||
value = value.lower() == "true"
|
||||
|
||||
|
||||
@@ -38,14 +38,6 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def _is_int(self, s):
|
||||
"""Check if a string can be converted to int."""
|
||||
try:
|
||||
int(s)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def Health(self, request, context):
|
||||
"""
|
||||
Returns a health check message.
|
||||
@@ -87,9 +79,10 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||
|
||||
# Convert numeric values to appropriate types
|
||||
if self._is_float(value):
|
||||
value = float(value)
|
||||
elif self._is_int(value):
|
||||
value = int(value)
|
||||
if float(value).is_integer():
|
||||
value = int(value)
|
||||
else:
|
||||
value = float(value)
|
||||
elif value.lower() in ["true", "false"]:
|
||||
value = value.lower() == "true"
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
--extra-index-url https://download.pytorch.org/whl/cpu
|
||||
torch==2.7.1
|
||||
llvmlite==0.43.0
|
||||
numba==0.60.0
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
torch==2.7.1
|
||||
accelerate
|
||||
llvmlite==0.43.0
|
||||
numba==0.60.0
|
||||
transformers
|
||||
bitsandbytes
|
||||
outetts
|
||||
sentence-transformers==5.1.0
|
||||
protobuf==6.32.0
|
||||
16
cmd/launcher/icon.go
Normal file
16
cmd/launcher/icon.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
//go:embed logo.png
|
||||
var logoData []byte
|
||||
|
||||
// resourceIconPng is the LocalAI logo icon
|
||||
var resourceIconPng = &fyne.StaticResource{
|
||||
StaticName: "logo.png",
|
||||
StaticContent: logoData,
|
||||
}
|
||||
858
cmd/launcher/internal/launcher.go
Normal file
858
cmd/launcher/internal/launcher.go
Normal file
@@ -0,0 +1,858 @@
|
||||
package launcher
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// Config represents the launcher configuration
|
||||
type Config struct {
|
||||
ModelsPath string `json:"models_path"`
|
||||
BackendsPath string `json:"backends_path"`
|
||||
Address string `json:"address"`
|
||||
AutoStart bool `json:"auto_start"`
|
||||
StartOnBoot bool `json:"start_on_boot"`
|
||||
LogLevel string `json:"log_level"`
|
||||
EnvironmentVars map[string]string `json:"environment_vars"`
|
||||
}
|
||||
|
||||
// Launcher represents the main launcher application
|
||||
type Launcher struct {
|
||||
// Core components
|
||||
releaseManager *ReleaseManager
|
||||
config *Config
|
||||
ui *LauncherUI
|
||||
systray *SystrayManager
|
||||
ctx context.Context
|
||||
window fyne.Window
|
||||
app fyne.App
|
||||
|
||||
// Process management
|
||||
localaiCmd *exec.Cmd
|
||||
isRunning bool
|
||||
logBuffer *strings.Builder
|
||||
logMutex sync.RWMutex
|
||||
statusChannel chan string
|
||||
|
||||
// Logging
|
||||
logFile *os.File
|
||||
logPath string
|
||||
|
||||
// UI state
|
||||
lastUpdateCheck time.Time
|
||||
}
|
||||
|
||||
// NewLauncher creates a new launcher instance
|
||||
func NewLauncher(ui *LauncherUI, window fyne.Window, app fyne.App) *Launcher {
|
||||
return &Launcher{
|
||||
releaseManager: NewReleaseManager(),
|
||||
config: &Config{},
|
||||
logBuffer: &strings.Builder{},
|
||||
statusChannel: make(chan string, 100),
|
||||
ctx: context.Background(),
|
||||
ui: ui,
|
||||
window: window,
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
// setupLogging sets up log file for LocalAI process output
|
||||
func (l *Launcher) setupLogging() error {
|
||||
// Create logs directory in data folder
|
||||
dataPath := l.GetDataPath()
|
||||
logsDir := filepath.Join(dataPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create log file with timestamp
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
l.logPath = filepath.Join(logsDir, fmt.Sprintf("localai_%s.log", timestamp))
|
||||
|
||||
logFile, err := os.Create(l.logPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
l.logFile = logFile
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize sets up the launcher
|
||||
func (l *Launcher) Initialize() error {
|
||||
if l.app == nil {
|
||||
return fmt.Errorf("app is nil")
|
||||
}
|
||||
log.Printf("Initializing launcher...")
|
||||
|
||||
// Setup logging
|
||||
if err := l.setupLogging(); err != nil {
|
||||
return fmt.Errorf("failed to setup logging: %w", err)
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
log.Printf("Loading configuration...")
|
||||
if err := l.loadConfig(); err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
log.Printf("Configuration loaded, current state: ModelsPath=%s, BackendsPath=%s, Address=%s, LogLevel=%s",
|
||||
l.config.ModelsPath, l.config.BackendsPath, l.config.Address, l.config.LogLevel)
|
||||
|
||||
// Clean up any partial downloads
|
||||
log.Printf("Cleaning up partial downloads...")
|
||||
if err := l.releaseManager.CleanupPartialDownloads(); err != nil {
|
||||
log.Printf("Warning: failed to cleanup partial downloads: %v", err)
|
||||
}
|
||||
|
||||
if l.config.StartOnBoot {
|
||||
l.StartLocalAI()
|
||||
}
|
||||
// Set default paths if not configured (only if not already loaded from config)
|
||||
if l.config.ModelsPath == "" {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
l.config.ModelsPath = filepath.Join(homeDir, ".localai", "models")
|
||||
log.Printf("Setting default ModelsPath: %s", l.config.ModelsPath)
|
||||
}
|
||||
if l.config.BackendsPath == "" {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
l.config.BackendsPath = filepath.Join(homeDir, ".localai", "backends")
|
||||
log.Printf("Setting default BackendsPath: %s", l.config.BackendsPath)
|
||||
}
|
||||
if l.config.Address == "" {
|
||||
l.config.Address = "127.0.0.1:8080"
|
||||
log.Printf("Setting default Address: %s", l.config.Address)
|
||||
}
|
||||
if l.config.LogLevel == "" {
|
||||
l.config.LogLevel = "info"
|
||||
log.Printf("Setting default LogLevel: %s", l.config.LogLevel)
|
||||
}
|
||||
if l.config.EnvironmentVars == nil {
|
||||
l.config.EnvironmentVars = make(map[string]string)
|
||||
log.Printf("Initializing empty EnvironmentVars map")
|
||||
}
|
||||
|
||||
// Create directories
|
||||
os.MkdirAll(l.config.ModelsPath, 0755)
|
||||
os.MkdirAll(l.config.BackendsPath, 0755)
|
||||
|
||||
// Save the configuration with default values
|
||||
if err := l.saveConfig(); err != nil {
|
||||
log.Printf("Warning: failed to save default configuration: %v", err)
|
||||
}
|
||||
|
||||
// System tray is now handled in main.go using Fyne's built-in approach
|
||||
|
||||
// Check if LocalAI is installed
|
||||
if !l.releaseManager.IsLocalAIInstalled() {
|
||||
log.Printf("No LocalAI installation found")
|
||||
fyne.Do(func() {
|
||||
l.updateStatus("No LocalAI installation found")
|
||||
if l.ui != nil {
|
||||
// Show dialog offering to download LocalAI
|
||||
l.showDownloadLocalAIDialog()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Check for updates periodically
|
||||
go l.periodicUpdateCheck()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartLocalAI starts the LocalAI server
|
||||
func (l *Launcher) StartLocalAI() error {
|
||||
if l.isRunning {
|
||||
return fmt.Errorf("LocalAI is already running")
|
||||
}
|
||||
|
||||
// Verify binary integrity before starting
|
||||
if err := l.releaseManager.VerifyInstalledBinary(); err != nil {
|
||||
// Binary is corrupted, remove it and offer to reinstall
|
||||
binaryPath := l.releaseManager.GetBinaryPath()
|
||||
if removeErr := os.Remove(binaryPath); removeErr != nil {
|
||||
log.Printf("Failed to remove corrupted binary: %v", removeErr)
|
||||
}
|
||||
return fmt.Errorf("LocalAI binary is corrupted: %v. Please reinstall LocalAI", err)
|
||||
}
|
||||
|
||||
binaryPath := l.releaseManager.GetBinaryPath()
|
||||
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("LocalAI binary not found. Please download a release first")
|
||||
}
|
||||
|
||||
// Build command arguments
|
||||
args := []string{
|
||||
"run",
|
||||
"--models-path", l.config.ModelsPath,
|
||||
"--backends-path", l.config.BackendsPath,
|
||||
"--address", l.config.Address,
|
||||
"--log-level", l.config.LogLevel,
|
||||
}
|
||||
|
||||
l.localaiCmd = exec.CommandContext(l.ctx, binaryPath, args...)
|
||||
|
||||
// Apply environment variables
|
||||
if len(l.config.EnvironmentVars) > 0 {
|
||||
env := os.Environ()
|
||||
for key, value := range l.config.EnvironmentVars {
|
||||
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
l.localaiCmd.Env = env
|
||||
}
|
||||
|
||||
// Setup logging
|
||||
stdout, err := l.localaiCmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdout pipe: %w", err)
|
||||
}
|
||||
|
||||
stderr, err := l.localaiCmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stderr pipe: %w", err)
|
||||
}
|
||||
|
||||
// Start the process
|
||||
if err := l.localaiCmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start LocalAI: %w", err)
|
||||
}
|
||||
|
||||
l.isRunning = true
|
||||
|
||||
fyne.Do(func() {
|
||||
l.updateStatus("LocalAI is starting...")
|
||||
l.updateRunningState(true)
|
||||
})
|
||||
|
||||
// Start log monitoring
|
||||
go l.monitorLogs(stdout, "STDOUT")
|
||||
go l.monitorLogs(stderr, "STDERR")
|
||||
|
||||
// Monitor process with startup timeout
|
||||
go func() {
|
||||
// Wait for process to start or fail
|
||||
err := l.localaiCmd.Wait()
|
||||
l.isRunning = false
|
||||
fyne.Do(func() {
|
||||
l.updateRunningState(false)
|
||||
if err != nil {
|
||||
l.updateStatus(fmt.Sprintf("LocalAI stopped with error: %v", err))
|
||||
} else {
|
||||
l.updateStatus("LocalAI stopped")
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
// Add startup timeout detection
|
||||
go func() {
|
||||
time.Sleep(10 * time.Second) // Wait 10 seconds for startup
|
||||
if l.isRunning {
|
||||
// Check if process is still alive
|
||||
if l.localaiCmd.Process != nil {
|
||||
if err := l.localaiCmd.Process.Signal(syscall.Signal(0)); err != nil {
|
||||
// Process is dead, mark as not running
|
||||
l.isRunning = false
|
||||
fyne.Do(func() {
|
||||
l.updateRunningState(false)
|
||||
l.updateStatus("LocalAI failed to start properly")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopLocalAI stops the LocalAI server
|
||||
func (l *Launcher) StopLocalAI() error {
|
||||
if !l.isRunning || l.localaiCmd == nil {
|
||||
return fmt.Errorf("LocalAI is not running")
|
||||
}
|
||||
|
||||
// Gracefully terminate the process
|
||||
if err := l.localaiCmd.Process.Signal(os.Interrupt); err != nil {
|
||||
// If graceful termination fails, force kill
|
||||
if killErr := l.localaiCmd.Process.Kill(); killErr != nil {
|
||||
return fmt.Errorf("failed to kill LocalAI process: %w", killErr)
|
||||
}
|
||||
}
|
||||
|
||||
l.isRunning = false
|
||||
fyne.Do(func() {
|
||||
l.updateRunningState(false)
|
||||
l.updateStatus("LocalAI stopped")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns whether LocalAI is currently running
|
||||
func (l *Launcher) IsRunning() bool {
|
||||
return l.isRunning
|
||||
}
|
||||
|
||||
// Shutdown performs cleanup when the application is closing
|
||||
func (l *Launcher) Shutdown() error {
|
||||
log.Printf("Launcher shutting down, stopping LocalAI...")
|
||||
|
||||
// Stop LocalAI if it's running
|
||||
if l.isRunning {
|
||||
if err := l.StopLocalAI(); err != nil {
|
||||
log.Printf("Error stopping LocalAI during shutdown: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Close log file if open
|
||||
if l.logFile != nil {
|
||||
if err := l.logFile.Close(); err != nil {
|
||||
log.Printf("Error closing log file: %v", err)
|
||||
}
|
||||
l.logFile = nil
|
||||
}
|
||||
|
||||
log.Printf("Launcher shutdown complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLogs returns the current log buffer
|
||||
func (l *Launcher) GetLogs() string {
|
||||
l.logMutex.RLock()
|
||||
defer l.logMutex.RUnlock()
|
||||
return l.logBuffer.String()
|
||||
}
|
||||
|
||||
// GetRecentLogs returns the most recent logs (last 50 lines) for better error display
|
||||
func (l *Launcher) GetRecentLogs() string {
|
||||
l.logMutex.RLock()
|
||||
defer l.logMutex.RUnlock()
|
||||
|
||||
content := l.logBuffer.String()
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
// Get last 50 lines
|
||||
if len(lines) > 50 {
|
||||
lines = lines[len(lines)-50:]
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// GetConfig returns the current configuration
|
||||
func (l *Launcher) GetConfig() *Config {
|
||||
return l.config
|
||||
}
|
||||
|
||||
// SetConfig updates the configuration
|
||||
func (l *Launcher) SetConfig(config *Config) error {
|
||||
l.config = config
|
||||
return l.saveConfig()
|
||||
}
|
||||
|
||||
func (l *Launcher) GetUI() *LauncherUI {
|
||||
return l.ui
|
||||
}
|
||||
|
||||
func (l *Launcher) SetSystray(systray *SystrayManager) {
|
||||
l.systray = systray
|
||||
}
|
||||
|
||||
// GetReleaseManager returns the release manager
|
||||
func (l *Launcher) GetReleaseManager() *ReleaseManager {
|
||||
return l.releaseManager
|
||||
}
|
||||
|
||||
// GetWebUIURL returns the URL for the WebUI
|
||||
func (l *Launcher) GetWebUIURL() string {
|
||||
address := l.config.Address
|
||||
if strings.HasPrefix(address, ":") {
|
||||
address = "localhost" + address
|
||||
}
|
||||
if !strings.HasPrefix(address, "http") {
|
||||
address = "http://" + address
|
||||
}
|
||||
return address
|
||||
}
|
||||
|
||||
// GetDataPath returns the path where LocalAI data and logs are stored
|
||||
func (l *Launcher) GetDataPath() string {
|
||||
// LocalAI typically stores data in the current working directory or a models directory
|
||||
// First check if models path is configured
|
||||
if l.config != nil && l.config.ModelsPath != "" {
|
||||
// Return the parent directory of models path
|
||||
return filepath.Dir(l.config.ModelsPath)
|
||||
}
|
||||
|
||||
// Fallback to home directory LocalAI folder
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
return filepath.Join(homeDir, ".localai")
|
||||
}
|
||||
|
||||
// CheckForUpdates checks if there are any available updates
|
||||
func (l *Launcher) CheckForUpdates() (bool, string, error) {
|
||||
log.Printf("CheckForUpdates: checking for available updates...")
|
||||
available, version, err := l.releaseManager.IsUpdateAvailable()
|
||||
if err != nil {
|
||||
log.Printf("CheckForUpdates: error occurred: %v", err)
|
||||
return false, "", err
|
||||
}
|
||||
log.Printf("CheckForUpdates: result - available=%v, version=%s", available, version)
|
||||
l.lastUpdateCheck = time.Now()
|
||||
return available, version, nil
|
||||
}
|
||||
|
||||
// DownloadUpdate downloads the latest version
|
||||
func (l *Launcher) DownloadUpdate(version string, progressCallback func(float64)) error {
|
||||
return l.releaseManager.DownloadRelease(version, progressCallback)
|
||||
}
|
||||
|
||||
// GetCurrentVersion returns the current installed version
|
||||
func (l *Launcher) GetCurrentVersion() string {
|
||||
return l.releaseManager.GetInstalledVersion()
|
||||
}
|
||||
|
||||
// GetCurrentStatus returns the current status
|
||||
func (l *Launcher) GetCurrentStatus() string {
|
||||
select {
|
||||
case status := <-l.statusChannel:
|
||||
return status
|
||||
default:
|
||||
if l.isRunning {
|
||||
return "LocalAI is running"
|
||||
}
|
||||
return "Ready"
|
||||
}
|
||||
}
|
||||
|
||||
// GetLastStatus returns the last known status without consuming from channel
|
||||
func (l *Launcher) GetLastStatus() string {
|
||||
if l.isRunning {
|
||||
return "LocalAI is running"
|
||||
}
|
||||
|
||||
// Check if LocalAI is installed
|
||||
if !l.releaseManager.IsLocalAIInstalled() {
|
||||
return "LocalAI not installed"
|
||||
}
|
||||
|
||||
return "Ready"
|
||||
}
|
||||
|
||||
func (l *Launcher) githubReleaseNotesURL(version string) (*url.URL, error) {
|
||||
// Construct GitHub release URL
|
||||
releaseURL := fmt.Sprintf("https://github.com/%s/%s/releases/tag/%s",
|
||||
l.releaseManager.GitHubOwner,
|
||||
l.releaseManager.GitHubRepo,
|
||||
version)
|
||||
|
||||
// Convert string to *url.URL
|
||||
return url.Parse(releaseURL)
|
||||
}
|
||||
|
||||
// showDownloadLocalAIDialog shows a dialog offering to download LocalAI
|
||||
func (l *Launcher) showDownloadLocalAIDialog() {
|
||||
if l.app == nil {
|
||||
log.Printf("Cannot show download dialog: app is nil")
|
||||
return
|
||||
}
|
||||
|
||||
fyne.DoAndWait(func() {
|
||||
// Create a standalone window for the download dialog
|
||||
dialogWindow := l.app.NewWindow("LocalAI Installation Required")
|
||||
dialogWindow.Resize(fyne.NewSize(500, 350))
|
||||
dialogWindow.CenterOnScreen()
|
||||
dialogWindow.SetCloseIntercept(func() {
|
||||
dialogWindow.Close()
|
||||
})
|
||||
|
||||
// Create the dialog content
|
||||
titleLabel := widget.NewLabel("LocalAI Not Found")
|
||||
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||
titleLabel.Alignment = fyne.TextAlignCenter
|
||||
|
||||
messageLabel := widget.NewLabel("LocalAI is not installed on your system.\n\nWould you like to download and install the latest version?")
|
||||
messageLabel.Wrapping = fyne.TextWrapWord
|
||||
messageLabel.Alignment = fyne.TextAlignCenter
|
||||
|
||||
// Buttons
|
||||
downloadButton := widget.NewButton("Download & Install", func() {
|
||||
dialogWindow.Close()
|
||||
l.downloadAndInstallLocalAI()
|
||||
if l.systray != nil {
|
||||
l.systray.recreateMenu()
|
||||
}
|
||||
})
|
||||
downloadButton.Importance = widget.HighImportance
|
||||
|
||||
// Release notes button
|
||||
releaseNotesButton := widget.NewButton("View Release Notes", func() {
|
||||
// Get latest release info and open release notes
|
||||
go func() {
|
||||
release, err := l.releaseManager.GetLatestRelease()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get latest release info: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
releaseNotesURL, err := l.githubReleaseNotesURL(release.Version)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse URL: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
l.app.OpenURL(releaseNotesURL)
|
||||
}()
|
||||
})
|
||||
|
||||
skipButton := widget.NewButton("Skip for Now", func() {
|
||||
dialogWindow.Close()
|
||||
})
|
||||
|
||||
// Layout - put release notes button above the main action buttons
|
||||
actionButtons := container.NewHBox(skipButton, downloadButton)
|
||||
content := container.NewVBox(
|
||||
titleLabel,
|
||||
widget.NewSeparator(),
|
||||
messageLabel,
|
||||
widget.NewSeparator(),
|
||||
releaseNotesButton,
|
||||
widget.NewSeparator(),
|
||||
actionButtons,
|
||||
)
|
||||
|
||||
dialogWindow.SetContent(content)
|
||||
dialogWindow.Show()
|
||||
})
|
||||
}
|
||||
|
||||
// downloadAndInstallLocalAI downloads and installs the latest LocalAI version
|
||||
func (l *Launcher) downloadAndInstallLocalAI() {
|
||||
if l.app == nil {
|
||||
log.Printf("Cannot download LocalAI: app is nil")
|
||||
return
|
||||
}
|
||||
|
||||
// First check what the latest version is
|
||||
go func() {
|
||||
log.Printf("Checking for latest LocalAI version...")
|
||||
available, version, err := l.CheckForUpdates()
|
||||
if err != nil {
|
||||
log.Printf("Failed to check for updates: %v", err)
|
||||
l.showDownloadError("Failed to check for latest version", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !available {
|
||||
log.Printf("No updates available, but LocalAI is not installed")
|
||||
l.showDownloadError("No Version Available", "Could not determine the latest LocalAI version. Please check your internet connection and try again.")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Latest version available: %s", version)
|
||||
// Show progress window with the specific version
|
||||
l.showDownloadProgress(version, fmt.Sprintf("Downloading LocalAI %s...", version))
|
||||
}()
|
||||
}
|
||||
|
||||
// showDownloadError shows an error dialog for download failures
|
||||
func (l *Launcher) showDownloadError(title, message string) {
|
||||
fyne.DoAndWait(func() {
|
||||
// Create error window
|
||||
errorWindow := l.app.NewWindow("Download Error")
|
||||
errorWindow.Resize(fyne.NewSize(400, 200))
|
||||
errorWindow.CenterOnScreen()
|
||||
errorWindow.SetCloseIntercept(func() {
|
||||
errorWindow.Close()
|
||||
})
|
||||
|
||||
// Error content
|
||||
titleLabel := widget.NewLabel(title)
|
||||
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||
titleLabel.Alignment = fyne.TextAlignCenter
|
||||
|
||||
messageLabel := widget.NewLabel(message)
|
||||
messageLabel.Wrapping = fyne.TextWrapWord
|
||||
messageLabel.Alignment = fyne.TextAlignCenter
|
||||
|
||||
// Close button
|
||||
closeButton := widget.NewButton("Close", func() {
|
||||
errorWindow.Close()
|
||||
})
|
||||
|
||||
// Layout
|
||||
content := container.NewVBox(
|
||||
titleLabel,
|
||||
widget.NewSeparator(),
|
||||
messageLabel,
|
||||
widget.NewSeparator(),
|
||||
closeButton,
|
||||
)
|
||||
|
||||
errorWindow.SetContent(content)
|
||||
errorWindow.Show()
|
||||
})
|
||||
}
|
||||
|
||||
// showDownloadProgress shows a standalone progress window for downloading LocalAI
|
||||
func (l *Launcher) showDownloadProgress(version, title string) {
|
||||
fyne.DoAndWait(func() {
|
||||
// Create progress window
|
||||
progressWindow := l.app.NewWindow("Downloading LocalAI")
|
||||
progressWindow.Resize(fyne.NewSize(400, 250))
|
||||
progressWindow.CenterOnScreen()
|
||||
progressWindow.SetCloseIntercept(func() {
|
||||
progressWindow.Close()
|
||||
})
|
||||
|
||||
// Progress bar
|
||||
progressBar := widget.NewProgressBar()
|
||||
progressBar.SetValue(0)
|
||||
|
||||
// Status label
|
||||
statusLabel := widget.NewLabel("Preparing download...")
|
||||
|
||||
// Release notes button
|
||||
releaseNotesButton := widget.NewButton("View Release Notes", func() {
|
||||
releaseNotesURL, err := l.githubReleaseNotesURL(version)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse URL: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
l.app.OpenURL(releaseNotesURL)
|
||||
})
|
||||
|
||||
// Progress container
|
||||
progressContainer := container.NewVBox(
|
||||
widget.NewLabel(title),
|
||||
progressBar,
|
||||
statusLabel,
|
||||
widget.NewSeparator(),
|
||||
releaseNotesButton,
|
||||
)
|
||||
|
||||
progressWindow.SetContent(progressContainer)
|
||||
progressWindow.Show()
|
||||
|
||||
// Start download in background
|
||||
go func() {
|
||||
err := l.DownloadUpdate(version, func(progress float64) {
|
||||
// Update progress bar
|
||||
fyne.Do(func() {
|
||||
progressBar.SetValue(progress)
|
||||
percentage := int(progress * 100)
|
||||
statusLabel.SetText(fmt.Sprintf("Downloading... %d%%", percentage))
|
||||
})
|
||||
})
|
||||
|
||||
// Handle completion
|
||||
fyne.Do(func() {
|
||||
if err != nil {
|
||||
statusLabel.SetText(fmt.Sprintf("Download failed: %v", err))
|
||||
// Show error dialog
|
||||
dialog.ShowError(err, progressWindow)
|
||||
} else {
|
||||
statusLabel.SetText("Download completed successfully!")
|
||||
progressBar.SetValue(1.0)
|
||||
|
||||
// Show success dialog
|
||||
dialog.ShowConfirm("Installation Complete",
|
||||
"LocalAI has been downloaded and installed successfully. You can now start LocalAI from the launcher.",
|
||||
func(close bool) {
|
||||
progressWindow.Close()
|
||||
// Update status and refresh systray menu
|
||||
l.updateStatus("LocalAI installed successfully")
|
||||
|
||||
if l.systray != nil {
|
||||
l.systray.recreateMenu()
|
||||
}
|
||||
}, progressWindow)
|
||||
}
|
||||
})
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// monitorLogs monitors the output of LocalAI and adds it to the log buffer
|
||||
func (l *Launcher) monitorLogs(reader io.Reader, prefix string) {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
timestamp := time.Now().Format("15:04:05")
|
||||
logLine := fmt.Sprintf("[%s] %s: %s\n", timestamp, prefix, line)
|
||||
|
||||
l.logMutex.Lock()
|
||||
l.logBuffer.WriteString(logLine)
|
||||
// Keep log buffer size reasonable
|
||||
if l.logBuffer.Len() > 100000 { // 100KB
|
||||
content := l.logBuffer.String()
|
||||
// Keep last 50KB
|
||||
if len(content) > 50000 {
|
||||
l.logBuffer.Reset()
|
||||
l.logBuffer.WriteString(content[len(content)-50000:])
|
||||
}
|
||||
}
|
||||
l.logMutex.Unlock()
|
||||
|
||||
// Write to log file if available
|
||||
if l.logFile != nil {
|
||||
if _, err := l.logFile.WriteString(logLine); err != nil {
|
||||
log.Printf("Failed to write to log file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
fyne.Do(func() {
|
||||
// Notify UI of new log content
|
||||
if l.ui != nil {
|
||||
l.ui.OnLogUpdate(logLine)
|
||||
}
|
||||
|
||||
// Check for startup completion
|
||||
if strings.Contains(line, "API server listening") {
|
||||
l.updateStatus("LocalAI is running")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// updateStatus updates the status and notifies UI
|
||||
func (l *Launcher) updateStatus(status string) {
|
||||
select {
|
||||
case l.statusChannel <- status:
|
||||
default:
|
||||
// Channel full, skip
|
||||
}
|
||||
|
||||
if l.ui != nil {
|
||||
l.ui.UpdateStatus(status)
|
||||
}
|
||||
|
||||
if l.systray != nil {
|
||||
l.systray.UpdateStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
// updateRunningState updates the running state in UI and systray
|
||||
func (l *Launcher) updateRunningState(isRunning bool) {
|
||||
if l.ui != nil {
|
||||
l.ui.UpdateRunningState(isRunning)
|
||||
}
|
||||
|
||||
if l.systray != nil {
|
||||
l.systray.UpdateRunningState(isRunning)
|
||||
}
|
||||
}
|
||||
|
||||
// periodicUpdateCheck checks for updates periodically
|
||||
func (l *Launcher) periodicUpdateCheck() {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
available, version, err := l.CheckForUpdates()
|
||||
if err == nil && available {
|
||||
fyne.Do(func() {
|
||||
l.updateStatus(fmt.Sprintf("Update available: %s", version))
|
||||
if l.systray != nil {
|
||||
l.systray.NotifyUpdateAvailable(version)
|
||||
}
|
||||
if l.ui != nil {
|
||||
l.ui.NotifyUpdateAvailable(version)
|
||||
}
|
||||
})
|
||||
}
|
||||
case <-l.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loadConfig loads configuration from file
|
||||
func (l *Launcher) loadConfig() error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(homeDir, ".localai", "launcher.json")
|
||||
log.Printf("Loading config from: %s", configPath)
|
||||
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
log.Printf("Config file not found, creating default config")
|
||||
// Create default config
|
||||
return l.saveConfig()
|
||||
}
|
||||
|
||||
// Load existing config
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Config file content: %s", string(configData))
|
||||
|
||||
log.Printf("loadConfig: about to unmarshal JSON data")
|
||||
if err := json.Unmarshal(configData, l.config); err != nil {
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
log.Printf("loadConfig: JSON unmarshaled successfully")
|
||||
|
||||
log.Printf("Loaded config: ModelsPath=%s, BackendsPath=%s, Address=%s, LogLevel=%s",
|
||||
l.config.ModelsPath, l.config.BackendsPath, l.config.Address, l.config.LogLevel)
|
||||
log.Printf("Environment vars: %v", l.config.EnvironmentVars)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveConfig saves configuration to file
|
||||
func (l *Launcher) saveConfig() error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".localai")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
// Marshal config to JSON
|
||||
log.Printf("saveConfig: marshaling config with EnvironmentVars: %v", l.config.EnvironmentVars)
|
||||
configData, err := json.MarshalIndent(l.config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
log.Printf("saveConfig: JSON marshaled successfully, length: %d", len(configData))
|
||||
|
||||
configPath := filepath.Join(configDir, "launcher.json")
|
||||
log.Printf("Saving config to: %s", configPath)
|
||||
log.Printf("Config content: %s", string(configData))
|
||||
|
||||
if err := os.WriteFile(configPath, configData, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Config saved successfully")
|
||||
return nil
|
||||
}
|
||||
13
cmd/launcher/internal/launcher_suite_test.go
Normal file
13
cmd/launcher/internal/launcher_suite_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package launcher_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestLauncher(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Launcher Suite")
|
||||
}
|
||||
205
cmd/launcher/internal/launcher_test.go
Normal file
205
cmd/launcher/internal/launcher_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package launcher_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"fyne.io/fyne/v2/app"
|
||||
|
||||
launcher "github.com/mudler/LocalAI/cmd/launcher/internal"
|
||||
)
|
||||
|
||||
var _ = Describe("Launcher", func() {
|
||||
var (
|
||||
launcherInstance *launcher.Launcher
|
||||
tempDir string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
tempDir, err = os.MkdirTemp("", "launcher-test-*")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
ui := launcher.NewLauncherUI()
|
||||
app := app.NewWithID("com.localai.launcher")
|
||||
|
||||
launcherInstance = launcher.NewLauncher(ui, nil, app)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
os.RemoveAll(tempDir)
|
||||
})
|
||||
|
||||
Describe("NewLauncher", func() {
|
||||
It("should create a launcher with default configuration", func() {
|
||||
Expect(launcherInstance.GetConfig()).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Initialize", func() {
|
||||
It("should set default paths when not configured", func() {
|
||||
err := launcherInstance.Initialize()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
config := launcherInstance.GetConfig()
|
||||
Expect(config.ModelsPath).ToNot(BeEmpty())
|
||||
Expect(config.BackendsPath).ToNot(BeEmpty())
|
||||
Expect(config.Address).To(Equal("127.0.0.1:8080"))
|
||||
Expect(config.LogLevel).To(Equal("info"))
|
||||
})
|
||||
|
||||
It("should create models and backends directories", func() {
|
||||
// Set custom paths for testing
|
||||
config := launcherInstance.GetConfig()
|
||||
config.ModelsPath = filepath.Join(tempDir, "models")
|
||||
config.BackendsPath = filepath.Join(tempDir, "backends")
|
||||
launcherInstance.SetConfig(config)
|
||||
|
||||
err := launcherInstance.Initialize()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Check if directories were created
|
||||
_, err = os.Stat(config.ModelsPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
_, err = os.Stat(config.BackendsPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Configuration", func() {
|
||||
It("should get and set configuration", func() {
|
||||
config := launcherInstance.GetConfig()
|
||||
config.ModelsPath = "/test/models"
|
||||
config.BackendsPath = "/test/backends"
|
||||
config.Address = ":9090"
|
||||
config.LogLevel = "debug"
|
||||
|
||||
err := launcherInstance.SetConfig(config)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
retrievedConfig := launcherInstance.GetConfig()
|
||||
Expect(retrievedConfig.ModelsPath).To(Equal("/test/models"))
|
||||
Expect(retrievedConfig.BackendsPath).To(Equal("/test/backends"))
|
||||
Expect(retrievedConfig.Address).To(Equal(":9090"))
|
||||
Expect(retrievedConfig.LogLevel).To(Equal("debug"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("WebUI URL", func() {
|
||||
It("should return correct WebUI URL for localhost", func() {
|
||||
config := launcherInstance.GetConfig()
|
||||
config.Address = ":8080"
|
||||
launcherInstance.SetConfig(config)
|
||||
|
||||
url := launcherInstance.GetWebUIURL()
|
||||
Expect(url).To(Equal("http://localhost:8080"))
|
||||
})
|
||||
|
||||
It("should return correct WebUI URL for full address", func() {
|
||||
config := launcherInstance.GetConfig()
|
||||
config.Address = "127.0.0.1:8080"
|
||||
launcherInstance.SetConfig(config)
|
||||
|
||||
url := launcherInstance.GetWebUIURL()
|
||||
Expect(url).To(Equal("http://127.0.0.1:8080"))
|
||||
})
|
||||
|
||||
It("should handle http prefix correctly", func() {
|
||||
config := launcherInstance.GetConfig()
|
||||
config.Address = "http://localhost:8080"
|
||||
launcherInstance.SetConfig(config)
|
||||
|
||||
url := launcherInstance.GetWebUIURL()
|
||||
Expect(url).To(Equal("http://localhost:8080"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Process Management", func() {
|
||||
It("should not be running initially", func() {
|
||||
Expect(launcherInstance.IsRunning()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should handle start when binary doesn't exist", func() {
|
||||
err := launcherInstance.StartLocalAI()
|
||||
Expect(err).To(HaveOccurred())
|
||||
// Could be either "not found" or "permission denied" depending on test environment
|
||||
errMsg := err.Error()
|
||||
hasExpectedError := strings.Contains(errMsg, "LocalAI binary") ||
|
||||
strings.Contains(errMsg, "permission denied")
|
||||
Expect(hasExpectedError).To(BeTrue(), "Expected error about binary not found or permission denied, got: %s", errMsg)
|
||||
})
|
||||
|
||||
It("should handle stop when not running", func() {
|
||||
err := launcherInstance.StopLocalAI()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("LocalAI is not running"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Logs", func() {
|
||||
It("should return empty logs initially", func() {
|
||||
logs := launcherInstance.GetLogs()
|
||||
Expect(logs).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Version Management", func() {
|
||||
It("should return empty version when no binary installed", func() {
|
||||
version := launcherInstance.GetCurrentVersion()
|
||||
Expect(version).To(BeEmpty()) // No binary installed in test environment
|
||||
})
|
||||
|
||||
It("should handle update checks", func() {
|
||||
// This test would require mocking HTTP responses
|
||||
// For now, we'll just test that the method doesn't panic
|
||||
_, _, err := launcherInstance.CheckForUpdates()
|
||||
// We expect either success or a network error, not a panic
|
||||
if err != nil {
|
||||
// Network error is acceptable in tests
|
||||
Expect(err.Error()).To(ContainSubstring("failed to fetch"))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Config", func() {
|
||||
It("should have proper JSON tags", func() {
|
||||
config := &launcher.Config{
|
||||
ModelsPath: "/test/models",
|
||||
BackendsPath: "/test/backends",
|
||||
Address: ":8080",
|
||||
AutoStart: true,
|
||||
LogLevel: "info",
|
||||
EnvironmentVars: map[string]string{"TEST": "value"},
|
||||
}
|
||||
|
||||
Expect(config.ModelsPath).To(Equal("/test/models"))
|
||||
Expect(config.BackendsPath).To(Equal("/test/backends"))
|
||||
Expect(config.Address).To(Equal(":8080"))
|
||||
Expect(config.AutoStart).To(BeTrue())
|
||||
Expect(config.LogLevel).To(Equal("info"))
|
||||
Expect(config.EnvironmentVars).To(HaveKeyWithValue("TEST", "value"))
|
||||
})
|
||||
|
||||
It("should initialize environment variables map", func() {
|
||||
config := &launcher.Config{}
|
||||
Expect(config.EnvironmentVars).To(BeNil())
|
||||
|
||||
ui := launcher.NewLauncherUI()
|
||||
app := app.NewWithID("com.localai.launcher")
|
||||
|
||||
launcher := launcher.NewLauncher(ui, nil, app)
|
||||
|
||||
err := launcher.Initialize()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
retrievedConfig := launcher.GetConfig()
|
||||
Expect(retrievedConfig.EnvironmentVars).ToNot(BeNil())
|
||||
Expect(retrievedConfig.EnvironmentVars).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
502
cmd/launcher/internal/release_manager.go
Normal file
502
cmd/launcher/internal/release_manager.go
Normal file
@@ -0,0 +1,502 @@
|
||||
package launcher
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAI/internal"
|
||||
)
|
||||
|
||||
// Release represents a LocalAI release
|
||||
type Release struct {
|
||||
Version string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
Assets []Asset `json:"assets"`
|
||||
}
|
||||
|
||||
// Asset represents a release asset
|
||||
type Asset struct {
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// ReleaseManager handles LocalAI release management
|
||||
type ReleaseManager struct {
|
||||
// GitHubOwner is the GitHub repository owner
|
||||
GitHubOwner string
|
||||
// GitHubRepo is the GitHub repository name
|
||||
GitHubRepo string
|
||||
// BinaryPath is where the LocalAI binary is stored locally
|
||||
BinaryPath string
|
||||
// CurrentVersion is the currently installed version
|
||||
CurrentVersion string
|
||||
// ChecksumsPath is where checksums are stored
|
||||
ChecksumsPath string
|
||||
// MetadataPath is where version metadata is stored
|
||||
MetadataPath string
|
||||
}
|
||||
|
||||
// NewReleaseManager creates a new release manager
|
||||
func NewReleaseManager() *ReleaseManager {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
binaryPath := filepath.Join(homeDir, ".localai", "bin")
|
||||
checksumsPath := filepath.Join(homeDir, ".localai", "checksums")
|
||||
metadataPath := filepath.Join(homeDir, ".localai", "metadata")
|
||||
|
||||
return &ReleaseManager{
|
||||
GitHubOwner: "mudler",
|
||||
GitHubRepo: "LocalAI",
|
||||
BinaryPath: binaryPath,
|
||||
CurrentVersion: internal.PrintableVersion(),
|
||||
ChecksumsPath: checksumsPath,
|
||||
MetadataPath: metadataPath,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLatestRelease fetches the latest release information from GitHub
|
||||
func (rm *ReleaseManager) GetLatestRelease() (*Release, error) {
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", rm.GitHubOwner, rm.GitHubRepo)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch latest release: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to fetch latest release: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Parse the JSON response properly
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
release := &Release{}
|
||||
if err := json.Unmarshal(body, release); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSON response: %w", err)
|
||||
}
|
||||
|
||||
// Validate the release data
|
||||
if release.Version == "" {
|
||||
return nil, fmt.Errorf("no version found in release data")
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
// DownloadRelease downloads a specific version of LocalAI
|
||||
func (rm *ReleaseManager) DownloadRelease(version string, progressCallback func(float64)) error {
|
||||
// Ensure the binary directory exists
|
||||
if err := os.MkdirAll(rm.BinaryPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create binary directory: %w", err)
|
||||
}
|
||||
|
||||
// Determine the binary name based on OS and architecture
|
||||
binaryName := rm.GetBinaryName(version)
|
||||
localPath := filepath.Join(rm.BinaryPath, "local-ai")
|
||||
|
||||
// Download the binary
|
||||
downloadURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s",
|
||||
rm.GitHubOwner, rm.GitHubRepo, version, binaryName)
|
||||
|
||||
if err := rm.downloadFile(downloadURL, localPath, progressCallback); err != nil {
|
||||
return fmt.Errorf("failed to download binary: %w", err)
|
||||
}
|
||||
|
||||
// Download and verify checksums
|
||||
checksumURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/LocalAI-%s-checksums.txt",
|
||||
rm.GitHubOwner, rm.GitHubRepo, version, version)
|
||||
|
||||
checksumPath := filepath.Join(rm.BinaryPath, "checksums.txt")
|
||||
if err := rm.downloadFile(checksumURL, checksumPath, nil); err != nil {
|
||||
return fmt.Errorf("failed to download checksums: %w", err)
|
||||
}
|
||||
|
||||
// Verify the checksum
|
||||
if err := rm.VerifyChecksum(localPath, checksumPath, binaryName); err != nil {
|
||||
return fmt.Errorf("checksum verification failed: %w", err)
|
||||
}
|
||||
|
||||
// Save checksums persistently for future verification
|
||||
if err := rm.saveChecksums(version, checksumPath, binaryName); err != nil {
|
||||
log.Printf("Warning: failed to save checksums: %v", err)
|
||||
}
|
||||
|
||||
// Make the binary executable
|
||||
if err := os.Chmod(localPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBinaryName returns the appropriate binary name for the current platform
|
||||
func (rm *ReleaseManager) GetBinaryName(version string) string {
|
||||
versionStr := strings.TrimPrefix(version, "v")
|
||||
os := runtime.GOOS
|
||||
arch := runtime.GOARCH
|
||||
|
||||
// Map Go arch names to the release naming convention
|
||||
switch arch {
|
||||
case "amd64":
|
||||
arch = "amd64"
|
||||
case "arm64":
|
||||
arch = "arm64"
|
||||
default:
|
||||
arch = "amd64" // fallback
|
||||
}
|
||||
|
||||
return fmt.Sprintf("local-ai-v%s-%s-%s", versionStr, os, arch)
|
||||
}
|
||||
|
||||
// downloadFile downloads a file from a URL to a local path with optional progress callback
|
||||
func (rm *ReleaseManager) downloadFile(url, filepath string, progressCallback func(float64)) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
out, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Create a progress reader if callback is provided
|
||||
var reader io.Reader = resp.Body
|
||||
if progressCallback != nil && resp.ContentLength > 0 {
|
||||
reader = &progressReader{
|
||||
Reader: resp.Body,
|
||||
Total: resp.ContentLength,
|
||||
Callback: progressCallback,
|
||||
}
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, reader)
|
||||
return err
|
||||
}
|
||||
|
||||
// saveChecksums saves checksums persistently for future verification
|
||||
func (rm *ReleaseManager) saveChecksums(version, checksumPath, binaryName string) error {
|
||||
// Ensure checksums directory exists
|
||||
if err := os.MkdirAll(rm.ChecksumsPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create checksums directory: %w", err)
|
||||
}
|
||||
|
||||
// Read the downloaded checksums file
|
||||
checksumData, err := os.ReadFile(checksumPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read checksums file: %w", err)
|
||||
}
|
||||
|
||||
// Save to persistent location with version info
|
||||
persistentPath := filepath.Join(rm.ChecksumsPath, fmt.Sprintf("checksums-%s.txt", version))
|
||||
if err := os.WriteFile(persistentPath, checksumData, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write persistent checksums: %w", err)
|
||||
}
|
||||
|
||||
// Also save a "latest" checksums file for the current version
|
||||
latestPath := filepath.Join(rm.ChecksumsPath, "checksums-latest.txt")
|
||||
if err := os.WriteFile(latestPath, checksumData, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write latest checksums: %w", err)
|
||||
}
|
||||
|
||||
// Save version metadata
|
||||
if err := rm.saveVersionMetadata(version); err != nil {
|
||||
log.Printf("Warning: failed to save version metadata: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Checksums saved for version %s", version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveVersionMetadata saves the installed version information
|
||||
func (rm *ReleaseManager) saveVersionMetadata(version string) error {
|
||||
// Ensure metadata directory exists
|
||||
if err := os.MkdirAll(rm.MetadataPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create metadata directory: %w", err)
|
||||
}
|
||||
|
||||
// Create metadata structure
|
||||
metadata := struct {
|
||||
Version string `json:"version"`
|
||||
InstalledAt time.Time `json:"installed_at"`
|
||||
BinaryPath string `json:"binary_path"`
|
||||
}{
|
||||
Version: version,
|
||||
InstalledAt: time.Now(),
|
||||
BinaryPath: rm.GetBinaryPath(),
|
||||
}
|
||||
|
||||
// Marshal to JSON
|
||||
metadataData, err := json.MarshalIndent(metadata, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
// Save metadata file
|
||||
metadataPath := filepath.Join(rm.MetadataPath, "installed-version.json")
|
||||
if err := os.WriteFile(metadataPath, metadataData, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write metadata file: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Version metadata saved: %s", version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// progressReader wraps an io.Reader to provide download progress
|
||||
type progressReader struct {
|
||||
io.Reader
|
||||
Total int64
|
||||
Current int64
|
||||
Callback func(float64)
|
||||
}
|
||||
|
||||
func (pr *progressReader) Read(p []byte) (int, error) {
|
||||
n, err := pr.Reader.Read(p)
|
||||
pr.Current += int64(n)
|
||||
if pr.Callback != nil {
|
||||
progress := float64(pr.Current) / float64(pr.Total)
|
||||
pr.Callback(progress)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// VerifyChecksum verifies the downloaded file against the provided checksums
|
||||
func (rm *ReleaseManager) VerifyChecksum(filePath, checksumPath, binaryName string) error {
|
||||
// Calculate the SHA256 of the downloaded file
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file for checksum: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
return fmt.Errorf("failed to calculate checksum: %w", err)
|
||||
}
|
||||
|
||||
calculatedHash := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
// Read the checksums file
|
||||
checksumFile, err := os.Open(checksumPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open checksums file: %w", err)
|
||||
}
|
||||
defer checksumFile.Close()
|
||||
|
||||
scanner := bufio.NewScanner(checksumFile)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.Contains(line, binaryName) {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
expectedHash := parts[0]
|
||||
if calculatedHash == expectedHash {
|
||||
return nil // Checksum verified
|
||||
}
|
||||
return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedHash, calculatedHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("checksum not found for %s", binaryName)
|
||||
}
|
||||
|
||||
// GetInstalledVersion returns the currently installed version
|
||||
func (rm *ReleaseManager) GetInstalledVersion() string {
|
||||
|
||||
// Fallback: Check if the LocalAI binary exists and try to get its version
|
||||
binaryPath := rm.GetBinaryPath()
|
||||
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||
return "" // No version installed
|
||||
}
|
||||
|
||||
// try to get version from metadata
|
||||
if version := rm.loadVersionMetadata(); version != "" {
|
||||
return version
|
||||
}
|
||||
|
||||
// Try to run the binary to get the version (fallback method)
|
||||
version, err := exec.Command(binaryPath, "--version").Output()
|
||||
if err != nil {
|
||||
// If binary exists but --version fails, try to determine from filename or other means
|
||||
log.Printf("Binary exists but --version failed: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
stringVersion := strings.TrimSpace(string(version))
|
||||
stringVersion = strings.TrimRight(stringVersion, "\n")
|
||||
|
||||
return stringVersion
|
||||
}
|
||||
|
||||
// loadVersionMetadata loads the installed version from metadata file
|
||||
func (rm *ReleaseManager) loadVersionMetadata() string {
|
||||
metadataPath := filepath.Join(rm.MetadataPath, "installed-version.json")
|
||||
|
||||
// Check if metadata file exists
|
||||
if _, err := os.Stat(metadataPath); os.IsNotExist(err) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Read metadata file
|
||||
metadataData, err := os.ReadFile(metadataPath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to read metadata file: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Parse metadata
|
||||
var metadata struct {
|
||||
Version string `json:"version"`
|
||||
InstalledAt time.Time `json:"installed_at"`
|
||||
BinaryPath string `json:"binary_path"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(metadataData, &metadata); err != nil {
|
||||
log.Printf("Failed to parse metadata file: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Verify that the binary path in metadata matches current binary path
|
||||
if metadata.BinaryPath != rm.GetBinaryPath() {
|
||||
log.Printf("Binary path mismatch in metadata, ignoring")
|
||||
return ""
|
||||
}
|
||||
|
||||
log.Printf("Loaded version from metadata: %s (installed at %s)", metadata.Version, metadata.InstalledAt.Format("2006-01-02 15:04:05"))
|
||||
return metadata.Version
|
||||
}
|
||||
|
||||
// GetBinaryPath returns the path to the LocalAI binary
|
||||
func (rm *ReleaseManager) GetBinaryPath() string {
|
||||
return filepath.Join(rm.BinaryPath, "local-ai")
|
||||
}
|
||||
|
||||
// IsUpdateAvailable checks if an update is available
|
||||
func (rm *ReleaseManager) IsUpdateAvailable() (bool, string, error) {
|
||||
log.Printf("IsUpdateAvailable: checking for updates...")
|
||||
|
||||
latest, err := rm.GetLatestRelease()
|
||||
if err != nil {
|
||||
log.Printf("IsUpdateAvailable: failed to get latest release: %v", err)
|
||||
return false, "", err
|
||||
}
|
||||
log.Printf("IsUpdateAvailable: latest release version: %s", latest.Version)
|
||||
|
||||
current := rm.GetInstalledVersion()
|
||||
log.Printf("IsUpdateAvailable: current installed version: %s", current)
|
||||
|
||||
if current == "" {
|
||||
// No version installed, offer to download latest
|
||||
log.Printf("IsUpdateAvailable: no version installed, offering latest: %s", latest.Version)
|
||||
return true, latest.Version, nil
|
||||
}
|
||||
|
||||
updateAvailable := latest.Version != current
|
||||
log.Printf("IsUpdateAvailable: update available: %v (latest: %s, current: %s)", updateAvailable, latest.Version, current)
|
||||
return updateAvailable, latest.Version, nil
|
||||
}
|
||||
|
||||
// IsLocalAIInstalled checks if LocalAI binary exists and is valid
|
||||
func (rm *ReleaseManager) IsLocalAIInstalled() bool {
|
||||
binaryPath := rm.GetBinaryPath()
|
||||
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify the binary integrity
|
||||
if err := rm.VerifyInstalledBinary(); err != nil {
|
||||
log.Printf("Binary integrity check failed: %v", err)
|
||||
// Remove corrupted binary
|
||||
if removeErr := os.Remove(binaryPath); removeErr != nil {
|
||||
log.Printf("Failed to remove corrupted binary: %v", removeErr)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// VerifyInstalledBinary verifies the installed binary against saved checksums
|
||||
func (rm *ReleaseManager) VerifyInstalledBinary() error {
|
||||
binaryPath := rm.GetBinaryPath()
|
||||
|
||||
// Check if we have saved checksums
|
||||
latestChecksumsPath := filepath.Join(rm.ChecksumsPath, "checksums-latest.txt")
|
||||
if _, err := os.Stat(latestChecksumsPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("no saved checksums found")
|
||||
}
|
||||
|
||||
// Get the binary name for the current version from metadata
|
||||
currentVersion := rm.loadVersionMetadata()
|
||||
if currentVersion == "" {
|
||||
return fmt.Errorf("cannot determine current version from metadata")
|
||||
}
|
||||
|
||||
binaryName := rm.GetBinaryName(currentVersion)
|
||||
|
||||
// Verify against saved checksums
|
||||
return rm.VerifyChecksum(binaryPath, latestChecksumsPath, binaryName)
|
||||
}
|
||||
|
||||
// CleanupPartialDownloads removes any partial or corrupted downloads
|
||||
func (rm *ReleaseManager) CleanupPartialDownloads() error {
|
||||
binaryPath := rm.GetBinaryPath()
|
||||
|
||||
// Check if binary exists but is corrupted
|
||||
if _, err := os.Stat(binaryPath); err == nil {
|
||||
// Binary exists, verify it
|
||||
if verifyErr := rm.VerifyInstalledBinary(); verifyErr != nil {
|
||||
log.Printf("Found corrupted binary, removing: %v", verifyErr)
|
||||
if removeErr := os.Remove(binaryPath); removeErr != nil {
|
||||
log.Printf("Failed to remove corrupted binary: %v", removeErr)
|
||||
}
|
||||
// Clear metadata since binary is corrupted
|
||||
rm.clearVersionMetadata()
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any temporary checksum files
|
||||
tempChecksumsPath := filepath.Join(rm.BinaryPath, "checksums.txt")
|
||||
if _, err := os.Stat(tempChecksumsPath); err == nil {
|
||||
if removeErr := os.Remove(tempChecksumsPath); removeErr != nil {
|
||||
log.Printf("Failed to remove temporary checksums: %v", removeErr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearVersionMetadata clears the version metadata (used when binary is corrupted or removed)
|
||||
func (rm *ReleaseManager) clearVersionMetadata() {
|
||||
metadataPath := filepath.Join(rm.MetadataPath, "installed-version.json")
|
||||
if err := os.Remove(metadataPath); err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("Failed to clear version metadata: %v", err)
|
||||
} else {
|
||||
log.Printf("Version metadata cleared")
|
||||
}
|
||||
}
|
||||
178
cmd/launcher/internal/release_manager_test.go
Normal file
178
cmd/launcher/internal/release_manager_test.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package launcher_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
launcher "github.com/mudler/LocalAI/cmd/launcher/internal"
|
||||
)
|
||||
|
||||
var _ = Describe("ReleaseManager", func() {
|
||||
var (
|
||||
rm *launcher.ReleaseManager
|
||||
tempDir string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
tempDir, err = os.MkdirTemp("", "launcher-test-*")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
rm = launcher.NewReleaseManager()
|
||||
// Override binary path for testing
|
||||
rm.BinaryPath = tempDir
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
os.RemoveAll(tempDir)
|
||||
})
|
||||
|
||||
Describe("NewReleaseManager", func() {
|
||||
It("should create a release manager with correct defaults", func() {
|
||||
newRM := launcher.NewReleaseManager()
|
||||
Expect(newRM.GitHubOwner).To(Equal("mudler"))
|
||||
Expect(newRM.GitHubRepo).To(Equal("LocalAI"))
|
||||
Expect(newRM.BinaryPath).To(ContainSubstring(".localai"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetBinaryName", func() {
|
||||
It("should return correct binary name for current platform", func() {
|
||||
binaryName := rm.GetBinaryName("v3.4.0")
|
||||
expectedOS := runtime.GOOS
|
||||
expectedArch := runtime.GOARCH
|
||||
|
||||
expected := "local-ai-v3.4.0-" + expectedOS + "-" + expectedArch
|
||||
Expect(binaryName).To(Equal(expected))
|
||||
})
|
||||
|
||||
It("should handle version with and without 'v' prefix", func() {
|
||||
withV := rm.GetBinaryName("v3.4.0")
|
||||
withoutV := rm.GetBinaryName("3.4.0")
|
||||
|
||||
// Both should produce the same result
|
||||
Expect(withV).To(Equal(withoutV))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetBinaryPath", func() {
|
||||
It("should return the correct binary path", func() {
|
||||
path := rm.GetBinaryPath()
|
||||
expected := filepath.Join(tempDir, "local-ai")
|
||||
Expect(path).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetInstalledVersion", func() {
|
||||
It("should return empty when no binary exists", func() {
|
||||
version := rm.GetInstalledVersion()
|
||||
Expect(version).To(BeEmpty()) // No binary installed in test
|
||||
})
|
||||
|
||||
It("should return empty version when binary exists but no metadata", func() {
|
||||
// Create a fake binary for testing
|
||||
err := os.MkdirAll(rm.BinaryPath, 0755)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
binaryPath := rm.GetBinaryPath()
|
||||
err = os.WriteFile(binaryPath, []byte("fake binary"), 0755)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
version := rm.GetInstalledVersion()
|
||||
Expect(version).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Context("with mocked responses", func() {
|
||||
// Note: In a real implementation, we'd mock HTTP responses
|
||||
// For now, we'll test the structure and error handling
|
||||
|
||||
Describe("GetLatestRelease", func() {
|
||||
It("should handle network errors gracefully", func() {
|
||||
// This test would require mocking HTTP client
|
||||
// For demonstration, we're just testing the method exists
|
||||
_, err := rm.GetLatestRelease()
|
||||
// We expect either success or a network error, not a panic
|
||||
// In a real test, we'd mock the HTTP response
|
||||
if err != nil {
|
||||
Expect(err.Error()).To(ContainSubstring("failed to fetch"))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Describe("DownloadRelease", func() {
|
||||
It("should create binary directory if it doesn't exist", func() {
|
||||
// Remove the temp directory to test creation
|
||||
os.RemoveAll(tempDir)
|
||||
|
||||
// This will fail due to network, but should create the directory
|
||||
rm.DownloadRelease("v3.4.0", nil)
|
||||
|
||||
// Check if directory was created
|
||||
_, err := os.Stat(tempDir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("VerifyChecksum functionality", func() {
|
||||
var (
|
||||
testFile string
|
||||
checksumFile string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
testFile = filepath.Join(tempDir, "test-binary")
|
||||
checksumFile = filepath.Join(tempDir, "checksums.txt")
|
||||
})
|
||||
|
||||
It("should verify checksums correctly", func() {
|
||||
// Create a test file with known content
|
||||
testContent := []byte("test content for checksum")
|
||||
err := os.WriteFile(testFile, testContent, 0644)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Calculate expected SHA256
|
||||
// This is a simplified test - in practice we'd use the actual checksum
|
||||
checksumContent := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 test-binary\n"
|
||||
err = os.WriteFile(checksumFile, []byte(checksumContent), 0644)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Test checksum verification
|
||||
// Note: This will fail because our content doesn't match the empty string hash
|
||||
// In a real test, we'd calculate the actual hash
|
||||
err = rm.VerifyChecksum(testFile, checksumFile, "test-binary")
|
||||
// We expect this to fail since we're using a dummy checksum
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("checksum mismatch"))
|
||||
})
|
||||
|
||||
It("should handle missing checksum file", func() {
|
||||
// Create test file but no checksum file
|
||||
err := os.WriteFile(testFile, []byte("test"), 0644)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = rm.VerifyChecksum(testFile, checksumFile, "test-binary")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("failed to open checksums file"))
|
||||
})
|
||||
|
||||
It("should handle missing binary in checksums", func() {
|
||||
// Create files but checksum doesn't contain our binary
|
||||
err := os.WriteFile(testFile, []byte("test"), 0644)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
checksumContent := "hash other-binary\n"
|
||||
err = os.WriteFile(checksumFile, []byte(checksumContent), 0644)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = rm.VerifyChecksum(testFile, checksumFile, "test-binary")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("checksum not found"))
|
||||
})
|
||||
})
|
||||
})
|
||||
513
cmd/launcher/internal/systray_manager.go
Normal file
513
cmd/launcher/internal/systray_manager.go
Normal file
@@ -0,0 +1,513 @@
|
||||
package launcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// SystrayManager manages the system tray functionality
|
||||
type SystrayManager struct {
|
||||
launcher *Launcher
|
||||
window fyne.Window
|
||||
app fyne.App
|
||||
desk desktop.App
|
||||
|
||||
// Menu items that need dynamic updates
|
||||
startStopItem *fyne.MenuItem
|
||||
hasUpdateAvailable bool
|
||||
latestVersion string
|
||||
icon *fyne.StaticResource
|
||||
}
|
||||
|
||||
// NewSystrayManager creates a new systray manager
|
||||
func NewSystrayManager(launcher *Launcher, window fyne.Window, desktop desktop.App, app fyne.App, icon *fyne.StaticResource) *SystrayManager {
|
||||
sm := &SystrayManager{
|
||||
launcher: launcher,
|
||||
window: window,
|
||||
app: app,
|
||||
desk: desktop,
|
||||
icon: icon,
|
||||
}
|
||||
sm.setupMenu(desktop)
|
||||
return sm
|
||||
}
|
||||
|
||||
// setupMenu sets up the system tray menu
|
||||
func (sm *SystrayManager) setupMenu(desk desktop.App) {
|
||||
sm.desk = desk
|
||||
|
||||
// Create the start/stop toggle item
|
||||
sm.startStopItem = fyne.NewMenuItem("Start LocalAI", func() {
|
||||
sm.toggleLocalAI()
|
||||
})
|
||||
|
||||
desk.SetSystemTrayIcon(sm.icon)
|
||||
|
||||
// Initialize the menu state using recreateMenu
|
||||
sm.recreateMenu()
|
||||
}
|
||||
|
||||
// toggleLocalAI starts or stops LocalAI based on current state
|
||||
func (sm *SystrayManager) toggleLocalAI() {
|
||||
if sm.launcher.IsRunning() {
|
||||
go func() {
|
||||
if err := sm.launcher.StopLocalAI(); err != nil {
|
||||
log.Printf("Failed to stop LocalAI: %v", err)
|
||||
sm.showErrorDialog("Failed to Stop LocalAI", err.Error())
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
go func() {
|
||||
if err := sm.launcher.StartLocalAI(); err != nil {
|
||||
log.Printf("Failed to start LocalAI: %v", err)
|
||||
sm.showStartupErrorDialog(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// openWebUI opens the LocalAI WebUI in the default browser
|
||||
func (sm *SystrayManager) openWebUI() {
|
||||
if !sm.launcher.IsRunning() {
|
||||
return // LocalAI is not running
|
||||
}
|
||||
|
||||
webURL := sm.launcher.GetWebUIURL()
|
||||
if parsedURL, err := url.Parse(webURL); err == nil {
|
||||
sm.app.OpenURL(parsedURL)
|
||||
}
|
||||
}
|
||||
|
||||
// openDocumentation opens the LocalAI documentation
|
||||
func (sm *SystrayManager) openDocumentation() {
|
||||
if parsedURL, err := url.Parse("https://localai.io"); err == nil {
|
||||
sm.app.OpenURL(parsedURL)
|
||||
}
|
||||
}
|
||||
|
||||
// updateStartStopItem updates the start/stop menu item based on current state
|
||||
func (sm *SystrayManager) updateStartStopItem() {
|
||||
// Since Fyne menu items can't change text dynamically, we recreate the menu
|
||||
sm.recreateMenu()
|
||||
}
|
||||
|
||||
// recreateMenu recreates the entire menu with updated state
|
||||
func (sm *SystrayManager) recreateMenu() {
|
||||
if sm.desk == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Determine the action based on LocalAI installation and running state
|
||||
var actionItem *fyne.MenuItem
|
||||
if !sm.launcher.GetReleaseManager().IsLocalAIInstalled() {
|
||||
// LocalAI not installed - show install option
|
||||
actionItem = fyne.NewMenuItem("📥 Install Latest Version", func() {
|
||||
sm.launcher.showDownloadLocalAIDialog()
|
||||
})
|
||||
} else if sm.launcher.IsRunning() {
|
||||
// LocalAI is running - show stop option
|
||||
actionItem = fyne.NewMenuItem("🛑 Stop LocalAI", func() {
|
||||
sm.toggleLocalAI()
|
||||
})
|
||||
} else {
|
||||
// LocalAI is installed but not running - show start option
|
||||
actionItem = fyne.NewMenuItem("▶️ Start LocalAI", func() {
|
||||
sm.toggleLocalAI()
|
||||
})
|
||||
}
|
||||
|
||||
menuItems := []*fyne.MenuItem{}
|
||||
|
||||
// Add status at the top (clickable for details)
|
||||
status := sm.launcher.GetLastStatus()
|
||||
statusText := sm.truncateText(status, 30)
|
||||
statusItem := fyne.NewMenuItem("📊 Status: "+statusText, func() {
|
||||
sm.showStatusDetails(status, "")
|
||||
})
|
||||
menuItems = append(menuItems, statusItem)
|
||||
|
||||
// Only show version if LocalAI is installed
|
||||
if sm.launcher.GetReleaseManager().IsLocalAIInstalled() {
|
||||
version := sm.launcher.GetCurrentVersion()
|
||||
versionText := sm.truncateText(version, 25)
|
||||
versionItem := fyne.NewMenuItem("🔧 Version: "+versionText, func() {
|
||||
sm.showStatusDetails(status, version)
|
||||
})
|
||||
menuItems = append(menuItems, versionItem)
|
||||
}
|
||||
|
||||
menuItems = append(menuItems, fyne.NewMenuItemSeparator())
|
||||
|
||||
// Add update notification if available
|
||||
if sm.hasUpdateAvailable {
|
||||
updateItem := fyne.NewMenuItem("🔔 New version available ("+sm.latestVersion+")", func() {
|
||||
sm.downloadUpdate()
|
||||
})
|
||||
menuItems = append(menuItems, updateItem)
|
||||
menuItems = append(menuItems, fyne.NewMenuItemSeparator())
|
||||
}
|
||||
|
||||
// Core actions
|
||||
menuItems = append(menuItems,
|
||||
actionItem,
|
||||
)
|
||||
|
||||
// Only show WebUI option if LocalAI is installed
|
||||
if sm.launcher.GetReleaseManager().IsLocalAIInstalled() && sm.launcher.IsRunning() {
|
||||
menuItems = append(menuItems,
|
||||
fyne.NewMenuItem("Open WebUI", func() {
|
||||
sm.openWebUI()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
menuItems = append(menuItems,
|
||||
fyne.NewMenuItemSeparator(),
|
||||
fyne.NewMenuItem("Check for Updates", func() {
|
||||
sm.checkForUpdates()
|
||||
}),
|
||||
fyne.NewMenuItemSeparator(),
|
||||
fyne.NewMenuItem("Settings", func() {
|
||||
sm.showSettings()
|
||||
}),
|
||||
fyne.NewMenuItem("Open Data Folder", func() {
|
||||
sm.openDataFolder()
|
||||
}),
|
||||
fyne.NewMenuItemSeparator(),
|
||||
fyne.NewMenuItem("Documentation", func() {
|
||||
sm.openDocumentation()
|
||||
}),
|
||||
fyne.NewMenuItemSeparator(),
|
||||
fyne.NewMenuItem("Quit", func() {
|
||||
// Perform cleanup before quitting
|
||||
if err := sm.launcher.Shutdown(); err != nil {
|
||||
log.Printf("Error during shutdown: %v", err)
|
||||
}
|
||||
sm.app.Quit()
|
||||
}),
|
||||
)
|
||||
|
||||
menu := fyne.NewMenu("LocalAI", menuItems...)
|
||||
sm.desk.SetSystemTrayMenu(menu)
|
||||
}
|
||||
|
||||
// UpdateRunningState updates the systray based on running state
|
||||
func (sm *SystrayManager) UpdateRunningState(isRunning bool) {
|
||||
sm.updateStartStopItem()
|
||||
}
|
||||
|
||||
// UpdateStatus updates the systray menu to reflect status changes
|
||||
func (sm *SystrayManager) UpdateStatus(status string) {
|
||||
sm.recreateMenu()
|
||||
}
|
||||
|
||||
// checkForUpdates checks for available updates
|
||||
func (sm *SystrayManager) checkForUpdates() {
|
||||
go func() {
|
||||
log.Printf("Checking for updates...")
|
||||
available, version, err := sm.launcher.CheckForUpdates()
|
||||
if err != nil {
|
||||
log.Printf("Failed to check for updates: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Update check result: available=%v, version=%s", available, version)
|
||||
if available {
|
||||
sm.hasUpdateAvailable = true
|
||||
sm.latestVersion = version
|
||||
sm.recreateMenu()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// downloadUpdate downloads the latest update
|
||||
func (sm *SystrayManager) downloadUpdate() {
|
||||
if !sm.hasUpdateAvailable {
|
||||
return
|
||||
}
|
||||
|
||||
// Show progress window
|
||||
sm.showDownloadProgress(sm.latestVersion)
|
||||
}
|
||||
|
||||
// showSettings shows the settings window
|
||||
func (sm *SystrayManager) showSettings() {
|
||||
sm.window.Show()
|
||||
sm.window.RequestFocus()
|
||||
}
|
||||
|
||||
// openDataFolder opens the data folder in file manager
|
||||
func (sm *SystrayManager) openDataFolder() {
|
||||
dataPath := sm.launcher.GetDataPath()
|
||||
if parsedURL, err := url.Parse("file://" + dataPath); err == nil {
|
||||
sm.app.OpenURL(parsedURL)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyUpdateAvailable sets update notification in systray
|
||||
func (sm *SystrayManager) NotifyUpdateAvailable(version string) {
|
||||
sm.hasUpdateAvailable = true
|
||||
sm.latestVersion = version
|
||||
sm.recreateMenu()
|
||||
}
|
||||
|
||||
// truncateText truncates text to specified length and adds ellipsis if needed
|
||||
func (sm *SystrayManager) truncateText(text string, maxLength int) string {
|
||||
if len(text) <= maxLength {
|
||||
return text
|
||||
}
|
||||
return text[:maxLength-3] + "..."
|
||||
}
|
||||
|
||||
// showStatusDetails shows a detailed status window with full information
|
||||
func (sm *SystrayManager) showStatusDetails(status, version string) {
|
||||
fyne.DoAndWait(func() {
|
||||
// Create status details window
|
||||
statusWindow := sm.app.NewWindow("LocalAI Status Details")
|
||||
statusWindow.Resize(fyne.NewSize(500, 400))
|
||||
statusWindow.CenterOnScreen()
|
||||
|
||||
// Status information
|
||||
statusLabel := widget.NewLabel("Current Status:")
|
||||
statusValue := widget.NewLabel(status)
|
||||
statusValue.Wrapping = fyne.TextWrapWord
|
||||
|
||||
// Version information (only show if version exists)
|
||||
var versionContainer fyne.CanvasObject
|
||||
if version != "" {
|
||||
versionLabel := widget.NewLabel("Installed Version:")
|
||||
versionValue := widget.NewLabel(version)
|
||||
versionValue.Wrapping = fyne.TextWrapWord
|
||||
versionContainer = container.NewVBox(versionLabel, versionValue)
|
||||
}
|
||||
|
||||
// Running state
|
||||
runningLabel := widget.NewLabel("Running State:")
|
||||
runningValue := widget.NewLabel("")
|
||||
if sm.launcher.IsRunning() {
|
||||
runningValue.SetText("🟢 Running")
|
||||
} else {
|
||||
runningValue.SetText("🔴 Stopped")
|
||||
}
|
||||
|
||||
// WebUI URL
|
||||
webuiLabel := widget.NewLabel("WebUI URL:")
|
||||
webuiValue := widget.NewLabel(sm.launcher.GetWebUIURL())
|
||||
webuiValue.Wrapping = fyne.TextWrapWord
|
||||
|
||||
// Recent logs (last 20 lines)
|
||||
logsLabel := widget.NewLabel("Recent Logs:")
|
||||
logsText := widget.NewMultiLineEntry()
|
||||
logsText.SetText(sm.launcher.GetRecentLogs())
|
||||
logsText.Wrapping = fyne.TextWrapWord
|
||||
logsText.Disable() // Make it read-only
|
||||
|
||||
// Buttons
|
||||
closeButton := widget.NewButton("Close", func() {
|
||||
statusWindow.Close()
|
||||
})
|
||||
|
||||
refreshButton := widget.NewButton("Refresh", func() {
|
||||
// Refresh the status information
|
||||
statusValue.SetText(sm.launcher.GetLastStatus())
|
||||
|
||||
// Note: Version refresh is not implemented for simplicity
|
||||
// The version will be updated when the status details window is reopened
|
||||
|
||||
if sm.launcher.IsRunning() {
|
||||
runningValue.SetText("🟢 Running")
|
||||
} else {
|
||||
runningValue.SetText("🔴 Stopped")
|
||||
}
|
||||
logsText.SetText(sm.launcher.GetRecentLogs())
|
||||
})
|
||||
|
||||
openWebUIButton := widget.NewButton("Open WebUI", func() {
|
||||
sm.openWebUI()
|
||||
})
|
||||
|
||||
// Layout
|
||||
buttons := container.NewHBox(closeButton, refreshButton, openWebUIButton)
|
||||
|
||||
// Build info container dynamically
|
||||
infoItems := []fyne.CanvasObject{
|
||||
statusLabel, statusValue,
|
||||
widget.NewSeparator(),
|
||||
}
|
||||
|
||||
// Add version section if it exists
|
||||
if versionContainer != nil {
|
||||
infoItems = append(infoItems, versionContainer, widget.NewSeparator())
|
||||
}
|
||||
|
||||
infoItems = append(infoItems,
|
||||
runningLabel, runningValue,
|
||||
widget.NewSeparator(),
|
||||
webuiLabel, webuiValue,
|
||||
)
|
||||
|
||||
infoContainer := container.NewVBox(infoItems...)
|
||||
|
||||
content := container.NewVBox(
|
||||
infoContainer,
|
||||
widget.NewSeparator(),
|
||||
logsLabel,
|
||||
logsText,
|
||||
widget.NewSeparator(),
|
||||
buttons,
|
||||
)
|
||||
|
||||
statusWindow.SetContent(content)
|
||||
statusWindow.Show()
|
||||
})
|
||||
}
|
||||
|
||||
// showErrorDialog shows a simple error dialog
|
||||
func (sm *SystrayManager) showErrorDialog(title, message string) {
|
||||
fyne.DoAndWait(func() {
|
||||
dialog.ShowError(fmt.Errorf(message), sm.window)
|
||||
})
|
||||
}
|
||||
|
||||
// showStartupErrorDialog shows a detailed error dialog with process logs
|
||||
func (sm *SystrayManager) showStartupErrorDialog(err error) {
|
||||
fyne.DoAndWait(func() {
|
||||
// Get the recent process logs (more useful for debugging)
|
||||
logs := sm.launcher.GetRecentLogs()
|
||||
|
||||
// Create error window
|
||||
errorWindow := sm.app.NewWindow("LocalAI Startup Failed")
|
||||
errorWindow.Resize(fyne.NewSize(600, 500))
|
||||
errorWindow.CenterOnScreen()
|
||||
|
||||
// Error message
|
||||
errorLabel := widget.NewLabel(fmt.Sprintf("Failed to start LocalAI:\n%s", err.Error()))
|
||||
errorLabel.Wrapping = fyne.TextWrapWord
|
||||
|
||||
// Logs display
|
||||
logsLabel := widget.NewLabel("Process Logs:")
|
||||
logsText := widget.NewMultiLineEntry()
|
||||
logsText.SetText(logs)
|
||||
logsText.Wrapping = fyne.TextWrapWord
|
||||
logsText.Disable() // Make it read-only
|
||||
|
||||
// Buttons
|
||||
closeButton := widget.NewButton("Close", func() {
|
||||
errorWindow.Close()
|
||||
})
|
||||
|
||||
retryButton := widget.NewButton("Retry", func() {
|
||||
errorWindow.Close()
|
||||
// Try to start again
|
||||
go func() {
|
||||
if retryErr := sm.launcher.StartLocalAI(); retryErr != nil {
|
||||
sm.showStartupErrorDialog(retryErr)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
openLogsButton := widget.NewButton("Open Logs Folder", func() {
|
||||
sm.openDataFolder()
|
||||
})
|
||||
|
||||
// Layout
|
||||
buttons := container.NewHBox(closeButton, retryButton, openLogsButton)
|
||||
content := container.NewVBox(
|
||||
errorLabel,
|
||||
widget.NewSeparator(),
|
||||
logsLabel,
|
||||
logsText,
|
||||
widget.NewSeparator(),
|
||||
buttons,
|
||||
)
|
||||
|
||||
errorWindow.SetContent(content)
|
||||
errorWindow.Show()
|
||||
})
|
||||
}
|
||||
|
||||
// showDownloadProgress shows a progress window for downloading updates
|
||||
func (sm *SystrayManager) showDownloadProgress(version string) {
|
||||
// Create a new window for download progress
|
||||
progressWindow := sm.app.NewWindow("Downloading LocalAI Update")
|
||||
progressWindow.Resize(fyne.NewSize(400, 250))
|
||||
progressWindow.CenterOnScreen()
|
||||
|
||||
// Progress bar
|
||||
progressBar := widget.NewProgressBar()
|
||||
progressBar.SetValue(0)
|
||||
|
||||
// Status label
|
||||
statusLabel := widget.NewLabel("Preparing download...")
|
||||
|
||||
// Release notes button
|
||||
releaseNotesButton := widget.NewButton("View Release Notes", func() {
|
||||
releaseNotesURL, err := sm.launcher.githubReleaseNotesURL(version)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse URL: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
sm.app.OpenURL(releaseNotesURL)
|
||||
})
|
||||
|
||||
// Progress container
|
||||
progressContainer := container.NewVBox(
|
||||
widget.NewLabel(fmt.Sprintf("Downloading LocalAI version %s", version)),
|
||||
progressBar,
|
||||
statusLabel,
|
||||
widget.NewSeparator(),
|
||||
releaseNotesButton,
|
||||
)
|
||||
|
||||
progressWindow.SetContent(progressContainer)
|
||||
progressWindow.Show()
|
||||
|
||||
// Start download in background
|
||||
go func() {
|
||||
err := sm.launcher.DownloadUpdate(version, func(progress float64) {
|
||||
// Update progress bar
|
||||
fyne.Do(func() {
|
||||
progressBar.SetValue(progress)
|
||||
percentage := int(progress * 100)
|
||||
statusLabel.SetText(fmt.Sprintf("Downloading... %d%%", percentage))
|
||||
})
|
||||
})
|
||||
|
||||
// Handle completion
|
||||
fyne.Do(func() {
|
||||
if err != nil {
|
||||
statusLabel.SetText(fmt.Sprintf("Download failed: %v", err))
|
||||
// Show error dialog
|
||||
dialog.ShowError(err, progressWindow)
|
||||
} else {
|
||||
statusLabel.SetText("Download completed successfully!")
|
||||
progressBar.SetValue(1.0)
|
||||
|
||||
// Show restart dialog
|
||||
dialog.ShowConfirm("Update Downloaded",
|
||||
"LocalAI has been updated successfully. Please restart the launcher to use the new version.",
|
||||
func(restart bool) {
|
||||
if restart {
|
||||
sm.app.Quit()
|
||||
}
|
||||
progressWindow.Close()
|
||||
}, progressWindow)
|
||||
}
|
||||
})
|
||||
|
||||
// Update systray menu
|
||||
if err == nil {
|
||||
sm.hasUpdateAvailable = false
|
||||
sm.latestVersion = ""
|
||||
sm.recreateMenu()
|
||||
}
|
||||
}()
|
||||
}
|
||||
677
cmd/launcher/internal/ui.go
Normal file
677
cmd/launcher/internal/ui.go
Normal file
@@ -0,0 +1,677 @@
|
||||
package launcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// EnvVar represents an environment variable
|
||||
type EnvVar struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// LauncherUI handles the user interface
|
||||
type LauncherUI struct {
|
||||
// Status display
|
||||
statusLabel *widget.Label
|
||||
versionLabel *widget.Label
|
||||
|
||||
// Control buttons
|
||||
startStopButton *widget.Button
|
||||
webUIButton *widget.Button
|
||||
updateButton *widget.Button
|
||||
downloadButton *widget.Button
|
||||
|
||||
// Configuration
|
||||
modelsPathEntry *widget.Entry
|
||||
backendsPathEntry *widget.Entry
|
||||
addressEntry *widget.Entry
|
||||
logLevelSelect *widget.Select
|
||||
startOnBootCheck *widget.Check
|
||||
|
||||
// Environment Variables
|
||||
envVarsData []EnvVar
|
||||
newEnvKeyEntry *widget.Entry
|
||||
newEnvValueEntry *widget.Entry
|
||||
updateEnvironmentDisplay func()
|
||||
|
||||
// Logs
|
||||
logText *widget.Entry
|
||||
|
||||
// Progress
|
||||
progressBar *widget.ProgressBar
|
||||
|
||||
// Update management
|
||||
latestVersion string
|
||||
|
||||
// Reference to launcher
|
||||
launcher *Launcher
|
||||
}
|
||||
|
||||
// NewLauncherUI creates a new UI instance
|
||||
func NewLauncherUI() *LauncherUI {
|
||||
return &LauncherUI{
|
||||
statusLabel: widget.NewLabel("Initializing..."),
|
||||
versionLabel: widget.NewLabel("Version: Unknown"),
|
||||
startStopButton: widget.NewButton("Start LocalAI", nil),
|
||||
webUIButton: widget.NewButton("Open WebUI", nil),
|
||||
updateButton: widget.NewButton("Check for Updates", nil),
|
||||
modelsPathEntry: widget.NewEntry(),
|
||||
backendsPathEntry: widget.NewEntry(),
|
||||
addressEntry: widget.NewEntry(),
|
||||
logLevelSelect: widget.NewSelect([]string{"error", "warn", "info", "debug", "trace"}, nil),
|
||||
startOnBootCheck: widget.NewCheck("Start LocalAI on system boot", nil),
|
||||
logText: widget.NewMultiLineEntry(),
|
||||
progressBar: widget.NewProgressBar(),
|
||||
envVarsData: []EnvVar{}, // Initialize the environment variables slice
|
||||
}
|
||||
}
|
||||
|
||||
// CreateMainUI creates the main UI layout
|
||||
func (ui *LauncherUI) CreateMainUI(launcher *Launcher) *fyne.Container {
|
||||
ui.launcher = launcher
|
||||
ui.setupBindings()
|
||||
|
||||
// Main tab with status and controls
|
||||
// Configuration is now the main content
|
||||
configTab := ui.createConfigTab()
|
||||
|
||||
// Create a simple container instead of tabs since we only have settings
|
||||
tabs := container.NewVBox(
|
||||
widget.NewCard("LocalAI Launcher Settings", "", configTab),
|
||||
)
|
||||
|
||||
return tabs
|
||||
}
|
||||
|
||||
// createConfigTab creates the configuration tab
|
||||
func (ui *LauncherUI) createConfigTab() *fyne.Container {
|
||||
// Path configuration
|
||||
pathsCard := widget.NewCard("Paths", "", container.NewGridWithColumns(2,
|
||||
widget.NewLabel("Models Path:"),
|
||||
ui.modelsPathEntry,
|
||||
widget.NewLabel("Backends Path:"),
|
||||
ui.backendsPathEntry,
|
||||
))
|
||||
|
||||
// Server configuration
|
||||
serverCard := widget.NewCard("Server", "", container.NewVBox(
|
||||
container.NewGridWithColumns(2,
|
||||
widget.NewLabel("Address:"),
|
||||
ui.addressEntry,
|
||||
widget.NewLabel("Log Level:"),
|
||||
ui.logLevelSelect,
|
||||
),
|
||||
ui.startOnBootCheck,
|
||||
))
|
||||
|
||||
// Save button
|
||||
saveButton := widget.NewButton("Save Configuration", func() {
|
||||
ui.saveConfiguration()
|
||||
})
|
||||
|
||||
// Environment Variables section
|
||||
envCard := ui.createEnvironmentSection()
|
||||
|
||||
return container.NewVBox(
|
||||
pathsCard,
|
||||
serverCard,
|
||||
envCard,
|
||||
saveButton,
|
||||
)
|
||||
}
|
||||
|
||||
// createEnvironmentSection creates the environment variables section for the config tab
|
||||
func (ui *LauncherUI) createEnvironmentSection() *fyne.Container {
|
||||
// Initialize environment variables widgets
|
||||
ui.newEnvKeyEntry = widget.NewEntry()
|
||||
ui.newEnvKeyEntry.SetPlaceHolder("Environment Variable Name")
|
||||
|
||||
ui.newEnvValueEntry = widget.NewEntry()
|
||||
ui.newEnvValueEntry.SetPlaceHolder("Environment Variable Value")
|
||||
|
||||
// Add button
|
||||
addButton := widget.NewButton("Add Environment Variable", func() {
|
||||
ui.addEnvironmentVariable()
|
||||
})
|
||||
|
||||
// Environment variables list with delete buttons
|
||||
ui.envVarsData = []EnvVar{}
|
||||
|
||||
// Create container for environment variables
|
||||
envVarsContainer := container.NewVBox()
|
||||
|
||||
// Update function to rebuild the environment variables display
|
||||
ui.updateEnvironmentDisplay = func() {
|
||||
envVarsContainer.Objects = nil
|
||||
for i, envVar := range ui.envVarsData {
|
||||
index := i // Capture index for closure
|
||||
|
||||
// Create row with label and delete button
|
||||
envLabel := widget.NewLabel(fmt.Sprintf("%s = %s", envVar.Key, envVar.Value))
|
||||
deleteBtn := widget.NewButton("Delete", func() {
|
||||
ui.confirmDeleteEnvironmentVariable(index)
|
||||
})
|
||||
deleteBtn.Importance = widget.DangerImportance
|
||||
|
||||
row := container.NewBorder(nil, nil, nil, deleteBtn, envLabel)
|
||||
envVarsContainer.Add(row)
|
||||
}
|
||||
envVarsContainer.Refresh()
|
||||
}
|
||||
|
||||
// Create a scrollable container for the environment variables
|
||||
envScroll := container.NewScroll(envVarsContainer)
|
||||
envScroll.SetMinSize(fyne.NewSize(400, 150))
|
||||
|
||||
// Input section for adding new environment variables
|
||||
inputSection := container.NewVBox(
|
||||
container.NewGridWithColumns(2,
|
||||
ui.newEnvKeyEntry,
|
||||
ui.newEnvValueEntry,
|
||||
),
|
||||
addButton,
|
||||
)
|
||||
|
||||
// Environment variables card
|
||||
envCard := widget.NewCard("Environment Variables", "", container.NewVBox(
|
||||
inputSection,
|
||||
widget.NewSeparator(),
|
||||
envScroll,
|
||||
))
|
||||
|
||||
return container.NewVBox(envCard)
|
||||
}
|
||||
|
||||
// addEnvironmentVariable adds a new environment variable
|
||||
func (ui *LauncherUI) addEnvironmentVariable() {
|
||||
key := ui.newEnvKeyEntry.Text
|
||||
value := ui.newEnvValueEntry.Text
|
||||
|
||||
log.Printf("addEnvironmentVariable: attempting to add %s=%s", key, value)
|
||||
log.Printf("addEnvironmentVariable: current ui.envVarsData has %d items: %v", len(ui.envVarsData), ui.envVarsData)
|
||||
|
||||
if key == "" {
|
||||
log.Printf("addEnvironmentVariable: key is empty, showing error")
|
||||
dialog.ShowError(fmt.Errorf("environment variable name cannot be empty"), ui.launcher.window)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if key already exists
|
||||
for _, envVar := range ui.envVarsData {
|
||||
if envVar.Key == key {
|
||||
log.Printf("addEnvironmentVariable: key %s already exists, showing error", key)
|
||||
dialog.ShowError(fmt.Errorf("environment variable '%s' already exists", key), ui.launcher.window)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("addEnvironmentVariable: adding new env var %s=%s", key, value)
|
||||
ui.envVarsData = append(ui.envVarsData, EnvVar{Key: key, Value: value})
|
||||
log.Printf("addEnvironmentVariable: after adding, ui.envVarsData has %d items: %v", len(ui.envVarsData), ui.envVarsData)
|
||||
|
||||
fyne.Do(func() {
|
||||
if ui.updateEnvironmentDisplay != nil {
|
||||
ui.updateEnvironmentDisplay()
|
||||
}
|
||||
// Clear input fields
|
||||
ui.newEnvKeyEntry.SetText("")
|
||||
ui.newEnvValueEntry.SetText("")
|
||||
})
|
||||
|
||||
log.Printf("addEnvironmentVariable: calling saveEnvironmentVariables")
|
||||
// Save to configuration
|
||||
ui.saveEnvironmentVariables()
|
||||
}
|
||||
|
||||
// removeEnvironmentVariable removes an environment variable by index
|
||||
func (ui *LauncherUI) removeEnvironmentVariable(index int) {
|
||||
if index >= 0 && index < len(ui.envVarsData) {
|
||||
ui.envVarsData = append(ui.envVarsData[:index], ui.envVarsData[index+1:]...)
|
||||
fyne.Do(func() {
|
||||
if ui.updateEnvironmentDisplay != nil {
|
||||
ui.updateEnvironmentDisplay()
|
||||
}
|
||||
})
|
||||
ui.saveEnvironmentVariables()
|
||||
}
|
||||
}
|
||||
|
||||
// saveEnvironmentVariables saves environment variables to the configuration
|
||||
func (ui *LauncherUI) saveEnvironmentVariables() {
|
||||
if ui.launcher == nil {
|
||||
log.Printf("saveEnvironmentVariables: launcher is nil")
|
||||
return
|
||||
}
|
||||
|
||||
config := ui.launcher.GetConfig()
|
||||
log.Printf("saveEnvironmentVariables: before - Environment vars: %v", config.EnvironmentVars)
|
||||
|
||||
config.EnvironmentVars = make(map[string]string)
|
||||
for _, envVar := range ui.envVarsData {
|
||||
config.EnvironmentVars[envVar.Key] = envVar.Value
|
||||
log.Printf("saveEnvironmentVariables: adding %s=%s", envVar.Key, envVar.Value)
|
||||
}
|
||||
|
||||
log.Printf("saveEnvironmentVariables: after - Environment vars: %v", config.EnvironmentVars)
|
||||
log.Printf("saveEnvironmentVariables: calling SetConfig with %d environment variables", len(config.EnvironmentVars))
|
||||
|
||||
err := ui.launcher.SetConfig(config)
|
||||
if err != nil {
|
||||
log.Printf("saveEnvironmentVariables: failed to save config: %v", err)
|
||||
} else {
|
||||
log.Printf("saveEnvironmentVariables: config saved successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// confirmDeleteEnvironmentVariable shows confirmation dialog for deleting an environment variable
|
||||
func (ui *LauncherUI) confirmDeleteEnvironmentVariable(index int) {
|
||||
if index >= 0 && index < len(ui.envVarsData) {
|
||||
envVar := ui.envVarsData[index]
|
||||
dialog.ShowConfirm("Remove Environment Variable",
|
||||
fmt.Sprintf("Remove environment variable '%s'?", envVar.Key),
|
||||
func(remove bool) {
|
||||
if remove {
|
||||
ui.removeEnvironmentVariable(index)
|
||||
}
|
||||
}, ui.launcher.window)
|
||||
}
|
||||
}
|
||||
|
||||
// setupBindings sets up event handlers for UI elements
|
||||
func (ui *LauncherUI) setupBindings() {
|
||||
// Start/Stop button
|
||||
ui.startStopButton.OnTapped = func() {
|
||||
if ui.launcher.IsRunning() {
|
||||
ui.stopLocalAI()
|
||||
} else {
|
||||
ui.startLocalAI()
|
||||
}
|
||||
}
|
||||
|
||||
// WebUI button
|
||||
ui.webUIButton.OnTapped = func() {
|
||||
ui.openWebUI()
|
||||
}
|
||||
ui.webUIButton.Disable() // Disabled until LocalAI is running
|
||||
|
||||
// Update button
|
||||
ui.updateButton.OnTapped = func() {
|
||||
ui.checkForUpdates()
|
||||
}
|
||||
|
||||
// Log level selection
|
||||
ui.logLevelSelect.OnChanged = func(selected string) {
|
||||
if ui.launcher != nil {
|
||||
config := ui.launcher.GetConfig()
|
||||
config.LogLevel = selected
|
||||
ui.launcher.SetConfig(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startLocalAI starts the LocalAI service
|
||||
func (ui *LauncherUI) startLocalAI() {
|
||||
fyne.Do(func() {
|
||||
ui.startStopButton.Disable()
|
||||
})
|
||||
ui.UpdateStatus("Starting LocalAI...")
|
||||
|
||||
go func() {
|
||||
err := ui.launcher.StartLocalAI()
|
||||
if err != nil {
|
||||
ui.UpdateStatus("Failed to start: " + err.Error())
|
||||
fyne.DoAndWait(func() {
|
||||
dialog.ShowError(err, ui.launcher.window)
|
||||
})
|
||||
} else {
|
||||
fyne.Do(func() {
|
||||
ui.startStopButton.SetText("Stop LocalAI")
|
||||
ui.webUIButton.Enable()
|
||||
})
|
||||
}
|
||||
fyne.Do(func() {
|
||||
ui.startStopButton.Enable()
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// stopLocalAI stops the LocalAI service
|
||||
func (ui *LauncherUI) stopLocalAI() {
|
||||
fyne.Do(func() {
|
||||
ui.startStopButton.Disable()
|
||||
})
|
||||
ui.UpdateStatus("Stopping LocalAI...")
|
||||
|
||||
go func() {
|
||||
err := ui.launcher.StopLocalAI()
|
||||
if err != nil {
|
||||
fyne.DoAndWait(func() {
|
||||
dialog.ShowError(err, ui.launcher.window)
|
||||
})
|
||||
} else {
|
||||
fyne.Do(func() {
|
||||
ui.startStopButton.SetText("Start LocalAI")
|
||||
ui.webUIButton.Disable()
|
||||
})
|
||||
}
|
||||
fyne.Do(func() {
|
||||
ui.startStopButton.Enable()
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// openWebUI opens the LocalAI WebUI in the default browser
|
||||
func (ui *LauncherUI) openWebUI() {
|
||||
webURL := ui.launcher.GetWebUIURL()
|
||||
parsedURL, err := url.Parse(webURL)
|
||||
if err != nil {
|
||||
dialog.ShowError(err, ui.launcher.window)
|
||||
return
|
||||
}
|
||||
|
||||
// Open URL in default browser
|
||||
fyne.CurrentApp().OpenURL(parsedURL)
|
||||
}
|
||||
|
||||
// saveConfiguration saves the current configuration
|
||||
func (ui *LauncherUI) saveConfiguration() {
|
||||
log.Printf("saveConfiguration: starting to save configuration")
|
||||
|
||||
config := ui.launcher.GetConfig()
|
||||
log.Printf("saveConfiguration: current config Environment vars: %v", config.EnvironmentVars)
|
||||
log.Printf("saveConfiguration: ui.envVarsData has %d items: %v", len(ui.envVarsData), ui.envVarsData)
|
||||
|
||||
config.ModelsPath = ui.modelsPathEntry.Text
|
||||
config.BackendsPath = ui.backendsPathEntry.Text
|
||||
config.Address = ui.addressEntry.Text
|
||||
config.LogLevel = ui.logLevelSelect.Selected
|
||||
config.StartOnBoot = ui.startOnBootCheck.Checked
|
||||
|
||||
// Ensure environment variables are included in the configuration
|
||||
config.EnvironmentVars = make(map[string]string)
|
||||
for _, envVar := range ui.envVarsData {
|
||||
config.EnvironmentVars[envVar.Key] = envVar.Value
|
||||
log.Printf("saveConfiguration: adding env var %s=%s", envVar.Key, envVar.Value)
|
||||
}
|
||||
|
||||
log.Printf("saveConfiguration: final config Environment vars: %v", config.EnvironmentVars)
|
||||
|
||||
err := ui.launcher.SetConfig(config)
|
||||
if err != nil {
|
||||
log.Printf("saveConfiguration: failed to save config: %v", err)
|
||||
dialog.ShowError(err, ui.launcher.window)
|
||||
} else {
|
||||
log.Printf("saveConfiguration: config saved successfully")
|
||||
dialog.ShowInformation("Configuration", "Configuration saved successfully", ui.launcher.window)
|
||||
}
|
||||
}
|
||||
|
||||
// checkForUpdates checks for available updates
|
||||
func (ui *LauncherUI) checkForUpdates() {
|
||||
fyne.Do(func() {
|
||||
ui.updateButton.Disable()
|
||||
})
|
||||
ui.UpdateStatus("Checking for updates...")
|
||||
|
||||
go func() {
|
||||
available, version, err := ui.launcher.CheckForUpdates()
|
||||
if err != nil {
|
||||
ui.UpdateStatus("Failed to check updates: " + err.Error())
|
||||
fyne.DoAndWait(func() {
|
||||
dialog.ShowError(err, ui.launcher.window)
|
||||
})
|
||||
} else if available {
|
||||
ui.latestVersion = version // Store the latest version
|
||||
ui.UpdateStatus("Update available: " + version)
|
||||
fyne.Do(func() {
|
||||
if ui.downloadButton != nil {
|
||||
ui.downloadButton.Enable()
|
||||
}
|
||||
})
|
||||
ui.NotifyUpdateAvailable(version)
|
||||
} else {
|
||||
ui.UpdateStatus("No updates available")
|
||||
fyne.DoAndWait(func() {
|
||||
dialog.ShowInformation("Updates", "You are running the latest version", ui.launcher.window)
|
||||
})
|
||||
}
|
||||
fyne.Do(func() {
|
||||
ui.updateButton.Enable()
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// downloadUpdate downloads the latest update
|
||||
func (ui *LauncherUI) downloadUpdate() {
|
||||
// Use stored version or check for updates
|
||||
version := ui.latestVersion
|
||||
if version == "" {
|
||||
_, v, err := ui.launcher.CheckForUpdates()
|
||||
if err != nil {
|
||||
dialog.ShowError(err, ui.launcher.window)
|
||||
return
|
||||
}
|
||||
version = v
|
||||
ui.latestVersion = version
|
||||
}
|
||||
|
||||
if version == "" {
|
||||
dialog.ShowError(fmt.Errorf("no version information available"), ui.launcher.window)
|
||||
return
|
||||
}
|
||||
|
||||
// Disable buttons during download
|
||||
if ui.downloadButton != nil {
|
||||
fyne.Do(func() {
|
||||
ui.downloadButton.Disable()
|
||||
})
|
||||
}
|
||||
|
||||
fyne.Do(func() {
|
||||
ui.progressBar.Show()
|
||||
ui.progressBar.SetValue(0)
|
||||
})
|
||||
ui.UpdateStatus("Downloading update " + version + "...")
|
||||
|
||||
go func() {
|
||||
err := ui.launcher.DownloadUpdate(version, func(progress float64) {
|
||||
// Update progress bar
|
||||
fyne.Do(func() {
|
||||
ui.progressBar.SetValue(progress)
|
||||
})
|
||||
// Update status with percentage
|
||||
percentage := int(progress * 100)
|
||||
ui.UpdateStatus(fmt.Sprintf("Downloading update %s... %d%%", version, percentage))
|
||||
})
|
||||
|
||||
fyne.Do(func() {
|
||||
ui.progressBar.Hide()
|
||||
})
|
||||
|
||||
// Re-enable buttons after download
|
||||
if ui.downloadButton != nil {
|
||||
fyne.Do(func() {
|
||||
ui.downloadButton.Enable()
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fyne.DoAndWait(func() {
|
||||
ui.UpdateStatus("Failed to download update: " + err.Error())
|
||||
dialog.ShowError(err, ui.launcher.window)
|
||||
})
|
||||
} else {
|
||||
fyne.DoAndWait(func() {
|
||||
ui.UpdateStatus("Update downloaded successfully")
|
||||
dialog.ShowInformation("Update", "Update downloaded successfully. Please restart the launcher to use the new version.", ui.launcher.window)
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// UpdateStatus updates the status label
|
||||
func (ui *LauncherUI) UpdateStatus(status string) {
|
||||
if ui.statusLabel != nil {
|
||||
fyne.Do(func() {
|
||||
ui.statusLabel.SetText(status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OnLogUpdate handles new log content
|
||||
func (ui *LauncherUI) OnLogUpdate(logLine string) {
|
||||
if ui.logText != nil {
|
||||
fyne.Do(func() {
|
||||
currentText := ui.logText.Text
|
||||
ui.logText.SetText(currentText + logLine)
|
||||
|
||||
// Auto-scroll to bottom (simplified)
|
||||
ui.logText.CursorRow = len(ui.logText.Text)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyUpdateAvailable shows an update notification
|
||||
func (ui *LauncherUI) NotifyUpdateAvailable(version string) {
|
||||
if ui.launcher != nil && ui.launcher.window != nil {
|
||||
fyne.DoAndWait(func() {
|
||||
dialog.ShowConfirm("Update Available",
|
||||
"A new version ("+version+") is available. Would you like to download it?",
|
||||
func(confirmed bool) {
|
||||
if confirmed {
|
||||
ui.downloadUpdate()
|
||||
}
|
||||
}, ui.launcher.window)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfiguration loads the current configuration into UI elements
|
||||
func (ui *LauncherUI) LoadConfiguration() {
|
||||
if ui.launcher == nil {
|
||||
log.Printf("UI LoadConfiguration: launcher is nil")
|
||||
return
|
||||
}
|
||||
|
||||
config := ui.launcher.GetConfig()
|
||||
log.Printf("UI LoadConfiguration: loading config - ModelsPath=%s, BackendsPath=%s, Address=%s, LogLevel=%s",
|
||||
config.ModelsPath, config.BackendsPath, config.Address, config.LogLevel)
|
||||
log.Printf("UI LoadConfiguration: Environment vars: %v", config.EnvironmentVars)
|
||||
|
||||
ui.modelsPathEntry.SetText(config.ModelsPath)
|
||||
ui.backendsPathEntry.SetText(config.BackendsPath)
|
||||
ui.addressEntry.SetText(config.Address)
|
||||
ui.logLevelSelect.SetSelected(config.LogLevel)
|
||||
ui.startOnBootCheck.SetChecked(config.StartOnBoot)
|
||||
|
||||
// Load environment variables
|
||||
ui.envVarsData = []EnvVar{}
|
||||
for key, value := range config.EnvironmentVars {
|
||||
ui.envVarsData = append(ui.envVarsData, EnvVar{Key: key, Value: value})
|
||||
}
|
||||
if ui.updateEnvironmentDisplay != nil {
|
||||
fyne.Do(func() {
|
||||
ui.updateEnvironmentDisplay()
|
||||
})
|
||||
}
|
||||
|
||||
// Update version display
|
||||
version := ui.launcher.GetCurrentVersion()
|
||||
ui.versionLabel.SetText("Version: " + version)
|
||||
|
||||
log.Printf("UI LoadConfiguration: configuration loaded successfully")
|
||||
}
|
||||
|
||||
// showDownloadProgress shows a progress window for downloading LocalAI
|
||||
func (ui *LauncherUI) showDownloadProgress(version, title string) {
|
||||
fyne.DoAndWait(func() {
|
||||
// Create progress window using the launcher's app
|
||||
progressWindow := ui.launcher.app.NewWindow("Downloading LocalAI")
|
||||
progressWindow.Resize(fyne.NewSize(400, 250))
|
||||
progressWindow.CenterOnScreen()
|
||||
|
||||
// Progress bar
|
||||
progressBar := widget.NewProgressBar()
|
||||
progressBar.SetValue(0)
|
||||
|
||||
// Status label
|
||||
statusLabel := widget.NewLabel("Preparing download...")
|
||||
|
||||
// Release notes button
|
||||
releaseNotesButton := widget.NewButton("View Release Notes", func() {
|
||||
releaseNotesURL, err := ui.launcher.githubReleaseNotesURL(version)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse URL: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ui.launcher.app.OpenURL(releaseNotesURL)
|
||||
})
|
||||
|
||||
// Progress container
|
||||
progressContainer := container.NewVBox(
|
||||
widget.NewLabel(title),
|
||||
progressBar,
|
||||
statusLabel,
|
||||
widget.NewSeparator(),
|
||||
releaseNotesButton,
|
||||
)
|
||||
|
||||
progressWindow.SetContent(progressContainer)
|
||||
progressWindow.Show()
|
||||
|
||||
// Start download in background
|
||||
go func() {
|
||||
err := ui.launcher.DownloadUpdate(version, func(progress float64) {
|
||||
// Update progress bar
|
||||
fyne.Do(func() {
|
||||
progressBar.SetValue(progress)
|
||||
percentage := int(progress * 100)
|
||||
statusLabel.SetText(fmt.Sprintf("Downloading... %d%%", percentage))
|
||||
})
|
||||
})
|
||||
|
||||
// Handle completion
|
||||
fyne.Do(func() {
|
||||
if err != nil {
|
||||
statusLabel.SetText(fmt.Sprintf("Download failed: %v", err))
|
||||
// Show error dialog
|
||||
dialog.ShowError(err, progressWindow)
|
||||
} else {
|
||||
statusLabel.SetText("Download completed successfully!")
|
||||
progressBar.SetValue(1.0)
|
||||
|
||||
// Show success dialog
|
||||
dialog.ShowConfirm("Installation Complete",
|
||||
"LocalAI has been downloaded and installed successfully. You can now start LocalAI from the launcher.",
|
||||
func(close bool) {
|
||||
progressWindow.Close()
|
||||
// Update status
|
||||
ui.UpdateStatus("LocalAI installed successfully")
|
||||
}, progressWindow)
|
||||
}
|
||||
})
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRunningState updates UI based on LocalAI running state
|
||||
func (ui *LauncherUI) UpdateRunningState(isRunning bool) {
|
||||
fyne.Do(func() {
|
||||
if isRunning {
|
||||
ui.startStopButton.SetText("Stop LocalAI")
|
||||
ui.webUIButton.Enable()
|
||||
} else {
|
||||
ui.startStopButton.SetText("Start LocalAI")
|
||||
ui.webUIButton.Disable()
|
||||
}
|
||||
})
|
||||
}
|
||||
BIN
cmd/launcher/logo.png
Normal file
BIN
cmd/launcher/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
86
cmd/launcher/main.go
Normal file
86
cmd/launcher/main.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
coreLauncher "github.com/mudler/LocalAI/cmd/launcher/internal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create the application with unique ID
|
||||
myApp := app.NewWithID("com.localai.launcher")
|
||||
myApp.SetIcon(resourceIconPng)
|
||||
myWindow := myApp.NewWindow("LocalAI Launcher")
|
||||
myWindow.Resize(fyne.NewSize(800, 600))
|
||||
|
||||
// Create the launcher UI
|
||||
ui := coreLauncher.NewLauncherUI()
|
||||
|
||||
// Initialize the launcher with UI context
|
||||
launcher := coreLauncher.NewLauncher(ui, myWindow, myApp)
|
||||
|
||||
// Setup the UI
|
||||
content := ui.CreateMainUI(launcher)
|
||||
myWindow.SetContent(content)
|
||||
|
||||
// Setup window close behavior - minimize to tray instead of closing
|
||||
myWindow.SetCloseIntercept(func() {
|
||||
myWindow.Hide()
|
||||
})
|
||||
|
||||
// Setup system tray using Fyne's built-in approach``
|
||||
if desk, ok := myApp.(desktop.App); ok {
|
||||
// Create a dynamic systray manager
|
||||
systray := coreLauncher.NewSystrayManager(launcher, myWindow, desk, myApp, resourceIconPng)
|
||||
launcher.SetSystray(systray)
|
||||
}
|
||||
|
||||
// Setup signal handling for graceful shutdown
|
||||
setupSignalHandling(launcher)
|
||||
|
||||
// Initialize the launcher state
|
||||
go func() {
|
||||
if err := launcher.Initialize(); err != nil {
|
||||
log.Printf("Failed to initialize launcher: %v", err)
|
||||
if launcher.GetUI() != nil {
|
||||
launcher.GetUI().UpdateStatus("Failed to initialize: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
// Load configuration into UI
|
||||
launcher.GetUI().LoadConfiguration()
|
||||
launcher.GetUI().UpdateStatus("Ready")
|
||||
}
|
||||
}()
|
||||
|
||||
// Run the application in background (window only shown when "Settings" is clicked)
|
||||
myApp.Run()
|
||||
}
|
||||
|
||||
// setupSignalHandling sets up signal handlers for graceful shutdown
|
||||
func setupSignalHandling(launcher *coreLauncher.Launcher) {
|
||||
// Create a channel to receive OS signals
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
|
||||
// Register for interrupt and terminate signals
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Handle signals in a separate goroutine
|
||||
go func() {
|
||||
sig := <-sigChan
|
||||
log.Printf("Received signal %v, shutting down gracefully...", sig)
|
||||
|
||||
// Perform cleanup
|
||||
if err := launcher.Shutdown(); err != nil {
|
||||
log.Printf("Error during shutdown: %v", err)
|
||||
}
|
||||
|
||||
// Exit the application
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func main() {
|
||||
|
||||
for _, envFile := range envFiles {
|
||||
if _, err := os.Stat(envFile); err == nil {
|
||||
log.Info().Str("envFile", envFile).Msg("env file found, loading environment variables from file")
|
||||
log.Debug().Str("envFile", envFile).Msg("env file found, loading environment variables from file")
|
||||
err = godotenv.Load(envFile)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("envFile", envFile).Msg("failed to load environment variables from file")
|
||||
@@ -97,19 +97,19 @@ Version: ${version}
|
||||
switch *cli.CLI.LogLevel {
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
log.Info().Msg("Setting logging to error")
|
||||
log.Debug().Msg("Setting logging to error")
|
||||
case "warn":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
log.Info().Msg("Setting logging to warn")
|
||||
log.Debug().Msg("Setting logging to warn")
|
||||
case "info":
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
log.Info().Msg("Setting logging to info")
|
||||
log.Debug().Msg("Setting logging to info")
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
log.Debug().Msg("Setting logging to debug")
|
||||
case "trace":
|
||||
zerolog.SetGlobalLevel(zerolog.TraceLevel)
|
||||
log.Trace().Msg("Setting logging to trace")
|
||||
log.Debug().Msg("Setting logging to trace")
|
||||
}
|
||||
|
||||
// Run the thing!
|
||||
@@ -56,12 +56,12 @@ func New(opts ...config.AppOption) (*Application, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := coreStartup.InstallModels(options.Galleries, options.BackendGalleries, options.SystemState, options.EnforcePredownloadScans, options.AutoloadBackendGalleries, nil, options.ModelsURL...); err != nil {
|
||||
if err := coreStartup.InstallModels(options.Galleries, options.BackendGalleries, options.SystemState, application.ModelLoader(), options.EnforcePredownloadScans, options.AutoloadBackendGalleries, nil, options.ModelsURL...); err != nil {
|
||||
log.Error().Err(err).Msg("error installing models")
|
||||
}
|
||||
|
||||
for _, backend := range options.ExternalBackends {
|
||||
if err := coreStartup.InstallExternalBackends(options.BackendGalleries, options.SystemState, nil, backend, "", ""); err != nil {
|
||||
if err := coreStartup.InstallExternalBackends(options.BackendGalleries, options.SystemState, application.ModelLoader(), nil, backend, "", ""); err != nil {
|
||||
log.Error().Err(err).Msg("error installing external backend")
|
||||
}
|
||||
}
|
||||
@@ -87,13 +87,13 @@ func New(opts ...config.AppOption) (*Application, error) {
|
||||
}
|
||||
|
||||
if options.PreloadJSONModels != "" {
|
||||
if err := services.ApplyGalleryFromString(options.SystemState, options.EnforcePredownloadScans, options.AutoloadBackendGalleries, options.Galleries, options.BackendGalleries, options.PreloadJSONModels); err != nil {
|
||||
if err := services.ApplyGalleryFromString(options.SystemState, application.ModelLoader(), options.EnforcePredownloadScans, options.AutoloadBackendGalleries, options.Galleries, options.BackendGalleries, options.PreloadJSONModels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if options.PreloadModelsFromPath != "" {
|
||||
if err := services.ApplyGalleryFromFile(options.SystemState, options.EnforcePredownloadScans, options.AutoloadBackendGalleries, options.Galleries, options.BackendGalleries, options.PreloadModelsFromPath); err != nil {
|
||||
if err := services.ApplyGalleryFromFile(options.SystemState, application.ModelLoader(), options.EnforcePredownloadScans, options.AutoloadBackendGalleries, options.Galleries, options.BackendGalleries, options.PreloadModelsFromPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func ModelInference(ctx context.Context, s string, messages []schema.Message, im
|
||||
if !slices.Contains(modelNames, c.Name) {
|
||||
utils.ResetDownloadTimers()
|
||||
// if we failed to load the model, we try to download it
|
||||
err := gallery.InstallModelFromGallery(o.Galleries, o.BackendGalleries, o.SystemState, c.Name, gallery.GalleryModel{}, utils.DisplayDownloadFunction, o.EnforcePredownloadScans, o.AutoloadBackendGalleries)
|
||||
err := gallery.InstallModelFromGallery(o.Galleries, o.BackendGalleries, o.SystemState, loader, c.Name, gallery.GalleryModel{}, utils.DisplayDownloadFunction, o.EnforcePredownloadScans, o.AutoloadBackendGalleries)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed to install model %q from gallery", modelFile)
|
||||
//return nil, err
|
||||
|
||||
@@ -78,6 +78,12 @@ func grpcModelOpts(c config.ModelConfig) *pb.ModelOptions {
|
||||
b = c.Batch
|
||||
}
|
||||
|
||||
flashAttention := "auto"
|
||||
|
||||
if c.FlashAttention != nil {
|
||||
flashAttention = *c.FlashAttention
|
||||
}
|
||||
|
||||
f16 := false
|
||||
if c.F16 != nil {
|
||||
f16 = *c.F16
|
||||
@@ -166,7 +172,7 @@ func grpcModelOpts(c config.ModelConfig) *pb.ModelOptions {
|
||||
LimitVideoPerPrompt: int32(c.LimitMMPerPrompt.LimitVideoPerPrompt),
|
||||
LimitAudioPerPrompt: int32(c.LimitMMPerPrompt.LimitAudioPerPrompt),
|
||||
MMProj: c.MMProj,
|
||||
FlashAttention: c.FlashAttention,
|
||||
FlashAttention: flashAttention,
|
||||
CacheTypeKey: c.CacheTypeK,
|
||||
CacheTypeValue: c.CacheTypeV,
|
||||
NoKVOffload: c.NoKVOffloading,
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
model "github.com/mudler/LocalAI/pkg/model"
|
||||
)
|
||||
|
||||
func VideoGeneration(height, width int32, prompt, startImage, endImage, dst string, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig) (func() error, error) {
|
||||
func VideoGeneration(height, width int32, prompt, negativePrompt, startImage, endImage, dst string, numFrames, fps, seed int32, cfgScale float32, step int32, loader *model.ModelLoader, modelConfig config.ModelConfig, appConfig *config.ApplicationConfig) (func() error, error) {
|
||||
|
||||
opts := ModelOptions(modelConfig, appConfig)
|
||||
inferenceModel, err := loader.Load(
|
||||
@@ -22,12 +22,18 @@ func VideoGeneration(height, width int32, prompt, startImage, endImage, dst stri
|
||||
_, err := inferenceModel.GenerateVideo(
|
||||
appConfig.Context,
|
||||
&proto.GenerateVideoRequest{
|
||||
Height: height,
|
||||
Width: width,
|
||||
Prompt: prompt,
|
||||
StartImage: startImage,
|
||||
EndImage: endImage,
|
||||
Dst: dst,
|
||||
Height: height,
|
||||
Width: width,
|
||||
Prompt: prompt,
|
||||
NegativePrompt: negativePrompt,
|
||||
StartImage: startImage,
|
||||
EndImage: endImage,
|
||||
NumFrames: numFrames,
|
||||
Fps: fps,
|
||||
Seed: seed,
|
||||
CfgScale: cfgScale,
|
||||
Step: step,
|
||||
Dst: dst,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
cliContext "github.com/mudler/LocalAI/core/cli/context"
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
|
||||
"github.com/mudler/LocalAI/core/gallery"
|
||||
@@ -100,7 +101,8 @@ func (bi *BackendsInstall) Run(ctx *cliContext.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = startup.InstallExternalBackends(galleries, systemState, progressCallback, bi.BackendArgs, bi.Name, bi.Alias)
|
||||
modelLoader := model.NewModelLoader(systemState, true)
|
||||
err = startup.InstallExternalBackends(galleries, systemState, modelLoader, progressCallback, bi.BackendArgs, bi.Name, bi.Alias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/mudler/LocalAI/core/gallery"
|
||||
"github.com/mudler/LocalAI/core/startup"
|
||||
"github.com/mudler/LocalAI/pkg/downloader"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
@@ -125,7 +126,8 @@ func (mi *ModelsInstall) Run(ctx *cliContext.Context) error {
|
||||
log.Info().Str("model", modelName).Str("license", model.License).Msg("installing model")
|
||||
}
|
||||
|
||||
err = startup.InstallModels(galleries, backendGalleries, systemState, !mi.DisablePredownloadScan, mi.AutoloadBackendGalleries, progressCallback, modelName)
|
||||
modelLoader := model.NewModelLoader(systemState, true)
|
||||
err = startup.InstallModels(galleries, backendGalleries, systemState, modelLoader, !mi.DisablePredownloadScan, mi.AutoloadBackendGalleries, progressCallback, modelName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/core/http"
|
||||
"github.com/mudler/LocalAI/core/p2p"
|
||||
"github.com/mudler/LocalAI/internal"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -73,9 +74,16 @@ type RunCMD struct {
|
||||
DisableGalleryEndpoint bool `env:"LOCALAI_DISABLE_GALLERY_ENDPOINT,DISABLE_GALLERY_ENDPOINT" help:"Disable the gallery endpoints" group:"api"`
|
||||
MachineTag string `env:"LOCALAI_MACHINE_TAG,MACHINE_TAG" help:"Add Machine-Tag header to each response which is useful to track the machine in the P2P network" group:"api"`
|
||||
LoadToMemory []string `env:"LOCALAI_LOAD_TO_MEMORY,LOAD_TO_MEMORY" help:"A list of models to load into memory at startup" group:"models"`
|
||||
|
||||
Version bool
|
||||
}
|
||||
|
||||
func (r *RunCMD) Run(ctx *cliContext.Context) error {
|
||||
if r.Version {
|
||||
fmt.Println(internal.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
os.MkdirAll(r.BackendsPath, 0750)
|
||||
os.MkdirAll(r.ModelsPath, 0750)
|
||||
|
||||
|
||||
@@ -164,10 +164,10 @@ type LLMConfig struct {
|
||||
LimitMMPerPrompt LimitMMPerPrompt `yaml:"limit_mm_per_prompt" json:"limit_mm_per_prompt"` // vLLM
|
||||
MMProj string `yaml:"mmproj" json:"mmproj"`
|
||||
|
||||
FlashAttention bool `yaml:"flash_attention" json:"flash_attention"`
|
||||
NoKVOffloading bool `yaml:"no_kv_offloading" json:"no_kv_offloading"`
|
||||
CacheTypeK string `yaml:"cache_type_k" json:"cache_type_k"`
|
||||
CacheTypeV string `yaml:"cache_type_v" json:"cache_type_v"`
|
||||
FlashAttention *string `yaml:"flash_attention" json:"flash_attention"`
|
||||
NoKVOffloading bool `yaml:"no_kv_offloading" json:"no_kv_offloading"`
|
||||
CacheTypeK string `yaml:"cache_type_k" json:"cache_type_k"`
|
||||
CacheTypeV string `yaml:"cache_type_v" json:"cache_type_v"`
|
||||
|
||||
RopeScaling string `yaml:"rope_scaling" json:"rope_scaling"`
|
||||
ModelType string `yaml:"type" json:"type"`
|
||||
|
||||
@@ -59,7 +59,7 @@ func writeBackendMetadata(backendPath string, metadata *BackendMetadata) error {
|
||||
}
|
||||
|
||||
// Installs a model from the gallery
|
||||
func InstallBackendFromGallery(galleries []config.Gallery, systemState *system.SystemState, name string, downloadStatus func(string, string, string, float64), force bool) error {
|
||||
func InstallBackendFromGallery(galleries []config.Gallery, systemState *system.SystemState, modelLoader *model.ModelLoader, name string, downloadStatus func(string, string, string, float64), force bool) error {
|
||||
if !force {
|
||||
// check if we already have the backend installed
|
||||
backends, err := ListSystemBackends(systemState)
|
||||
@@ -99,7 +99,7 @@ func InstallBackendFromGallery(galleries []config.Gallery, systemState *system.S
|
||||
log.Debug().Str("name", name).Str("bestBackend", bestBackend.Name).Msg("Installing backend from meta backend")
|
||||
|
||||
// Then, let's install the best backend
|
||||
if err := InstallBackend(systemState, bestBackend, downloadStatus); err != nil {
|
||||
if err := InstallBackend(systemState, modelLoader, bestBackend, downloadStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -124,10 +124,10 @@ func InstallBackendFromGallery(galleries []config.Gallery, systemState *system.S
|
||||
return nil
|
||||
}
|
||||
|
||||
return InstallBackend(systemState, backend, downloadStatus)
|
||||
return InstallBackend(systemState, modelLoader, backend, downloadStatus)
|
||||
}
|
||||
|
||||
func InstallBackend(systemState *system.SystemState, config *GalleryBackend, downloadStatus func(string, string, string, float64)) error {
|
||||
func InstallBackend(systemState *system.SystemState, modelLoader *model.ModelLoader, config *GalleryBackend, downloadStatus func(string, string, string, float64)) error {
|
||||
// Create base path if it doesn't exist
|
||||
err := os.MkdirAll(systemState.Backend.BackendsPath, 0750)
|
||||
if err != nil {
|
||||
@@ -185,7 +185,7 @@ func InstallBackend(systemState *system.SystemState, config *GalleryBackend, dow
|
||||
return fmt.Errorf("failed to write metadata for backend %q: %v", name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return RegisterBackends(systemState, modelLoader)
|
||||
}
|
||||
|
||||
func DeleteBackendFromSystem(systemState *system.SystemState, name string) error {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -19,8 +20,10 @@ const (
|
||||
|
||||
var _ = Describe("Gallery Backends", func() {
|
||||
var (
|
||||
tempDir string
|
||||
galleries []config.Gallery
|
||||
tempDir string
|
||||
galleries []config.Gallery
|
||||
ml *model.ModelLoader
|
||||
systemState *system.SystemState
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
@@ -35,6 +38,9 @@ var _ = Describe("Gallery Backends", func() {
|
||||
URL: "https://gist.githubusercontent.com/mudler/71d5376bc2aa168873fa519fa9f4bd56/raw/0557f9c640c159fa8e4eab29e8d98df6a3d6e80f/backend-gallery.yaml",
|
||||
},
|
||||
}
|
||||
systemState, err = system.GetSystemState(system.WithBackendPath(tempDir))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
ml = model.NewModelLoader(systemState, true)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
@@ -43,21 +49,13 @@ var _ = Describe("Gallery Backends", func() {
|
||||
|
||||
Describe("InstallBackendFromGallery", func() {
|
||||
It("should return error when backend is not found", func() {
|
||||
systemState, err := system.GetSystemState(
|
||||
system.WithBackendPath(tempDir),
|
||||
)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = InstallBackendFromGallery(galleries, systemState, "non-existent", nil, true)
|
||||
err := InstallBackendFromGallery(galleries, systemState, ml, "non-existent", nil, true)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("no backend found with name \"non-existent\""))
|
||||
})
|
||||
|
||||
It("should install backend from gallery", func() {
|
||||
systemState, err := system.GetSystemState(
|
||||
system.WithBackendPath(tempDir),
|
||||
)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = InstallBackendFromGallery(galleries, systemState, "test-backend", nil, true)
|
||||
err := InstallBackendFromGallery(galleries, systemState, ml, "test-backend", nil, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(filepath.Join(tempDir, "test-backend", "run.sh")).To(BeARegularFile())
|
||||
})
|
||||
@@ -233,7 +231,7 @@ var _ = Describe("Gallery Backends", func() {
|
||||
VRAM: 1000000000000,
|
||||
Backend: system.Backend{BackendsPath: tempDir},
|
||||
}
|
||||
err = InstallBackendFromGallery([]config.Gallery{gallery}, nvidiaSystemState, "meta-backend", nil, true)
|
||||
err = InstallBackendFromGallery([]config.Gallery{gallery}, nvidiaSystemState, ml, "meta-backend", nil, true)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
metaBackendPath := filepath.Join(tempDir, "meta-backend")
|
||||
@@ -313,7 +311,7 @@ var _ = Describe("Gallery Backends", func() {
|
||||
VRAM: 1000000000000,
|
||||
Backend: system.Backend{BackendsPath: tempDir},
|
||||
}
|
||||
err = InstallBackendFromGallery([]config.Gallery{gallery}, nvidiaSystemState, "meta-backend", nil, true)
|
||||
err = InstallBackendFromGallery([]config.Gallery{gallery}, nvidiaSystemState, ml, "meta-backend", nil, true)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
metaBackendPath := filepath.Join(tempDir, "meta-backend")
|
||||
@@ -397,7 +395,7 @@ var _ = Describe("Gallery Backends", func() {
|
||||
VRAM: 1000000000000,
|
||||
Backend: system.Backend{BackendsPath: tempDir},
|
||||
}
|
||||
err = InstallBackendFromGallery([]config.Gallery{gallery}, nvidiaSystemState, "meta-backend", nil, true)
|
||||
err = InstallBackendFromGallery([]config.Gallery{gallery}, nvidiaSystemState, ml, "meta-backend", nil, true)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
metaBackendPath := filepath.Join(tempDir, "meta-backend")
|
||||
@@ -496,7 +494,7 @@ var _ = Describe("Gallery Backends", func() {
|
||||
system.WithBackendPath(newPath),
|
||||
)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = InstallBackend(systemState, &backend, nil)
|
||||
err = InstallBackend(systemState, ml, &backend, nil)
|
||||
Expect(err).To(HaveOccurred()) // Will fail due to invalid URI, but path should be created
|
||||
Expect(newPath).To(BeADirectory())
|
||||
})
|
||||
@@ -528,7 +526,7 @@ var _ = Describe("Gallery Backends", func() {
|
||||
system.WithBackendPath(tempDir),
|
||||
)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = InstallBackend(systemState, &backend, nil)
|
||||
err = InstallBackend(systemState, ml, &backend, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(filepath.Join(tempDir, "test-backend", "metadata.json")).To(BeARegularFile())
|
||||
dat, err := os.ReadFile(filepath.Join(tempDir, "test-backend", "metadata.json"))
|
||||
@@ -561,7 +559,7 @@ var _ = Describe("Gallery Backends", func() {
|
||||
|
||||
Expect(filepath.Join(tempDir, "test-backend", "metadata.json")).ToNot(BeARegularFile())
|
||||
|
||||
err = InstallBackend(systemState, &backend, nil)
|
||||
err = InstallBackend(systemState, ml, &backend, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(filepath.Join(tempDir, "test-backend", "metadata.json")).To(BeARegularFile())
|
||||
})
|
||||
@@ -582,7 +580,7 @@ var _ = Describe("Gallery Backends", func() {
|
||||
system.WithBackendPath(tempDir),
|
||||
)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = InstallBackend(systemState, &backend, nil)
|
||||
err = InstallBackend(systemState, ml, &backend, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(filepath.Join(tempDir, "test-backend", "metadata.json")).To(BeARegularFile())
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
lconfig "github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/pkg/downloader"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
|
||||
@@ -73,6 +74,7 @@ type PromptTemplate struct {
|
||||
func InstallModelFromGallery(
|
||||
modelGalleries, backendGalleries []config.Gallery,
|
||||
systemState *system.SystemState,
|
||||
modelLoader *model.ModelLoader,
|
||||
name string, req GalleryModel, downloadStatus func(string, string, string, float64), enforceScan, automaticallyInstallBackend bool) error {
|
||||
|
||||
applyModel := func(model *GalleryModel) error {
|
||||
@@ -131,7 +133,7 @@ func InstallModelFromGallery(
|
||||
if automaticallyInstallBackend && installedModel.Backend != "" {
|
||||
log.Debug().Msgf("Installing backend %q", installedModel.Backend)
|
||||
|
||||
if err := InstallBackendFromGallery(backendGalleries, systemState, installedModel.Backend, downloadStatus, false); err != nil {
|
||||
if err := InstallBackendFromGallery(backendGalleries, systemState, modelLoader, installedModel.Backend, downloadStatus, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ var _ = Describe("Model test", func() {
|
||||
Expect(models[0].URL).To(Equal(bertEmbeddingsURL))
|
||||
Expect(models[0].Installed).To(BeFalse())
|
||||
|
||||
err = InstallModelFromGallery(galleries, []config.Gallery{}, systemState, "test@bert", GalleryModel{}, func(s1, s2, s3 string, f float64) {}, true, true)
|
||||
err = InstallModelFromGallery(galleries, []config.Gallery{}, systemState, nil, "test@bert", GalleryModel{}, func(s1, s2, s3 string, f float64) {}, true, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
dat, err := os.ReadFile(filepath.Join(tempdir, "bert.yaml"))
|
||||
|
||||
@@ -70,6 +70,24 @@ func infoButton(m *gallery.GalleryModel) elem.Node {
|
||||
)
|
||||
}
|
||||
|
||||
func getConfigButton(galleryName string) elem.Node {
|
||||
return elem.Button(
|
||||
attrs.Props{
|
||||
"data-twe-ripple-init": "",
|
||||
"data-twe-ripple-color": "light",
|
||||
"class": "float-right ml-2 inline-flex items-center rounded-lg bg-gray-700 hover:bg-gray-600 px-4 py-2 text-sm font-medium text-white transition duration-300 ease-in-out",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-post": "browse/config/model/" + galleryName,
|
||||
},
|
||||
elem.I(
|
||||
attrs.Props{
|
||||
"class": "fa-solid fa-download pr-2",
|
||||
},
|
||||
),
|
||||
elem.Text("Get Config"),
|
||||
)
|
||||
}
|
||||
|
||||
func deleteButton(galleryID string) elem.Node {
|
||||
return elem.Button(
|
||||
attrs.Props{
|
||||
|
||||
@@ -339,7 +339,12 @@ func modelActionItems(m *gallery.GalleryModel, processTracker ProcessTracker, ga
|
||||
reInstallButton(m.ID()),
|
||||
deleteButton(m.ID()),
|
||||
)),
|
||||
installButton(m.ID()),
|
||||
// otherwise, show the install button, and the get config button
|
||||
elem.Node(elem.Div(
|
||||
attrs.Props{},
|
||||
getConfigButton(m.ID()),
|
||||
installButton(m.ID()),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -226,3 +226,33 @@ func EditModelEndpoint(cl *config.ModelConfigLoader, appConfig *config.Applicati
|
||||
return c.JSON(response)
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadModelsEndpoint handles reloading model configurations from disk
|
||||
func ReloadModelsEndpoint(cl *config.ModelConfigLoader, appConfig *config.ApplicationConfig) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Reload configurations
|
||||
if err := cl.LoadModelConfigsFromPath(appConfig.SystemState.Model.ModelsPath); err != nil {
|
||||
response := ModelResponse{
|
||||
Success: false,
|
||||
Error: "Failed to reload configurations: " + err.Error(),
|
||||
}
|
||||
return c.Status(500).JSON(response)
|
||||
}
|
||||
|
||||
// Preload the models
|
||||
if err := cl.Preload(appConfig.SystemState.Model.ModelsPath); err != nil {
|
||||
response := ModelResponse{
|
||||
Success: false,
|
||||
Error: "Failed to preload models: " + err.Error(),
|
||||
}
|
||||
return c.Status(500).JSON(response)
|
||||
}
|
||||
|
||||
// Return success response
|
||||
response := ModelResponse{
|
||||
Success: true,
|
||||
Message: "Model configurations reloaded successfully",
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func downloadFile(url string) (string, error) {
|
||||
*/
|
||||
// VideoEndpoint
|
||||
// @Summary Creates a video given a prompt.
|
||||
// @Param request body schema.OpenAIRequest true "query params"
|
||||
// @Param request body schema.VideoRequest true "query params"
|
||||
// @Success 200 {object} schema.OpenAIResponse "Response"
|
||||
// @Router /video [post]
|
||||
func VideoEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
||||
@@ -166,7 +166,23 @@ func VideoEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfi
|
||||
|
||||
baseURL := c.BaseURL()
|
||||
|
||||
fn, err := backend.VideoGeneration(height, width, input.Prompt, src, input.EndImage, output, ml, *config, appConfig)
|
||||
fn, err := backend.VideoGeneration(
|
||||
height,
|
||||
width,
|
||||
input.Prompt,
|
||||
input.NegativePrompt,
|
||||
src,
|
||||
input.EndImage,
|
||||
output,
|
||||
input.NumFrames,
|
||||
input.FPS,
|
||||
input.Seed,
|
||||
input.CFGScale,
|
||||
input.Step,
|
||||
ml,
|
||||
*config,
|
||||
appConfig,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
||||
var id, textContentToReturn string
|
||||
var created int
|
||||
|
||||
process := func(s string, req *schema.OpenAIRequest, config *config.ModelConfig, loader *model.ModelLoader, responses chan schema.OpenAIResponse, extraUsage bool) {
|
||||
process := func(s string, req *schema.OpenAIRequest, config *config.ModelConfig, loader *model.ModelLoader, responses chan schema.OpenAIResponse, extraUsage bool) error {
|
||||
initialMessage := schema.OpenAIResponse{
|
||||
ID: id,
|
||||
Created: created,
|
||||
@@ -41,7 +41,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
||||
}
|
||||
responses <- initialMessage
|
||||
|
||||
ComputeChoices(req, s, config, cl, startupOptions, loader, func(s string, c *[]schema.Choice) {}, func(s string, tokenUsage backend.TokenUsage) bool {
|
||||
_, _, err := ComputeChoices(req, s, config, cl, startupOptions, loader, func(s string, c *[]schema.Choice) {}, func(s string, tokenUsage backend.TokenUsage) bool {
|
||||
usage := schema.OpenAIUsage{
|
||||
PromptTokens: tokenUsage.Prompt,
|
||||
CompletionTokens: tokenUsage.Completion,
|
||||
@@ -65,16 +65,19 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
||||
return true
|
||||
})
|
||||
close(responses)
|
||||
return err
|
||||
}
|
||||
processTools := func(noAction string, prompt string, req *schema.OpenAIRequest, config *config.ModelConfig, loader *model.ModelLoader, responses chan schema.OpenAIResponse, extraUsage bool) {
|
||||
processTools := func(noAction string, prompt string, req *schema.OpenAIRequest, config *config.ModelConfig, loader *model.ModelLoader, responses chan schema.OpenAIResponse, extraUsage bool) error {
|
||||
result := ""
|
||||
_, tokenUsage, _ := ComputeChoices(req, prompt, config, cl, startupOptions, loader, func(s string, c *[]schema.Choice) {}, func(s string, usage backend.TokenUsage) bool {
|
||||
_, tokenUsage, err := ComputeChoices(req, prompt, config, cl, startupOptions, loader, func(s string, c *[]schema.Choice) {}, func(s string, usage backend.TokenUsage) bool {
|
||||
result += s
|
||||
// TODO: Change generated BNF grammar to be compliant with the schema so we can
|
||||
// stream the result token by token here.
|
||||
return true
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
textContentToReturn = functions.ParseTextContent(result, config.FunctionsConfig)
|
||||
result = functions.CleanupLLMResult(result, config.FunctionsConfig)
|
||||
functionResults := functions.ParseFunctionCall(result, config.FunctionsConfig)
|
||||
@@ -95,7 +98,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
||||
result, err := handleQuestion(config, cl, req, ml, startupOptions, functionResults, result, prompt)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error handling question")
|
||||
return
|
||||
return err
|
||||
}
|
||||
usage := schema.OpenAIUsage{
|
||||
PromptTokens: tokenUsage.Prompt,
|
||||
@@ -169,6 +172,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
||||
}
|
||||
|
||||
close(responses)
|
||||
return err
|
||||
}
|
||||
|
||||
return func(c *fiber.Ctx) error {
|
||||
@@ -223,9 +227,11 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.Type == "json_object" {
|
||||
|
||||
switch d.Type {
|
||||
case "json_object":
|
||||
input.Grammar = functions.JSONBNF
|
||||
} else if d.Type == "json_schema" {
|
||||
case "json_schema":
|
||||
d := schema.JsonSchemaRequest{}
|
||||
dat, err := json.Marshal(config.ResponseFormatMap)
|
||||
if err != nil {
|
||||
@@ -326,31 +332,69 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
||||
c.Set("X-Correlation-ID", id)
|
||||
|
||||
responses := make(chan schema.OpenAIResponse)
|
||||
ended := make(chan error, 1)
|
||||
|
||||
if !shouldUseFn {
|
||||
go process(predInput, input, config, ml, responses, extraUsage)
|
||||
} else {
|
||||
go processTools(noActionName, predInput, input, config, ml, responses, extraUsage)
|
||||
}
|
||||
go func() {
|
||||
if !shouldUseFn {
|
||||
ended <- process(predInput, input, config, ml, responses, extraUsage)
|
||||
} else {
|
||||
ended <- processTools(noActionName, predInput, input, config, ml, responses, extraUsage)
|
||||
}
|
||||
}()
|
||||
|
||||
c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
|
||||
usage := &schema.OpenAIUsage{}
|
||||
toolsCalled := false
|
||||
for ev := range responses {
|
||||
usage = &ev.Usage // Copy a pointer to the latest usage chunk so that the stop message can reference it
|
||||
if len(ev.Choices[0].Delta.ToolCalls) > 0 {
|
||||
toolsCalled = true
|
||||
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
case ev := <-responses:
|
||||
if len(ev.Choices) == 0 {
|
||||
log.Debug().Msgf("No choices in the response, skipping")
|
||||
continue
|
||||
}
|
||||
usage = &ev.Usage // Copy a pointer to the latest usage chunk so that the stop message can reference it
|
||||
if len(ev.Choices[0].Delta.ToolCalls) > 0 {
|
||||
toolsCalled = true
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.Encode(ev)
|
||||
log.Debug().Msgf("Sending chunk: %s", buf.String())
|
||||
_, err := fmt.Fprintf(w, "data: %v\n", buf.String())
|
||||
if err != nil {
|
||||
log.Debug().Msgf("Sending chunk failed: %v", err)
|
||||
input.Cancel()
|
||||
}
|
||||
w.Flush()
|
||||
case err := <-ended:
|
||||
if err == nil {
|
||||
break LOOP
|
||||
}
|
||||
log.Error().Msgf("Stream ended with error: %v", err)
|
||||
|
||||
resp := &schema.OpenAIResponse{
|
||||
ID: id,
|
||||
Created: created,
|
||||
Model: input.Model, // we have to return what the user sent here, due to OpenAI spec.
|
||||
Choices: []schema.Choice{
|
||||
{
|
||||
FinishReason: "stop",
|
||||
Index: 0,
|
||||
Delta: &schema.Message{Content: "Internal error: " + err.Error()},
|
||||
}},
|
||||
Object: "chat.completion.chunk",
|
||||
Usage: *usage,
|
||||
}
|
||||
respData, _ := json.Marshal(resp)
|
||||
|
||||
w.WriteString(fmt.Sprintf("data: %s\n\n", respData))
|
||||
w.WriteString("data: [DONE]\n\n")
|
||||
w.Flush()
|
||||
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.Encode(ev)
|
||||
log.Debug().Msgf("Sending chunk: %s", buf.String())
|
||||
_, err := fmt.Fprintf(w, "data: %v\n", buf.String())
|
||||
if err != nil {
|
||||
log.Debug().Msgf("Sending chunk failed: %v", err)
|
||||
input.Cancel()
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
finishReason := "stop"
|
||||
@@ -378,7 +422,9 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
||||
w.WriteString(fmt.Sprintf("data: %s\n\n", respData))
|
||||
w.WriteString("data: [DONE]\n\n")
|
||||
w.Flush()
|
||||
log.Debug().Msgf("Stream ended")
|
||||
}))
|
||||
|
||||
return nil
|
||||
|
||||
// no streaming mode
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
func CompletionEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator *templates.Evaluator, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
||||
created := int(time.Now().Unix())
|
||||
|
||||
process := func(id string, s string, req *schema.OpenAIRequest, config *config.ModelConfig, loader *model.ModelLoader, responses chan schema.OpenAIResponse, extraUsage bool) {
|
||||
process := func(id string, s string, req *schema.OpenAIRequest, config *config.ModelConfig, loader *model.ModelLoader, responses chan schema.OpenAIResponse, extraUsage bool) error {
|
||||
tokenCallback := func(s string, tokenUsage backend.TokenUsage) bool {
|
||||
usage := schema.OpenAIUsage{
|
||||
PromptTokens: tokenUsage.Prompt,
|
||||
@@ -59,8 +59,9 @@ func CompletionEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, eva
|
||||
responses <- resp
|
||||
return true
|
||||
}
|
||||
ComputeChoices(req, s, config, cl, appConfig, loader, func(s string, c *[]schema.Choice) {}, tokenCallback)
|
||||
_, _, err := ComputeChoices(req, s, config, cl, appConfig, loader, func(s string, c *[]schema.Choice) {}, tokenCallback)
|
||||
close(responses)
|
||||
return err
|
||||
}
|
||||
|
||||
return func(c *fiber.Ctx) error {
|
||||
@@ -121,18 +122,37 @@ func CompletionEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, eva
|
||||
|
||||
responses := make(chan schema.OpenAIResponse)
|
||||
|
||||
go process(id, predInput, input, config, ml, responses, extraUsage)
|
||||
ended := make(chan error)
|
||||
go func() {
|
||||
ended <- process(id, predInput, input, config, ml, responses, extraUsage)
|
||||
}()
|
||||
|
||||
c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
|
||||
|
||||
for ev := range responses {
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.Encode(ev)
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
case ev := <-responses:
|
||||
if len(ev.Choices) == 0 {
|
||||
log.Debug().Msgf("No choices in the response, skipping")
|
||||
continue
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.Encode(ev)
|
||||
|
||||
log.Debug().Msgf("Sending chunk: %s", buf.String())
|
||||
fmt.Fprintf(w, "data: %v\n", buf.String())
|
||||
w.Flush()
|
||||
log.Debug().Msgf("Sending chunk: %s", buf.String())
|
||||
fmt.Fprintf(w, "data: %v\n", buf.String())
|
||||
w.Flush()
|
||||
case err := <-ended:
|
||||
if err == nil {
|
||||
break LOOP
|
||||
}
|
||||
log.Error().Msgf("Stream ended with error: %v", err)
|
||||
fmt.Fprintf(w, "data: %v\n", "Internal error: "+err.Error())
|
||||
w.Flush()
|
||||
break LOOP
|
||||
}
|
||||
}
|
||||
|
||||
resp := &schema.OpenAIResponse{
|
||||
@@ -153,7 +173,7 @@ func CompletionEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, eva
|
||||
w.WriteString("data: [DONE]\n\n")
|
||||
w.Flush()
|
||||
}))
|
||||
return nil
|
||||
return <-ended
|
||||
}
|
||||
|
||||
var result []schema.Choice
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
const (
|
||||
localSampleRate = 16000
|
||||
remoteSampleRate = 24000
|
||||
vadModel = "silero-vad-ggml"
|
||||
)
|
||||
|
||||
// A model can be "emulated" that is: transcribe audio to text -> feed text to the LLM -> generate audio as result
|
||||
@@ -233,7 +234,7 @@ func registerRealtime(application *application.Application) func(c *websocket.Co
|
||||
// TODO: The API has no way to configure the VAD model or other models that make up a pipeline to fake any-to-any
|
||||
// So possibly we could have a way to configure a composite model that can be used in situations where any-to-any is expected
|
||||
pipeline := config.Pipeline{
|
||||
VAD: "silero-vad",
|
||||
VAD: vadModel,
|
||||
Transcription: session.InputAudioTranscription.Model,
|
||||
}
|
||||
|
||||
@@ -568,7 +569,7 @@ func updateTransSession(session *Session, update *types.ClientSession, cl *confi
|
||||
|
||||
if trUpd != nil && trUpd.Model != "" && trUpd.Model != trCur.Model {
|
||||
pipeline := config.Pipeline{
|
||||
VAD: "silero-vad",
|
||||
VAD: vadModel,
|
||||
Transcription: trUpd.Model,
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ func RegisterLocalAIRoutes(router *fiber.App,
|
||||
|
||||
// Custom model edit endpoint
|
||||
router.Post("/models/edit/:name", localai.EditModelEndpoint(cl, appConfig))
|
||||
|
||||
// Reload models endpoint
|
||||
router.Post("/models/reload", localai.ReloadModelsEndpoint(cl, appConfig))
|
||||
}
|
||||
|
||||
router.Post("/v1/detection",
|
||||
|
||||
@@ -189,6 +189,34 @@ func registerGalleryRoutes(app *fiber.App, cl *config.ModelConfigLoader, appConf
|
||||
return c.SendString(elements.StartModelProgressBar(uid, "0", "Installation"))
|
||||
})
|
||||
|
||||
app.Post("/browse/config/model/:id", func(c *fiber.Ctx) error {
|
||||
galleryID := strings.Clone(c.Params("id")) // note: strings.Clone is required for multiple requests!
|
||||
log.Debug().Msgf("UI job submitted to get config for : %+v\n", galleryID)
|
||||
|
||||
models, err := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.SystemState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
model := gallery.FindGalleryElement(models, galleryID)
|
||||
if model == nil {
|
||||
return fmt.Errorf("model not found")
|
||||
}
|
||||
|
||||
config, err := gallery.GetGalleryConfigFromURL[gallery.ModelConfig](model.URL, appConfig.SystemState.Model.ModelsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the config file
|
||||
_, err = gallery.InstallModel(appConfig.SystemState, model.Name, &config, model.Overrides, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.SendString("Configuration file saved.")
|
||||
})
|
||||
|
||||
// This route is used when the "Install" button is pressed, we submit here a new job to the gallery service
|
||||
// https://htmx.org/examples/progress-bar/
|
||||
app.Post("/browse/delete/model/:id", func(c *fiber.Ctx) error {
|
||||
|
||||
@@ -41,13 +41,21 @@
|
||||
<div class="absolute inset-0 rounded-xl bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</a>
|
||||
|
||||
<a href="/import-model"
|
||||
<a href="/import-model"
|
||||
class="group relative inline-flex items-center bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white py-3 px-8 rounded-xl font-semibold transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:shadow-green-500/25">
|
||||
<i class="fas fa-plus mr-3 text-lg"></i>
|
||||
<span>Import Model</span>
|
||||
<i class="fas fa-upload ml-3 opacity-70 group-hover:opacity-100 transition-opacity"></i>
|
||||
<div class="absolute inset-0 rounded-xl bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</a>
|
||||
|
||||
<button id="reload-models-btn"
|
||||
class="group relative inline-flex items-center bg-gradient-to-r from-orange-600 to-amber-600 hover:from-orange-700 hover:to-amber-700 text-white py-3 px-8 rounded-xl font-semibold transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:shadow-orange-500/25">
|
||||
<i class="fas fa-sync-alt mr-3 text-lg"></i>
|
||||
<span>Update Models</span>
|
||||
<i class="fas fa-refresh ml-3 opacity-70 group-hover:opacity-100 transition-opacity"></i>
|
||||
<div class="absolute inset-0 rounded-xl bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,6 +327,72 @@ function handleShutdownResponse(event, modelName) {
|
||||
const response = event.detail.xhr;
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
// Handle reload models button
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const reloadBtn = document.getElementById('reload-models-btn');
|
||||
if (reloadBtn) {
|
||||
reloadBtn.addEventListener('click', function() {
|
||||
const button = this;
|
||||
const originalText = button.querySelector('span').textContent;
|
||||
const icon = button.querySelector('i');
|
||||
|
||||
// Show loading state
|
||||
button.disabled = true;
|
||||
button.querySelector('span').textContent = 'Updating...';
|
||||
icon.classList.add('fa-spin');
|
||||
|
||||
// Make the API call
|
||||
fetch('/models/reload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Show success state briefly
|
||||
button.querySelector('span').textContent = 'Updated!';
|
||||
icon.classList.remove('fa-spin', 'fa-sync-alt');
|
||||
icon.classList.add('fa-check');
|
||||
|
||||
// Reload the page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
// Show error state
|
||||
button.querySelector('span').textContent = 'Error!';
|
||||
icon.classList.remove('fa-spin');
|
||||
console.error('Failed to reload models:', data.error);
|
||||
|
||||
// Reset button after delay
|
||||
setTimeout(() => {
|
||||
button.disabled = false;
|
||||
button.querySelector('span').textContent = originalText;
|
||||
icon.classList.remove('fa-check');
|
||||
icon.classList.add('fa-sync-alt');
|
||||
}, 3000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Show error state
|
||||
button.querySelector('span').textContent = 'Error!';
|
||||
icon.classList.remove('fa-spin');
|
||||
console.error('Error reloading models:', error);
|
||||
|
||||
// Reset button after delay
|
||||
setTimeout(() => {
|
||||
button.disabled = false;
|
||||
button.querySelector('span').textContent = originalText;
|
||||
icon.classList.remove('fa-check');
|
||||
icon.classList.add('fa-sync-alt');
|
||||
}, 3000);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -28,6 +28,7 @@ type GalleryResponse struct {
|
||||
type VideoRequest struct {
|
||||
BasicModelRequest
|
||||
Prompt string `json:"prompt" yaml:"prompt"`
|
||||
NegativePrompt string `json:"negative_prompt" yaml:"negative_prompt"`
|
||||
StartImage string `json:"start_image" yaml:"start_image"`
|
||||
EndImage string `json:"end_image" yaml:"end_image"`
|
||||
Width int32 `json:"width" yaml:"width"`
|
||||
@@ -36,6 +37,7 @@ type VideoRequest struct {
|
||||
FPS int32 `json:"fps" yaml:"fps"`
|
||||
Seed int32 `json:"seed" yaml:"seed"`
|
||||
CFGScale float32 `json:"cfg_scale" yaml:"cfg_scale"`
|
||||
Step int32 `json:"step" yaml:"step"`
|
||||
ResponseFormat string `json:"response_format" yaml:"response_format"`
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,7 @@ func (g *GalleryService) backendHandler(op *GalleryOp[gallery.GalleryBackend], s
|
||||
} else {
|
||||
log.Warn().Msgf("installing backend %s", op.GalleryElementName)
|
||||
log.Debug().Msgf("backend galleries: %v", g.appConfig.BackendGalleries)
|
||||
err = gallery.InstallBackendFromGallery(g.appConfig.BackendGalleries, systemState, op.GalleryElementName, progressCallback, true)
|
||||
if err == nil {
|
||||
err = gallery.RegisterBackends(systemState, g.modelLoader)
|
||||
}
|
||||
err = gallery.InstallBackendFromGallery(g.appConfig.BackendGalleries, systemState, g.modelLoader, op.GalleryElementName, progressCallback, true)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("error installing backend %s", op.GalleryElementName)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/core/gallery"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -22,7 +23,7 @@ func (g *GalleryService) modelHandler(op *GalleryOp[gallery.GalleryModel], cl *c
|
||||
utils.DisplayDownloadFunction(fileName, current, total, percentage)
|
||||
}
|
||||
|
||||
err := processModelOperation(op, systemState, g.appConfig.EnforcePredownloadScans, g.appConfig.AutoloadBackendGalleries, progressCallback)
|
||||
err := processModelOperation(op, systemState, g.modelLoader, g.appConfig.EnforcePredownloadScans, g.appConfig.AutoloadBackendGalleries, progressCallback)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -49,7 +50,7 @@ func (g *GalleryService) modelHandler(op *GalleryOp[gallery.GalleryModel], cl *c
|
||||
return nil
|
||||
}
|
||||
|
||||
func installModelFromRemoteConfig(systemState *system.SystemState, req gallery.GalleryModel, downloadStatus func(string, string, string, float64), enforceScan, automaticallyInstallBackend bool, backendGalleries []config.Gallery) error {
|
||||
func installModelFromRemoteConfig(systemState *system.SystemState, modelLoader *model.ModelLoader, req gallery.GalleryModel, downloadStatus func(string, string, string, float64), enforceScan, automaticallyInstallBackend bool, backendGalleries []config.Gallery) error {
|
||||
config, err := gallery.GetGalleryConfigFromURL[gallery.ModelConfig](req.URL, systemState.Model.ModelsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -63,7 +64,7 @@ func installModelFromRemoteConfig(systemState *system.SystemState, req gallery.G
|
||||
}
|
||||
|
||||
if automaticallyInstallBackend && installedModel.Backend != "" {
|
||||
if err := gallery.InstallBackendFromGallery(backendGalleries, systemState, installedModel.Backend, downloadStatus, false); err != nil {
|
||||
if err := gallery.InstallBackendFromGallery(backendGalleries, systemState, modelLoader, installedModel.Backend, downloadStatus, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -76,22 +77,22 @@ type galleryModel struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func processRequests(systemState *system.SystemState, enforceScan, automaticallyInstallBackend bool, galleries []config.Gallery, backendGalleries []config.Gallery, requests []galleryModel) error {
|
||||
func processRequests(systemState *system.SystemState, modelLoader *model.ModelLoader, enforceScan, automaticallyInstallBackend bool, galleries []config.Gallery, backendGalleries []config.Gallery, requests []galleryModel) error {
|
||||
var err error
|
||||
for _, r := range requests {
|
||||
utils.ResetDownloadTimers()
|
||||
if r.ID == "" {
|
||||
err = installModelFromRemoteConfig(systemState, r.GalleryModel, utils.DisplayDownloadFunction, enforceScan, automaticallyInstallBackend, backendGalleries)
|
||||
err = installModelFromRemoteConfig(systemState, modelLoader, r.GalleryModel, utils.DisplayDownloadFunction, enforceScan, automaticallyInstallBackend, backendGalleries)
|
||||
|
||||
} else {
|
||||
err = gallery.InstallModelFromGallery(
|
||||
galleries, backendGalleries, systemState, r.ID, r.GalleryModel, utils.DisplayDownloadFunction, enforceScan, automaticallyInstallBackend)
|
||||
galleries, backendGalleries, systemState, modelLoader, r.ID, r.GalleryModel, utils.DisplayDownloadFunction, enforceScan, automaticallyInstallBackend)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ApplyGalleryFromFile(systemState *system.SystemState, enforceScan, automaticallyInstallBackend bool, galleries []config.Gallery, backendGalleries []config.Gallery, s string) error {
|
||||
func ApplyGalleryFromFile(systemState *system.SystemState, modelLoader *model.ModelLoader, enforceScan, automaticallyInstallBackend bool, galleries []config.Gallery, backendGalleries []config.Gallery, s string) error {
|
||||
dat, err := os.ReadFile(s)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -102,23 +103,24 @@ func ApplyGalleryFromFile(systemState *system.SystemState, enforceScan, automati
|
||||
return err
|
||||
}
|
||||
|
||||
return processRequests(systemState, enforceScan, automaticallyInstallBackend, galleries, backendGalleries, requests)
|
||||
return processRequests(systemState, modelLoader, enforceScan, automaticallyInstallBackend, galleries, backendGalleries, requests)
|
||||
}
|
||||
|
||||
func ApplyGalleryFromString(systemState *system.SystemState, enforceScan, automaticallyInstallBackend bool, galleries []config.Gallery, backendGalleries []config.Gallery, s string) error {
|
||||
func ApplyGalleryFromString(systemState *system.SystemState, modelLoader *model.ModelLoader, enforceScan, automaticallyInstallBackend bool, galleries []config.Gallery, backendGalleries []config.Gallery, s string) error {
|
||||
var requests []galleryModel
|
||||
err := json.Unmarshal([]byte(s), &requests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return processRequests(systemState, enforceScan, automaticallyInstallBackend, galleries, backendGalleries, requests)
|
||||
return processRequests(systemState, modelLoader, enforceScan, automaticallyInstallBackend, galleries, backendGalleries, requests)
|
||||
}
|
||||
|
||||
// processModelOperation handles the installation or deletion of a model
|
||||
func processModelOperation(
|
||||
op *GalleryOp[gallery.GalleryModel],
|
||||
systemState *system.SystemState,
|
||||
modelLoader *model.ModelLoader,
|
||||
enforcePredownloadScans bool,
|
||||
automaticallyInstallBackend bool,
|
||||
progressCallback func(string, string, string, float64),
|
||||
@@ -130,7 +132,7 @@ func processModelOperation(
|
||||
|
||||
// if the request contains a gallery name, we apply the gallery from the gallery list
|
||||
if op.GalleryElementName != "" {
|
||||
return gallery.InstallModelFromGallery(op.Galleries, op.BackendGalleries, systemState, op.GalleryElementName, op.Req, progressCallback, enforcePredownloadScans, automaticallyInstallBackend)
|
||||
return gallery.InstallModelFromGallery(op.Galleries, op.BackendGalleries, systemState, modelLoader, op.GalleryElementName, op.Req, progressCallback, enforcePredownloadScans, automaticallyInstallBackend)
|
||||
// } else if op.ConfigURL != "" {
|
||||
// err := startup.InstallModels(op.Galleries, modelPath, enforcePredownloadScans, progressCallback, op.ConfigURL)
|
||||
// if err != nil {
|
||||
@@ -138,6 +140,6 @@ func processModelOperation(
|
||||
// }
|
||||
// return cl.Preload(modelPath)
|
||||
} else {
|
||||
return installModelFromRemoteConfig(systemState, op.Req, progressCallback, enforcePredownloadScans, automaticallyInstallBackend, op.BackendGalleries)
|
||||
return installModelFromRemoteConfig(systemState, modelLoader, op.Req, progressCallback, enforcePredownloadScans, automaticallyInstallBackend, op.BackendGalleries)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/core/gallery"
|
||||
"github.com/mudler/LocalAI/pkg/downloader"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func InstallExternalBackends(galleries []config.Gallery, systemState *system.SystemState, downloadStatus func(string, string, string, float64), backend, name, alias string) error {
|
||||
func InstallExternalBackends(galleries []config.Gallery, systemState *system.SystemState, modelLoader *model.ModelLoader, downloadStatus func(string, string, string, float64), backend, name, alias string) error {
|
||||
uri := downloader.URI(backend)
|
||||
switch {
|
||||
case uri.LooksLikeDir():
|
||||
@@ -20,7 +21,7 @@ func InstallExternalBackends(galleries []config.Gallery, systemState *system.Sys
|
||||
name = filepath.Base(backend)
|
||||
}
|
||||
log.Info().Str("backend", backend).Str("name", name).Msg("Installing backend from path")
|
||||
if err := gallery.InstallBackend(systemState, &gallery.GalleryBackend{
|
||||
if err := gallery.InstallBackend(systemState, modelLoader, &gallery.GalleryBackend{
|
||||
Metadata: gallery.Metadata{
|
||||
Name: name,
|
||||
},
|
||||
@@ -34,7 +35,7 @@ func InstallExternalBackends(galleries []config.Gallery, systemState *system.Sys
|
||||
return fmt.Errorf("specifying a name is required for OCI images")
|
||||
}
|
||||
log.Info().Str("backend", backend).Str("name", name).Msg("Installing backend from OCI image")
|
||||
if err := gallery.InstallBackend(systemState, &gallery.GalleryBackend{
|
||||
if err := gallery.InstallBackend(systemState, modelLoader, &gallery.GalleryBackend{
|
||||
Metadata: gallery.Metadata{
|
||||
Name: name,
|
||||
},
|
||||
@@ -52,7 +53,7 @@ func InstallExternalBackends(galleries []config.Gallery, systemState *system.Sys
|
||||
name = strings.TrimSuffix(name, filepath.Ext(name))
|
||||
|
||||
log.Info().Str("backend", backend).Str("name", name).Msg("Installing backend from OCI image")
|
||||
if err := gallery.InstallBackend(systemState, &gallery.GalleryBackend{
|
||||
if err := gallery.InstallBackend(systemState, modelLoader, &gallery.GalleryBackend{
|
||||
Metadata: gallery.Metadata{
|
||||
Name: name,
|
||||
},
|
||||
@@ -65,10 +66,11 @@ func InstallExternalBackends(galleries []config.Gallery, systemState *system.Sys
|
||||
if name != "" || alias != "" {
|
||||
return fmt.Errorf("specifying a name or alias is not supported for this backend")
|
||||
}
|
||||
err := gallery.InstallBackendFromGallery(galleries, systemState, backend, downloadStatus, true)
|
||||
err := gallery.InstallBackendFromGallery(galleries, systemState, modelLoader, backend, downloadStatus, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error installing backend %s: %w", backend, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/core/gallery"
|
||||
"github.com/mudler/LocalAI/pkg/downloader"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -24,7 +25,7 @@ const (
|
||||
// InstallModels will preload models from the given list of URLs and galleries
|
||||
// It will download the model if it is not already present in the model path
|
||||
// It will also try to resolve if the model is an embedded model YAML configuration
|
||||
func InstallModels(galleries, backendGalleries []config.Gallery, systemState *system.SystemState, enforceScan, autoloadBackendGalleries bool, downloadStatus func(string, string, string, float64), models ...string) error {
|
||||
func InstallModels(galleries, backendGalleries []config.Gallery, systemState *system.SystemState, modelLoader *model.ModelLoader, enforceScan, autoloadBackendGalleries bool, downloadStatus func(string, string, string, float64), models ...string) error {
|
||||
// create an error that groups all errors
|
||||
var err error
|
||||
|
||||
@@ -47,7 +48,7 @@ func InstallModels(galleries, backendGalleries []config.Gallery, systemState *sy
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gallery.InstallBackendFromGallery(backendGalleries, systemState, model.Backend, downloadStatus, false); err != nil {
|
||||
if err := gallery.InstallBackendFromGallery(backendGalleries, systemState, modelLoader, model.Backend, downloadStatus, false); err != nil {
|
||||
log.Error().Err(err).Str("backend", model.Backend).Msg("error installing backend")
|
||||
return err
|
||||
}
|
||||
@@ -147,7 +148,7 @@ func InstallModels(galleries, backendGalleries []config.Gallery, systemState *sy
|
||||
}
|
||||
} else {
|
||||
// Check if it's a model gallery, or print a warning
|
||||
e, found := installModel(galleries, backendGalleries, url, systemState, downloadStatus, enforceScan, autoloadBackendGalleries)
|
||||
e, found := installModel(galleries, backendGalleries, url, systemState, modelLoader, downloadStatus, enforceScan, autoloadBackendGalleries)
|
||||
if e != nil && found {
|
||||
log.Error().Err(err).Msgf("[startup] failed installing model '%s'", url)
|
||||
err = errors.Join(err, e)
|
||||
@@ -161,7 +162,7 @@ func InstallModels(galleries, backendGalleries []config.Gallery, systemState *sy
|
||||
return err
|
||||
}
|
||||
|
||||
func installModel(galleries, backendGalleries []config.Gallery, modelName string, systemState *system.SystemState, downloadStatus func(string, string, string, float64), enforceScan, autoloadBackendGalleries bool) (error, bool) {
|
||||
func installModel(galleries, backendGalleries []config.Gallery, modelName string, systemState *system.SystemState, modelLoader *model.ModelLoader, downloadStatus func(string, string, string, float64), enforceScan, autoloadBackendGalleries bool) (error, bool) {
|
||||
models, err := gallery.AvailableGalleryModels(galleries, systemState)
|
||||
if err != nil {
|
||||
return err, false
|
||||
@@ -177,7 +178,7 @@ func installModel(galleries, backendGalleries []config.Gallery, modelName string
|
||||
}
|
||||
|
||||
log.Info().Str("model", modelName).Str("license", model.License).Msg("installing model")
|
||||
err = gallery.InstallModelFromGallery(galleries, backendGalleries, systemState, modelName, gallery.GalleryModel{}, downloadStatus, enforceScan, autoloadBackendGalleries)
|
||||
err = gallery.InstallModelFromGallery(galleries, backendGalleries, systemState, modelLoader, modelName, gallery.GalleryModel{}, downloadStatus, enforceScan, autoloadBackendGalleries)
|
||||
if err != nil {
|
||||
return err, true
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
. "github.com/mudler/LocalAI/core/startup"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -14,18 +15,25 @@ import (
|
||||
)
|
||||
|
||||
var _ = Describe("Preload test", func() {
|
||||
var tmpdir string
|
||||
var systemState *system.SystemState
|
||||
var ml *model.ModelLoader
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
tmpdir, err = os.MkdirTemp("", "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
systemState, err = system.GetSystemState(system.WithModelPath(tmpdir))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
ml = model.NewModelLoader(systemState, true)
|
||||
})
|
||||
|
||||
Context("Preloading from strings", func() {
|
||||
It("loads from embedded full-urls", func() {
|
||||
tmpdir, err := os.MkdirTemp("", "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
url := "https://raw.githubusercontent.com/mudler/LocalAI-examples/main/configurations/phi-2.yaml"
|
||||
fileName := fmt.Sprintf("%s.yaml", "phi-2")
|
||||
|
||||
systemState, err := system.GetSystemState(system.WithModelPath(tmpdir))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
InstallModels([]config.Gallery{}, []config.Gallery{}, systemState, true, true, nil, url)
|
||||
InstallModels([]config.Gallery{}, []config.Gallery{}, systemState, ml, true, true, nil, url)
|
||||
|
||||
resultFile := filepath.Join(tmpdir, fileName)
|
||||
|
||||
@@ -35,15 +43,10 @@ var _ = Describe("Preload test", func() {
|
||||
Expect(string(content)).To(ContainSubstring("name: phi-2"))
|
||||
})
|
||||
It("downloads from urls", func() {
|
||||
tmpdir, err := os.MkdirTemp("", "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
url := "huggingface://TheBloke/TinyLlama-1.1B-Chat-v0.3-GGUF/tinyllama-1.1b-chat-v0.3.Q2_K.gguf"
|
||||
fileName := fmt.Sprintf("%s.gguf", "tinyllama-1.1b-chat-v0.3.Q2_K")
|
||||
|
||||
systemState, err := system.GetSystemState(system.WithModelPath(tmpdir))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = InstallModels([]config.Gallery{}, []config.Gallery{}, systemState, true, true, nil, url)
|
||||
err := InstallModels([]config.Gallery{}, []config.Gallery{}, systemState, ml, true, true, nil, url)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
resultFile := filepath.Join(tmpdir, fileName)
|
||||
|
||||
@@ -134,8 +134,7 @@ Due to the nature of ROCm it is best to run all implementations in containers as
|
||||
|
||||
### Recommendations
|
||||
|
||||
- Do not use on a system running Wayland.
|
||||
- If running with Xorg do not use GPU assigned for compute for desktop rendering.
|
||||
- Make sure to do not use GPU assigned for compute for desktop rendering.
|
||||
- Ensure at least 100GB of free space on disk hosting container runtime and storing images prior to installation.
|
||||
|
||||
### Limitations
|
||||
|
||||
@@ -14,29 +14,77 @@ LocalAI will attempt to automatically load models which are not explicitly confi
|
||||
|
||||
{{% /alert %}}
|
||||
|
||||
## Text Generation & Language Models
|
||||
|
||||
{{< table "table-responsive" >}}
|
||||
| Backend and Bindings | Compatible models | Completion/Chat endpoint | Capability | Embeddings support | Token stream support | Acceleration |
|
||||
|----------------------------------------------------------------------------------|-----------------------|--------------------------|---------------------------|-----------------------------------|----------------------|--------------|
|
||||
| [llama.cpp]({{%relref "docs/features/text-generation#llama.cpp" %}}) | LLama, Mamba, RWKV, Falcon, Starcoder, GPT-2, [and many others](https://github.com/ggerganov/llama.cpp?tab=readme-ov-file#description) | yes | GPT and Functions | yes | yes | CUDA, openCL, cuBLAS, Metal |
|
||||
| [whisper](https://github.com/ggerganov/whisper.cpp) | whisper | no | Audio | no | no | N/A |
|
||||
| [llama.cpp]({{%relref "docs/features/text-generation#llama.cpp" %}}) | LLama, Mamba, RWKV, Falcon, Starcoder, GPT-2, [and many others](https://github.com/ggerganov/llama.cpp?tab=readme-ov-file#description) | yes | GPT and Functions | yes | yes | CUDA 11/12, ROCm, Intel SYCL, Vulkan, Metal, CPU |
|
||||
| [vLLM](https://github.com/vllm-project/vllm) | Various GPTs and quantization formats | yes | GPT | no | no | CUDA 12, ROCm, Intel |
|
||||
| [transformers](https://github.com/huggingface/transformers) | Various GPTs and quantization formats | yes | GPT, embeddings, Audio generation | yes | yes* | CUDA 11/12, ROCm, Intel, CPU |
|
||||
| [exllama2](https://github.com/turboderp-org/exllamav2) | GPTQ | yes | GPT only | no | no | CUDA 12 |
|
||||
| [MLX](https://github.com/ml-explore/mlx-lm) | Various LLMs | yes | GPT | no | no | Metal (Apple Silicon) |
|
||||
| [MLX-VLM](https://github.com/Blaizzy/mlx-vlm) | Vision-Language Models | yes | Multimodal GPT | no | no | Metal (Apple Silicon) |
|
||||
| [langchain-huggingface](https://github.com/tmc/langchaingo) | Any text generators available on HuggingFace through API | yes | GPT | no | no | N/A |
|
||||
| [piper](https://github.com/rhasspy/piper) ([binding](https://github.com/mudler/go-piper)) | Any piper onnx model | no | Text to voice | no | no | N/A |
|
||||
| [sentencetransformers](https://github.com/UKPLab/sentence-transformers) | BERT | no | Embeddings only | yes | no | N/A |
|
||||
| `bark` | bark | no | Audio generation | no | no | yes |
|
||||
| `autogptq` | GPTQ | yes | GPT | yes | no | N/A |
|
||||
| `diffusers` | SD,... | no | Image generation | no | no | N/A |
|
||||
| `vllm` | Various GPTs and quantization formats | yes | GPT | no | no | CPU/CUDA |
|
||||
| `exllama2` | GPTQ | yes | GPT only | no | no | N/A |
|
||||
| `transformers-musicgen` | | no | Audio generation | no | no | N/A |
|
||||
| stablediffusion | no | Image | no | no | N/A |
|
||||
| `coqui` | Coqui | no | Audio generation and Voice cloning | no | no | CPU/CUDA |
|
||||
| [rerankers](https://github.com/AnswerDotAI/rerankers) | Reranking API | no | Reranking | no | no | CPU/CUDA |
|
||||
| `transformers` | Various GPTs and quantization formats | yes | GPT, embeddings, Audio generation | yes | yes* | CPU/CUDA/XPU |
|
||||
| [bark-cpp](https://github.com/PABannier/bark.cpp) | bark | no | Audio-Only | no | no | yes |
|
||||
| [stablediffusion-cpp](https://github.com/leejet/stable-diffusion.cpp) | stablediffusion-1, stablediffusion-2, stablediffusion-3, flux, PhotoMaker | no | Image | no | no | N/A |
|
||||
{{< /table >}}
|
||||
|
||||
## Audio & Speech Processing
|
||||
|
||||
{{< table "table-responsive" >}}
|
||||
| Backend and Bindings | Compatible models | Completion/Chat endpoint | Capability | Embeddings support | Token stream support | Acceleration |
|
||||
|----------------------------------------------------------------------------------|-----------------------|--------------------------|---------------------------|-----------------------------------|----------------------|--------------|
|
||||
| [whisper.cpp](https://github.com/ggml-org/whisper.cpp) | whisper | no | Audio transcription | no | no | CUDA 12, ROCm, Intel SYCL, Vulkan, CPU |
|
||||
| [faster-whisper](https://github.com/SYSTRAN/faster-whisper) | whisper | no | Audio transcription | no | no | CUDA 12, ROCm, Intel, CPU |
|
||||
| [piper](https://github.com/rhasspy/piper) ([binding](https://github.com/mudler/go-piper)) | Any piper onnx model | no | Text to voice | no | no | CPU |
|
||||
| [bark](https://github.com/suno-ai/bark) | bark | no | Audio generation | no | no | CUDA 12, ROCm, Intel |
|
||||
| [bark-cpp](https://github.com/PABannier/bark.cpp) | bark | no | Audio-Only | no | no | CUDA, Metal, CPU |
|
||||
| [coqui](https://github.com/idiap/coqui-ai-TTS) | Coqui TTS | no | Audio generation and Voice cloning | no | no | CUDA 12, ROCm, Intel, CPU |
|
||||
| [kokoro](https://github.com/hexgrad/kokoro) | Kokoro TTS | no | Text-to-speech | no | no | CUDA 12, ROCm, Intel, CPU |
|
||||
| [chatterbox](https://github.com/resemble-ai/chatterbox) | Chatterbox TTS | no | Text-to-speech | no | no | CUDA 11/12, CPU |
|
||||
| [kitten-tts](https://github.com/KittenML/KittenTTS) | Kitten TTS | no | Text-to-speech | no | no | CPU |
|
||||
| [silero-vad](https://github.com/snakers4/silero-vad) with [Golang bindings](https://github.com/streamer45/silero-vad-go) | Silero VAD | no | Voice Activity Detection | no | no | CPU |
|
||||
{{< /table >}}
|
||||
|
||||
## Image & Video Generation
|
||||
|
||||
{{< table "table-responsive" >}}
|
||||
| Backend and Bindings | Compatible models | Completion/Chat endpoint | Capability | Embeddings support | Token stream support | Acceleration |
|
||||
|----------------------------------------------------------------------------------|-----------------------|--------------------------|---------------------------|-----------------------------------|----------------------|--------------|
|
||||
| [stablediffusion.cpp](https://github.com/leejet/stable-diffusion.cpp) | stablediffusion-1, stablediffusion-2, stablediffusion-3, flux, PhotoMaker | no | Image | no | no | CUDA 12, Intel SYCL, Vulkan, CPU |
|
||||
| [diffusers](https://github.com/huggingface/diffusers) | SD, various diffusion models,... | no | Image/Video generation | no | no | CUDA 11/12, ROCm, Intel, Metal, CPU |
|
||||
| [transformers-musicgen](https://github.com/huggingface/transformers) | MusicGen | no | Audio generation | no | no | CUDA, CPU |
|
||||
{{< /table >}}
|
||||
|
||||
## Specialized AI Tasks
|
||||
|
||||
{{< table "table-responsive" >}}
|
||||
| Backend and Bindings | Compatible models | Completion/Chat endpoint | Capability | Embeddings support | Token stream support | Acceleration |
|
||||
|----------------------------------------------------------------------------------|-----------------------|--------------------------|---------------------------|-----------------------------------|----------------------|--------------|
|
||||
| [rfdetr](https://github.com/roboflow/rf-detr) | RF-DETR | no | Object Detection | no | no | CUDA 12, Intel, CPU |
|
||||
| [rerankers](https://github.com/AnswerDotAI/rerankers) | Reranking API | no | Reranking | no | no | CUDA 11/12, ROCm, Intel, CPU |
|
||||
| [local-store](https://github.com/mudler/LocalAI) | Vector database | no | Vector storage | yes | no | CPU |
|
||||
| [huggingface](https://huggingface.co/docs/hub/en/api) | HuggingFace API models | yes | Various AI tasks | yes | yes | API-based |
|
||||
{{< /table >}}
|
||||
|
||||
## Acceleration Support Summary
|
||||
|
||||
### GPU Acceleration
|
||||
- **NVIDIA CUDA**: CUDA 11.7, CUDA 12.0 support across most backends
|
||||
- **AMD ROCm**: HIP-based acceleration for AMD GPUs
|
||||
- **Intel oneAPI**: SYCL-based acceleration for Intel GPUs (F16/F32 precision)
|
||||
- **Vulkan**: Cross-platform GPU acceleration
|
||||
- **Metal**: Apple Silicon GPU acceleration (M1/M2/M3+)
|
||||
|
||||
### Specialized Hardware
|
||||
- **NVIDIA Jetson (L4T)**: ARM64 support for embedded AI
|
||||
- **Apple Silicon**: Native Metal acceleration for Mac M1/M2/M3+
|
||||
- **Darwin x86**: Intel Mac support
|
||||
|
||||
### CPU Optimization
|
||||
- **AVX/AVX2/AVX512**: Advanced vector extensions for x86
|
||||
- **Quantization**: 4-bit, 5-bit, 8-bit integer quantization support
|
||||
- **Mixed Precision**: F16/F32 mixed precision support
|
||||
|
||||
Note: any backend name listed above can be used in the `backend` field of the model configuration file (See [the advanced section]({{%relref "docs/advanced" %}})).
|
||||
|
||||
- \* Only for CUDA and OpenVINO CPU/XPU acceleration.
|
||||
|
||||
@@ -1,4 +1,181 @@
|
||||
---
|
||||
- &mimo
|
||||
license: mit
|
||||
tags:
|
||||
- gguf
|
||||
- GPU
|
||||
- CPU
|
||||
- text-to-text
|
||||
icon: https://cdn-uploads.huggingface.co/production/uploads/634262af8d8089ebaefd410e/9Bnn2AnIjfQFWBGkhDNmI.png
|
||||
name: "aurore-reveil_koto-small-7b-it"
|
||||
urls:
|
||||
- https://huggingface.co/Aurore-Reveil/Koto-Small-7B-IT
|
||||
- https://huggingface.co/bartowski/Aurore-Reveil_Koto-Small-7B-IT-GGUF
|
||||
description: |
|
||||
Koto-Small-7B-IT is an instruct-tuned version of Koto-Small-7B-PT, which was trained on MiMo-7B-Base for almost a billion tokens of creative-writing data. This model is meant for roleplaying and instruct usecases.
|
||||
overrides:
|
||||
parameters:
|
||||
model: Aurore-Reveil_Koto-Small-7B-IT-Q4_K_M.gguf
|
||||
files:
|
||||
- filename: Aurore-Reveil_Koto-Small-7B-IT-Q4_K_M.gguf
|
||||
sha256: c5c38bfa5d8d5100e91a2e0050a0b2f3e082cd4bfd423cb527abc3b6f1ae180c
|
||||
uri: huggingface://bartowski/Aurore-Reveil_Koto-Small-7B-IT-GGUF/Aurore-Reveil_Koto-Small-7B-IT-Q4_K_M.gguf
|
||||
- &internvl35
|
||||
name: "opengvlab_internvl3_5-30b-a3b"
|
||||
url: "github:mudler/LocalAI/gallery/qwen3.yaml@master"
|
||||
icon: https://cdn-uploads.huggingface.co/production/uploads/64006c09330a45b03605bba3/zJsd2hqd3EevgXo6fNgC-.png
|
||||
urls:
|
||||
- https://huggingface.co/OpenGVLab/InternVL3_5-30B-A3B
|
||||
- https://huggingface.co/bartowski/OpenGVLab_InternVL3_5-30B-A3B-GGUF
|
||||
license: apache-2.0
|
||||
tags:
|
||||
- multimodal
|
||||
- gguf
|
||||
- GPU
|
||||
- Cpu
|
||||
- image-to-text
|
||||
- text-to-text
|
||||
description: |
|
||||
We introduce InternVL3.5, a new family of open-source multimodal models that significantly advances versatility, reasoning capability, and inference efficiency along the InternVL series. A key innovation is the Cascade Reinforcement Learning (Cascade RL) framework, which enhances reasoning through a two-stage process: offline RL for stable convergence and online RL for refined alignment. This coarse-to-fine training strategy leads to substantial improvements on downstream reasoning tasks, e.g., MMMU and MathVista. To optimize efficiency, we propose a Visual Resolution Router (ViR) that dynamically adjusts the resolution of visual tokens without compromising performance. Coupled with ViR, our Decoupled Vision-Language Deployment (DvD) strategy separates the vision encoder and language model across different GPUs, effectively balancing computational load. These contributions collectively enable InternVL3.5 to achieve up to a +16.0% gain in overall reasoning performance and a 4.05 ×\times× inference speedup compared to its predecessor, i.e., InternVL3. In addition, InternVL3.5 supports novel capabilities such as GUI interaction and embodied agency. Notably, our largest model, i.e., InternVL3.5-241B-A28B, attains state-of-the-art results among open-source MLLMs across general multimodal, reasoning, text, and agentic tasks—narrowing the performance gap with leading commercial models like GPT-5. All models and code are publicly released.
|
||||
overrides:
|
||||
parameters:
|
||||
model: OpenGVLab_InternVL3_5-30B-A3B-Q4_K_M.gguf
|
||||
mmproj: mmproj-OpenGVLab_InternVL3_5-30B-A3B-f16.gguf
|
||||
files:
|
||||
- filename: OpenGVLab_InternVL3_5-30B-A3B-Q4_K_M.gguf
|
||||
sha256: c352004ac811cf9aa198e11f698ebd5fd3c49b483cb31a2b081fb415dd8347c2
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-30B-A3B-GGUF/OpenGVLab_InternVL3_5-30B-A3B-Q4_K_M.gguf
|
||||
- filename: mmproj-OpenGVLab_InternVL3_5-30B-A3B-f16.gguf
|
||||
sha256: fa362a7396c3dddecf6f9a714144ed86207211d6c68ef39ea0d7dfe21b969b8d
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-30B-A3B-GGUF/mmproj-OpenGVLab_InternVL3_5-30B-A3B-f16.gguf
|
||||
- !!merge <<: *internvl35
|
||||
name: "opengvlab_internvl3_5-30b-a3b-q8_0"
|
||||
urls:
|
||||
- https://huggingface.co/OpenGVLab/InternVL3_5-30B-A3B
|
||||
- https://huggingface.co/bartowski/OpenGVLab_InternVL3_5-30B-A3B-GGUF
|
||||
overrides:
|
||||
parameters:
|
||||
model: OpenGVLab_InternVL3_5-30B-A3B-Q8_0.gguf
|
||||
mmproj: mmproj-OpenGVLab_InternVL3_5-30B-A3B-f16.gguf
|
||||
files:
|
||||
- filename: OpenGVLab_InternVL3_5-30B-A3B-Q8_0.gguf
|
||||
sha256: 79ac13df1d3f784cd5702b2835ede749cdfd274f141d1e0df25581af2a2a6720
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-30B-A3B-GGUF/OpenGVLab_InternVL3_5-30B-A3B-Q8_0.gguf
|
||||
- filename: mmproj-OpenGVLab_InternVL3_5-30B-A3B-f16.gguf
|
||||
sha256: fa362a7396c3dddecf6f9a714144ed86207211d6c68ef39ea0d7dfe21b969b8d
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-30B-A3B-GGUF/mmproj-OpenGVLab_InternVL3_5-30B-A3B-f16.gguf
|
||||
- !!merge <<: *internvl35
|
||||
name: "opengvlab_internvl3_5-14b-q8_0"
|
||||
urls:
|
||||
- https://huggingface.co/OpenGVLab/InternVL3_5-14B
|
||||
- https://huggingface.co/bartowski/OpenGVLab_InternVL3_5-14B-GGUF
|
||||
overrides:
|
||||
parameters:
|
||||
model: OpenGVLab_InternVL3_5-14B-Q8_0.gguf
|
||||
mmproj: mmproj-OpenGVLab_InternVL3_5-14B-f16.gguf
|
||||
files:
|
||||
- filename: OpenGVLab_InternVL3_5-14B-Q8_0.gguf
|
||||
sha256: e097b9c837347ec8050f9ed95410d1001030a4701eb9551c1be04793af16677a
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-14B-GGUF/OpenGVLab_InternVL3_5-14B-Q8_0.gguf
|
||||
- filename: mmproj-OpenGVLab_InternVL3_5-14B-f16.gguf
|
||||
sha256: c9625c981969d267052464e2d345f8ff5bc7e841871f5284a2bd972461c7356d
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-14B-GGUF/mmproj-OpenGVLab_InternVL3_5-14B-f16.gguf
|
||||
- !!merge <<: *internvl35
|
||||
name: "opengvlab_internvl3_5-14b"
|
||||
urls:
|
||||
- https://huggingface.co/OpenGVLab/InternVL3_5-14B
|
||||
- https://huggingface.co/bartowski/OpenGVLab_InternVL3_5-14B-GGUF
|
||||
overrides:
|
||||
mmproj: mmproj-OpenGVLab_InternVL3_5-14B-f16.gguf
|
||||
parameters:
|
||||
model: OpenGVLab_InternVL3_5-14B-Q4_K_M.gguf
|
||||
files:
|
||||
- filename: OpenGVLab_InternVL3_5-14B-Q4_K_M.gguf
|
||||
sha256: 5bb86ab56ee543bb72ba0cab58658ecb54713504f1bc9d1d075d202a35419032
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-14B-GGUF/OpenGVLab_InternVL3_5-14B-Q4_K_M.gguf
|
||||
- filename: mmproj-OpenGVLab_InternVL3_5-14B-f16.gguf
|
||||
sha256: c9625c981969d267052464e2d345f8ff5bc7e841871f5284a2bd972461c7356d
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-14B-GGUF/mmproj-OpenGVLab_InternVL3_5-14B-f16.gguf
|
||||
- !!merge <<: *internvl35
|
||||
name: "opengvlab_internvl3_5-8b"
|
||||
urls:
|
||||
- https://huggingface.co/OpenGVLab/InternVL3_5-8B
|
||||
- https://huggingface.co/bartowski/OpenGVLab_InternVL3_5-8B-GGUF
|
||||
overrides:
|
||||
mmproj: mmproj-OpenGVLab_InternVL3_5-8B-f16.gguf
|
||||
parameters:
|
||||
model: OpenGVLab_InternVL3_5-8B-Q4_K_M.gguf
|
||||
files:
|
||||
- filename: OpenGVLab_InternVL3_5-8B-Q4_K_M.gguf
|
||||
sha256: f3792d241a77a88be986445fed2498489e7360947ae4556e58cb0833e9fbc697
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-8B-GGUF/OpenGVLab_InternVL3_5-8B-Q4_K_M.gguf
|
||||
- filename: mmproj-OpenGVLab_InternVL3_5-8B-f16.gguf
|
||||
sha256: 212cc090f81ea2981b870186d4b424fae69489a5313a14e52ffdb2e877852389
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-8B-GGUF/mmproj-OpenGVLab_InternVL3_5-8B-f16.gguf
|
||||
- !!merge <<: *internvl35
|
||||
name: "opengvlab_internvl3_5-8b-q8_0"
|
||||
urls:
|
||||
- https://huggingface.co/OpenGVLab/InternVL3_5-8B
|
||||
- https://huggingface.co/bartowski/OpenGVLab_InternVL3_5-8B-GGUF
|
||||
overrides:
|
||||
mmproj: mmproj-OpenGVLab_InternVL3_5-8B-f16.gguf
|
||||
parameters:
|
||||
model: OpenGVLab_InternVL3_5-8B-Q8_0.gguf
|
||||
files:
|
||||
- filename: OpenGVLab_InternVL3_5-8B-Q8_0.gguf
|
||||
sha256: d81138703d9a641485c8bb064faa87f18cbc2adc9975bbedd20ab21dc7318260
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-8B-GGUF/OpenGVLab_InternVL3_5-8B-Q8_0.gguf
|
||||
- filename: mmproj-OpenGVLab_InternVL3_5-8B-f16.gguf
|
||||
sha256: 212cc090f81ea2981b870186d4b424fae69489a5313a14e52ffdb2e877852389
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-8B-GGUF/mmproj-OpenGVLab_InternVL3_5-8B-f16.gguf
|
||||
- !!merge <<: *internvl35
|
||||
name: "opengvlab_internvl3_5-4b"
|
||||
urls:
|
||||
- https://huggingface.co/OpenGVLab/InternVL3_5-4B
|
||||
- https://huggingface.co/bartowski/OpenGVLab_InternVL3_5-4B-GGUF
|
||||
overrides:
|
||||
mmproj: mmproj-OpenGVLab_InternVL3_5-4B-f16.gguf
|
||||
parameters:
|
||||
model: OpenGVLab_InternVL3_5-4B-Q4_K_M.gguf
|
||||
files:
|
||||
- filename: OpenGVLab_InternVL3_5-4B-Q4_K_M.gguf
|
||||
sha256: 7c1612b6896ad14caa501238e72afa17a600651d0984225e3ff78b39de86099c
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-4B-GGUF/OpenGVLab_InternVL3_5-4B-Q4_K_M.gguf
|
||||
- filename: mmproj-OpenGVLab_InternVL3_5-4B-f16.gguf
|
||||
sha256: 0f9704972fcb9cb0a4f2c0f4eb7fe4f58e53ccd4b06ec17cf7a80271aa963eb7
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-8B-GGUF/mmproj-OpenGVLab_InternVL3_5-4B-f16.gguf
|
||||
- !!merge <<: *internvl35
|
||||
name: "opengvlab_internvl3_5-4b-q8_0"
|
||||
urls:
|
||||
- https://huggingface.co/OpenGVLab/InternVL3_5-4B
|
||||
- https://huggingface.co/bartowski/OpenGVLab_InternVL3_5-4B-GGUF
|
||||
overrides:
|
||||
mmproj: mmproj-OpenGVLab_InternVL3_5-4B-f16.gguf
|
||||
parameters:
|
||||
model: OpenGVLab_InternVL3_5-4B-Q8_0.gguf
|
||||
files:
|
||||
- filename: OpenGVLab_InternVL3_5-4B-Q8_0.gguf
|
||||
sha256: ece87031e20486b1a4b86a0ba0f06b8b3b6eed676c8c6842e31041524489992d
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-4B-GGUF/OpenGVLab_InternVL3_5-4B-Q8_0.gguf
|
||||
- filename: mmproj-OpenGVLab_InternVL3_5-4B-f16.gguf
|
||||
sha256: 0f9704972fcb9cb0a4f2c0f4eb7fe4f58e53ccd4b06ec17cf7a80271aa963eb7
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-8B-GGUF/mmproj-OpenGVLab_InternVL3_5-4B-f16.gguf
|
||||
- !!merge <<: *internvl35
|
||||
name: "opengvlab_internvl3_5-2b"
|
||||
urls:
|
||||
- https://huggingface.co/OpenGVLab/InternVL3_5-2B
|
||||
- https://huggingface.co/bartowski/OpenGVLab_InternVL3_5-2B-GGUF
|
||||
overrides:
|
||||
mmproj: mmproj-OpenGVLab_InternVL3_5-2B-f16.gguf
|
||||
parameters:
|
||||
model: OpenGVLab_InternVL3_5-2B-Q8_0.gguf
|
||||
files:
|
||||
- filename: OpenGVLab_InternVL3_5-2B-Q8_0.gguf
|
||||
sha256: 6997c6e3a1fe5920ac1429a21a3ec15d545e14eb695ee3656834859e617800b5
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-2B-GGUF/OpenGVLab_InternVL3_5-2B-Q8_0.gguf
|
||||
- filename: mmproj-OpenGVLab_InternVL3_5-2B-f16.gguf
|
||||
sha256: e83ba6e675b747f7801557dc24594f43c17a7850b6129d4972d55e3e9b010359
|
||||
uri: huggingface://bartowski/OpenGVLab_InternVL3_5-8B-GGUF/mmproj-OpenGVLab_InternVL3_5-2B-f16.gguf
|
||||
- &lfm2
|
||||
url: "github:mudler/LocalAI/gallery/chatml.yaml@master"
|
||||
name: "lfm2-vl-450m"
|
||||
@@ -4629,6 +4806,25 @@
|
||||
- filename: sophosympatheia_Strawberrylemonade-70B-v1.1-Q4_K_M.gguf
|
||||
sha256: f0ca05ca40b8133f2fd5c7ae2e5c42af9200f559e54f37b46a76146ba09fa422
|
||||
uri: huggingface://bartowski/sophosympatheia_Strawberrylemonade-70B-v1.1-GGUF/sophosympatheia_Strawberrylemonade-70B-v1.1-Q4_K_M.gguf
|
||||
- !!merge <<: *llama33
|
||||
icon: https://huggingface.co/invisietch/L3.3-Ignition-v0.1-70B/resolve/main/header.png
|
||||
name: "invisietch_l3.3-ignition-v0.1-70b"
|
||||
urls:
|
||||
- https://huggingface.co/invisietch/L3.3-Ignition-v0.1-70B
|
||||
- https://huggingface.co/bartowski/invisietch_L3.3-Ignition-v0.1-70B-GGUF
|
||||
description: |
|
||||
Ignition v0.1 is a Llama 3.3-based model merge designed for creative roleplay and fiction writing purposes. The model underwent a multi-stage merge process designed to optimise for creative writing capability, minimising slop, and improving coherence when compared with its constituent models.
|
||||
|
||||
The model shows a preference for detailed character cards and is sensitive to detailed system prompting. If you want a specific behavior from the model, try prompting for it directly.
|
||||
|
||||
Inferencing has been tested at fp8 and fp16, and both are coherent up to ~64k context.
|
||||
overrides:
|
||||
parameters:
|
||||
model: invisietch_L3.3-Ignition-v0.1-70B-Q4_K_M.gguf
|
||||
files:
|
||||
- filename: invisietch_L3.3-Ignition-v0.1-70B-Q4_K_M.gguf
|
||||
sha256: 55fad5010cb16193ca05a90ef5a76d06de79cd5fd7d16ff474ca4ddb008dbe75
|
||||
uri: huggingface://bartowski/invisietch_L3.3-Ignition-v0.1-70B-GGUF/invisietch_L3.3-Ignition-v0.1-70B-Q4_K_M.gguf
|
||||
- &rwkv
|
||||
url: "github:mudler/LocalAI/gallery/rwkv.yaml@master"
|
||||
name: "rwkv-6-world-7b"
|
||||
@@ -12128,6 +12324,34 @@
|
||||
- filename: Wingless_Imp_8B.i1-Q4_K_M.gguf
|
||||
sha256: 3a5ff776ab3286f43937c3c2d8e2e1e09c5ea1c91a79945c34ec071e23f31e3b
|
||||
uri: huggingface://mradermacher/Wingless_Imp_8B-i1-GGUF/Wingless_Imp_8B.i1-Q4_K_M.gguf
|
||||
- !!merge <<: *llama31
|
||||
name: "nousresearch_hermes-4-70b"
|
||||
icon: https://cdn-uploads.huggingface.co/production/uploads/6317aade83d8d2fd903192d9/roT9o5bMYBtQziRMlaSDf.jpeg
|
||||
urls:
|
||||
- https://huggingface.co/NousResearch/Hermes-4-70B
|
||||
- https://huggingface.co/bartowski/NousResearch_Hermes-4-70B-GGUF
|
||||
description: |
|
||||
Hermes 4 70B is a frontier, hybrid-mode reasoning model based on Llama-3.1-70B by Nous Research that is aligned to you.
|
||||
|
||||
Read the Hermes 4 technical report here: Hermes 4 Technical Report
|
||||
|
||||
Chat with Hermes in Nous Chat: https://chat.nousresearch.com
|
||||
|
||||
Training highlights include a newly synthesized post-training corpus emphasizing verified reasoning traces, massive improvements in math, code, STEM, logic, creativity, and format-faithful outputs, while preserving general assistant quality and broadly neutral alignment.
|
||||
What’s new vs Hermes 3
|
||||
|
||||
Post-training corpus: Massively increased dataset size from 1M samples and 1.2B tokens to ~5M samples / ~60B tokens blended across reasoning and non-reasoning data.
|
||||
Hybrid reasoning mode with explicit <think>…</think> segments when the model decides to deliberate, and options to make your responses faster when you want.
|
||||
Reasoning that is top quality, expressive, improves math, code, STEM, logic, and even creative writing and subjective responses.
|
||||
Schema adherence & structured outputs: trained to produce valid JSON for given schemas and to repair malformed objects.
|
||||
Much easier to steer and align: extreme improvements on steerability, especially on reduced refusal rates.
|
||||
overrides:
|
||||
parameters:
|
||||
model: NousResearch_Hermes-4-70B-Q4_K_M.gguf
|
||||
files:
|
||||
- filename: NousResearch_Hermes-4-70B-Q4_K_M.gguf
|
||||
sha256: ab9b59dd1df27c039952915aa4669a82b5f45e5e9532b98679c65dffe2fe9ee2
|
||||
uri: huggingface://bartowski/NousResearch_Hermes-4-70B-GGUF/NousResearch_Hermes-4-70B-Q4_K_M.gguf
|
||||
- &deepseek
|
||||
url: "github:mudler/LocalAI/gallery/deepseek.yaml@master" ## Deepseek
|
||||
name: "deepseek-coder-v2-lite-instruct"
|
||||
@@ -20574,7 +20798,8 @@
|
||||
- filename: nomic-embed-text-v1.5.f16.gguf
|
||||
uri: https://huggingface.co/mradermacher/nomic-embed-text-v1.5-GGUF/resolve/main/nomic-embed-text-v1.5.f16.gguf
|
||||
sha256: af8cb9e4ca0bf19eb54d08c612fdf325059264abbbd2c619527e5d2dda8de655
|
||||
- name: "silero-vad"
|
||||
- &silero
|
||||
name: "silero-vad"
|
||||
icon: https://github.com/snakers4/silero-models/raw/master/files/silero_logo.jpg
|
||||
url: github:mudler/LocalAI/gallery/virtual.yaml@master
|
||||
urls:
|
||||
@@ -20594,6 +20819,22 @@
|
||||
- filename: silero-vad.onnx
|
||||
uri: https://huggingface.co/onnx-community/silero-vad/resolve/main/onnx/model.onnx
|
||||
sha256: a4a068cd6cf1ea8355b84327595838ca748ec29a25bc91fc82e6c299ccdc5808
|
||||
- !!merge <<: *silero
|
||||
name: "silero-vad-ggml"
|
||||
urls:
|
||||
- https://github.com/snakers4/silero-vad
|
||||
- https://github.com/ggml-org/whisper.cpp
|
||||
- https://huggingface.co/ggml-org/whisper-vad
|
||||
overrides:
|
||||
backend: whisper-vad
|
||||
parameters:
|
||||
model: ggml-silero-v5.1.2.bin
|
||||
options:
|
||||
- "vad_only"
|
||||
files:
|
||||
- filename: ggml-silero-v5.1.2.bin
|
||||
uri: https://huggingface.co/ggml-org/whisper-vad/resolve/main/ggml-silero-v5.1.2.bin
|
||||
sha256: 29940d98d42b91fbd05ce489f3ecf7c72f0a42f027e4875919a28fb4c04ea2cf
|
||||
- &bark
|
||||
name: "bark-cpp"
|
||||
icon: https://avatars.githubusercontent.com/u/99442120
|
||||
|
||||
116
go.mod
116
go.mod
@@ -6,18 +6,18 @@ toolchain go1.24.5
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1
|
||||
fyne.io/fyne/v2 v2.6.3
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/alecthomas/kong v0.9.0
|
||||
github.com/charmbracelet/glamour v0.7.0
|
||||
github.com/chasefleming/elem-go v0.26.0
|
||||
github.com/containerd/containerd v1.7.19
|
||||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/chasefleming/elem-go v0.31.0
|
||||
github.com/containerd/containerd v1.7.27
|
||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
|
||||
github.com/ebitengine/purego v0.8.4
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/ggerganov/whisper.cpp/bindings/go v0.0.0-20240626202019-c118733a29ad
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-audio/wav v1.1.0
|
||||
github.com/go-skynet/go-llama.cpp v0.0.0-20240314183750-6a8041ef6b46
|
||||
github.com/gofiber/fiber/v2 v2.52.5
|
||||
github.com/gofiber/fiber/v2 v2.52.9
|
||||
github.com/gofiber/swagger v1.0.0
|
||||
github.com/gofiber/template/html/v2 v2.1.2
|
||||
github.com/gofiber/websocket/v2 v2.2.1
|
||||
@@ -27,17 +27,17 @@ require (
|
||||
github.com/gpustack/gguf-parser-go v0.17.0
|
||||
github.com/hpcloud/tail v1.0.0
|
||||
github.com/ipfs/go-log v1.0.5
|
||||
github.com/jaypipes/ghw v0.12.0
|
||||
github.com/jaypipes/ghw v0.19.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/klauspost/cpuid/v2 v2.2.10
|
||||
github.com/libp2p/go-libp2p v0.43.0
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/mudler/edgevpn v0.31.0
|
||||
github.com/mudler/go-processmanager v0.0.0-20240820160718-8b802d3ecf82
|
||||
github.com/nikolalohinski/gonja/v2 v2.3.2
|
||||
github.com/onsi/ginkgo/v2 v2.23.3
|
||||
github.com/onsi/gomega v1.36.2
|
||||
github.com/onsi/ginkgo/v2 v2.25.1
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/otiai10/copy v1.14.1
|
||||
github.com/otiai10/openaigo v1.7.0
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
|
||||
@@ -51,37 +51,64 @@ require (
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
github.com/testcontainers/testcontainers-go v0.35.0
|
||||
github.com/tmc/langchaingo v0.1.12
|
||||
github.com/tmc/langchaingo v0.1.13
|
||||
github.com/valyala/fasthttp v1.55.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.50.0
|
||||
go.opentelemetry.io/otel/metric v1.35.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.28.0
|
||||
google.golang.org/grpc v1.67.1
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
oras.land/oras-go/v2 v2.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.11.0 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fasthttp/websocket v1.5.8 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fredbi/uri v1.1.0 // indirect
|
||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
||||
github.com/fyne-io/glfw-js v0.3.0 // indirect
|
||||
github.com/fyne-io/image v0.1.1 // indirect
|
||||
github.com/fyne-io/oksvg v0.1.0 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-text/render v0.2.0 // indirect
|
||||
github.com/go-text/typesetting v0.2.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/libp2p/go-yamux/v5 v5.0.1 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.1.0 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||
github.com/otiai10/mint v1.6.3 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
@@ -103,24 +130,31 @@ require (
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pion/webrtc/v4 v4.1.2 // indirect
|
||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
|
||||
github.com/rymdport/portal v0.4.1 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.24.7 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/image v0.25.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.7 // indirect
|
||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
@@ -130,28 +164,27 @@ require (
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/containerd/continuity v0.4.2 // indirect
|
||||
github.com/containerd/errdefs v0.1.0 // indirect
|
||||
github.com/containerd/continuity v0.4.4 // indirect
|
||||
github.com/containerd/errdefs v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||
github.com/creachadair/otp v0.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/docker/cli v27.0.3+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker v27.1.1+incompatible
|
||||
github.com/docker/docker v28.3.3+incompatible
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/flynn/noise v1.1.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-audio/audio v1.0.0
|
||||
github.com/go-audio/riff v1.0.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
@@ -167,7 +200,7 @@ require (
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
@@ -181,7 +214,7 @@ require (
|
||||
github.com/ipfs/go-log/v2 v2.6.0 // indirect
|
||||
github.com/ipld/go-ipld-prime v0.21.0 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jaypipes/pcidb v1.0.0 // indirect
|
||||
github.com/jaypipes/pcidb v1.1.1 // indirect
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
@@ -206,7 +239,7 @@ require (
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/miekg/dns v1.1.66 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
@@ -215,13 +248,13 @@ require (
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/mudler/go-piper v0.0.0-20241023091659-2494246fd9fc
|
||||
github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multiaddr v0.16.0
|
||||
@@ -233,13 +266,12 @@ require (
|
||||
github.com/multiformats/go-multistream v0.6.1 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
@@ -261,10 +293,10 @@ require (
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/tinylib/msgp v1.2.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.9 // indirect
|
||||
github.com/ulikunitz/xz v0.5.14 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||
@@ -272,8 +304,8 @@ require (
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/yuin/goldmark v1.5.4 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.2 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
|
||||
@@ -282,15 +314,15 @@ require (
|
||||
go.uber.org/fx v1.24.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/term v0.34.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
@@ -298,6 +330,6 @@ require (
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
|
||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
||||
274
go.sum
274
go.sum
@@ -8,33 +8,37 @@ dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
fyne.io/fyne/v2 v2.6.3 h1:cvtM2KHeRuH+WhtHiA63z5wJVBkQ9+Ay0UMl9PxFHyA=
|
||||
fyne.io/fyne/v2 v2.6.3/go.mod h1:NGSurpRElVoI1G3h+ab2df3O5KLGh1CGbsMMcX0bPIs=
|
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
|
||||
github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
|
||||
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
|
||||
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
|
||||
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
@@ -45,6 +49,8 @@ github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer5
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
@@ -61,20 +67,36 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/chasefleming/elem-go v0.26.0 h1:RB20oElG4J8W2aQx6jfUuESPQJ52KvC37eLEAPxwJDA=
|
||||
github.com/chasefleming/elem-go v0.26.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
||||
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/chasefleming/elem-go v0.31.0 h1:vZsuKmKdv6idnUbu3awMruxTiFqZ/ertFJFAyBCkVhI=
|
||||
github.com/chasefleming/elem-go v0.31.0/go.mod h1:UBmmZfso2LkXA0HZInbcwsmhE/LXFClEcBPNCGeARtA=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
|
||||
github.com/containerd/containerd v1.7.19 h1:/xQ4XRJ0tamDkdzrrBAUy/LE5nCcxFKdBm4EcPrSMEE=
|
||||
github.com/containerd/containerd v1.7.19/go.mod h1:h4FtNYUUMB4Phr6v+xG89RYKj9XccvbNSCKjdufCrkc=
|
||||
github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
|
||||
github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
||||
github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
|
||||
github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
|
||||
github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII=
|
||||
github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0=
|
||||
github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII=
|
||||
github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
|
||||
github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
@@ -106,14 +128,14 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/cli v27.0.3+incompatible h1:usGs0/BoBW8MWxGeEtqPMkzOY56jZ6kYlSN5BLDioCQ=
|
||||
github.com/docker/cli v27.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
|
||||
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
|
||||
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
@@ -128,15 +150,14 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
|
||||
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fasthttp/websocket v1.5.8 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8=
|
||||
github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
@@ -146,12 +167,19 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
|
||||
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/ggerganov/whisper.cpp/bindings/go v0.0.0-20240626202019-c118733a29ad h1:dQ93Vd6i25o+zH9vvnZ8mu7jtJQ6jT3D+zE3V8Q49n0=
|
||||
github.com/ggerganov/whisper.cpp/bindings/go v0.0.0-20240626202019-c118733a29ad/go.mod h1:QIjZ9OktHFG7p+/m3sMvrAJKKdWrr1fZIK0rM6HZlyo=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
|
||||
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
||||
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
|
||||
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
|
||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
|
||||
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
|
||||
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4=
|
||||
@@ -161,12 +189,15 @@ github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38r
|
||||
github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g=
|
||||
github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -182,12 +213,20 @@ github.com/go-skynet/go-llama.cpp v0.0.0-20240314183750-6a8041ef6b46 h1:lALhXzDk
|
||||
github.com/go-skynet/go-llama.cpp v0.0.0-20240314183750-6a8041ef6b46/go.mod h1:iub0ugfTnflE3rcIuqV2pQSo15nEw3GLW/utm5gyERo=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
|
||||
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
|
||||
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
|
||||
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofiber/contrib/fiberzerolog v1.0.2 h1:LMa/luarQVeINoRwZLHtLQYepLPDIwUNB5OmdZKk+s8=
|
||||
github.com/gofiber/contrib/fiberzerolog v1.0.2/go.mod h1:aTPsgArSgxRWcUeJ/K6PiICz3mbQENR1QOR426QwOoQ=
|
||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/swagger v1.0.0 h1:BzUzDS9ZT6fDUa692kxmfOjc1DZiloLiPK/W5z1H1tc=
|
||||
github.com/gofiber/swagger v1.0.0/go.mod h1:QrYNF1Yrc7ggGK6ATsJ6yfH/8Zi5bu9lA7wB8TmCecg=
|
||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
||||
@@ -245,8 +284,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc=
|
||||
github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -267,6 +306,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
@@ -304,12 +347,14 @@ github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH
|
||||
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jaypipes/ghw v0.12.0 h1:xU2/MDJfWmBhJnujHY9qwXQLs3DBsf0/Xa9vECY0Tho=
|
||||
github.com/jaypipes/ghw v0.12.0/go.mod h1:jeJGbkRB2lL3/gxYzNYzEDETV1ZJ56OKr+CSeSEym+g=
|
||||
github.com/jaypipes/pcidb v1.0.0 h1:vtZIfkiCUE42oYbJS0TAq9XSfSmcsgo9IdxSm9qzYU8=
|
||||
github.com/jaypipes/pcidb v1.0.0/go.mod h1:TnYUvqhPBzCKnH34KrIX22kAeEbDCSRJ9cqLRCuNDfk=
|
||||
github.com/jaypipes/ghw v0.19.1 h1:Lhybk6aadgEJqIxeS0h07UOL/EgMGIdxbAy6V8J7RgY=
|
||||
github.com/jaypipes/ghw v0.19.1/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE=
|
||||
github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro=
|
||||
github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
@@ -320,6 +365,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
@@ -398,16 +445,15 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||
@@ -431,14 +477,20 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
|
||||
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -462,8 +514,8 @@ github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025 h1:WFLP5FHInarYGXi6B/
|
||||
github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025/go.mod h1:QuIFdRstyGJt+MTTkWY+mtD7U6xwjOR6SwKUjmLZtR4=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||
@@ -490,20 +542,22 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
||||
github.com/nikolalohinski/gonja/v2 v2.3.2 h1:UgLFfqi7L9XfX0PEcE4eUpvGojVQL5KhBfJJaBp7ZxY=
|
||||
github.com/nikolalohinski/gonja/v2 v2.3.2/go.mod h1:1Wcc/5huTu6y36e0sOFR1XQoFlylw3c3H3L5WOz0RDg=
|
||||
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
|
||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
@@ -523,8 +577,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
|
||||
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
@@ -572,6 +626,8 @@ github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZ
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -580,6 +636,8 @@ github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4
|
||||
github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
@@ -616,6 +674,8 @@ github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3V
|
||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
|
||||
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
github.com/sashabaranov/go-openai v1.26.2 h1:cVlQa3gn3eYqNXRW03pPlpy6zLG52EU4g0FrWXc0EFI=
|
||||
github.com/sashabaranov/go-openai v1.26.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8=
|
||||
@@ -674,6 +734,10 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/streamer45/silero-vad-go v0.2.1 h1:Li1/tTC4H/3cyw6q4weX+U8GWwEL3lTekK/nYa1Cvuk=
|
||||
github.com/streamer45/silero-vad-go v0.2.1/go.mod h1:B+2FXs/5fZ6pzl6unUZYhZqkYdOB+3saBVzjOzdZnUs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -699,17 +763,18 @@ github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo=
|
||||
github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4=
|
||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
||||
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/tmc/langchaingo v0.1.12 h1:yXwSu54f3b1IKw0jJ5/DWu+qFVH1NBblwC0xddBzGJE=
|
||||
github.com/tmc/langchaingo v0.1.12/go.mod h1:cd62xD6h+ouk8k/QQFhOsjRYBSA1JJ5UVKXSIgm7Ni4=
|
||||
github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
|
||||
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
||||
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
@@ -736,14 +801,16 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
||||
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
||||
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
@@ -773,6 +840,8 @@ go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeX
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
||||
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
|
||||
@@ -789,6 +858,8 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
|
||||
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -804,11 +875,13 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -820,10 +893,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -842,14 +914,13 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -865,8 +936,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -890,7 +961,6 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -900,31 +970,29 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
@@ -945,10 +1013,9 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -975,7 +1042,7 @@ google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
|
||||
google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
|
||||
@@ -999,8 +1066,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@@ -1011,7 +1078,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
@@ -1019,8 +1085,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||
@@ -1029,8 +1095,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 h1:eeH1AIcPvSc0Z25ThsYF+Xoqbn0CI/YnXVYoTLFdGQw=
|
||||
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9/go.mod h1:fyFX5Hj5tP1Mpk8obqA9MZgXT416Q5711SDT7dQLTLk=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
|
||||
|
||||
@@ -98,6 +98,7 @@ func (ml *ModelLoader) grpcModel(backend string, o *Options) func(string, string
|
||||
client = NewModel(modelID, uri, nil)
|
||||
}
|
||||
} else {
|
||||
log.Error().Msgf("Backend not found: %s", backend)
|
||||
return nil, fmt.Errorf("backend not found: %s", backend)
|
||||
}
|
||||
|
||||
@@ -172,6 +173,7 @@ func (ml *ModelLoader) backendLoader(opts ...Option) (client grpc.Backend, err e
|
||||
|
||||
model, err := ml.LoadModel(o.modelID, o.model, ml.grpcModel(backend, o))
|
||||
if err != nil {
|
||||
log.Error().Str("modelID", o.modelID).Err(err).Msgf("Failed to load model %s with backend %s", o.modelID, o.backendString)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
17
scripts/build/golang-darwin.sh
Normal file
17
scripts/build/golang-darwin.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash -eux
|
||||
|
||||
export BUILD_TYPE="${BUILD_TYPE:-metal}"
|
||||
|
||||
mkdir -p backend-images
|
||||
make -C backend/go/${BACKEND} build
|
||||
|
||||
PLATFORMARCH="${PLATFORMARCH:-darwin/arm64}"
|
||||
IMAGE_NAME="${IMAGE_NAME:-localai/${BACKEND}-darwin}"
|
||||
|
||||
./local-ai util create-oci-image \
|
||||
backend/go/${BACKEND}/. \
|
||||
--output ./backend-images/${BACKEND}.tar \
|
||||
--image-name $IMAGE_NAME \
|
||||
--platform $PLATFORMARCH
|
||||
|
||||
make -C backend/go/${BACKEND} clean
|
||||
@@ -28,7 +28,7 @@ cp -rf backend/cpp/llama-cpp/llama-cpp-rpc-server build/darwin/
|
||||
|
||||
# Set default additional libs only for Darwin on M chips (arm64)
|
||||
if [[ "$(uname -s)" == "Darwin" && "$(uname -m)" == "arm64" ]]; then
|
||||
ADDITIONAL_LIBS=${ADDITIONAL_LIBS:-$(ls /opt/homebrew/Cellar/protobuf/**/lib/libutf8_validity.dylib 2>/dev/null || true)}
|
||||
ADDITIONAL_LIBS=${ADDITIONAL_LIBS:-$(ls /opt/homebrew/Cellar/protobuf/**/lib/libutf8_validity*.dylib 2>/dev/null)}
|
||||
else
|
||||
ADDITIONAL_LIBS=${ADDITIONAL_LIBS:-""}
|
||||
fi
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Octokit } from "@octokit/core";
|
||||
// Load backend.yml and parse matrix.include
|
||||
const backendYml = yaml.load(fs.readFileSync(".github/workflows/backend.yml", "utf8"));
|
||||
const jobs = backendYml.jobs;
|
||||
const backendJob = jobs["backend-jobs"];
|
||||
const strategy = backendJob.strategy;
|
||||
const matrix = strategy.matrix;
|
||||
const includes = matrix.include;
|
||||
const backendJobs = jobs["backend-jobs"];
|
||||
const backendJobsDarwin = jobs["backend-jobs-darwin"];
|
||||
const includes = backendJobs.strategy.matrix.include;
|
||||
const includesDarwin = backendJobsDarwin.strategy.matrix.include;
|
||||
|
||||
// Set up Octokit for PR changed files
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
@@ -47,17 +47,25 @@ async function getChangedFiles() {
|
||||
// Infer backend path
|
||||
function inferBackendPath(item) {
|
||||
if (item.dockerfile.endsWith("python")) {
|
||||
return `backend/python/${item.backend}`;
|
||||
return `backend/python/${item.backend}/`;
|
||||
}
|
||||
if (item.dockerfile.endsWith("golang")) {
|
||||
return `backend/go/${item.backend}`;
|
||||
return `backend/go/${item.backend}/`;
|
||||
}
|
||||
if (item.dockerfile.endsWith("llama-cpp")) {
|
||||
return `backend/cpp/llama-cpp`;
|
||||
return `backend/cpp/llama-cpp/`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inferBackendPathDarwin(item) {
|
||||
if (!item.lang) {
|
||||
return `backend/python/${item.backend}/`;
|
||||
}
|
||||
|
||||
return `backend/${item.lang}/${item.backend}/`;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const changedFiles = await getChangedFiles();
|
||||
|
||||
@@ -69,11 +77,21 @@ function inferBackendPath(item) {
|
||||
return changedFiles.some(file => file.startsWith(backendPath));
|
||||
});
|
||||
|
||||
const filteredDarwin = includesDarwin.filter(item => {
|
||||
const backendPath = inferBackendPathDarwin(item);
|
||||
return changedFiles.some(file => file.startsWith(backendPath));
|
||||
})
|
||||
|
||||
console.log("Filtered files:", filtered);
|
||||
console.log("Filtered files Darwin:", filteredDarwin);
|
||||
|
||||
const hasBackends = filtered.length > 0 ? 'true' : 'false';
|
||||
const hasBackendsDarwin = filteredDarwin.length > 0 ? 'true' : 'false';
|
||||
console.log("Has backends?:", hasBackends);
|
||||
console.log("Has Darwin backends?:", hasBackendsDarwin);
|
||||
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends=${hasBackends}\n`);
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-darwin=${hasBackendsDarwin}\n`);
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix=${JSON.stringify({ include: filtered })}\n`);
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-darwin=${JSON.stringify({ include: filteredDarwin })}\n`);
|
||||
})();
|
||||
|
||||
@@ -782,7 +782,7 @@ const docTemplate = `{
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.OpenAIRequest"
|
||||
"$ref": "#/definitions/schema.VideoRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1823,6 +1823,50 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.VideoRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cfg_scale": {
|
||||
"type": "number"
|
||||
},
|
||||
"end_image": {
|
||||
"type": "string"
|
||||
},
|
||||
"fps": {
|
||||
"type": "integer"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"num_frames": {
|
||||
"type": "integer"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"response_format": {
|
||||
"type": "string"
|
||||
},
|
||||
"seed": {
|
||||
"type": "integer"
|
||||
},
|
||||
"start_image": {
|
||||
"type": "string"
|
||||
},
|
||||
"step": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services.GalleryOpStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user