mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-09 12:45:15 -04:00
Compare commits
26
Commits
No files matched your search
@@ -35,19 +35,33 @@ All contributions must comply with LocalAI's licensing requirements:
|
||||
|
||||
## Signed-off-by and Developer Certificate of Origin
|
||||
|
||||
**AI agents MUST NOT add `Signed-off-by` tags.** Only humans can legally
|
||||
certify the Developer Certificate of Origin (DCO). The human submitter
|
||||
is responsible for:
|
||||
Only humans can certify the Developer Certificate of Origin (DCO). AI
|
||||
agents MUST NOT invent or guess a human identity for `Signed-off-by` —
|
||||
doing so forges the DCO certification.
|
||||
|
||||
- Reviewing all AI-generated code
|
||||
However, when a human operator explicitly directs the AI to commit on
|
||||
their behalf, the AI is acting as a typing tool — no different from an
|
||||
editor macro or `git commit -s`. In that case the AI SHOULD add
|
||||
`Signed-off-by:` using the **configured `user.name` / `user.email`** of
|
||||
the current git repository (i.e. the operator's own identity). The
|
||||
resulting trailer is the operator's signature; they take responsibility
|
||||
for it by reviewing and pushing the commit. The AI MUST NOT use any
|
||||
other identity and MUST NOT add its own name to the sign-off.
|
||||
|
||||
When running `git commit`, prefer `git commit --signoff` (or `-s`) so
|
||||
the trailer is emitted by git itself from the configured identity,
|
||||
rather than hand-writing it in a heredoc — this guarantees the sign-off
|
||||
matches whatever identity the operator is currently using.
|
||||
|
||||
The human submitter remains responsible for:
|
||||
|
||||
- Reviewing all AI-generated code before it's pushed or merged
|
||||
- Ensuring compliance with licensing requirements
|
||||
- Adding their own `Signed-off-by` tag (when the project requires DCO)
|
||||
to certify the contribution
|
||||
- Taking full responsibility for the contribution
|
||||
|
||||
AI agents MUST NOT add `Co-Authored-By` trailers for themselves either.
|
||||
A human reviewer owns the contribution; the AI's involvement is recorded
|
||||
via `Assisted-by` (see below).
|
||||
AI agents MUST NOT add `Co-Authored-By` trailers for themselves. A human
|
||||
reviewer owns the contribution; the AI's involvement is recorded via
|
||||
`Assisted-by` (see below).
|
||||
|
||||
## Attribution
|
||||
|
||||
@@ -84,6 +98,12 @@ Assisted-by: Claude:claude-opus-4-7 golangci-lint
|
||||
Signed-off-by: Jane Developer <jane@example.com>
|
||||
```
|
||||
|
||||
The `Signed-off-by` line uses Jane's own identity because Jane is the
|
||||
submitter operating the AI. If Jane asks Claude to create the commit via
|
||||
`git commit -s`, git emits that exact trailer from Jane's configured
|
||||
identity — no separate human step is needed beyond Jane reviewing the
|
||||
diff before pushing.
|
||||
|
||||
## Scope and Responsibility
|
||||
|
||||
Using an AI assistant does not reduce the contributor's responsibility.
|
||||
|
||||
@@ -480,6 +480,22 @@ include:
|
||||
dockerfile: "./backend/Dockerfile.turboquant"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "12"
|
||||
cuda-minor-version: "8"
|
||||
platforms: 'linux/amd64'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-gpu-nvidia-cuda-12-buun-llama-cpp'
|
||||
builder-base-image: 'quay.io/go-skynet/ci-cache:base-grpc-cuda-12-amd64'
|
||||
# bigger-runner: same rationale as -gpu-nvidia-cuda-12-llama-cpp above
|
||||
# (observed 6h5m wall-clock on v4.2.1, just past the 6h job timeout).
|
||||
runs-on: 'bigger-runner'
|
||||
base-image: "ubuntu:24.04"
|
||||
skip-drivers: 'false'
|
||||
backend: "buun-llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.buun-llama-cpp"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "12"
|
||||
cuda-minor-version: "8"
|
||||
@@ -1178,6 +1194,21 @@ include:
|
||||
dockerfile: "./backend/Dockerfile.turboquant"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "13"
|
||||
cuda-minor-version: "0"
|
||||
platforms: 'linux/amd64'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-gpu-nvidia-cuda-13-buun-llama-cpp'
|
||||
builder-base-image: 'quay.io/go-skynet/ci-cache:base-grpc-cuda-13-amd64'
|
||||
# bigger-runner: observed 6h5m wall-clock on v4.2.1 — at the GHA timeout.
|
||||
runs-on: 'bigger-runner'
|
||||
base-image: "ubuntu:24.04"
|
||||
skip-drivers: 'false'
|
||||
backend: "buun-llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.buun-llama-cpp"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "13"
|
||||
cuda-minor-version: "0"
|
||||
@@ -1221,6 +1252,20 @@ include:
|
||||
backend: "turboquant"
|
||||
dockerfile: "./backend/Dockerfile.turboquant"
|
||||
context: "./"
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "13"
|
||||
cuda-minor-version: "0"
|
||||
platforms: 'linux/arm64'
|
||||
skip-drivers: 'false'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-nvidia-l4t-cuda-13-arm64-buun-llama-cpp'
|
||||
builder-base-image: 'quay.io/go-skynet/ci-cache:base-grpc-cuda-13-arm64'
|
||||
base-image: "ubuntu:24.04"
|
||||
runs-on: 'ubuntu-24.04-arm'
|
||||
ubuntu-version: '2404'
|
||||
backend: "buun-llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.buun-llama-cpp"
|
||||
context: "./"
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "13"
|
||||
cuda-minor-version: "0"
|
||||
@@ -2521,6 +2566,20 @@ include:
|
||||
dockerfile: "./backend/Dockerfile.turboquant"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: 'sycl_f32'
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
platforms: 'linux/amd64'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-gpu-intel-sycl-f32-buun-llama-cpp'
|
||||
builder-base-image: 'quay.io/go-skynet/ci-cache:base-grpc-intel-amd64'
|
||||
runs-on: 'ubuntu-latest'
|
||||
base-image: "intel/oneapi-basekit:2025.3.0-0-devel-ubuntu24.04"
|
||||
skip-drivers: 'false'
|
||||
backend: "buun-llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.buun-llama-cpp"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: 'sycl_f32'
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
@@ -2563,6 +2622,20 @@ include:
|
||||
dockerfile: "./backend/Dockerfile.turboquant"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: 'sycl_f16'
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
platforms: 'linux/amd64'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-gpu-intel-sycl-f16-buun-llama-cpp'
|
||||
builder-base-image: 'quay.io/go-skynet/ci-cache:base-grpc-intel-amd64'
|
||||
runs-on: 'ubuntu-latest'
|
||||
base-image: "intel/oneapi-basekit:2025.3.0-0-devel-ubuntu24.04"
|
||||
skip-drivers: 'false'
|
||||
backend: "buun-llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.buun-llama-cpp"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: 'sycl_f16'
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
@@ -3029,6 +3102,21 @@ include:
|
||||
dockerfile: "./backend/Dockerfile.turboquant"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: ''
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
platforms: 'linux/amd64'
|
||||
platform-tag: 'amd64'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-cpu-buun-llama-cpp'
|
||||
builder-base-image: 'quay.io/go-skynet/ci-cache:base-grpc-amd64'
|
||||
runs-on: 'ubuntu-latest'
|
||||
base-image: "ubuntu:24.04"
|
||||
skip-drivers: 'false'
|
||||
backend: "buun-llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.buun-llama-cpp"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: ''
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
@@ -3059,6 +3147,21 @@ include:
|
||||
dockerfile: "./backend/Dockerfile.turboquant"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: ''
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
platforms: 'linux/arm64'
|
||||
platform-tag: 'arm64'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-cpu-buun-llama-cpp'
|
||||
builder-base-image: 'quay.io/go-skynet/ci-cache:base-grpc-arm64'
|
||||
runs-on: 'ubuntu-24.04-arm'
|
||||
base-image: "ubuntu:24.04"
|
||||
skip-drivers: 'false'
|
||||
backend: "buun-llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.buun-llama-cpp"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
- build-type: ''
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
@@ -3320,6 +3423,20 @@ include:
|
||||
dockerfile: "./backend/Dockerfile.turboquant"
|
||||
context: "./"
|
||||
ubuntu-version: '2204'
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "12"
|
||||
cuda-minor-version: "0"
|
||||
platforms: 'linux/arm64'
|
||||
skip-drivers: 'false'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-nvidia-l4t-arm64-buun-llama-cpp'
|
||||
builder-base-image: 'quay.io/go-skynet/ci-cache:base-grpc-l4t-cuda-12-arm64'
|
||||
base-image: "nvcr.io/nvidia/l4t-jetpack:r36.4.0"
|
||||
runs-on: 'ubuntu-24.04-arm'
|
||||
backend: "buun-llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.buun-llama-cpp"
|
||||
context: "./"
|
||||
ubuntu-version: '2204'
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "12"
|
||||
cuda-minor-version: "0"
|
||||
@@ -3380,6 +3497,22 @@ include:
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
# Stablediffusion-ggml
|
||||
- build-type: 'vulkan'
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
platforms: 'linux/amd64'
|
||||
platform-tag: 'amd64'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-gpu-vulkan-buun-llama-cpp'
|
||||
builder-base-image: 'quay.io/go-skynet/ci-cache:base-grpc-vulkan-amd64'
|
||||
runs-on: 'ubuntu-latest'
|
||||
base-image: "ubuntu:24.04"
|
||||
skip-drivers: 'false'
|
||||
backend: "buun-llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.buun-llama-cpp"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
# Stablediffusion-ggml
|
||||
- build-type: 'vulkan'
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
@@ -3412,6 +3545,22 @@ include:
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
# Stablediffusion-ggml
|
||||
- build-type: 'vulkan'
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
platforms: 'linux/arm64'
|
||||
platform-tag: 'arm64'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-gpu-vulkan-buun-llama-cpp'
|
||||
builder-base-image: 'quay.io/go-skynet/ci-cache:base-grpc-vulkan-arm64'
|
||||
runs-on: 'ubuntu-24.04-arm'
|
||||
base-image: "ubuntu:24.04"
|
||||
skip-drivers: 'false'
|
||||
backend: "buun-llama-cpp"
|
||||
dockerfile: "./backend/Dockerfile.buun-llama-cpp"
|
||||
context: "./"
|
||||
ubuntu-version: '2404'
|
||||
# Stablediffusion-ggml
|
||||
- build-type: 'vulkan'
|
||||
cuda-major-version: ""
|
||||
cuda-minor-version: ""
|
||||
|
||||
@@ -33,6 +33,7 @@ jobs:
|
||||
llama-cpp: ${{ steps.detect.outputs.llama-cpp }}
|
||||
ik-llama-cpp: ${{ steps.detect.outputs.ik-llama-cpp }}
|
||||
turboquant: ${{ steps.detect.outputs.turboquant }}
|
||||
buun-llama-cpp: ${{ steps.detect.outputs['buun-llama-cpp'] }}
|
||||
vllm: ${{ steps.detect.outputs.vllm }}
|
||||
sglang: ${{ steps.detect.outputs.sglang }}
|
||||
acestep-cpp: ${{ steps.detect.outputs.acestep-cpp }}
|
||||
@@ -717,6 +718,30 @@ jobs:
|
||||
- name: Build turboquant backend image and run gRPC e2e tests
|
||||
run: |
|
||||
make test-extra-backend-turboquant
|
||||
tests-buun-llama-cpp-grpc:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs['buun-llama-cpp'] == 'true' || needs.detect-changes.outputs.run-all == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
# Exercises the buun-llama-cpp (fork-of-a-fork) backend with the
|
||||
# fork-specific TurboQuant/TCQ KV-cache types. BACKEND_TEST_CACHE_TYPE_V
|
||||
# is set to turbo3 so the test round-trips through the fork's KV
|
||||
# allow-list — picking a stock llama.cpp type would only re-test the
|
||||
# shared code path. DFlash speculative decoding is not exercised here
|
||||
# because the one known public target/drafter pair (Qwen3.5-27B) is too
|
||||
# large for CI.
|
||||
- name: Build buun-llama-cpp backend image and run gRPC e2e tests
|
||||
run: |
|
||||
make test-extra-backend-buun-llama-cpp
|
||||
# tests-vllm-grpc is currently disabled in CI.
|
||||
#
|
||||
# The prebuilt vllm CPU wheel is compiled with AVX-512 VNNI/BF16
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Disable parallel execution for backend builds
|
||||
.NOTPARALLEL: backends/buun-llama-cpp
|
||||
.NOTPARALLEL: backends/diffusers backends/llama-cpp backends/turboquant backends/bonsai backends/outetts backends/piper backends/stablediffusion-ggml backends/trellis2cpp backends/trellis2cpp-darwin backends/whisper backends/crispasr backends/parakeet-cpp backends/moss-transcribe-cpp backends/nemo-speech-cpp backends/faster-whisper backends/silero-vad backends/local-store backends/valkey-store backends/cloud-proxy backends/huggingface backends/rfdetr backends/rfdetr-cpp backends/insightface backends/speaker-recognition backends/kitten-tts backends/kokoro backends/chatterbox backends/llama-cpp-darwin backends/neutts build-darwin-python-backend build-darwin-go-backend backends/mlx backends/diffuser-darwin backends/mlx-vlm backends/mlx-audio backends/mlx-distributed backends/stablediffusion-ggml-darwin backends/vllm backends/vllm-omni backends/longcat-video backends/sglang backends/moonshine backends/pocket-tts backends/qwen-tts backends/faster-qwen3-tts backends/qwen-asr backends/nemo backends/voxcpm backends/whisperx backends/ace-step backends/acestep-cpp backends/fish-speech backends/voxtral backends/opus backends/trl backends/llama-cpp-quantization backends/kokoros backends/sam3-cpp backends/qwen3-tts-cpp backends/moss-tts-cpp backends/magpie-tts-cpp backends/vllm-cpp backends/omnivoice-cpp backends/vibevoice-cpp backends/localvqe backends/tinygrad backends/sherpa-onnx backends/ds4 backends/ds4-darwin backends/liquid-audio backends/supertonic backends/depth-anything-cpp backends/privacy-filter backends/privacy-filter-darwin backends/audio-cpp backends/audio-cpp-darwin
|
||||
|
||||
GOCMD=go
|
||||
@@ -749,6 +750,19 @@ test-extra-backend-bonsai: docker-build-bonsai
|
||||
BACKEND_TEST_MODEL_URL=https://huggingface.co/prism-ml/Bonsai-8B-gguf/resolve/main/Bonsai-8B-Q1_0.gguf \
|
||||
$(MAKE) test-extra-backend
|
||||
|
||||
## buun-llama-cpp: exercises the fork-of-a-fork backend (spiritbuun/buun-llama-cpp)
|
||||
## with the *TurboQuant/TCQ-specific* KV-cache types (turbo3 for V). Same rationale
|
||||
## as turboquant above: picking a standard llama.cpp type would only re-test the
|
||||
## shared code path. buun inherits turboquant's turbo2/turbo3/turbo4 and adds
|
||||
## turbo2_tcq / turbo3_tcq on top. DFlash speculative decoding is not exercised
|
||||
## here because no small DFlash drafter model exists (the known public pair is
|
||||
## Qwen3.5-27B, ~54 GB).
|
||||
test-extra-backend-buun-llama-cpp: docker-build-buun-llama-cpp
|
||||
BACKEND_IMAGE=local-ai-backend:buun-llama-cpp \
|
||||
BACKEND_TEST_CACHE_TYPE_K=q8_0 \
|
||||
BACKEND_TEST_CACHE_TYPE_V=turbo3 \
|
||||
$(MAKE) test-extra-backend
|
||||
|
||||
## Audio transcription wrapper for the llama-cpp backend.
|
||||
## Drives the new AudioTranscription / AudioTranscriptionStream RPCs against
|
||||
## ggml-org/Qwen3-ASR-0.6B-GGUF (a small ASR model that requires its mmproj
|
||||
@@ -1285,6 +1299,11 @@ BACKEND_PRIVACY_FILTER = privacy-filter|privacy-filter|.|false|false
|
||||
# against apt gRPC/protobuf rather than a prebuilt base-grpc image; the reason
|
||||
# is on the audio-cpp block in .github/backend-matrix.yml.
|
||||
BACKEND_AUDIO_CPP = audio-cpp|audio-cpp|.|false|false
|
||||
# buun-llama-cpp is a fork-of-a-fork (spiritbuun/buun-llama-cpp forks
|
||||
# TheTom/llama-cpp-turboquant) that adds DFlash block-diffusion speculative
|
||||
# decoding and extra TCQ KV-cache variants on top of TurboQuant. Same thin
|
||||
# wrapper pattern as turboquant — reuses backend/cpp/llama-cpp grpc-server.
|
||||
BACKEND_BUUN_LLAMA_CPP = buun-llama-cpp|buun-llama-cpp|.|false|false
|
||||
|
||||
# Golang backends
|
||||
BACKEND_PIPER = piper|golang|.|false|true
|
||||
@@ -1390,6 +1409,7 @@ $(eval $(call generate-docker-build-target,$(BACKEND_BONSAI)))
|
||||
$(eval $(call generate-docker-build-target,$(BACKEND_DS4)))
|
||||
$(eval $(call generate-docker-build-target,$(BACKEND_PRIVACY_FILTER)))
|
||||
$(eval $(call generate-docker-build-target,$(BACKEND_AUDIO_CPP)))
|
||||
$(eval $(call generate-docker-build-target,$(BACKEND_BUUN_LLAMA_CPP)))
|
||||
$(eval $(call generate-docker-build-target,$(BACKEND_PIPER)))
|
||||
$(eval $(call generate-docker-build-target,$(BACKEND_LOCAL_STORE)))
|
||||
$(eval $(call generate-docker-build-target,$(BACKEND_VALKEY_STORE)))
|
||||
@@ -1459,7 +1479,7 @@ $(eval $(call generate-docker-build-target,$(BACKEND_SUPERTONIC)))
|
||||
docker-save-%: backend-images
|
||||
docker save local-ai-backend:$* -o backend-images/$*.tar
|
||||
|
||||
docker-build-backends: docker-build-llama-cpp docker-build-ik-llama-cpp docker-build-turboquant docker-build-bonsai docker-build-ds4 docker-build-rerankers docker-build-vllm docker-build-vllm-omni docker-build-longcat-video docker-build-sglang docker-build-transformers docker-build-outetts docker-build-diffusers docker-build-kokoro docker-build-faster-whisper docker-build-crispasr docker-build-coqui docker-build-chatterbox docker-build-vibevoice docker-build-liquid-audio docker-build-moonshine docker-build-pocket-tts docker-build-qwen-tts docker-build-fish-speech docker-build-faster-qwen3-tts docker-build-qwen-asr docker-build-nemo docker-build-voxcpm docker-build-whisperx docker-build-ace-step docker-build-acestep-cpp docker-build-voxtral docker-build-mlx-distributed docker-build-trl docker-build-llama-cpp-quantization docker-build-tinygrad docker-build-kokoros docker-build-sam3-cpp docker-build-rfdetr-cpp docker-build-qwen3-tts-cpp docker-build-moss-tts-cpp docker-build-magpie-tts-cpp docker-build-vllm-cpp docker-build-omnivoice-cpp docker-build-vibevoice-cpp docker-build-localvqe docker-build-insightface docker-build-speaker-recognition docker-build-sherpa-onnx docker-build-cloud-proxy docker-build-supertonic docker-build-depth-anything-cpp docker-build-moss-transcribe-cpp docker-build-nemo-speech-cpp docker-build-privacy-filter docker-build-trellis2cpp docker-build-valkey-store docker-build-audio-cpp
|
||||
docker-build-backends: docker-build-llama-cpp docker-build-ik-llama-cpp docker-build-turboquant docker-build-buun-llama-cpp docker-build-bonsai docker-build-ds4 docker-build-rerankers docker-build-vllm docker-build-vllm-omni docker-build-longcat-video docker-build-sglang docker-build-transformers docker-build-outetts docker-build-diffusers docker-build-kokoro docker-build-faster-whisper docker-build-crispasr docker-build-coqui docker-build-chatterbox docker-build-vibevoice docker-build-liquid-audio docker-build-moonshine docker-build-pocket-tts docker-build-qwen-tts docker-build-fish-speech docker-build-faster-qwen3-tts docker-build-qwen-asr docker-build-nemo docker-build-voxcpm docker-build-whisperx docker-build-ace-step docker-build-acestep-cpp docker-build-voxtral docker-build-mlx-distributed docker-build-trl docker-build-llama-cpp-quantization docker-build-tinygrad docker-build-kokoros docker-build-sam3-cpp docker-build-rfdetr-cpp docker-build-qwen3-tts-cpp docker-build-moss-tts-cpp docker-build-magpie-tts-cpp docker-build-vllm-cpp docker-build-omnivoice-cpp docker-build-vibevoice-cpp docker-build-localvqe docker-build-insightface docker-build-speaker-recognition docker-build-sherpa-onnx docker-build-cloud-proxy docker-build-supertonic docker-build-depth-anything-cpp docker-build-moss-transcribe-cpp docker-build-nemo-speech-cpp docker-build-privacy-filter docker-build-trellis2cpp docker-build-valkey-store docker-build-audio-cpp
|
||||
|
||||
########################################################
|
||||
### Mock Backend for E2E Tests
|
||||
|
||||
@@ -231,7 +231,7 @@ Most backends wrap a best-in-class upstream engine. A handful of them are native
|
||||
|
||||
| Backend | What it does |
|
||||
|---------|-------------|
|
||||
| [vllm.cpp](https://github.com/mudler/vllm.cpp) | From-scratch C++20 port of vLLM for text generation: paged KV cache, continuous batching, prefix caching, safetensors + GGUF loading, engine-enforced structured output, on CPU, CUDA, Metal and Vulkan |
|
||||
| [vllm.cpp](https://github.com/mudler/vllm.cpp) | From-scratch C++20 port of vLLM for text generation: paged KV cache, continuous batching, prefix caching, safetensors + GGUF loading, engine-enforced structured output, on CPU, CUDA, Metal and Vulkan. Also serves MiniMax-H3 joint video+audio generation |
|
||||
| [parakeet.cpp](https://github.com/mudler/parakeet.cpp) | C++/GGML port of NVIDIA NeMo Parakeet ASR (tdt/ctc/rnnt/hybrid), with cache-aware streaming transcription |
|
||||
| [moss-transcribe.cpp](https://github.com/localai-org/moss-transcribe.cpp) | C++/GGML port of OpenMOSS MOSS-Transcribe-Diarize: joint long-form transcription, speaker diarization and timestamping in a single pass |
|
||||
| [moss-tts.cpp](https://github.com/mudler/moss-tts.cpp) | C++/GGML port of the OpenMOSS MOSS-TTS family: text-to-speech (MOSS-TTS-Local v1.5, 48 kHz stereo) with reference-audio voice cloning, through the MOSS-Audio-Tokenizer neural codec |
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
ARG BASE_IMAGE=ubuntu:24.04
|
||||
ARG GRPC_BASE_IMAGE=${BASE_IMAGE}
|
||||
|
||||
|
||||
# The grpc target does one thing, it builds and installs GRPC. This is in it's own layer so that it can be effectively cached by CI.
|
||||
# You probably don't need to change anything here, and if you do, make sure that CI is adjusted so that the cache continues to work.
|
||||
FROM ${GRPC_BASE_IMAGE} AS grpc
|
||||
|
||||
# This is a bit of a hack, but it's required in order to be able to effectively cache this layer in CI
|
||||
ARG GRPC_MAKEFLAGS="-j4 -Otarget"
|
||||
ARG GRPC_VERSION=v1.65.0
|
||||
ARG CMAKE_FROM_SOURCE=false
|
||||
# CUDA Toolkit 13.x compatibility: CMake 3.31.9+ fixes toolchain detection/arch table issues
|
||||
ARG CMAKE_VERSION=3.31.10
|
||||
|
||||
ENV MAKEFLAGS=${GRPC_MAKEFLAGS}
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
build-essential curl libssl-dev \
|
||||
git wget && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install CMake (the version in 22.04 is too old)
|
||||
RUN <<EOT bash
|
||||
if [ "${CMAKE_FROM_SOURCE}" = "true" ]; then
|
||||
curl -L -s https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}.tar.gz -o cmake.tar.gz && tar xvf cmake.tar.gz && cd cmake-${CMAKE_VERSION} && ./configure && make && make install
|
||||
else
|
||||
apt-get update && \
|
||||
apt-get install -y \
|
||||
cmake && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
fi
|
||||
EOT
|
||||
|
||||
# We install GRPC to a different prefix here so that we can copy in only the build artifacts later
|
||||
# saves several hundred MB on the final docker image size vs copying in the entire GRPC source tree
|
||||
# and running make install in the target container
|
||||
RUN git clone --recurse-submodules --jobs 4 -b ${GRPC_VERSION} --depth 1 --shallow-submodules https://github.com/grpc/grpc && \
|
||||
mkdir -p /build/grpc/cmake/build && \
|
||||
cd /build/grpc/cmake/build && \
|
||||
sed -i "216i\ TESTONLY" "../../third_party/abseil-cpp/absl/container/CMakeLists.txt" && \
|
||||
cmake -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX:PATH=/opt/grpc ../.. && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf /build
|
||||
|
||||
FROM ${BASE_IMAGE} AS builder
|
||||
ARG CMAKE_FROM_SOURCE=false
|
||||
ARG CMAKE_VERSION=3.31.10
|
||||
# We can target specific CUDA ARCHITECTURES like --build-arg CUDA_DOCKER_ARCH='75;86;89;120'
|
||||
ARG CUDA_DOCKER_ARCH
|
||||
ENV CUDA_DOCKER_ARCH=${CUDA_DOCKER_ARCH}
|
||||
ARG CMAKE_ARGS
|
||||
ENV CMAKE_ARGS=${CMAKE_ARGS}
|
||||
ARG BACKEND=rerankers
|
||||
ARG BUILD_TYPE
|
||||
ENV BUILD_TYPE=${BUILD_TYPE}
|
||||
ARG CUDA_MAJOR_VERSION
|
||||
ARG CUDA_MINOR_VERSION
|
||||
ARG SKIP_DRIVERS=false
|
||||
ENV CUDA_MAJOR_VERSION=${CUDA_MAJOR_VERSION}
|
||||
ENV CUDA_MINOR_VERSION=${CUDA_MINOR_VERSION}
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG GO_VERSION=1.25.4
|
||||
ARG UBUNTU_VERSION=2404
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ccache git \
|
||||
ca-certificates \
|
||||
make \
|
||||
pkg-config libcurl4-openssl-dev \
|
||||
curl unzip \
|
||||
libssl-dev wget && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Cuda
|
||||
ENV PATH=/usr/local/cuda/bin:${PATH}
|
||||
|
||||
# HipBLAS requirements
|
||||
ENV PATH=/opt/rocm/bin:${PATH}
|
||||
|
||||
|
||||
# Vulkan requirements
|
||||
RUN <<EOT bash
|
||||
if [ "${BUILD_TYPE}" = "vulkan" ] && [ "${SKIP_DRIVERS}" = "false" ]; then
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
software-properties-common pciutils wget gpg-agent && \
|
||||
apt-get install -y libglm-dev cmake libxcb-dri3-0 libxcb-present0 libpciaccess0 \
|
||||
libpng-dev libxcb-keysyms1-dev libxcb-dri3-dev libx11-dev g++ gcc \
|
||||
libwayland-dev libxrandr-dev libxcb-randr0-dev libxcb-ewmh-dev \
|
||||
git python-is-python3 bison libx11-xcb-dev liblz4-dev libzstd-dev \
|
||||
ocaml-core ninja-build pkg-config libxml2-dev wayland-protocols python3-jsonschema \
|
||||
clang-format qtbase5-dev qt6-base-dev libxcb-glx0-dev sudo xz-utils
|
||||
if [ "amd64" = "$TARGETARCH" ]; then
|
||||
wget "https://sdk.lunarg.com/sdk/download/1.4.335.0/linux/vulkansdk-linux-x86_64-1.4.335.0.tar.xz" && \
|
||||
tar -xf vulkansdk-linux-x86_64-1.4.335.0.tar.xz && \
|
||||
rm vulkansdk-linux-x86_64-1.4.335.0.tar.xz && \
|
||||
mkdir -p /opt/vulkan-sdk && \
|
||||
mv 1.4.335.0 /opt/vulkan-sdk/ && \
|
||||
cd /opt/vulkan-sdk/1.4.335.0 && \
|
||||
./vulkansdk --no-deps --maxjobs \
|
||||
vulkan-loader \
|
||||
vulkan-validationlayers \
|
||||
vulkan-extensionlayer \
|
||||
vulkan-tools \
|
||||
shaderc && \
|
||||
cp -rfv /opt/vulkan-sdk/1.4.335.0/x86_64/bin/* /usr/bin/ && \
|
||||
cp -rfv /opt/vulkan-sdk/1.4.335.0/x86_64/lib/* /usr/lib/x86_64-linux-gnu/ && \
|
||||
cp -rfv /opt/vulkan-sdk/1.4.335.0/x86_64/include/* /usr/include/ && \
|
||||
cp -rfv /opt/vulkan-sdk/1.4.335.0/x86_64/share/* /usr/share/ && \
|
||||
rm -rf /opt/vulkan-sdk
|
||||
fi
|
||||
if [ "arm64" = "$TARGETARCH" ]; then
|
||||
mkdir vulkan && cd vulkan && \
|
||||
curl -L -o vulkan-sdk.tar.xz https://github.com/mudler/vulkan-sdk-arm/releases/download/1.4.335.0/vulkansdk-ubuntu-24.04-arm-1.4.335.0.tar.xz && \
|
||||
tar -xvf vulkan-sdk.tar.xz && \
|
||||
rm vulkan-sdk.tar.xz && \
|
||||
cd 1.4.335.0 && \
|
||||
cp -rfv aarch64/bin/* /usr/bin/ && \
|
||||
cp -rfv aarch64/lib/* /usr/lib/aarch64-linux-gnu/ && \
|
||||
cp -rfv aarch64/include/* /usr/include/ && \
|
||||
cp -rfv aarch64/share/* /usr/share/ && \
|
||||
cd ../.. && \
|
||||
rm -rf vulkan
|
||||
fi
|
||||
ldconfig && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
fi
|
||||
EOT
|
||||
|
||||
# CuBLAS requirements
|
||||
RUN <<EOT bash
|
||||
if ( [ "${BUILD_TYPE}" = "cublas" ] || [ "${BUILD_TYPE}" = "l4t" ] ) && [ "${SKIP_DRIVERS}" = "false" ]; then
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
software-properties-common pciutils
|
||||
if [ "amd64" = "$TARGETARCH" ]; then
|
||||
curl -O https://developer.download.nvidia.com/compute/cuda/repos/ubuntu${UBUNTU_VERSION}/x86_64/cuda-keyring_1.1-1_all.deb
|
||||
fi
|
||||
if [ "arm64" = "$TARGETARCH" ]; then
|
||||
if [ "${CUDA_MAJOR_VERSION}" = "13" ]; then
|
||||
curl -O https://developer.download.nvidia.com/compute/cuda/repos/ubuntu${UBUNTU_VERSION}/sbsa/cuda-keyring_1.1-1_all.deb
|
||||
else
|
||||
curl -O https://developer.download.nvidia.com/compute/cuda/repos/ubuntu${UBUNTU_VERSION}/arm64/cuda-keyring_1.1-1_all.deb
|
||||
fi
|
||||
fi
|
||||
dpkg -i cuda-keyring_1.1-1_all.deb && \
|
||||
rm -f cuda-keyring_1.1-1_all.deb && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
cuda-nvcc-${CUDA_MAJOR_VERSION}-${CUDA_MINOR_VERSION} \
|
||||
libcufft-dev-${CUDA_MAJOR_VERSION}-${CUDA_MINOR_VERSION} \
|
||||
libcurand-dev-${CUDA_MAJOR_VERSION}-${CUDA_MINOR_VERSION} \
|
||||
libcublas-dev-${CUDA_MAJOR_VERSION}-${CUDA_MINOR_VERSION} \
|
||||
libcusparse-dev-${CUDA_MAJOR_VERSION}-${CUDA_MINOR_VERSION} \
|
||||
libcusolver-dev-${CUDA_MAJOR_VERSION}-${CUDA_MINOR_VERSION}
|
||||
if [ "${CUDA_MAJOR_VERSION}" = "13" ] && [ "arm64" = "$TARGETARCH" ]; then
|
||||
apt-get install -y --no-install-recommends \
|
||||
libcufile-${CUDA_MAJOR_VERSION}-${CUDA_MINOR_VERSION} libcudnn9-cuda-${CUDA_MAJOR_VERSION} cuda-cupti-${CUDA_MAJOR_VERSION}-${CUDA_MINOR_VERSION} libnvjitlink-${CUDA_MAJOR_VERSION}-${CUDA_MINOR_VERSION}
|
||||
fi
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
fi
|
||||
EOT
|
||||
|
||||
|
||||
# https://github.com/NVIDIA/Isaac-GR00T/issues/343
|
||||
RUN <<EOT bash
|
||||
if [ "${BUILD_TYPE}" = "cublas" ] && [ "${TARGETARCH}" = "arm64" ]; then
|
||||
wget https://developer.download.nvidia.com/compute/cudss/0.6.0/local_installers/cudss-local-tegra-repo-ubuntu${UBUNTU_VERSION}-0.6.0_0.6.0-1_arm64.deb && \
|
||||
dpkg -i cudss-local-tegra-repo-ubuntu${UBUNTU_VERSION}-0.6.0_0.6.0-1_arm64.deb && \
|
||||
cp /var/cudss-local-tegra-repo-ubuntu${UBUNTU_VERSION}-0.6.0/cudss-*-keyring.gpg /usr/share/keyrings/ && \
|
||||
apt-get update && apt-get -y install cudss cudss-cuda-${CUDA_MAJOR_VERSION} && \
|
||||
wget https://developer.download.nvidia.com/compute/nvpl/25.5/local_installers/nvpl-local-repo-ubuntu${UBUNTU_VERSION}-25.5_1.0-1_arm64.deb && \
|
||||
dpkg -i nvpl-local-repo-ubuntu${UBUNTU_VERSION}-25.5_1.0-1_arm64.deb && \
|
||||
cp /var/nvpl-local-repo-ubuntu${UBUNTU_VERSION}-25.5/nvpl-*-keyring.gpg /usr/share/keyrings/ && \
|
||||
apt-get update && apt-get install -y nvpl
|
||||
fi
|
||||
EOT
|
||||
|
||||
# If we are building with clblas support, we need the libraries for the builds
|
||||
RUN if [ "${BUILD_TYPE}" = "clblas" ] && [ "${SKIP_DRIVERS}" = "false" ]; then \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libclblast-dev && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* \
|
||||
; fi
|
||||
|
||||
RUN if [ "${BUILD_TYPE}" = "hipblas" ] && [ "${SKIP_DRIVERS}" = "false" ]; then \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
hipblas-dev \
|
||||
rocblas-dev && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
# I have no idea why, but the ROCM lib packages don't trigger ldconfig after they install, which results in local-ai and others not being able
|
||||
# to locate the libraries. We run ldconfig ourselves to work around this packaging deficiency
|
||||
ldconfig && \
|
||||
# Log which GPU architectures have rocBLAS kernel support
|
||||
echo "rocBLAS library data architectures:" && \
|
||||
(ls /opt/rocm*/lib/rocblas/library/Kernels* 2>/dev/null || ls /opt/rocm*/lib64/rocblas/library/Kernels* 2>/dev/null) | grep -oP 'gfx[0-9a-z+-]+' | sort -u || \
|
||||
echo "WARNING: No rocBLAS kernel data found" \
|
||||
; fi
|
||||
|
||||
RUN echo "TARGETARCH: $TARGETARCH"
|
||||
|
||||
# We need protoc installed, and the version in 22.04 is too old. We will create one as part installing the GRPC build below
|
||||
# but that will also being in a newer version of absl which stablediffusion cannot compile with. This version of protoc is only
|
||||
# here so that we can generate the grpc code for the stablediffusion build
|
||||
RUN <<EOT bash
|
||||
if [ "amd64" = "$TARGETARCH" ]; then
|
||||
curl -L -s https://github.com/protocolbuffers/protobuf/releases/download/v27.1/protoc-27.1-linux-x86_64.zip -o protoc.zip && \
|
||||
unzip -j -d /usr/local/bin protoc.zip bin/protoc && \
|
||||
rm protoc.zip
|
||||
fi
|
||||
if [ "arm64" = "$TARGETARCH" ]; then
|
||||
curl -L -s https://github.com/protocolbuffers/protobuf/releases/download/v27.1/protoc-27.1-linux-aarch_64.zip -o protoc.zip && \
|
||||
unzip -j -d /usr/local/bin protoc.zip bin/protoc && \
|
||||
rm protoc.zip
|
||||
fi
|
||||
EOT
|
||||
|
||||
# Install CMake (the version in 22.04 is too old)
|
||||
RUN <<EOT bash
|
||||
if [ "${CMAKE_FROM_SOURCE}" = "true" ]; then
|
||||
curl -L -s https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}.tar.gz -o cmake.tar.gz && tar xvf cmake.tar.gz && cd cmake-${CMAKE_VERSION} && ./configure && make && make install
|
||||
else
|
||||
apt-get update && \
|
||||
apt-get install -y \
|
||||
cmake && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
fi
|
||||
EOT
|
||||
|
||||
COPY --from=grpc /opt/grpc /usr/local
|
||||
|
||||
|
||||
COPY . /LocalAI
|
||||
|
||||
RUN <<'EOT' bash
|
||||
set -euxo pipefail
|
||||
|
||||
if [[ -n "${CUDA_DOCKER_ARCH:-}" ]]; then
|
||||
CUDA_ARCH_ESC="${CUDA_DOCKER_ARCH//;/\\;}"
|
||||
export CMAKE_ARGS="${CMAKE_ARGS:-} -DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCH_ESC}"
|
||||
echo "CMAKE_ARGS(env) = ${CMAKE_ARGS}"
|
||||
rm -rf /LocalAI/backend/cpp/buun-llama-cpp-*-build
|
||||
fi
|
||||
|
||||
cd /LocalAI/backend/cpp/buun-llama-cpp
|
||||
|
||||
if [ "${TARGETARCH}" = "arm64" ] || [ "${BUILD_TYPE}" = "hipblas" ]; then
|
||||
make buun-llama-cpp-fallback
|
||||
make buun-llama-cpp-grpc
|
||||
make buun-llama-cpp-rpc-server
|
||||
else
|
||||
make buun-llama-cpp-avx
|
||||
make buun-llama-cpp-avx2
|
||||
make buun-llama-cpp-avx512
|
||||
make buun-llama-cpp-fallback
|
||||
make buun-llama-cpp-grpc
|
||||
make buun-llama-cpp-rpc-server
|
||||
fi
|
||||
EOT
|
||||
|
||||
|
||||
# Copy libraries using a script to handle architecture differences
|
||||
RUN make -BC /LocalAI/backend/cpp/buun-llama-cpp package
|
||||
|
||||
|
||||
FROM scratch
|
||||
|
||||
|
||||
# Copy all available binaries (the build process only creates the appropriate ones for the target architecture)
|
||||
COPY --from=builder /LocalAI/backend/cpp/buun-llama-cpp/package/. ./
|
||||
@@ -0,0 +1,92 @@
|
||||
|
||||
# Pinned to the HEAD of master on https://github.com/spiritbuun/buun-llama-cpp.
|
||||
# Auto-bumped nightly by .github/workflows/bump_deps.yaml.
|
||||
BUUN_LLAMA_VERSION?=22464d0848b87c5d56b52fdf6af2e5da46bf803e
|
||||
LLAMA_REPO?=https://github.com/spiritbuun/buun-llama-cpp
|
||||
|
||||
CMAKE_ARGS?=
|
||||
BUILD_TYPE?=
|
||||
NATIVE?=false
|
||||
ONEAPI_VARS?=/opt/intel/oneapi/setvars.sh
|
||||
TARGET?=--target grpc-server
|
||||
JOBS?=$(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1)
|
||||
ARCH?=$(shell uname -m)
|
||||
|
||||
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
LLAMA_CPP_DIR := $(CURRENT_MAKEFILE_DIR)/../llama-cpp
|
||||
|
||||
GREEN := \033[0;32m
|
||||
RESET := \033[0m
|
||||
|
||||
# buun-llama-cpp is a llama.cpp fork-of-a-fork (spiritbuun/buun-llama-cpp forked
|
||||
# TheTom/llama-cpp-turboquant, which itself forked ggml-org/llama.cpp). Rather
|
||||
# than duplicating grpc-server.cpp / CMakeLists.txt / prepare.sh we reuse the
|
||||
# ones in backend/cpp/llama-cpp, and only swap which repo+sha the fetch step
|
||||
# pulls. Each flavor target copies ../llama-cpp into a sibling
|
||||
# ../buun-llama-cpp-<flavor>-build directory, then invokes llama-cpp's own
|
||||
# build-llama-cpp-grpc-server with LLAMA_REPO/LLAMA_VERSION overridden to point
|
||||
# at the fork.
|
||||
PATCHES_DIR := $(CURRENT_MAKEFILE_DIR)/patches
|
||||
|
||||
# Each flavor target:
|
||||
# 1. copies backend/cpp/llama-cpp/ (grpc-server.cpp + prepare.sh + CMakeLists.txt + Makefile)
|
||||
# into a sibling buun-llama-cpp-<flavor>-build directory;
|
||||
# 2. clones the buun fork into buun-llama-cpp-<flavor>-build/llama.cpp via the
|
||||
# copy's own `llama.cpp` target, overriding LLAMA_REPO/LLAMA_VERSION;
|
||||
# 3. applies patches from backend/cpp/buun-llama-cpp/patches/ to the cloned
|
||||
# fork sources (for backporting upstream commits the fork hasn't pulled);
|
||||
# 4. runs the copy's `grpc-server` target, which produces the binary we copy
|
||||
# up as buun-llama-cpp-<flavor>.
|
||||
define buun-llama-cpp-build
|
||||
rm -rf $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-$(1)-build
|
||||
cp -rf $(LLAMA_CPP_DIR) $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-$(1)-build
|
||||
# Stock llama.cpp patches target upstream and may not apply to this fork.
|
||||
# The buun-specific compatibility series is applied explicitly below.
|
||||
rm -rf $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-$(1)-build/patches
|
||||
$(MAKE) -C $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-$(1)-build purge
|
||||
# Augment the copied grpc-server.cpp's KV-cache allow-list with the
|
||||
# fork's turbo2/turbo3/turbo4/turbo2_tcq/turbo3_tcq types and wire up the
|
||||
# DFlash-specific option handlers (tree_budget / draft_topk). We patch the
|
||||
# *copy*, never the original under backend/cpp/llama-cpp/, so the stock
|
||||
# llama-cpp build stays compiling against vanilla upstream.
|
||||
bash $(CURRENT_MAKEFILE_DIR)/patch-grpc-server.sh $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-$(1)-build/grpc-server.cpp
|
||||
bash $(LLAMA_CPP_DIR)/disable-score-task.sh $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-$(1)-build/grpc-server.cpp
|
||||
$(info $(GREEN)I buun-llama-cpp build info:$(1)$(RESET))
|
||||
LLAMA_REPO=$(LLAMA_REPO) LLAMA_VERSION=$(BUUN_LLAMA_VERSION) \
|
||||
$(MAKE) -C $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-$(1)-build llama.cpp
|
||||
bash $(CURRENT_MAKEFILE_DIR)/apply-patches.sh $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-$(1)-build/llama.cpp $(PATCHES_DIR)
|
||||
CMAKE_ARGS="$(CMAKE_ARGS) $(2)" TARGET="$(3)" \
|
||||
LLAMA_REPO=$(LLAMA_REPO) LLAMA_VERSION=$(BUUN_LLAMA_VERSION) \
|
||||
$(MAKE) -C $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-$(1)-build grpc-server
|
||||
cp -rfv $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-$(1)-build/grpc-server buun-llama-cpp-$(1)
|
||||
endef
|
||||
|
||||
buun-llama-cpp-avx2:
|
||||
$(call buun-llama-cpp-build,avx2,-DGGML_AVX=on -DGGML_AVX2=on -DGGML_AVX512=off -DGGML_FMA=on -DGGML_F16C=on,--target grpc-server)
|
||||
|
||||
buun-llama-cpp-avx512:
|
||||
$(call buun-llama-cpp-build,avx512,-DGGML_AVX=on -DGGML_AVX2=off -DGGML_AVX512=on -DGGML_FMA=on -DGGML_F16C=on,--target grpc-server)
|
||||
|
||||
buun-llama-cpp-avx:
|
||||
$(call buun-llama-cpp-build,avx,-DGGML_AVX=on -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off,--target grpc-server)
|
||||
|
||||
buun-llama-cpp-fallback:
|
||||
$(call buun-llama-cpp-build,fallback,-DGGML_AVX=off -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off,--target grpc-server)
|
||||
|
||||
buun-llama-cpp-grpc:
|
||||
$(call buun-llama-cpp-build,grpc,-DGGML_RPC=ON -DGGML_AVX=off -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off,--target grpc-server --target rpc-server)
|
||||
|
||||
buun-llama-cpp-rpc-server: buun-llama-cpp-grpc
|
||||
cp -rf $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-grpc-build/llama.cpp/build/bin/rpc-server buun-llama-cpp-rpc-server
|
||||
|
||||
package:
|
||||
bash package.sh
|
||||
|
||||
test:
|
||||
bash test-patch-grpc-server.sh
|
||||
|
||||
purge:
|
||||
rm -rf $(CURRENT_MAKEFILE_DIR)/../buun-llama-cpp-*-build
|
||||
rm -rf buun-llama-cpp-* package
|
||||
|
||||
clean: purge
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# Apply the buun-llama-cpp patch series to a cloned buun-llama-cpp checkout.
|
||||
#
|
||||
# buun-llama-cpp is a fork-of-a-fork that branched off upstream llama.cpp
|
||||
# before some API changes the shared backend/cpp/llama-cpp/grpc-server.cpp
|
||||
# depends on. We carry those upstream commits as patch files under
|
||||
# backend/cpp/buun-llama-cpp/patches/ and apply them here so the reused
|
||||
# grpc-server source compiles against the fork unmodified.
|
||||
#
|
||||
# Drop the corresponding patch from patches/ whenever the fork catches up with
|
||||
# upstream — the build will fail fast if a patch stops applying, which is the
|
||||
# signal to retire it.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "usage: $0 <llama.cpp-src-dir> <patches-dir>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
SRC_DIR=$1
|
||||
PATCHES_DIR=$2
|
||||
|
||||
if [[ ! -d "$SRC_DIR" ]]; then
|
||||
echo "source dir does not exist: $SRC_DIR" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -d "$PATCHES_DIR" ]]; then
|
||||
echo "no patches dir at $PATCHES_DIR, nothing to apply"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
patches=("$PATCHES_DIR"/*.patch)
|
||||
shopt -u nullglob
|
||||
|
||||
if [[ ${#patches[@]} -eq 0 ]]; then
|
||||
echo "no .patch files in $PATCHES_DIR, nothing to apply"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$SRC_DIR"
|
||||
|
||||
for patch in "${patches[@]}"; do
|
||||
echo "==> applying $patch"
|
||||
git apply --verbose "$patch"
|
||||
done
|
||||
|
||||
echo "all buun-llama-cpp patches applied successfully"
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to copy the appropriate libraries based on architecture
|
||||
# This script is used in the final stage of the Dockerfile
|
||||
|
||||
set -e
|
||||
|
||||
CURDIR=$(dirname "$(realpath $0)")
|
||||
REPO_ROOT="${CURDIR}/../../.."
|
||||
|
||||
# Create lib directory
|
||||
mkdir -p $CURDIR/package/lib
|
||||
|
||||
cp -avrf $CURDIR/buun-llama-cpp-* $CURDIR/package/
|
||||
cp -rfv $CURDIR/run.sh $CURDIR/package/
|
||||
|
||||
# Detect architecture and copy appropriate libraries
|
||||
if [ -f "/lib64/ld-linux-x86-64.so.2" ]; then
|
||||
# x86_64 architecture
|
||||
echo "Detected x86_64 architecture, copying x86_64 libraries..."
|
||||
cp -arfLv /lib64/ld-linux-x86-64.so.2 $CURDIR/package/lib/ld.so
|
||||
cp -arfLv /lib/x86_64-linux-gnu/libc.so.6 $CURDIR/package/lib/libc.so.6
|
||||
cp -arfLv /lib/x86_64-linux-gnu/libgcc_s.so.1 $CURDIR/package/lib/libgcc_s.so.1
|
||||
cp -arfLv /lib/x86_64-linux-gnu/libstdc++.so.6 $CURDIR/package/lib/libstdc++.so.6
|
||||
cp -arfLv /lib/x86_64-linux-gnu/libm.so.6 $CURDIR/package/lib/libm.so.6
|
||||
cp -arfLv /lib/x86_64-linux-gnu/libgomp.so.1 $CURDIR/package/lib/libgomp.so.1
|
||||
cp -arfLv /lib/x86_64-linux-gnu/libdl.so.2 $CURDIR/package/lib/libdl.so.2
|
||||
cp -arfLv /lib/x86_64-linux-gnu/librt.so.1 $CURDIR/package/lib/librt.so.1
|
||||
cp -arfLv /lib/x86_64-linux-gnu/libpthread.so.0 $CURDIR/package/lib/libpthread.so.0
|
||||
elif [ -f "/lib/ld-linux-aarch64.so.1" ]; then
|
||||
# ARM64 architecture
|
||||
echo "Detected ARM64 architecture, copying ARM64 libraries..."
|
||||
cp -arfLv /lib/ld-linux-aarch64.so.1 $CURDIR/package/lib/ld.so
|
||||
cp -arfLv /lib/aarch64-linux-gnu/libc.so.6 $CURDIR/package/lib/libc.so.6
|
||||
cp -arfLv /lib/aarch64-linux-gnu/libgcc_s.so.1 $CURDIR/package/lib/libgcc_s.so.1
|
||||
cp -arfLv /lib/aarch64-linux-gnu/libstdc++.so.6 $CURDIR/package/lib/libstdc++.so.6
|
||||
cp -arfLv /lib/aarch64-linux-gnu/libm.so.6 $CURDIR/package/lib/libm.so.6
|
||||
cp -arfLv /lib/aarch64-linux-gnu/libgomp.so.1 $CURDIR/package/lib/libgomp.so.1
|
||||
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
|
||||
else
|
||||
echo "Error: Could not detect architecture"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Package GPU libraries based on BUILD_TYPE
|
||||
GPU_LIB_SCRIPT="${REPO_ROOT}/scripts/build/package-gpu-libs.sh"
|
||||
if [ -f "$GPU_LIB_SCRIPT" ]; then
|
||||
echo "Packaging GPU libraries for BUILD_TYPE=${BUILD_TYPE:-cpu}..."
|
||||
source "$GPU_LIB_SCRIPT" "$CURDIR/package/lib"
|
||||
package_gpu_libs
|
||||
fi
|
||||
|
||||
echo "Packaging completed successfully"
|
||||
ls -liah $CURDIR/package/
|
||||
ls -liah $CURDIR/package/lib/
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
#!/bin/bash
|
||||
# Patch the shared backend/cpp/llama-cpp/grpc-server.cpp *copy* used by the
|
||||
# buun-llama-cpp build to account for three gaps between upstream and the fork:
|
||||
#
|
||||
# 1. Augment the kv_cache_types[] allow-list so `LoadModel` accepts the
|
||||
# fork-specific `turbo2` / `turbo3` / `turbo4` cache types plus the buun
|
||||
# additions `turbo2_tcq` / `turbo3_tcq`.
|
||||
#
|
||||
# 2. Adapt the post-refactor speculative-decoding fields and option handlers
|
||||
# to the fork's legacy flat common_params_speculative layout, while adding
|
||||
# buun-exclusive tree_budget / draft_topk support.
|
||||
# These reference struct fields (common_params.speculative.tree_budget
|
||||
# and .draft_topk) that only exist in buun's common/common.h — adding
|
||||
# them to the shared backend/cpp/llama-cpp/grpc-server.cpp would break
|
||||
# the stock llama-cpp build, so we inject them only into the buun copy.
|
||||
#
|
||||
# 3. Replace `get_media_marker()` (added upstream in ggml-org/llama.cpp#21962,
|
||||
# server-side random per-instance marker) with the legacy "<__media__>"
|
||||
# literal. The fork branched before that PR, so server-common.cpp has no
|
||||
# get_media_marker symbol. The fork's mtmd_default_marker() still returns
|
||||
# "<__media__>", and Go-side tooling falls back to that sentinel when the
|
||||
# backend does not expose media_marker, so substituting the literal keeps
|
||||
# behavior identical on the buun path.
|
||||
#
|
||||
# We patch the *copy* sitting in buun-llama-cpp-<flavor>-build/, never the
|
||||
# original under backend/cpp/llama-cpp/, so the stock llama-cpp build keeps
|
||||
# compiling against vanilla upstream.
|
||||
#
|
||||
# Idempotent: skips each insertion if its marker is already present (so re-runs
|
||||
# of the same build dir don't double-insert).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "usage: $0 <grpc-server.cpp>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
SRC=$1
|
||||
|
||||
if [[ ! -f "$SRC" ]]; then
|
||||
echo "grpc-server.cpp not found at $SRC" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if grep -q 'GGML_TYPE_TURBO2_TCQ' "$SRC"; then
|
||||
echo "==> $SRC already has buun cache types, skipping KV allow-list patch"
|
||||
else
|
||||
echo "==> patching $SRC to allow turbo2/turbo3/turbo4/turbo2_tcq/turbo3_tcq KV-cache types"
|
||||
|
||||
# Insert the five TURBO entries right after the first ` GGML_TYPE_Q5_1,`
|
||||
# line (the kv_cache_types[] allow-list). Using awk because the builder
|
||||
# image does not ship python3, and GNU sed's multi-line `a\` quoting is
|
||||
# awkward.
|
||||
awk '
|
||||
/^ GGML_TYPE_Q5_1,$/ && !done {
|
||||
print
|
||||
print " // buun-llama-cpp fork extras — added by patch-grpc-server.sh"
|
||||
print " GGML_TYPE_TURBO2_0,"
|
||||
print " GGML_TYPE_TURBO3_0,"
|
||||
print " GGML_TYPE_TURBO4_0,"
|
||||
print " GGML_TYPE_TURBO2_TCQ,"
|
||||
print " GGML_TYPE_TURBO3_TCQ,"
|
||||
done = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!done) {
|
||||
print "patch-grpc-server.sh: anchor ` GGML_TYPE_Q5_1,` not found" > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "$SRC" > "$SRC.tmp"
|
||||
mv "$SRC.tmp" "$SRC"
|
||||
|
||||
echo "==> KV allow-list patch OK"
|
||||
fi
|
||||
|
||||
if grep -q 'buun-llama-cpp legacy speculative options' "$SRC"; then
|
||||
echo "==> $SRC already has legacy speculative option handlers, skipping"
|
||||
else
|
||||
echo "==> replacing modern speculative option handlers with the fork-compatible set"
|
||||
|
||||
# Replace the whole speculative option section. The fork predates chained
|
||||
# speculative types and the nested draft/ngram families, so retaining any
|
||||
# modern-only handler makes the copied server fail at compile time.
|
||||
awk '
|
||||
/} else if \(!strcmp\(optname, "spec_type"\)/ && !done {
|
||||
print " // buun-llama-cpp legacy speculative options"
|
||||
print " } else if (!strcmp(optname, \"spec_type\") || !strcmp(optname, \"speculative_type\")) {"
|
||||
print " auto type = common_speculative_type_from_name(optval_str.substr(0, optval_str.find(\",\")));"
|
||||
print " if (type != COMMON_SPECULATIVE_TYPE_COUNT) params.speculative.type = type;"
|
||||
print " } else if (!strcmp(optname, \"spec_n_max\") || !strcmp(optname, \"draft_max\")) {"
|
||||
print " if (optval != NULL) { try { params.speculative.n_max = std::stoi(optval_str); } catch (...) {} }"
|
||||
print " } else if (!strcmp(optname, \"spec_n_min\") || !strcmp(optname, \"draft_min\")) {"
|
||||
print " if (optval != NULL) { try { params.speculative.n_min = std::stoi(optval_str); } catch (...) {} }"
|
||||
print " } else if (!strcmp(optname, \"spec_p_min\") || !strcmp(optname, \"draft_p_min\")) {"
|
||||
print " if (optval != NULL) { try { params.speculative.p_min = std::stof(optval_str); } catch (...) {} }"
|
||||
print " } else if (!strcmp(optname, \"spec_p_split\")) {"
|
||||
print " if (optval != NULL) { try { params.speculative.p_split = std::stof(optval_str); } catch (...) {} }"
|
||||
print " } else if (!strcmp(optname, \"spec_ngram_size_n\") || !strcmp(optname, \"ngram_size_n\")) {"
|
||||
print " if (optval != NULL) { try { params.speculative.ngram_size_n = (uint16_t)std::stoi(optval_str); } catch (...) {} }"
|
||||
print " } else if (!strcmp(optname, \"spec_ngram_size_m\") || !strcmp(optname, \"ngram_size_m\")) {"
|
||||
print " if (optval != NULL) { try { params.speculative.ngram_size_m = (uint16_t)std::stoi(optval_str); } catch (...) {} }"
|
||||
print " } else if (!strcmp(optname, \"spec_ngram_min_hits\") || !strcmp(optname, \"ngram_min_hits\")) {"
|
||||
print " if (optval != NULL) { try { params.speculative.ngram_min_hits = (uint16_t)std::stoi(optval_str); } catch (...) {} }"
|
||||
print " } else if (!strcmp(optname, \"draft_gpu_layers\")) {"
|
||||
print " if (optval != NULL) { try { params.speculative.n_gpu_layers = std::stoi(optval_str); } catch (...) {} }"
|
||||
print " } else if (!strcmp(optname, \"tree_budget\")) {"
|
||||
print " if (optval != NULL) { try { params.speculative.tree_budget = std::stoi(optval_str); } catch (...) {} }"
|
||||
print " } else if (!strcmp(optname, \"draft_topk\")) {"
|
||||
print " if (optval != NULL) { try { params.speculative.draft_topk = std::stoi(optval_str); } catch (...) {} }"
|
||||
skipping = 1
|
||||
next
|
||||
}
|
||||
skipping && /^ }$/ { skipping = 0; done = 1; print; next }
|
||||
!skipping { print }
|
||||
END {
|
||||
if (!done) {
|
||||
print "patch-grpc-server.sh: speculative option section not found" > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "$SRC" > "$SRC.tmp"
|
||||
mv "$SRC.tmp" "$SRC"
|
||||
|
||||
echo "==> legacy speculative option-handler patch OK"
|
||||
fi
|
||||
|
||||
# The modern server initializes a vector of speculative types when DraftModel
|
||||
# is present. The fork still exposes a single enum value.
|
||||
awk '
|
||||
/const bool no_spec_type = params\.speculative\.types\.empty\(\)/ && !done {
|
||||
print " if (params.speculative.type == COMMON_SPECULATIVE_TYPE_NONE) {"
|
||||
print " params.speculative.type = COMMON_SPECULATIVE_TYPE_DRAFT;"
|
||||
print " }"
|
||||
skipping = 1
|
||||
next
|
||||
}
|
||||
skipping && /^ }$/ { skipping = 0; done = 1; next }
|
||||
!skipping { print }
|
||||
' "$SRC" > "$SRC.tmp"
|
||||
mv "$SRC.tmp" "$SRC"
|
||||
|
||||
# Map supported post-refactor fields back to the names used by the pinned fork.
|
||||
sed -E \
|
||||
-e 's/params\.speculative\.draft\.mparams\.path/params.speculative.mparams_dft.path/g' \
|
||||
-e 's/params\.speculative\.draft\.n_gpu_layers/params.speculative.n_gpu_layers/g' \
|
||||
-e 's/ctx_server\.impl->model_tgt/ctx_server.impl->model/g' \
|
||||
-e '/params\.cache_idle_slots =/d' \
|
||||
-e '/params\.split_mode = LLAMA_SPLIT_MODE_TENSOR;/d' \
|
||||
-e '/params\.speculative\.draft\.tensor_buft_overrides/d' \
|
||||
"$SRC" > "$SRC.tmp"
|
||||
mv "$SRC.tmp" "$SRC"
|
||||
|
||||
if ! grep -q '^#define LOCALAI_TURBOQUANT_NO_CHECKPOINT_MIN_STEP' "$SRC"; then
|
||||
sed '0,/^#include/{s/^#include/#define LOCALAI_TURBOQUANT_NO_CHECKPOINT_MIN_STEP 1\n\n#include/}' "$SRC" > "$SRC.tmp"
|
||||
mv "$SRC.tmp" "$SRC"
|
||||
fi
|
||||
|
||||
if grep -qE 'ctx_server\.get_meta\(\)\.logit_bias_eog|params_base\.sampling\.logit_bias_eog,' "$SRC"; then
|
||||
echo "==> patching $SRC to drop the logit_bias_eog arg from params_from_json_cmpl() callsites (buun still uses the pre-refactor 4-arg signature)"
|
||||
# Upstream llama.cpp refactored params_from_json_cmpl to take a precomputed
|
||||
# logit_bias_eog vector after buun's 2026-04-05 fork-point — simultaneously
|
||||
# adding server_context_meta::logit_bias_eog as the supplier. Buun carries
|
||||
# neither change: its params_from_json_cmpl is still 4-arg, and internally
|
||||
# derives logit_bias_eog from the common_params it's passed. So we just
|
||||
# delete the argument line entirely — the remaining 4 args match buun's
|
||||
# signature and the resulting behavior matches upstream bit-for-bit
|
||||
# (upstream's 5th arg is the same data buun derives internally).
|
||||
#
|
||||
# Guard is broad so this works whether the line has been run through this
|
||||
# block before (leaving params_base.sampling.logit_bias_eog,) or not
|
||||
# (leaving the original ctx_server.get_meta().logit_bias_eog,).
|
||||
sed -E '/^[[:space:]]+(ctx_server\.get_meta\(\)\.logit_bias_eog|params_base\.sampling\.logit_bias_eog),$/d' "$SRC" > "$SRC.tmp"
|
||||
mv "$SRC.tmp" "$SRC"
|
||||
echo "==> logit_bias_eog arg drop OK"
|
||||
else
|
||||
echo "==> $SRC has no logit_bias_eog arg line, skipping"
|
||||
fi
|
||||
|
||||
if grep -q 'get_media_marker()' "$SRC"; then
|
||||
echo "==> patching $SRC to replace get_media_marker() with legacy \"<__media__>\" literal"
|
||||
# Only one call site today (ModelMetadata), but replace all occurrences to
|
||||
# stay robust if upstream adds more. Use a temp file to avoid relying on
|
||||
# sed -i portability (the builder image uses GNU sed, but keeping this
|
||||
# consistent with the awk block above).
|
||||
sed 's/get_media_marker()/"<__media__>"/g' "$SRC" > "$SRC.tmp"
|
||||
mv "$SRC.tmp" "$SRC"
|
||||
echo "==> get_media_marker() substitution OK"
|
||||
else
|
||||
echo "==> $SRC has no get_media_marker() call, skipping media-marker patch"
|
||||
fi
|
||||
|
||||
echo "==> all patches applied"
|
||||
@@ -0,0 +1,46 @@
|
||||
Subject: [PATCH] ggml-cuda/fattn: provide atomicAdd(double*,double) shim for pre-sm_60
|
||||
|
||||
Buun's Q² calibration path in ggml_cuda_turbo_scale_q calls
|
||||
atomicAdd(&d_q_channel_sq_fattn[threadIdx.x], (double)(val * val));
|
||||
but native double atomicAdd is only available on compute capability 6.0
|
||||
and newer. Compiling against a CUDA arch list that includes older
|
||||
architectures (LocalAI's CUDA 12 Docker image builds for the full
|
||||
published arch range) fails with:
|
||||
|
||||
fattn.cu(812): error: no instance of overloaded function "atomicAdd"
|
||||
matches the argument list, argument types are: (double *, double)
|
||||
|
||||
Add the canonical CUDA-programming-guide shim at the top of fattn.cu so
|
||||
pre-sm_60 codegen has a definition to call. On sm_60+ the native CUDA
|
||||
intrinsic is used and the shim is elided via __CUDA_ARCH__.
|
||||
|
||||
--- a/ggml/src/ggml-cuda/fattn.cu
|
||||
+++ b/ggml/src/ggml-cuda/fattn.cu
|
||||
@@ -7,6 +7,27 @@
|
||||
|
||||
#include <atomic>
|
||||
|
||||
+// Pre-sm_60 double atomicAdd shim. Native double atomicAdd(double*,double)
|
||||
+// is only available on CUDA compute capability 6.0+ (see CUDA C Programming
|
||||
+// Guide, B.15 Atomic Functions). Buun's Q² calibration path below calls
|
||||
+// atomicAdd with a double*; without this definition, nvcc fails to find a
|
||||
+// matching overload whenever the compile target list includes pre-sm_60
|
||||
+// architectures. The standard CAS loop implementation below matches the
|
||||
+// semantics of the native intrinsic.
|
||||
+#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ < 600
|
||||
+static __device__ double atomicAdd(double * address, double val) {
|
||||
+ unsigned long long int * address_as_ull = (unsigned long long int *)address;
|
||||
+ unsigned long long int old = *address_as_ull;
|
||||
+ unsigned long long int assumed;
|
||||
+ do {
|
||||
+ assumed = old;
|
||||
+ old = atomicCAS(address_as_ull, assumed,
|
||||
+ __double_as_longlong(val + __longlong_as_double(assumed)));
|
||||
+ } while (assumed != old);
|
||||
+ return __longlong_as_double(old);
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
// InnerQ: update the fattn-side inverse scale array from host (all devices)
|
||||
void turbo_innerq_update_fattn_scales(const float * scale_inv) {
|
||||
int cur_device;
|
||||
@@ -0,0 +1,32 @@
|
||||
Subject: [PATCH] ggml-cuda/argmax: pass WARP_SIZE to the top-K __shfl_xor_sync calls
|
||||
|
||||
Two __shfl_xor_sync calls in the top-K intra-warp merge drop the `width`
|
||||
argument and rely on the CUDA default (warpSize). Every other call in
|
||||
the same file already passes WARP_SIZE explicitly, and the HIP/ROCm
|
||||
compatibility shim at ggml/src/ggml-cuda/vendors/hip.h:33 is a 4-arg
|
||||
function-like macro — so the 3-arg form fails to preprocess when
|
||||
building with hipcc against ROCm:
|
||||
|
||||
argmax.cu:265: error: too few arguments provided to function-like
|
||||
macro invocation
|
||||
note: macro '__shfl_xor_sync' defined here:
|
||||
#define __shfl_xor_sync(mask, var, laneMask, width) \
|
||||
__shfl_xor(var, laneMask, width)
|
||||
|
||||
Align the two call sites with the rest of the file by passing WARP_SIZE
|
||||
explicitly. On CUDA the generated code is unchanged (warpSize is the
|
||||
default); on HIP it now matches the macro's arity.
|
||||
|
||||
--- a/ggml/src/ggml-cuda/argmax.cu
|
||||
+++ b/ggml/src/ggml-cuda/argmax.cu
|
||||
@@ -262,8 +262,8 @@
|
||||
// Each step: lane gets partner's min element, if it beats our min, replace and re-heapify
|
||||
for (int offset = WARP_SIZE / 2; offset > 0; offset >>= 1) {
|
||||
for (int i = 0; i < K; i++) {
|
||||
- float partner_val = __shfl_xor_sync(0xFFFFFFFF, heap_val[i], offset);
|
||||
- int partner_idx = __shfl_xor_sync(0xFFFFFFFF, heap_idx[i], offset);
|
||||
+ float partner_val = __shfl_xor_sync(0xFFFFFFFF, heap_val[i], offset, WARP_SIZE);
|
||||
+ int partner_idx = __shfl_xor_sync(0xFFFFFFFF, heap_idx[i], offset, WARP_SIZE);
|
||||
if (partner_val > heap_val[0]) {
|
||||
heap_val[0] = partner_val;
|
||||
heap_idx[0] = partner_idx;
|
||||
@@ -0,0 +1,24 @@
|
||||
Subject: [PATCH] ggml-cuda/vendors/hip: alias cudaMemcpy{To,From}Symbol to hip counterparts
|
||||
|
||||
Buun's Q² calibration + TCQ codebook upload paths in fattn.cu use
|
||||
cudaMemcpyToSymbol / cudaMemcpyFromSymbol. The HIP-compat header in
|
||||
ggml/src/ggml-cuda/vendors/hip.h already aliases the scalar cudaMemcpy
|
||||
family (cudaMemcpy, cudaMemcpyAsync, cudaMemcpy2DAsync, …) but is
|
||||
missing the symbol variants. Building with hipcc therefore fails with
|
||||
15+ "use of undeclared identifier 'cudaMemcpyToSymbol'" errors.
|
||||
|
||||
Add the two missing aliases alongside the existing memcpy block. HIP
|
||||
provides hipMemcpy{To,From}Symbol with the same signature as CUDA's
|
||||
equivalents, so this is a straight name substitution.
|
||||
|
||||
--- a/ggml/src/ggml-cuda/vendors/hip.h
|
||||
+++ b/ggml/src/ggml-cuda/vendors/hip.h
|
||||
@@ -85,6 +85,8 @@
|
||||
#define cudaMemcpyDeviceToDevice hipMemcpyDeviceToDevice
|
||||
#define cudaMemcpyDeviceToHost hipMemcpyDeviceToHost
|
||||
#define cudaMemcpyHostToDevice hipMemcpyHostToDevice
|
||||
+#define cudaMemcpyToSymbol hipMemcpyToSymbol
|
||||
+#define cudaMemcpyFromSymbol hipMemcpyFromSymbol
|
||||
#define cudaMemcpyKind hipMemcpyKind
|
||||
#define cudaMemset hipMemset
|
||||
#define cudaMemsetAsync hipMemsetAsync
|
||||
@@ -0,0 +1,36 @@
|
||||
Subject: [PATCH] ggml-cuda/fattn: pass WARP_SIZE to fwht128 __shfl_xor_sync calls
|
||||
|
||||
Same issue as the argmax top-K fix: two __shfl_xor_sync call sites in
|
||||
the FWHT-128 butterfly kernels (ggml_cuda_fwht128 and fwht128_store_half)
|
||||
use the 3-arg CUDA form and omit the `width` argument that the HIP
|
||||
function-like macro in vendors/hip.h:33 requires. Hipcc fails with:
|
||||
|
||||
fattn.cu:512: too few arguments provided to function-like macro
|
||||
invocation
|
||||
note: macro '__shfl_xor_sync' defined here:
|
||||
#define __shfl_xor_sync(mask, var, laneMask, width) \
|
||||
__shfl_xor(var, laneMask, width)
|
||||
|
||||
Add WARP_SIZE to both calls. CUDA codegen is unchanged (warpSize is the
|
||||
default); HIP now matches the macro arity.
|
||||
|
||||
--- a/ggml/src/ggml-cuda/fattn.cu
|
||||
+++ b/ggml/src/ggml-cuda/fattn.cu
|
||||
@@ -509,7 +509,7 @@
|
||||
// Intra-warp passes: shuffle xor with stride h, no smem, no sync.
|
||||
#pragma unroll
|
||||
for (int h = 1; h <= 16; h *= 2) {
|
||||
- const float other = __shfl_xor_sync(0xFFFFFFFF, val, h);
|
||||
+ const float other = __shfl_xor_sync(0xFFFFFFFF, val, h, WARP_SIZE);
|
||||
val = (tid & h) ? (other - val) : (val + other);
|
||||
}
|
||||
|
||||
@@ -533,7 +533,7 @@
|
||||
static __device__ __forceinline__ void fwht128_store_half(
|
||||
float val, half * dst_base) {
|
||||
const int tid = threadIdx.x;
|
||||
- const float neighbor = __shfl_xor_sync(0xFFFFFFFF, val, 1);
|
||||
+ const float neighbor = __shfl_xor_sync(0xFFFFFFFF, val, 1, WARP_SIZE);
|
||||
if ((tid & 1) == 0) {
|
||||
const half2 packed = __floats2half2_rn(val, neighbor);
|
||||
*((half2 *)(dst_base + tid)) = packed;
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
# Get the absolute current dir where the script is located
|
||||
CURDIR=$(dirname "$(realpath $0)")
|
||||
|
||||
cd /
|
||||
|
||||
echo "CPU info:"
|
||||
grep -e "model\sname" /proc/cpuinfo | head -1
|
||||
grep -e "flags" /proc/cpuinfo | head -1
|
||||
|
||||
BINARY=buun-llama-cpp-fallback
|
||||
|
||||
if grep -q -e "\savx\s" /proc/cpuinfo ; then
|
||||
echo "CPU: AVX found OK"
|
||||
if [ -e $CURDIR/buun-llama-cpp-avx ]; then
|
||||
BINARY=buun-llama-cpp-avx
|
||||
fi
|
||||
fi
|
||||
|
||||
if grep -q -e "\savx2\s" /proc/cpuinfo ; then
|
||||
echo "CPU: AVX2 found OK"
|
||||
if [ -e $CURDIR/buun-llama-cpp-avx2 ]; then
|
||||
BINARY=buun-llama-cpp-avx2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check avx 512
|
||||
if grep -q -e "\savx512f\s" /proc/cpuinfo ; then
|
||||
echo "CPU: AVX512F found OK"
|
||||
if [ -e $CURDIR/buun-llama-cpp-avx512 ]; then
|
||||
BINARY=buun-llama-cpp-avx512
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$LLAMACPP_GRPC_SERVERS" ]; then
|
||||
if [ -e $CURDIR/buun-llama-cpp-grpc ]; then
|
||||
BINARY=buun-llama-cpp-grpc
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extend ld library path with the dir where this script is located/lib
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
export DYLD_LIBRARY_PATH=$CURDIR/lib:$DYLD_LIBRARY_PATH
|
||||
else
|
||||
export LD_LIBRARY_PATH=$CURDIR/lib:$LD_LIBRARY_PATH
|
||||
# Tell rocBLAS where to find TensileLibrary data (GPU kernel tuning files)
|
||||
if [ -d "$CURDIR/lib/rocblas/library" ]; then
|
||||
export ROCBLAS_TENSILE_LIBPATH=$CURDIR/lib/rocblas/library
|
||||
fi
|
||||
fi
|
||||
|
||||
# If there is a lib/ld.so, use it
|
||||
if [ -f $CURDIR/lib/ld.so ]; then
|
||||
echo "Using lib/ld.so"
|
||||
echo "Using binary: $BINARY"
|
||||
exec $CURDIR/lib/ld.so $CURDIR/$BINARY "$@"
|
||||
fi
|
||||
|
||||
echo "Using binary: $BINARY"
|
||||
exec $CURDIR/$BINARY "$@"
|
||||
|
||||
# We should never reach this point, however just in case we do, run fallback
|
||||
exec $CURDIR/buun-llama-cpp-fallback "$@"
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
SOURCE="$SCRIPT_DIR/../llama-cpp/grpc-server.cpp"
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
cp "$SOURCE" "$TMP_DIR/grpc-server.cpp"
|
||||
bash "$SCRIPT_DIR/patch-grpc-server.sh" "$TMP_DIR/grpc-server.cpp"
|
||||
bash "$SCRIPT_DIR/../llama-cpp/disable-score-task.sh" "$TMP_DIR/grpc-server.cpp"
|
||||
bash "$SCRIPT_DIR/patch-grpc-server.sh" "$TMP_DIR/grpc-server.cpp"
|
||||
|
||||
for unsupported in \
|
||||
'params.cache_idle_slots' \
|
||||
'params.speculative.types' \
|
||||
'params.speculative.draft.' \
|
||||
'common_speculative_types_from_names' \
|
||||
'COMMON_SPECULATIVE_TYPE_DRAFT_SIMPLE' \
|
||||
'ctx_server.impl->model_tgt'; do
|
||||
if grep -Fq "$unsupported" "$TMP_DIR/grpc-server.cpp"; then
|
||||
echo "unsupported buun API remains: $unsupported" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
grep -Fq '#define LOCALAI_LLAMA_CPP_NO_SCORE_TASK 1' "$TMP_DIR/grpc-server.cpp"
|
||||
grep -Fq '#define LOCALAI_TURBOQUANT_NO_CHECKPOINT_MIN_STEP 1' "$TMP_DIR/grpc-server.cpp"
|
||||
grep -Fq 'params.speculative.mparams_dft.path = request->draftmodel();' "$TMP_DIR/grpc-server.cpp"
|
||||
grep -Fq 'params.speculative.type = COMMON_SPECULATIVE_TYPE_DRAFT;' "$TMP_DIR/grpc-server.cpp"
|
||||
grep -Fq 'ctx_server.impl->model' "$TMP_DIR/grpc-server.cpp"
|
||||
|
||||
echo "buun grpc-server compatibility transform passed"
|
||||
@@ -1,10 +1,10 @@
|
||||
# ds4 backend Makefile.
|
||||
#
|
||||
# Upstream pin lives below as DS4_VERSION?=b0309611041655f4e45671cfd9c9886aff161406
|
||||
# Upstream pin lives below as DS4_VERSION?=84cc882352757baf628a1776badf7cc54d584e28
|
||||
# (.github/bump_deps.sh) can find and update it - matches the
|
||||
# llama-cpp / ik-llama-cpp / turboquant convention.
|
||||
|
||||
DS4_VERSION?=b0309611041655f4e45671cfd9c9886aff161406
|
||||
DS4_VERSION?=84cc882352757baf628a1776badf7cc54d584e28
|
||||
DS4_REPO?=https://github.com/antirez/ds4
|
||||
|
||||
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
IK_LLAMA_VERSION?=cf1aa57e1a0fabfd015831718fc99d1aec01ada5
|
||||
IK_LLAMA_VERSION?=a7c81affa48c6800d63111bdb33469a01d062daa
|
||||
LLAMA_REPO?=https://github.com/ikawrakow/ik_llama.cpp
|
||||
|
||||
CMAKE_ARGS?=
|
||||
|
||||
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
|
||||
|
||||
# CrispASR version (release tag)
|
||||
CRISPASR_REPO?=https://github.com/CrispStrobe/CrispASR
|
||||
CRISPASR_VERSION?=21901d3f7c23554f072964828363e49ddbc2dc68
|
||||
CRISPASR_VERSION?=17a6cc99422bfafadf7161e96dd7294c89da9c36
|
||||
SO_TARGET?=libgocrispasr.so
|
||||
|
||||
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
|
||||
|
||||
@@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e
|
||||
|
||||
# vllm.cpp version
|
||||
VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp
|
||||
VLLM_CPP_VERSION?=0757cac231ecd571a83c4fd2f50805c9251fc225
|
||||
VLLM_CPP_VERSION?=2b08dd246e04b3f0a4bf1f276170fd28004ced01
|
||||
|
||||
# MLX GEMM provider (darwin/metal only; see the metal branch below for why).
|
||||
# Consumed as the prebuilt pip wheel: building MLX from source needs `xcrun
|
||||
@@ -106,13 +106,24 @@ else
|
||||
LIB=libvllm.so
|
||||
endif
|
||||
|
||||
sources/vllm.cpp:
|
||||
# patches/ carries fixes the pinned engine SHA does not have yet. `git apply`
|
||||
# is deliberately unguarded: a patch that no longer applies must FAIL the clone
|
||||
# loudly, because the alternative is a pin that silently ships without a fix it
|
||||
# is documented to carry. Each patch header says which pin retires it.
|
||||
VLLM_CPP_PATCHES=$(wildcard patches/*.patch)
|
||||
|
||||
sources/vllm.cpp: $(VLLM_CPP_PATCHES)
|
||||
rm -rf sources/vllm.cpp
|
||||
mkdir -p sources/vllm.cpp
|
||||
cd sources/vllm.cpp && \
|
||||
git init && \
|
||||
git remote add origin $(VLLM_CPP_REPO) && \
|
||||
git fetch --depth 1 origin $(VLLM_CPP_VERSION) && \
|
||||
git checkout FETCH_HEAD
|
||||
git checkout FETCH_HEAD && \
|
||||
for p in $(VLLM_CPP_PATCHES); do \
|
||||
echo "==> applying $$p"; \
|
||||
git apply ../../$$p || exit 1; \
|
||||
done
|
||||
|
||||
ifeq ($(MLX_ENABLED),1)
|
||||
# A stamp FILE, not a phony target: a phony prerequisite is always "newer" than
|
||||
@@ -165,7 +176,7 @@ $(LIB): sources/vllm.cpp $(MLX_STAMP)
|
||||
cmake --build . --config Release -j$(JOBS) --target vllm_shared
|
||||
cp -fL build/$(LIB) ./$(LIB)
|
||||
|
||||
vllm-cpp: main.go govllmcpp.go backend.go options.go $(LIB)
|
||||
vllm-cpp: main.go govllmcpp.go backend.go chat.go options.go video.go $(LIB)
|
||||
CGO_ENABLED=0 $(GOCMD) build -tags "$(GO_TAGS)" -o vllm-cpp ./
|
||||
|
||||
package: vllm-cpp
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# vllm-cpp backend
|
||||
|
||||
LocalAI text-generation backend for [vllm.cpp](https://github.com/mudler/vllm.cpp),
|
||||
LocalAI backend for [vllm.cpp](https://github.com/mudler/vllm.cpp),
|
||||
the LocalAI-team C++20 port of vLLM (paged KV cache, continuous batching,
|
||||
safetensors + GGUF loading, CUDA / CPU / Metal / Vulkan) with no Python at
|
||||
inference time.
|
||||
|
||||
It serves two things: text generation, and MiniMax-H3 joint video+audio
|
||||
generation.
|
||||
|
||||
The backend dlopens the engine's stable C ABI (`libvllm`, `include/vllm.h`,
|
||||
ABI v10) through purego:
|
||||
ABI v16) through purego:
|
||||
|
||||
- `Load` -> `vllm_engine_load`: accepts a `.gguf` file or a HF-style model
|
||||
directory (`config.json` + safetensors). `context_size` maps to
|
||||
@@ -29,6 +32,12 @@ ABI v10) through purego:
|
||||
LocalAI's Go-side grammar-constrained tool calling; JSON-schema / regex /
|
||||
choice constraints are also exposed by the ABI.
|
||||
|
||||
`patches/` carries fixes the pinned engine SHA does not have yet, applied to
|
||||
the clone the same way `longcat-video` patches its upstream. `git apply` is
|
||||
unguarded on purpose: a patch that stops applying must fail the clone loudly
|
||||
rather than leave a pin silently missing a fix it is documented to carry. Each
|
||||
patch header says what retires it.
|
||||
|
||||
The struct mirrors in `govllmcpp.go` are hand-written against one ABI version,
|
||||
and the engine refuses to load against any other. Moving `VLLM_CPP_VERSION` in
|
||||
the Makefile therefore means updating `abiVersion` plus the mirrors (and their
|
||||
@@ -47,6 +56,68 @@ options:
|
||||
- max_num_seqs:16
|
||||
```
|
||||
|
||||
## MiniMax-H3 video+audio generation
|
||||
|
||||
`GenerateVideo` -> `vllm_video_generate` (ABI v12). H3 renders picture and sound
|
||||
together, so the output MP4 carries a real AAC track.
|
||||
|
||||
The video engine is a SECOND handle (`vllm_video_engine`), not a mode of the
|
||||
text one, because H3 is a checkpoint SET rather than a model directory: the DiT,
|
||||
the text encoder and two VAEs are separate artifacts, and vllm.cpp has the two
|
||||
loaders refuse each other's checkpoints. `Load` takes the video branch when the
|
||||
model config carries any of the video options below; `parameters.model` is the
|
||||
DiT and everything else is named in `options:`.
|
||||
|
||||
```yaml
|
||||
name: minimax-h3-fl2va-q4
|
||||
backend: vllm-cpp
|
||||
cuda: true
|
||||
known_usecases: [video]
|
||||
parameters:
|
||||
model: minimax-h3/MiniMax-H3-FL2VA-Q4_K_M.gguf
|
||||
options:
|
||||
- video_encoder:minimax-h3/qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf
|
||||
- video_tokenizer:minimax-h3/tokenizer.json
|
||||
- video_vae:minimax-h3/video_vae.safetensors
|
||||
- video_vae_config:minimax-h3/video_vae_config.json
|
||||
- audio_vae:minimax-h3/audio_vae.safetensors
|
||||
- audio_vae_config:minimax-h3/audio_vae_config.json
|
||||
- video_partition:fl2va
|
||||
- video_device:cuda
|
||||
- video_dequant_bf16:true
|
||||
- video_width:1344
|
||||
- video_height:768
|
||||
- video_num_frames:124
|
||||
```
|
||||
|
||||
Three things are worth knowing before touching this path.
|
||||
|
||||
**The partition is declared, not detected, and a mismatch does not fail
|
||||
cleanly.** The FL2VA DiT serves `t2va` and `fl2va`; `ref2va` is a different
|
||||
checkpoint. The community GGUF/NVFP4 quantisations strip the release metadata
|
||||
and the two DiTs are byte-structurally identical, so the engine refuses every
|
||||
generate until `video_partition` says which one it has. Handing reference
|
||||
conditioning to an FL2VA DiT renders for hours and returns a coloured lattice
|
||||
over the frame, so `checkPartitionConditioning` refuses that combination here,
|
||||
before the engine is called.
|
||||
|
||||
**ffmpeg comes from the host.** libvllm writes the frames and the WAV and
|
||||
COMPOSES the mux argv, then spawns nothing — that process boundary is upstream's
|
||||
decision. `muxVideo` takes the composed argv, substitutes `argv[0]` with the
|
||||
resolved binary and execs it; the backend image is `FROM scratch` and carries no
|
||||
ffmpeg, the same arrangement `vibevoice-cpp` uses for transcoding. ffmpeg also
|
||||
converts a `start_image`/`end_image` upload into the binary PPM at the exact
|
||||
output canvas the engine requires, since libvllm vendors neither an image codec
|
||||
nor a resampler.
|
||||
|
||||
**It is slow.** Roughly 176 s per denoise step at 1344x768 on a 20-SM device, so
|
||||
the 50-step default is hours. Nothing here imposes a deadline.
|
||||
|
||||
Geometry mirrors the engine so the two agree: the canvas is truncated onto a
|
||||
32-pixel grid, the frame count sits on the 17n+5 grid, and an unspecified canvas
|
||||
with a keyframe is derived from that image's aspect on a 768-pixel short edge
|
||||
(`MiniMaxH3ResolveShape`, `minimax_h3_planner.cpp`).
|
||||
|
||||
## Apple Silicon: the MLX GEMM provider (ON by default, gated to prefill)
|
||||
|
||||
`BUILD_TYPE=metal` builds vllm.cpp's MLX provider for the dense GEMM
|
||||
|
||||
@@ -28,7 +28,12 @@ type VllmCpp struct {
|
||||
base.Base
|
||||
|
||||
engine uintptr
|
||||
opts loadOptions
|
||||
// videoEngine is the MiniMax-H3 handle (ABI v12). It is deliberately a
|
||||
// SECOND handle, not a mode of the first: H3 is a checkpoint set rather
|
||||
// than a model directory, and vllm.cpp has the two loaders refuse each
|
||||
// other's checkpoints. Exactly one of the two is ever non-zero.
|
||||
videoEngine uintptr
|
||||
opts loadOptions
|
||||
}
|
||||
|
||||
// Stream registry: the per-request bridge between the C token callback and
|
||||
@@ -109,6 +114,14 @@ func (v *VllmCpp) Load(opts *pb.ModelOptions) error {
|
||||
|
||||
v.opts = parseOptions(opts)
|
||||
|
||||
// MiniMax-H3 is a checkpoint SET behind its own engine handle, so the
|
||||
// branch is taken before any text-engine knob is resolved. The two loaders
|
||||
// refuse each other's checkpoints, which is why this is decided from the
|
||||
// config rather than probed.
|
||||
if v.opts.video.engaged() {
|
||||
return v.loadVideo(opts, model)
|
||||
}
|
||||
|
||||
// A DFlash draft is a second checkpoint the engine opens by path, and the
|
||||
// engine never downloads one. Resolve it against LocalAI's models directory
|
||||
// now so a repo-id spelling works, and so a missing draft fails here with an
|
||||
@@ -194,6 +207,10 @@ func (v *VllmCpp) Free() error {
|
||||
vllmEngineFree(v.engine)
|
||||
v.engine = 0
|
||||
}
|
||||
if v.videoEngine != 0 {
|
||||
vllmVideoEngineFree(v.videoEngine)
|
||||
v.videoEngine = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
// purego bindings for the vllm.cpp stable C ABI (include/vllm.h, ABI v10).
|
||||
// purego bindings for the vllm.cpp stable C ABI (include/vllm.h, ABI v16).
|
||||
//
|
||||
// The structs below are hand-mirrored PODs of the C declarations, with
|
||||
// explicit padding so the Go layout matches the C layout on linux/darwin
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// the header of the VLLM_CPP_VERSION pinned in the Makefile: the build checks
|
||||
// the two against each other, because a mismatch is only caught at runtime by
|
||||
// registerLib, where it takes the backend down on every load (issue #11379).
|
||||
const abiVersion = 10
|
||||
const abiVersion = 17
|
||||
|
||||
// The ABI's tri-state toggles (enable_prefix_caching ABI v7,
|
||||
// enable_jump_forward ABI v10) share one encoding: 0 is NOT "off", it is
|
||||
@@ -70,7 +70,15 @@ type cModelParams struct {
|
||||
SchedulingPolicy uintptr // const char*; NULL = "fcfs" (ABI v9)
|
||||
KVTransferConfig uintptr // const char* JSON; NULL = no connector (ABI v9)
|
||||
EnableJumpForward int32 // tri-state 0/1/2 (ABI v10)
|
||||
_ [4]byte // trailing pad to the struct's 8-byte alignment
|
||||
// v14/v16 tail. LocalAI sets none of these (0 is "auto" for the device and
|
||||
// "unset" for both sizing knobs, i.e. the pre-v14 engine byte for byte), but
|
||||
// the fields MUST be mirrored: the C side reads sizeof(vllm_model_params)
|
||||
// bytes off the pointer we hand it, so a Go struct that stopped at
|
||||
// EnableJumpForward would have vllm_engine_load read 24 bytes past our
|
||||
// allocation and size the KV pool from whatever sat there.
|
||||
Device int32 // 0 auto, 1 cpu, 2 cuda (ABI v14)
|
||||
GPUMemoryUtil float64 // 0 => 0.92 (ABI v16)
|
||||
KVCacheMemoryBytes int64 // 0 => unset (ABI v16)
|
||||
}
|
||||
|
||||
// cSamplingParams mirrors vllm_sampling_params (structured fields included).
|
||||
@@ -117,6 +125,79 @@ type cCompletion struct {
|
||||
CompletionTokens int32
|
||||
}
|
||||
|
||||
// ── Video+audio generation (ABI v12, MiniMax-H3) ────────────────────────────
|
||||
//
|
||||
// A video engine is a SEPARATE handle from vllm_engine: H3 is a checkpoint SET
|
||||
// (DiT + text encoder + two VAEs), not one model directory, and the two loaders
|
||||
// refuse each other's checkpoints on purpose. Offsets are asserted in
|
||||
// video_test.go the same way the text PODs are in vllmcpp_test.go.
|
||||
|
||||
// cVideoModelParams mirrors vllm_video_model_params. Nine pointers then three
|
||||
// int32s, so only the trailing pad is implicit.
|
||||
type cVideoModelParams struct {
|
||||
DitPath uintptr // const char*
|
||||
EncoderPath uintptr // const char*
|
||||
TokenizerPath uintptr // const char*
|
||||
VideoVaePath uintptr // const char*
|
||||
VideoVaeConfigPath uintptr // const char*
|
||||
AudioVaePath uintptr // const char*
|
||||
AudioVaeConfigPath uintptr // const char*
|
||||
PromptEmbedsPath uintptr // const char*
|
||||
Partition uintptr // const char*; "fl2va" | "ref2va", REQUIRED
|
||||
Device int32 // 0 cpu, 1 cuda
|
||||
DequantBf16 int32 // 0 keep-quant, 1 dequant/stream bf16
|
||||
Fp4Resident int32 // NVFP4+cuda: keep FP4 packed, Marlin W4A16
|
||||
_ [4]byte // trailing pad to the struct's 8-byte alignment
|
||||
}
|
||||
|
||||
// cVideoParams mirrors vllm_video_params. `width`/`height` and `num_frames`/
|
||||
// `steps` pair up into 8-byte slots; the uint64 seed forces the alignment after
|
||||
// them, and the float noise_aug leaves a pad before output_dir.
|
||||
type cVideoParams struct {
|
||||
Prompt uintptr // const char*
|
||||
Width int32
|
||||
Height int32
|
||||
NumFrames int32 // <= 1 => per-task default (124 for t2va/fl2va)
|
||||
Steps int32 // <= 0 => the H3 default (50)
|
||||
Seed uint64
|
||||
HasSeed int32
|
||||
_ [4]byte
|
||||
FirstFrame uintptr // const char*; fl2va keyframe, binary PPM (P6)
|
||||
LastFrame uintptr // const char*
|
||||
RefImage uintptr // const char*; ref2va only
|
||||
RefVideo uintptr // const char*; ref2va only, a frame_%06d.ppm DIRECTORY
|
||||
RefAudio uintptr // const char*; ref2va only, 16-bit PCM WAV
|
||||
NoiseAug float32 // <= 0 => 1.0
|
||||
_ [4]byte
|
||||
OutputDir uintptr // const char*; REQUIRED
|
||||
}
|
||||
|
||||
// cVideoResult mirrors vllm_video_result. Every member is library-allocated and
|
||||
// released together by vllm_video_result_free.
|
||||
type cVideoResult struct {
|
||||
FrameDir uintptr // char*, holds frame_%06d.ppm
|
||||
AudioPath uintptr // char*, 16-bit PCM WAV
|
||||
FrameCount int32
|
||||
Width int32
|
||||
Height int32
|
||||
Fps int32
|
||||
SampleRate int32
|
||||
_ [4]byte
|
||||
MuxArgv uintptr // char**, NULL-terminated at MuxArgc
|
||||
MuxArgc int32
|
||||
_ [4]byte
|
||||
}
|
||||
|
||||
// cVideoMuxParams mirrors vllm_video_mux_params. The library composes the argv;
|
||||
// spawning it is the CALLER's job, which is why no ffmpeg lives in libvllm.
|
||||
type cVideoMuxParams struct {
|
||||
Frames uintptr // const char*; printf pattern, dir/frame_%06d.ppm
|
||||
AudioPath uintptr // const char*; NULL/empty => a silent clip
|
||||
OutputPath uintptr // const char*; the .mp4 to write
|
||||
Fps int32 // <= 0 => the H3 default (24)
|
||||
Crf int32 // <= 0 => the library default (18)
|
||||
}
|
||||
|
||||
// defaultSamplingParams mirrors vllm_sampling_params_default().
|
||||
func defaultSamplingParams() cSamplingParams {
|
||||
return cSamplingParams{
|
||||
@@ -148,6 +229,14 @@ var (
|
||||
vllmLastError func() string
|
||||
vllmVersion func() string
|
||||
vllmABIVersion func() int32
|
||||
|
||||
// Video+audio generation (ABI v12).
|
||||
vllmVideoEngineLoad func(params, out unsafe.Pointer) int32
|
||||
vllmVideoEngineFree func(engine uintptr)
|
||||
vllmVideoGenerate func(engine uintptr, params, out unsafe.Pointer) int32
|
||||
vllmVideoResultFree func(out unsafe.Pointer)
|
||||
vllmVideoMuxArgv func(params, outArgv, outArgc unsafe.Pointer) int32
|
||||
vllmVideoMuxArgvFre func(argv uintptr, argc int32)
|
||||
)
|
||||
|
||||
type libFunc struct {
|
||||
@@ -175,6 +264,12 @@ func registerLib(libName string) error {
|
||||
{&vllmLastError, "vllm_last_error"},
|
||||
{&vllmVersion, "vllm_version"},
|
||||
{&vllmABIVersion, "vllm_abi_version"},
|
||||
{&vllmVideoEngineLoad, "vllm_video_engine_load"},
|
||||
{&vllmVideoEngineFree, "vllm_video_engine_free"},
|
||||
{&vllmVideoGenerate, "vllm_video_generate"},
|
||||
{&vllmVideoResultFree, "vllm_video_result_free"},
|
||||
{&vllmVideoMuxArgv, "vllm_video_mux_argv"},
|
||||
{&vllmVideoMuxArgvFre, "vllm_video_mux_argv_free"},
|
||||
} {
|
||||
purego.RegisterLibFunc(lf.ptr, lib, lf.name)
|
||||
}
|
||||
@@ -222,3 +317,19 @@ func goString(p uintptr) string {
|
||||
}
|
||||
return string(unsafe.Slice((*byte)(base), n))
|
||||
}
|
||||
|
||||
// goStringSlice copies a C `char*` array of n entries. Used for the ffmpeg argv
|
||||
// the library composes: it is copied out immediately so the caller can free the
|
||||
// C allocation before ever spawning the process.
|
||||
func goStringSlice(p uintptr, n int32) []string {
|
||||
if p == 0 || n <= 0 {
|
||||
return nil
|
||||
}
|
||||
//nolint:govet // C-owned pointer handed over by purego, valid for this call
|
||||
entries := unsafe.Slice((**byte)(unsafe.Pointer(p)), int(n)) // #nosec G103 -- C-owned, copied out immediately
|
||||
out := make([]string, 0, n)
|
||||
for _, e := range entries {
|
||||
out = append(out, goString(uintptr(unsafe.Pointer(e)))) // #nosec G103 -- ditto
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -62,6 +62,66 @@ type loadOptions struct {
|
||||
// Override for the tokenizer_config.json the chat template is read from
|
||||
// (ABI v9). Empty = <model_dir>/tokenizer_config.json.
|
||||
tokenizerConfigPath string
|
||||
// MiniMax-H3 video+audio generation (ABI v12). Present only when the config
|
||||
// carries at least one of its keys; see videoOptions.engaged.
|
||||
video videoOptions
|
||||
}
|
||||
|
||||
// videoOptions is the MiniMax-H3 checkpoint SET plus its generation defaults.
|
||||
//
|
||||
// H3 is not one model directory: the DiT, the text encoder and the two VAEs are
|
||||
// separate artifacts, which is why vllm.cpp gives video its own engine handle
|
||||
// (vllm_video_engine, ABI v12) rather than another vllm_engine. The DiT is the
|
||||
// model config's `parameters.model`; everything else arrives through these
|
||||
// options, so one gallery entry can name five files.
|
||||
//
|
||||
// The geometry/frame defaults exist because H3's trained canvas is nothing like
|
||||
// the generic /video defaults: 1344x768 at 124 frames is a ~5.2 s clip, and the
|
||||
// frame count must sit on the 17n+5 grid. A request that leaves a field unset
|
||||
// gets the model's own default from here instead of a canvas the checkpoint was
|
||||
// never trained at.
|
||||
type videoOptions struct {
|
||||
encoderPath string // H3-Encoder GGUF or bf16 shard dir
|
||||
tokenizerPath string // tokenizer.json, needed with an encoder
|
||||
videoVaePath string
|
||||
videoVaeConfig string
|
||||
audioVaePath string
|
||||
audioVaeConfig string
|
||||
promptEmbedsPath string // fallback conditioning when there is no encoder
|
||||
// The served checkpoint PARTITION. Community GGUF/NVFP4 files strip the
|
||||
// release metadata and the FL2VA/Ref2VA DiTs are byte-structurally
|
||||
// identical, so the engine refuses every generate until it is DECLARED.
|
||||
// "fl2va" serves t2va + fl2va; "ref2va" serves reference conditioning.
|
||||
partition string
|
||||
device int32 // 0 cpu, 1 cuda (the ABI's own encoding, no auto slot)
|
||||
deviceSet bool
|
||||
dequantBf16 int32
|
||||
fp4Resident int32
|
||||
// Per-model generation defaults, applied when the request leaves the field
|
||||
// at 0.
|
||||
width int32
|
||||
height int32
|
||||
numFrames int32
|
||||
steps int32
|
||||
// Where frames + WAV are written. Empty = a temporary directory beside the
|
||||
// requested output, removed once the mux succeeds. Set it to keep the
|
||||
// frame_%06d.ppm runs around (they are what ref2va's ref_video consumes).
|
||||
workdir string
|
||||
// The ffmpeg binary the composed mux argv is exec'd with. Empty = "ffmpeg"
|
||||
// from PATH. libvllm composes the argv and spawns nothing, by design.
|
||||
ffmpeg string
|
||||
crf int32
|
||||
}
|
||||
|
||||
// engaged reports whether this config describes an H3 video engine. Load uses
|
||||
// it to choose which of the two mutually exclusive engine handles to open: the
|
||||
// checkpoints refuse each other, so guessing is not an option, and every key
|
||||
// below is meaningless to the text engine.
|
||||
func (v videoOptions) engaged() bool {
|
||||
return v.encoderPath != "" || v.tokenizerPath != "" ||
|
||||
v.videoVaePath != "" || v.videoVaeConfig != "" ||
|
||||
v.audioVaePath != "" || v.audioVaeConfig != "" ||
|
||||
v.promptEmbedsPath != "" || v.partition != ""
|
||||
}
|
||||
|
||||
func parseOptions(opts *pb.ModelOptions) loadOptions {
|
||||
@@ -110,10 +170,94 @@ func applyOptionsList(lo *loadOptions, options []string) {
|
||||
if b, err := strconv.ParseBool(strings.TrimSpace(v)); err == nil {
|
||||
lo.enableJumpForward = boolTriState(b)
|
||||
}
|
||||
default:
|
||||
applyVideoOption(&lo.video, strings.TrimSpace(k), v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// applyVideoOption reads one MiniMax-H3 key. Split out of applyOptionsList so
|
||||
// the video surface stays legible next to the videoOptions it fills, and so
|
||||
// video_test.go can exercise it directly.
|
||||
func applyVideoOption(vo *videoOptions, key, value string) bool {
|
||||
v := strings.TrimSpace(value)
|
||||
switch key {
|
||||
case "video_encoder":
|
||||
vo.encoderPath = v
|
||||
case "video_tokenizer":
|
||||
vo.tokenizerPath = v
|
||||
case "video_vae":
|
||||
vo.videoVaePath = v
|
||||
case "video_vae_config":
|
||||
vo.videoVaeConfig = v
|
||||
case "audio_vae":
|
||||
vo.audioVaePath = v
|
||||
case "audio_vae_config":
|
||||
vo.audioVaeConfig = v
|
||||
case "video_prompt_embeds":
|
||||
vo.promptEmbedsPath = v
|
||||
case "video_partition":
|
||||
vo.partition = strings.ToLower(v)
|
||||
case "video_device":
|
||||
switch strings.ToLower(v) {
|
||||
case "cpu":
|
||||
vo.device, vo.deviceSet = videoDeviceCPU, true
|
||||
case "cuda", "gpu":
|
||||
vo.device, vo.deviceSet = videoDeviceCUDA, true
|
||||
default:
|
||||
xlog.Warn("[vllm-cpp] ignoring unknown video_device", "value", v)
|
||||
}
|
||||
case "video_dequant_bf16":
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
vo.dequantBf16 = boolInt32(b)
|
||||
}
|
||||
case "video_fp4_resident":
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
vo.fp4Resident = boolInt32(b)
|
||||
}
|
||||
case "video_width":
|
||||
vo.width = parseInt32(v, vo.width)
|
||||
case "video_height":
|
||||
vo.height = parseInt32(v, vo.height)
|
||||
case "video_num_frames":
|
||||
vo.numFrames = parseInt32(v, vo.numFrames)
|
||||
case "video_steps":
|
||||
vo.steps = parseInt32(v, vo.steps)
|
||||
case "video_workdir":
|
||||
vo.workdir = v
|
||||
case "video_crf":
|
||||
vo.crf = parseInt32(v, vo.crf)
|
||||
case "ffmpeg", "ffmpeg_path":
|
||||
vo.ffmpeg = v
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// videoScalarString renders an engine_args scalar so the video keys can share
|
||||
// one parser with the "key:value" list. Objects and arrays have no video
|
||||
// meaning and are left to the caller's unknown-key path.
|
||||
func videoScalarString(v any) (string, bool) {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return t, true
|
||||
case bool:
|
||||
return strconv.FormatBool(t), true
|
||||
case float64:
|
||||
return strconv.FormatFloat(t, 'f', -1, 64), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func boolInt32(b bool) int32 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// applyEngineArgs overlays the `engine_args:` JSON object. A document that does
|
||||
// not parse is logged and skipped: engine_args is shared with the other engines
|
||||
// (the vLLM and SGLang backends read the same field), so a stray key must not
|
||||
@@ -160,6 +304,9 @@ func applyEngineArgs(lo *loadOptions, engineArgs string) {
|
||||
lo.enableJumpForward = boolTriState(b)
|
||||
}
|
||||
default:
|
||||
if s, ok := videoScalarString(v); ok && applyVideoOption(&lo.video, k, s) {
|
||||
continue
|
||||
}
|
||||
xlog.Debug("[vllm-cpp] ignoring unknown engine_args key", "key", k)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,634 @@
|
||||
package main
|
||||
|
||||
// MiniMax-H3 video+audio generation over the vllm.cpp C ABI (v12).
|
||||
//
|
||||
// Two things make this different from the text path, and both come from the
|
||||
// engine's own shape rather than from LocalAI:
|
||||
//
|
||||
// 1. A video engine is loaded from a checkpoint SET - the DiT, the text
|
||||
// encoder and two VAEs are separate artifacts - so it is its own handle
|
||||
// (vllm_video_engine) and its own Load branch. The two loaders refuse each
|
||||
// other's checkpoints on purpose.
|
||||
// 2. libvllm writes frames + a WAV and COMPOSES the ffmpeg argv, but spawns
|
||||
// nothing. That process boundary is deliberate upstream, so the mux lives
|
||||
// here: we take the composed argv, substitute argv[0], and exec it. ffmpeg
|
||||
// comes from PATH the same way the vibevoice-cpp backend takes it.
|
||||
//
|
||||
// Generation is SLOW - roughly 176 s per denoise step at 1344x768 on a 20-SM
|
||||
// device, so a default 50-step render is hours, not seconds. Nothing here
|
||||
// imposes a deadline: GenerateVideo blocks for as long as the engine needs and
|
||||
// the gRPC call carries LocalAI's application context.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
// Registered for image.DecodeConfig only: a staged keyframe arrives as
|
||||
// whatever the caller uploaded, and we need its geometry to size the canvas.
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
||||
"github.com/mudler/xlog"
|
||||
)
|
||||
|
||||
// vllm_video_model_params.device (vllm.h): no auto slot, unlike the text
|
||||
// engine's v14 device field.
|
||||
const (
|
||||
videoDeviceCPU int32 = 0
|
||||
videoDeviceCUDA int32 = 1
|
||||
)
|
||||
|
||||
// H3's shipped geometry. The canvas is truncated onto a 32-pixel grid and the
|
||||
// frame count onto the 17n+5 grid by the engine itself
|
||||
// (MiniMaxH3ResolveShape / MiniMaxH3AlignFrameCount in
|
||||
// src/vllm/model_executor/models/minimax_h3_planner.cpp); mirrored here only so
|
||||
// a keyframe can be resampled to the exact canvas the engine will render at.
|
||||
const (
|
||||
h3CanvasMultiple int32 = 32
|
||||
h3FrameGrid int32 = 17
|
||||
h3FrameOffset int32 = 5
|
||||
h3ShortEdge int32 = 768
|
||||
)
|
||||
|
||||
// videoPartitions are the two DECLARED partitions of the H3 release. The FL2VA
|
||||
// checkpoint serves t2va and fl2va; ref2va is a different checkpoint. Passing
|
||||
// reference conditioning against an fl2va DiT is a partition mismatch that
|
||||
// renders a coloured lattice over the frame rather than failing cleanly, which
|
||||
// is why it is refused here before the engine is ever called.
|
||||
const (
|
||||
partitionFL2VA = "fl2va"
|
||||
partitionRef2VA = "ref2va"
|
||||
)
|
||||
|
||||
// videoRequestParams are the per-request `params` keys this backend accepts.
|
||||
// Unknown keys are an error rather than a silent drop: a misspelled reference
|
||||
// path would otherwise produce a perfectly successful render of the wrong
|
||||
// thing, hours later.
|
||||
var videoRequestParams = []string{"noise_aug", "ref_image", "ref_video", "crf"}
|
||||
|
||||
// loadVideo opens the H3 checkpoint set. `dit` is the model config's
|
||||
// parameters.model; every other artifact comes from the options.
|
||||
func (v *VllmCpp) loadVideo(opts *pb.ModelOptions, dit string) error {
|
||||
vo := &v.opts.video
|
||||
|
||||
// Relative option paths resolve against LocalAI's models directory, which
|
||||
// is where the gallery lands the five H3 files.
|
||||
resolve := func(p string) string {
|
||||
if p == "" || filepath.IsAbs(p) || opts.ModelPath == "" {
|
||||
return p
|
||||
}
|
||||
return filepath.Join(opts.ModelPath, p)
|
||||
}
|
||||
vo.encoderPath = resolve(vo.encoderPath)
|
||||
vo.tokenizerPath = resolve(vo.tokenizerPath)
|
||||
vo.videoVaePath = resolve(vo.videoVaePath)
|
||||
vo.videoVaeConfig = resolve(vo.videoVaeConfig)
|
||||
vo.audioVaePath = resolve(vo.audioVaePath)
|
||||
vo.audioVaeConfig = resolve(vo.audioVaeConfig)
|
||||
vo.promptEmbedsPath = resolve(vo.promptEmbedsPath)
|
||||
vo.workdir = resolve(vo.workdir)
|
||||
|
||||
// A VAE config carries the per-channel latents_mean/latents_std and the
|
||||
// temporal clip_length/token_drop; decode is wrong without it. The release
|
||||
// ships it beside the weights, so default to that rather than making every
|
||||
// config repeat it.
|
||||
if vo.videoVaeConfig == "" && vo.videoVaePath != "" {
|
||||
vo.videoVaeConfig = siblingConfigJSON(vo.videoVaePath)
|
||||
}
|
||||
if vo.audioVaeConfig == "" && vo.audioVaePath != "" {
|
||||
vo.audioVaeConfig = siblingConfigJSON(vo.audioVaePath)
|
||||
}
|
||||
|
||||
if vo.partition == "" {
|
||||
// The community GGUF/NVFP4 quantisations strip the release metadata and
|
||||
// the two DiTs are byte-structurally identical, so the engine cannot
|
||||
// infer this and refuses every generate until it is declared. The
|
||||
// shipped FL2VA checkpoint is the one the gallery entry installs.
|
||||
vo.partition = partitionFL2VA
|
||||
xlog.Warn("[vllm-cpp] video partition not declared, assuming the FL2VA checkpoint",
|
||||
"hint", "set options: [video_partition:fl2va] or [video_partition:ref2va] to match the DiT you installed")
|
||||
}
|
||||
if vo.partition != partitionFL2VA && vo.partition != partitionRef2VA {
|
||||
return fmt.Errorf("vllm-cpp: video_partition must be %q or %q, got %q",
|
||||
partitionFL2VA, partitionRef2VA, vo.partition)
|
||||
}
|
||||
if vo.videoVaePath == "" || vo.audioVaePath == "" {
|
||||
return fmt.Errorf("vllm-cpp: MiniMax-H3 needs both VAEs: set options: " +
|
||||
"[video_vae:<video vae .safetensors>, audio_vae:<audio vae .safetensors>]")
|
||||
}
|
||||
if vo.encoderPath == "" && vo.promptEmbedsPath == "" {
|
||||
return fmt.Errorf("vllm-cpp: MiniMax-H3 needs text conditioning: set options: " +
|
||||
"[video_encoder:<encoder .gguf>, video_tokenizer:<tokenizer.json>] " +
|
||||
"or [video_prompt_embeds:<f32 embeddings>]")
|
||||
}
|
||||
if !vo.deviceSet && opts.GetCUDA() {
|
||||
vo.device = videoDeviceCUDA
|
||||
}
|
||||
|
||||
mp := cVideoModelParams{
|
||||
Device: vo.device,
|
||||
DequantBf16: vo.dequantBf16,
|
||||
Fp4Resident: vo.fp4Resident,
|
||||
}
|
||||
var keep [][]byte
|
||||
setStr := func(dst *uintptr, s string) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
b := cString(s)
|
||||
keep = append(keep, b)
|
||||
*dst = uintptr(unsafe.Pointer(&b[0])) // #nosec G103 -- borrowed by C for the load call only
|
||||
}
|
||||
setStr(&mp.DitPath, dit)
|
||||
setStr(&mp.EncoderPath, vo.encoderPath)
|
||||
setStr(&mp.TokenizerPath, vo.tokenizerPath)
|
||||
setStr(&mp.VideoVaePath, vo.videoVaePath)
|
||||
setStr(&mp.VideoVaeConfigPath, vo.videoVaeConfig)
|
||||
setStr(&mp.AudioVaePath, vo.audioVaePath)
|
||||
setStr(&mp.AudioVaeConfigPath, vo.audioVaeConfig)
|
||||
setStr(&mp.PromptEmbedsPath, vo.promptEmbedsPath)
|
||||
setStr(&mp.Partition, vo.partition)
|
||||
|
||||
xlog.Info("[vllm-cpp] Load (MiniMax-H3 video)", "dit", dit, "engine", vllmVersion(),
|
||||
"encoder", vo.encoderPath, "tokenizer", vo.tokenizerPath,
|
||||
"videoVae", vo.videoVaePath, "audioVae", vo.audioVaePath,
|
||||
"partition", vo.partition, "device", videoDeviceName(vo.device),
|
||||
"dequantBf16", vo.dequantBf16 == 1, "fp4Resident", vo.fp4Resident == 1)
|
||||
|
||||
var engine uintptr
|
||||
rc := vllmVideoEngineLoad(unsafe.Pointer(&mp), unsafe.Pointer(&engine)) // #nosec G103 -- POD out-params
|
||||
runtime.KeepAlive(keep)
|
||||
if rc != vllmOK {
|
||||
return fmt.Errorf("vllm-cpp: video engine load failed: %s", vllmLastError())
|
||||
}
|
||||
v.videoEngine = engine
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateVideo renders one clip and muxes it to opts.Dst as an MP4 carrying
|
||||
// H3's jointly generated AAC audio track. It blocks for the whole render.
|
||||
func (v *VllmCpp) GenerateVideo(opts *pb.GenerateVideoRequest) error {
|
||||
if v.videoEngine == 0 {
|
||||
return fmt.Errorf("vllm-cpp: this model is not a MiniMax-H3 video engine " +
|
||||
"(load it with the video_vae / audio_vae / video_encoder options)")
|
||||
}
|
||||
if strings.TrimSpace(opts.GetPrompt()) == "" {
|
||||
return fmt.Errorf("vllm-cpp: video generation needs a prompt")
|
||||
}
|
||||
dst := opts.GetDst()
|
||||
if dst == "" {
|
||||
return fmt.Errorf("vllm-cpp: video generation needs an output path")
|
||||
}
|
||||
vo := v.opts.video
|
||||
|
||||
extra, err := parseVideoRequestParams(opts.GetParams())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkPartitionConditioning(vo.partition, opts, extra); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.GetNegativePrompt() != "" {
|
||||
xlog.Warn("[vllm-cpp] MiniMax-H3 has no negative prompt; ignoring it")
|
||||
}
|
||||
if opts.GetCfgScale() != 0 {
|
||||
xlog.Warn("[vllm-cpp] MiniMax-H3 has no classifier-free guidance scale; ignoring cfg_scale")
|
||||
}
|
||||
|
||||
workdir, cleanup, err := v.videoWorkdir(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
width, height := firstPositive(opts.GetWidth(), vo.width), firstPositive(opts.GetHeight(), vo.height)
|
||||
frames := firstPositive(opts.GetNumFrames(), vo.numFrames)
|
||||
steps := firstPositive(opts.GetStep(), vo.steps)
|
||||
|
||||
vp := cVideoParams{
|
||||
NumFrames: frames,
|
||||
Steps: steps,
|
||||
NoiseAug: extra.noiseAug,
|
||||
}
|
||||
if opts.GetSeed() > 0 {
|
||||
vp.Seed = uint64(opts.GetSeed())
|
||||
vp.HasSeed = 1
|
||||
}
|
||||
if aligned := alignFrameCount(frames); aligned != frames {
|
||||
xlog.Warn("[vllm-cpp] frame count is not on H3's 17n+5 grid; the engine rounds up",
|
||||
"requested", frames, "rendered", aligned)
|
||||
}
|
||||
|
||||
// Keyframes must be binary PPM (P6) at the exact output canvas: no image
|
||||
// codec and no resampler is vendored in libvllm. Resolve the canvas first,
|
||||
// then stage the frames through ffmpeg into it.
|
||||
//
|
||||
// The REQUEST's geometry is what is honoured here, not the model-level
|
||||
// default: that default is a t2va canvas, and applying it to a keyframe
|
||||
// would stretch a portrait photo into a 1344x768 letterbox. With no
|
||||
// requested geometry the canvas comes from the keyframe's own aspect, which
|
||||
// is the rule the engine itself applies (MiniMaxH3ResolveShape).
|
||||
first, last := opts.GetStartImage(), opts.GetEndImage()
|
||||
if first != "" || last != "" {
|
||||
width, height, err = resolveCanvas(opts.GetWidth(), opts.GetHeight(), first, last)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if first, err = stageKeyframe(vo.ffmpeg, first, width, height, workdir, "first"); err != nil {
|
||||
return err
|
||||
}
|
||||
if last, err = stageKeyframe(vo.ffmpeg, last, width, height, workdir, "last"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
vp.Width, vp.Height = truncateToGrid(width), truncateToGrid(height)
|
||||
|
||||
var keep [][]byte
|
||||
setStr := func(dst *uintptr, s string) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
b := cString(s)
|
||||
keep = append(keep, b)
|
||||
*dst = uintptr(unsafe.Pointer(&b[0])) // #nosec G103 -- borrowed by C for the call only
|
||||
}
|
||||
setStr(&vp.Prompt, opts.GetPrompt())
|
||||
setStr(&vp.OutputDir, workdir)
|
||||
setStr(&vp.FirstFrame, first)
|
||||
setStr(&vp.LastFrame, last)
|
||||
setStr(&vp.RefImage, extra.refImage)
|
||||
setStr(&vp.RefVideo, extra.refVideo)
|
||||
setStr(&vp.RefAudio, opts.GetAudio())
|
||||
|
||||
xlog.Info("[vllm-cpp] GenerateVideo", "dst", dst, "workdir", workdir,
|
||||
"width", vp.Width, "height", vp.Height, "frames", vp.NumFrames,
|
||||
"steps", vp.Steps, "seeded", vp.HasSeed == 1, "partition", vo.partition)
|
||||
|
||||
var out cVideoResult
|
||||
rc := vllmVideoGenerate(v.videoEngine, unsafe.Pointer(&vp), unsafe.Pointer(&out)) // #nosec G103 -- POD in/out params
|
||||
runtime.KeepAlive(keep)
|
||||
if rc != vllmOK {
|
||||
return fmt.Errorf("vllm-cpp: video generation failed: %s", vllmLastError())
|
||||
}
|
||||
defer vllmVideoResultFree(unsafe.Pointer(&out)) // #nosec G103 -- frees the library-owned members
|
||||
|
||||
frameDir, audioPath := goString(out.FrameDir), goString(out.AudioPath)
|
||||
xlog.Info("[vllm-cpp] rendered", "frames", out.FrameCount,
|
||||
"width", out.Width, "height", out.Height, "fps", out.Fps,
|
||||
"audio", audioPath, "sampleRate", out.SampleRate)
|
||||
if opts.GetFps() > 0 && opts.GetFps() != out.Fps {
|
||||
// Muxing at any other rate desynchronises the jointly generated audio.
|
||||
xlog.Warn("[vllm-cpp] MiniMax-H3 renders at a fixed frame rate; ignoring the requested fps",
|
||||
"requested", opts.GetFps(), "rendered", out.Fps)
|
||||
}
|
||||
|
||||
return v.muxVideo(frameDir, audioPath, dst, out.Fps, extra.crf)
|
||||
}
|
||||
|
||||
// muxVideo execs the argv libvllm composed. The encoding contract (h264 /
|
||||
// yuv420p + AAC, -shortest, +faststart) belongs to the library; only the spawn
|
||||
// is ours.
|
||||
func (v *VllmCpp) muxVideo(frameDir, audioPath, dst string, fps, crf int32) error {
|
||||
mx := cVideoMuxParams{Fps: fps, Crf: crf}
|
||||
var keep [][]byte
|
||||
setStr := func(dst *uintptr, s string) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
b := cString(s)
|
||||
keep = append(keep, b)
|
||||
*dst = uintptr(unsafe.Pointer(&b[0])) // #nosec G103 -- borrowed by C for the call only
|
||||
}
|
||||
setStr(&mx.Frames, filepath.Join(frameDir, "frame_%06d.ppm"))
|
||||
setStr(&mx.AudioPath, audioPath)
|
||||
setStr(&mx.OutputPath, dst)
|
||||
|
||||
var argvPtr uintptr
|
||||
var argc int32
|
||||
rc := vllmVideoMuxArgv(unsafe.Pointer(&mx), unsafe.Pointer(&argvPtr), unsafe.Pointer(&argc)) // #nosec G103 -- POD out-params
|
||||
runtime.KeepAlive(keep)
|
||||
if rc != vllmOK {
|
||||
return fmt.Errorf("vllm-cpp: composing the mux command failed: %s", vllmLastError())
|
||||
}
|
||||
argv := goStringSlice(argvPtr, argc)
|
||||
vllmVideoMuxArgvFre(argvPtr, argc)
|
||||
if len(argv) == 0 {
|
||||
return fmt.Errorf("vllm-cpp: the library composed an empty mux command")
|
||||
}
|
||||
|
||||
ffmpegBin, err := resolveFfmpeg(v.opts.video.ffmpeg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
argv[0] = ffmpegBin
|
||||
|
||||
xlog.Debug("[vllm-cpp] muxing", "argv", argv)
|
||||
output, err := exec.Command(argv[0], argv[1:]...).CombinedOutput() // #nosec G204 -- argv is composed by libvllm, argv[0] is a resolved binary
|
||||
if err != nil {
|
||||
return fmt.Errorf("vllm-cpp: ffmpeg mux failed: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveFfmpeg locates the mux binary. The backend image is FROM scratch and
|
||||
// carries no ffmpeg, exactly like vibevoice-cpp's transcode path: the host must
|
||||
// provide one, and saying so plainly beats a bare "exec: not found" after an
|
||||
// hours-long render.
|
||||
func resolveFfmpeg(configured string) (string, error) {
|
||||
name := configured
|
||||
if name == "" {
|
||||
name = "ffmpeg"
|
||||
}
|
||||
path, err := exec.LookPath(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("vllm-cpp: %q not found: MiniMax-H3 output is muxed with ffmpeg, "+
|
||||
"install it on the host or point options: [ffmpeg:<path>] at a binary: %w", name, err)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// videoWorkdir returns the directory the engine writes frame_%06d.ppm and
|
||||
// audio.wav into, plus its cleanup.
|
||||
//
|
||||
// It is ALWAYS a fresh directory. Reusing one would leave a longer previous
|
||||
// run's trailing frames in place for the mux to pick up, silently splicing two
|
||||
// renders together. With video_workdir set the run is kept (its frames are what
|
||||
// ref2va's ref_video consumes); otherwise it is removed once the mux succeeds.
|
||||
func (v *VllmCpp) videoWorkdir(dst string) (string, func(), error) {
|
||||
parent := v.opts.video.workdir
|
||||
keep := parent != ""
|
||||
if parent == "" {
|
||||
parent = filepath.Dir(dst)
|
||||
}
|
||||
if err := os.MkdirAll(parent, 0o750); err != nil {
|
||||
return "", nil, fmt.Errorf("vllm-cpp: creating the video work directory: %w", err)
|
||||
}
|
||||
dir, err := os.MkdirTemp(parent, "vllm-cpp-h3-")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("vllm-cpp: creating the video work directory: %w", err)
|
||||
}
|
||||
if keep {
|
||||
return dir, func() {}, nil
|
||||
}
|
||||
return dir, func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
xlog.Warn("[vllm-cpp] could not remove the video work directory", "dir", dir, "error", err)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// videoExtraParams holds the per-request knobs that have no proto field.
|
||||
type videoExtraParams struct {
|
||||
noiseAug float32
|
||||
refImage string
|
||||
refVideo string
|
||||
crf int32
|
||||
}
|
||||
|
||||
func parseVideoRequestParams(params map[string]string) (videoExtraParams, error) {
|
||||
var extra videoExtraParams
|
||||
for k, raw := range params {
|
||||
v := strings.TrimSpace(raw)
|
||||
switch k {
|
||||
case "noise_aug":
|
||||
f, err := strconv.ParseFloat(v, 32)
|
||||
if err != nil {
|
||||
return extra, fmt.Errorf("vllm-cpp: params.noise_aug must be a number, got %q", raw)
|
||||
}
|
||||
extra.noiseAug = float32(f)
|
||||
case "ref_image":
|
||||
extra.refImage = v
|
||||
case "ref_video":
|
||||
extra.refVideo = v
|
||||
case "crf":
|
||||
n, err := strconv.ParseInt(v, 10, 32)
|
||||
if err != nil {
|
||||
return extra, fmt.Errorf("vllm-cpp: params.crf must be an integer, got %q", raw)
|
||||
}
|
||||
extra.crf = int32(n)
|
||||
default:
|
||||
return extra, fmt.Errorf("vllm-cpp: unknown params key %q (accepted: %s)",
|
||||
k, strings.Join(videoRequestParams, ", "))
|
||||
}
|
||||
}
|
||||
return extra, nil
|
||||
}
|
||||
|
||||
// checkPartitionConditioning refuses conditioning the loaded checkpoint cannot
|
||||
// serve.
|
||||
//
|
||||
// This is the failure this backend most needs to catch early. The FL2VA
|
||||
// partition serves t2va and fl2va; handing it a reference image or audio is a
|
||||
// partition mismatch, and H3 does not fail cleanly on one - it renders, for
|
||||
// hours, and returns a coloured lattice over the frame. The engine's own #77
|
||||
// guard covers a missing declaration; this covers a declaration that does not
|
||||
// match the request.
|
||||
func checkPartitionConditioning(partition string, opts *pb.GenerateVideoRequest, extra videoExtraParams) error {
|
||||
hasKeyframe := opts.GetStartImage() != "" || opts.GetEndImage() != ""
|
||||
hasReference := extra.refImage != "" || extra.refVideo != "" || opts.GetAudio() != ""
|
||||
|
||||
if hasKeyframe && hasReference {
|
||||
return fmt.Errorf("vllm-cpp: fl2va keyframes (start_image/end_image) and ref2va reference " +
|
||||
"conditioning (params.ref_image/params.ref_video/audio) are exclusive in the H3 pipeline")
|
||||
}
|
||||
switch partition {
|
||||
case partitionFL2VA:
|
||||
if hasReference {
|
||||
return fmt.Errorf("vllm-cpp: the FL2VA checkpoint serves t2va and fl2va only - " +
|
||||
"reference conditioning (params.ref_image/params.ref_video/audio) needs a ref2va DiT. " +
|
||||
"Use start_image for first-frame conditioning instead")
|
||||
}
|
||||
case partitionRef2VA:
|
||||
if hasKeyframe {
|
||||
return fmt.Errorf("vllm-cpp: the Ref2VA checkpoint does not serve fl2va keyframes - " +
|
||||
"pass the image as params.ref_image, or install the FL2VA checkpoint")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveCanvas settles the output geometry BEFORE a keyframe is resampled,
|
||||
// because the two have to agree exactly: the engine refuses a keyframe that is
|
||||
// not already at the output resolution, and when no geometry is requested it
|
||||
// derives one from the keyframe's own aspect. Mirrors _resolve_shape
|
||||
// (src/vllm/model_executor/models/minimax_h3_planner.cpp:264-308).
|
||||
func resolveCanvas(width, height int32, keyframes ...string) (int32, int32, error) {
|
||||
if width > 0 && height > 0 {
|
||||
return width, height, nil
|
||||
}
|
||||
for _, k := range keyframes {
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
w, h, err := imageDimensions(k)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if w <= 0 || h <= 0 {
|
||||
continue
|
||||
}
|
||||
// A 768 short edge, the long edge snapped onto the 32 grid.
|
||||
if w >= h {
|
||||
return alignMultiple(float64(h3ShortEdge)*float64(w)/float64(h), h3CanvasMultiple), h3ShortEdge, nil
|
||||
}
|
||||
return h3ShortEdge, alignMultiple(float64(h3ShortEdge)*float64(h)/float64(w), h3CanvasMultiple), nil
|
||||
}
|
||||
// The shipped canvas.
|
||||
return 1344, h3ShortEdge, nil
|
||||
}
|
||||
|
||||
// stageKeyframe converts a staged upload into the binary PPM (P6) at exactly
|
||||
// width x height that the engine requires. libvllm vendors no image codec and
|
||||
// no resampler, so ffmpeg does both; a P6 already at the canvas passes through
|
||||
// untouched.
|
||||
func stageKeyframe(ffmpegPath, src string, width, height int32, workdir, name string) (string, error) {
|
||||
if src == "" {
|
||||
return "", nil
|
||||
}
|
||||
if w, h, err := ppmDimensions(src); err == nil && w == width && h == height {
|
||||
return src, nil
|
||||
}
|
||||
ffmpegBin, err := resolveFfmpeg(ffmpegPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("converting the %s keyframe to PPM: %w", name, err)
|
||||
}
|
||||
out := filepath.Join(workdir, name+"_frame.ppm")
|
||||
// -frames:v 1 because an animated upload (GIF) would otherwise write a
|
||||
// sequence; -pix_fmt rgb24 is what the image2/ppm muxer needs for P6.
|
||||
cmd := exec.Command(ffmpegBin, "-y", "-loglevel", "error", "-i", src, // #nosec G204 -- the binary is resolved, the rest are literals and staged paths
|
||||
"-frames:v", "1",
|
||||
"-vf", fmt.Sprintf("scale=%d:%d", width, height),
|
||||
"-pix_fmt", "rgb24", "-f", "image2", out)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return "", fmt.Errorf("vllm-cpp: converting the %s keyframe to PPM failed: %w (output: %s)",
|
||||
name, err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// imageDimensions reads geometry from a staged upload, PPM included (the Go
|
||||
// standard library has no netpbm decoder).
|
||||
func imageDimensions(path string) (int32, int32, error) {
|
||||
if w, h, err := ppmDimensions(path); err == nil {
|
||||
return w, h, nil
|
||||
}
|
||||
f, err := os.Open(path) // #nosec G304 -- a path staged by LocalAI for this request
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("vllm-cpp: reading the keyframe %q: %w", path, err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
cfg, _, err := image.DecodeConfig(f)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("vllm-cpp: the keyframe %q is not a PNG, JPEG, GIF or binary PPM: %w", path, err)
|
||||
}
|
||||
return int32(cfg.Width), int32(cfg.Height), nil
|
||||
}
|
||||
|
||||
// ppmDimensions parses a binary PPM (P6) header: magic, then width, height and
|
||||
// maxval as ASCII decimals separated by whitespace, with # comments allowed.
|
||||
func ppmDimensions(path string) (int32, int32, error) {
|
||||
f, err := os.Open(path) // #nosec G304 -- a path staged by LocalAI for this request
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
// A P6 header is a handful of bytes; 512 covers any sane comment run.
|
||||
buf := make([]byte, 512)
|
||||
n, err := f.Read(buf)
|
||||
if n < 2 || (err != nil && n == 0) {
|
||||
return 0, 0, fmt.Errorf("not a PPM")
|
||||
}
|
||||
if buf[0] != 'P' || buf[1] != '6' {
|
||||
return 0, 0, fmt.Errorf("not a binary PPM (P6)")
|
||||
}
|
||||
fields := make([]int32, 0, 2)
|
||||
for i := 2; i < n && len(fields) < 2; {
|
||||
switch {
|
||||
case buf[i] == '#':
|
||||
for i < n && buf[i] != '\n' {
|
||||
i++
|
||||
}
|
||||
case buf[i] >= '0' && buf[i] <= '9':
|
||||
value := int32(0)
|
||||
for i < n && buf[i] >= '0' && buf[i] <= '9' {
|
||||
value = value*10 + int32(buf[i]-'0')
|
||||
i++
|
||||
}
|
||||
fields = append(fields, value)
|
||||
default:
|
||||
i++
|
||||
}
|
||||
}
|
||||
if len(fields) < 2 {
|
||||
return 0, 0, fmt.Errorf("truncated PPM header")
|
||||
}
|
||||
return fields[0], fields[1], nil
|
||||
}
|
||||
|
||||
// alignMultiple mirrors MiniMaxH3AlignMultiple: round-half-to-even onto the
|
||||
// multiple, floored at one multiple. Half-to-even, not half-away-from-zero,
|
||||
// because the reference pipeline uses Python's round().
|
||||
func alignMultiple(value float64, multiple int32) int32 {
|
||||
snapped := int32(math.RoundToEven(value/float64(multiple))) * multiple
|
||||
if snapped < multiple {
|
||||
return multiple
|
||||
}
|
||||
return snapped
|
||||
}
|
||||
|
||||
// truncateToGrid mirrors the engine's canvas snap: truncation, not rounding.
|
||||
func truncateToGrid(v int32) int32 {
|
||||
if v <= 0 {
|
||||
return 0
|
||||
}
|
||||
return v / h3CanvasMultiple * h3CanvasMultiple
|
||||
}
|
||||
|
||||
// alignFrameCount mirrors MiniMaxH3AlignFrameCount: the next value on the
|
||||
// 17n+5 grid. Used only to warn - the engine does the real alignment.
|
||||
func alignFrameCount(frames int32) int32 {
|
||||
if frames <= 0 {
|
||||
return frames
|
||||
}
|
||||
for frames%h3FrameGrid != h3FrameOffset {
|
||||
frames++
|
||||
}
|
||||
return frames
|
||||
}
|
||||
|
||||
func firstPositive(values ...int32) int32 {
|
||||
for _, v := range values {
|
||||
if v > 0 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func videoDeviceName(device int32) string {
|
||||
if device == videoDeviceCUDA {
|
||||
return "cuda"
|
||||
}
|
||||
return "cpu"
|
||||
}
|
||||
|
||||
// siblingConfigJSON is the release layout: each VAE ships its config.json in
|
||||
// the directory holding its weights.
|
||||
func siblingConfigJSON(weights string) string {
|
||||
candidate := filepath.Join(filepath.Dir(weights), "config.json")
|
||||
if _, err := os.Stat(candidate); err != nil {
|
||||
return ""
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"unsafe"
|
||||
|
||||
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// The video PODs carry the same contract as the text ones in vllmcpp_test.go:
|
||||
// these are the C offsets of vllm.h on LP64, and a drift here is silent memory
|
||||
// corruption rather than a compile error.
|
||||
var _ = Describe("C ABI video struct mirrors", func() {
|
||||
It("cVideoModelParams matches vllm_video_model_params", func() {
|
||||
var p cVideoModelParams
|
||||
Expect(unsafe.Offsetof(p.DitPath)).To(Equal(uintptr(0)))
|
||||
Expect(unsafe.Offsetof(p.EncoderPath)).To(Equal(uintptr(8)))
|
||||
Expect(unsafe.Offsetof(p.TokenizerPath)).To(Equal(uintptr(16)))
|
||||
Expect(unsafe.Offsetof(p.VideoVaePath)).To(Equal(uintptr(24)))
|
||||
Expect(unsafe.Offsetof(p.VideoVaeConfigPath)).To(Equal(uintptr(32)))
|
||||
Expect(unsafe.Offsetof(p.AudioVaePath)).To(Equal(uintptr(40)))
|
||||
Expect(unsafe.Offsetof(p.AudioVaeConfigPath)).To(Equal(uintptr(48)))
|
||||
Expect(unsafe.Offsetof(p.PromptEmbedsPath)).To(Equal(uintptr(56)))
|
||||
Expect(unsafe.Offsetof(p.Partition)).To(Equal(uintptr(64)))
|
||||
Expect(unsafe.Offsetof(p.Device)).To(Equal(uintptr(72)))
|
||||
Expect(unsafe.Offsetof(p.DequantBf16)).To(Equal(uintptr(76)))
|
||||
Expect(unsafe.Offsetof(p.Fp4Resident)).To(Equal(uintptr(80)))
|
||||
Expect(unsafe.Sizeof(p)).To(Equal(uintptr(88)))
|
||||
})
|
||||
|
||||
It("cVideoParams matches vllm_video_params", func() {
|
||||
var p cVideoParams
|
||||
Expect(unsafe.Offsetof(p.Prompt)).To(Equal(uintptr(0)))
|
||||
Expect(unsafe.Offsetof(p.Width)).To(Equal(uintptr(8)))
|
||||
Expect(unsafe.Offsetof(p.Height)).To(Equal(uintptr(12)))
|
||||
Expect(unsafe.Offsetof(p.NumFrames)).To(Equal(uintptr(16)))
|
||||
Expect(unsafe.Offsetof(p.Steps)).To(Equal(uintptr(20)))
|
||||
Expect(unsafe.Offsetof(p.Seed)).To(Equal(uintptr(24)))
|
||||
Expect(unsafe.Offsetof(p.HasSeed)).To(Equal(uintptr(32)))
|
||||
Expect(unsafe.Offsetof(p.FirstFrame)).To(Equal(uintptr(40)))
|
||||
Expect(unsafe.Offsetof(p.LastFrame)).To(Equal(uintptr(48)))
|
||||
Expect(unsafe.Offsetof(p.RefImage)).To(Equal(uintptr(56)))
|
||||
Expect(unsafe.Offsetof(p.RefVideo)).To(Equal(uintptr(64)))
|
||||
Expect(unsafe.Offsetof(p.RefAudio)).To(Equal(uintptr(72)))
|
||||
Expect(unsafe.Offsetof(p.NoiseAug)).To(Equal(uintptr(80)))
|
||||
Expect(unsafe.Offsetof(p.OutputDir)).To(Equal(uintptr(88)))
|
||||
Expect(unsafe.Sizeof(p)).To(Equal(uintptr(96)))
|
||||
})
|
||||
|
||||
It("cVideoResult matches vllm_video_result", func() {
|
||||
var r cVideoResult
|
||||
Expect(unsafe.Offsetof(r.FrameDir)).To(Equal(uintptr(0)))
|
||||
Expect(unsafe.Offsetof(r.AudioPath)).To(Equal(uintptr(8)))
|
||||
Expect(unsafe.Offsetof(r.FrameCount)).To(Equal(uintptr(16)))
|
||||
Expect(unsafe.Offsetof(r.Width)).To(Equal(uintptr(20)))
|
||||
Expect(unsafe.Offsetof(r.Height)).To(Equal(uintptr(24)))
|
||||
Expect(unsafe.Offsetof(r.Fps)).To(Equal(uintptr(28)))
|
||||
Expect(unsafe.Offsetof(r.SampleRate)).To(Equal(uintptr(32)))
|
||||
Expect(unsafe.Offsetof(r.MuxArgv)).To(Equal(uintptr(40)))
|
||||
Expect(unsafe.Offsetof(r.MuxArgc)).To(Equal(uintptr(48)))
|
||||
Expect(unsafe.Sizeof(r)).To(Equal(uintptr(56)))
|
||||
})
|
||||
|
||||
It("cVideoMuxParams matches vllm_video_mux_params", func() {
|
||||
var p cVideoMuxParams
|
||||
Expect(unsafe.Offsetof(p.Frames)).To(Equal(uintptr(0)))
|
||||
Expect(unsafe.Offsetof(p.AudioPath)).To(Equal(uintptr(8)))
|
||||
Expect(unsafe.Offsetof(p.OutputPath)).To(Equal(uintptr(16)))
|
||||
Expect(unsafe.Offsetof(p.Fps)).To(Equal(uintptr(24)))
|
||||
Expect(unsafe.Offsetof(p.Crf)).To(Equal(uintptr(28)))
|
||||
Expect(unsafe.Sizeof(p)).To(Equal(uintptr(32)))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("video load options", func() {
|
||||
It("stays disengaged for a plain text config", func() {
|
||||
lo := parseOptions(&pb.ModelOptions{Options: []string{"max_num_seqs:16"}})
|
||||
Expect(lo.video.engaged()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("reads the H3 checkpoint set from the options list", func() {
|
||||
lo := parseOptions(&pb.ModelOptions{Options: []string{
|
||||
"video_encoder:qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf",
|
||||
"video_tokenizer:tokenizer.json",
|
||||
"video_vae:vae/diffusion_pytorch_model.safetensors",
|
||||
"audio_vae:audio_vae/model.safetensors",
|
||||
"video_partition:fl2va",
|
||||
"video_device:cuda",
|
||||
"video_dequant_bf16:true",
|
||||
"video_width:1344",
|
||||
"video_height:768",
|
||||
"video_num_frames:124",
|
||||
"video_steps:50",
|
||||
}})
|
||||
Expect(lo.video.engaged()).To(BeTrue())
|
||||
Expect(lo.video.encoderPath).To(Equal("qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf"))
|
||||
Expect(lo.video.tokenizerPath).To(Equal("tokenizer.json"))
|
||||
Expect(lo.video.videoVaePath).To(Equal("vae/diffusion_pytorch_model.safetensors"))
|
||||
Expect(lo.video.audioVaePath).To(Equal("audio_vae/model.safetensors"))
|
||||
Expect(lo.video.partition).To(Equal(partitionFL2VA))
|
||||
Expect(lo.video.device).To(Equal(videoDeviceCUDA))
|
||||
Expect(lo.video.deviceSet).To(BeTrue())
|
||||
Expect(lo.video.dequantBf16).To(Equal(int32(1)))
|
||||
Expect(lo.video.width).To(Equal(int32(1344)))
|
||||
Expect(lo.video.height).To(Equal(int32(768)))
|
||||
Expect(lo.video.numFrames).To(Equal(int32(124)))
|
||||
Expect(lo.video.steps).To(Equal(int32(50)))
|
||||
})
|
||||
|
||||
It("reads the same keys from engine_args", func() {
|
||||
lo := parseOptions(&pb.ModelOptions{
|
||||
EngineArgs: `{"video_vae":"vae/v.safetensors","audio_vae":"a.safetensors","video_num_frames":124,"video_dequant_bf16":true}`,
|
||||
})
|
||||
Expect(lo.video.engaged()).To(BeTrue())
|
||||
Expect(lo.video.videoVaePath).To(Equal("vae/v.safetensors"))
|
||||
Expect(lo.video.audioVaePath).To(Equal("a.safetensors"))
|
||||
Expect(lo.video.numFrames).To(Equal(int32(124)))
|
||||
Expect(lo.video.dequantBf16).To(Equal(int32(1)))
|
||||
})
|
||||
|
||||
It("ignores an unknown video_device rather than guessing", func() {
|
||||
lo := parseOptions(&pb.ModelOptions{Options: []string{"video_vae:v", "video_device:tpu"}})
|
||||
Expect(lo.video.deviceSet).To(BeFalse())
|
||||
Expect(lo.video.device).To(Equal(videoDeviceCPU))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("per-request params", func() {
|
||||
It("maps the accepted keys", func() {
|
||||
extra, err := parseVideoRequestParams(map[string]string{
|
||||
"noise_aug": "0.5", "ref_image": "/tmp/ref.ppm", "crf": "20",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(extra.noiseAug).To(BeNumerically("~", 0.5, 1e-6))
|
||||
Expect(extra.refImage).To(Equal("/tmp/ref.ppm"))
|
||||
Expect(extra.crf).To(Equal(int32(20)))
|
||||
})
|
||||
|
||||
It("refuses an unknown key instead of dropping it", func() {
|
||||
_, err := parseVideoRequestParams(map[string]string{"resolution": "480p"})
|
||||
Expect(err).To(MatchError(ContainSubstring("unknown params key")))
|
||||
})
|
||||
|
||||
It("refuses a non-numeric noise_aug", func() {
|
||||
_, err := parseVideoRequestParams(map[string]string{"noise_aug": "high"})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
// The partition guard is the correctness rule this backend exists to enforce:
|
||||
// the FL2VA DiT serves t2va and fl2va, and handing it reference conditioning
|
||||
// renders a broken lattice over the frame after a multi-hour generation rather
|
||||
// than failing.
|
||||
var _ = Describe("partition conditioning guard", func() {
|
||||
It("accepts a plain t2va request on fl2va", func() {
|
||||
Expect(checkPartitionConditioning(partitionFL2VA,
|
||||
&pb.GenerateVideoRequest{Prompt: "a llama"}, videoExtraParams{})).To(Succeed())
|
||||
})
|
||||
|
||||
It("accepts fl2va keyframes on fl2va", func() {
|
||||
Expect(checkPartitionConditioning(partitionFL2VA,
|
||||
&pb.GenerateVideoRequest{StartImage: "/tmp/a.png"}, videoExtraParams{})).To(Succeed())
|
||||
})
|
||||
|
||||
It("refuses a reference image on fl2va", func() {
|
||||
err := checkPartitionConditioning(partitionFL2VA,
|
||||
&pb.GenerateVideoRequest{}, videoExtraParams{refImage: "/tmp/ref.ppm"})
|
||||
Expect(err).To(MatchError(ContainSubstring("ref2va")))
|
||||
})
|
||||
|
||||
It("refuses reference audio on fl2va", func() {
|
||||
err := checkPartitionConditioning(partitionFL2VA,
|
||||
&pb.GenerateVideoRequest{Audio: "/tmp/voice.wav"}, videoExtraParams{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("refuses fl2va keyframes on ref2va", func() {
|
||||
err := checkPartitionConditioning(partitionRef2VA,
|
||||
&pb.GenerateVideoRequest{StartImage: "/tmp/a.png"}, videoExtraParams{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("refuses keyframes and references together on either partition", func() {
|
||||
err := checkPartitionConditioning(partitionRef2VA,
|
||||
&pb.GenerateVideoRequest{StartImage: "/tmp/a.png"}, videoExtraParams{refVideo: "/tmp/clip"})
|
||||
Expect(err).To(MatchError(ContainSubstring("exclusive")))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("H3 geometry", func() {
|
||||
It("keeps an explicitly requested canvas", func() {
|
||||
w, h, err := resolveCanvas(1280, 720)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(w).To(Equal(int32(1280)))
|
||||
Expect(h).To(Equal(int32(720)))
|
||||
})
|
||||
|
||||
It("falls back to the shipped 1344x768 canvas", func() {
|
||||
w, h, err := resolveCanvas(0, 0)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(w).To(Equal(int32(1344)))
|
||||
Expect(h).To(Equal(int32(768)))
|
||||
})
|
||||
|
||||
It("derives a landscape canvas from a keyframe's aspect", func() {
|
||||
path := writePPM(1920, 1080)
|
||||
w, h, err := resolveCanvas(0, 0, path)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(h).To(Equal(int32(768)))
|
||||
// 768 * 16/9 = 1365.33; /32 = 42.67, round-half-to-even to 43, x32.
|
||||
Expect(w).To(Equal(int32(1376)))
|
||||
})
|
||||
|
||||
It("derives a portrait canvas from a keyframe's aspect", func() {
|
||||
path := writePPM(1080, 1920)
|
||||
w, h, err := resolveCanvas(0, 0, path)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(w).To(Equal(int32(768)))
|
||||
Expect(h).To(Equal(int32(1376)))
|
||||
})
|
||||
|
||||
It("truncates onto the 32 grid the way the engine does", func() {
|
||||
Expect(truncateToGrid(1000)).To(Equal(int32(992)))
|
||||
Expect(truncateToGrid(768)).To(Equal(int32(768)))
|
||||
})
|
||||
|
||||
It("reports the 17n+5 frame grid", func() {
|
||||
Expect(alignFrameCount(124)).To(Equal(int32(124)))
|
||||
Expect(alignFrameCount(120)).To(Equal(int32(124)))
|
||||
Expect(alignFrameCount(100)).To(Equal(int32(107)))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("keyframe staging", func() {
|
||||
It("parses a binary PPM header, comments included", func() {
|
||||
dir := GinkgoT().TempDir()
|
||||
path := filepath.Join(dir, "commented.ppm")
|
||||
Expect(os.WriteFile(path, []byte("P6\n# made by a test\n64 32\n255\n"), 0o600)).To(Succeed())
|
||||
w, h, err := ppmDimensions(path)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(w).To(Equal(int32(64)))
|
||||
Expect(h).To(Equal(int32(32)))
|
||||
})
|
||||
|
||||
It("refuses an ASCII PPM (P3): the engine reads P6 only", func() {
|
||||
dir := GinkgoT().TempDir()
|
||||
path := filepath.Join(dir, "ascii.ppm")
|
||||
Expect(os.WriteFile(path, []byte("P3\n64 32\n255\n"), 0o600)).To(Succeed())
|
||||
_, _, err := ppmDimensions(path)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("passes a P6 already at the canvas straight through, without ffmpeg", func() {
|
||||
path := writePPM(64, 32)
|
||||
out, err := stageKeyframe("", path, 64, 32, GinkgoT().TempDir(), "first")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(out).To(Equal(path))
|
||||
})
|
||||
|
||||
It("is a no-op for an absent keyframe", func() {
|
||||
out, err := stageKeyframe("", "", 64, 32, GinkgoT().TempDir(), "first")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(out).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("GenerateVideo preconditions", func() {
|
||||
It("refuses when the model is not a video engine", func() {
|
||||
v := &VllmCpp{}
|
||||
Expect(v.GenerateVideo(&pb.GenerateVideoRequest{Prompt: "x", Dst: "/tmp/o.mp4"})).
|
||||
To(MatchError(ContainSubstring("not a MiniMax-H3 video engine")))
|
||||
})
|
||||
})
|
||||
|
||||
// writePPM writes a valid P6 header of the given geometry. Only the header is
|
||||
// read by anything under test, so the pixel payload is left off.
|
||||
func writePPM(width, height int) string {
|
||||
dir := GinkgoT().TempDir()
|
||||
path := filepath.Join(dir, "frame.ppm")
|
||||
header := []byte("P6\n" + itoa(width) + " " + itoa(height) + "\n255\n")
|
||||
Expect(os.WriteFile(path, header, 0o600)).To(Succeed())
|
||||
return path
|
||||
}
|
||||
|
||||
func itoa(v int) string {
|
||||
if v == 0 {
|
||||
return "0"
|
||||
}
|
||||
digits := ""
|
||||
for v > 0 {
|
||||
digits = string(rune('0'+v%10)) + digits
|
||||
v /= 10
|
||||
}
|
||||
return digits
|
||||
}
|
||||
@@ -16,7 +16,7 @@ func TestVllmCpp(t *testing.T) {
|
||||
RunSpecs(t, "vllm-cpp suite")
|
||||
}
|
||||
|
||||
// The Go POD mirrors must match the C struct layout of vllm.h (ABI v10)
|
||||
// The Go POD mirrors must match the C struct layout of vllm.h (ABI v16)
|
||||
// byte-for-byte: these offsets are the C offsets on LP64 (linux/darwin
|
||||
// amd64+arm64). A failure here means govllmcpp.go drifted from vllm.h.
|
||||
var _ = Describe("C ABI struct mirrors", func() {
|
||||
@@ -24,7 +24,7 @@ var _ = Describe("C ABI struct mirrors", func() {
|
||||
// VLLM_ABI_VERSION in the vllm.h of VLLM_CPP_VERSION (Makefile).
|
||||
// Moving the pin past this without growing the mirrors below ships a
|
||||
// backend that refuses every load at startup (issue #11379).
|
||||
Expect(abiVersion).To(Equal(10))
|
||||
Expect(abiVersion).To(Equal(16))
|
||||
})
|
||||
|
||||
It("cModelParams matches vllm_model_params", func() {
|
||||
@@ -43,9 +43,12 @@ var _ = Describe("C ABI struct mirrors", func() {
|
||||
Expect(unsafe.Offsetof(p.SchedulingPolicy)).To(Equal(uintptr(64)))
|
||||
Expect(unsafe.Offsetof(p.KVTransferConfig)).To(Equal(uintptr(72)))
|
||||
Expect(unsafe.Offsetof(p.EnableJumpForward)).To(Equal(uintptr(80)))
|
||||
// 88, not 84: the struct is 8-aligned (it holds pointers), so the
|
||||
// trailing int32 is padded out. Go pads identically.
|
||||
Expect(unsafe.Sizeof(p)).To(Equal(uintptr(88)))
|
||||
Expect(unsafe.Offsetof(p.Device)).To(Equal(uintptr(84)))
|
||||
// 88, not 92: gpu_memory_utilization is a double, so it takes the next
|
||||
// 8-aligned slot after the int32 pair. Go pads identically.
|
||||
Expect(unsafe.Offsetof(p.GPUMemoryUtil)).To(Equal(uintptr(88)))
|
||||
Expect(unsafe.Offsetof(p.KVCacheMemoryBytes)).To(Equal(uintptr(96)))
|
||||
Expect(unsafe.Sizeof(p)).To(Equal(uintptr(104)))
|
||||
})
|
||||
|
||||
It("cSamplingParams matches vllm_sampling_params (ABI v8)", func() {
|
||||
|
||||
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
|
||||
|
||||
# whisper.cpp version
|
||||
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
|
||||
WHISPER_CPP_VERSION?=306c88f4d1286aec1bf96e544632897886af5501
|
||||
WHISPER_CPP_VERSION?=592feef04a1802b18cbeffd0fd0eb5d02570c2ec
|
||||
SO_TARGET?=libgowhisper.so
|
||||
|
||||
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
|
||||
|
||||
@@ -320,6 +320,13 @@ impl Backend for KokorosService {
|
||||
Err(Status::unimplemented("Not supported"))
|
||||
}
|
||||
|
||||
async fn upscale_image(
|
||||
&self,
|
||||
_: Request<backend::UpscaleImageRequest>,
|
||||
) -> Result<Response<backend::Result>, Status> {
|
||||
Err(Status::unimplemented("Not supported"))
|
||||
}
|
||||
|
||||
async fn generate_image(
|
||||
&self,
|
||||
_: Request<backend::GenerateImageRequest>,
|
||||
|
||||
@@ -28,6 +28,8 @@ type ModelsCMDFlags struct {
|
||||
Color string `env:"COLOR" hidden:""`
|
||||
NoColor string `env:"NO_COLOR" hidden:""`
|
||||
HFToken string `env:"HF_TOKEN" hidden:""`
|
||||
|
||||
ArtifactDownloadConcurrency int `env:"LOCALAI_ARTIFACT_DOWNLOAD_CONCURRENCY" help:"How many files of a model artifact to download at once. 1 (the default) downloads sequentially. Raising it helps artifacts split into many files on a fast link, at the cost of more concurrent load on the models volume" group:"storage" default:"1"`
|
||||
}
|
||||
|
||||
type ModelsList struct {
|
||||
@@ -87,6 +89,7 @@ func (mi *ModelsInstall) Run(ctx *cliContext.Context) error {
|
||||
|
||||
artifactMaterializer := modelartifacts.NewDefaultManager(
|
||||
modelartifacts.WithHuggingFaceToken(mi.HFToken),
|
||||
modelartifacts.WithDownloadConcurrency(mi.ArtifactDownloadConcurrency),
|
||||
)
|
||||
galleryService := galleryop.NewGalleryService(&config.ApplicationConfig{
|
||||
SystemState: systemState,
|
||||
|
||||
@@ -41,6 +41,7 @@ type RunCMD struct {
|
||||
BackendsPath string `env:"LOCALAI_BACKENDS_PATH,BACKENDS_PATH" type:"path" default:"${basepath}/backends" help:"Path containing backends used for inferencing" group:"backends"`
|
||||
BackendsSystemPath string `env:"LOCALAI_BACKENDS_SYSTEM_PATH,BACKEND_SYSTEM_PATH" type:"path" default:"/var/lib/local-ai/backends" help:"Path containing system backends used for inferencing" group:"backends"`
|
||||
ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"`
|
||||
ArtifactDownloadConcurrency int `env:"LOCALAI_ARTIFACT_DOWNLOAD_CONCURRENCY" help:"How many files of a model artifact to download at once. 1 (the default) downloads sequentially. Raising it helps artifacts split into many files on a fast link, at the cost of more concurrent load on the models volume" group:"storage" default:"1"`
|
||||
GeneratedContentPath string `env:"LOCALAI_GENERATED_CONTENT_PATH,GENERATED_CONTENT_PATH" type:"path" default:"${generatedcontentpath}" help:"Location for generated content (e.g. images, audio, videos)" group:"storage"`
|
||||
UploadPath string `env:"LOCALAI_UPLOAD_PATH,UPLOAD_PATH" type:"path" default:"${uploadpath}" help:"Path to store uploads from files api" group:"storage"`
|
||||
DataPath string `env:"LOCALAI_DATA_PATH" type:"path" default:"${basepath}/data" help:"Path for persistent data (collectiondb, agent state, tasks, jobs). Separates mutable data from configuration" group:"storage"`
|
||||
@@ -278,8 +279,10 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
|
||||
|
||||
opts := []config.AppOption{
|
||||
config.WithContext(context.Background()),
|
||||
config.WithArtifactDownloadConcurrency(r.ArtifactDownloadConcurrency),
|
||||
config.WithModelArtifactMaterializer(modelartifacts.NewDefaultManager(
|
||||
modelartifacts.WithHuggingFaceToken(r.HFToken),
|
||||
modelartifacts.WithDownloadConcurrency(r.ArtifactDownloadConcurrency),
|
||||
)),
|
||||
config.WithModelPreloadDisplay(r.Color, r.NoColor != ""),
|
||||
config.WithConfigFile(r.ModelsConfigFile),
|
||||
|
||||
@@ -16,6 +16,15 @@ func (*applicationArtifactMaterializer) Ensure(context.Context, string, modelart
|
||||
return modelartifacts.Result{}, nil
|
||||
}
|
||||
|
||||
type configurableApplicationArtifactMaterializer struct {
|
||||
applicationArtifactMaterializer
|
||||
concurrency int
|
||||
}
|
||||
|
||||
func (m *configurableApplicationArtifactMaterializer) SetDownloadConcurrency(concurrency int) {
|
||||
m.concurrency = concurrency
|
||||
}
|
||||
|
||||
var _ = Describe("ApplicationConfig model artifact materializer", func() {
|
||||
It("provides a default materializer", func() {
|
||||
Expect(NewApplicationConfig().ModelArtifactMaterializer).NotTo(BeNil())
|
||||
@@ -31,4 +40,15 @@ var _ = Describe("ApplicationConfig model artifact materializer", func() {
|
||||
Expect(field.Tag.Get("json")).To(Equal("-"))
|
||||
Expect(field.Tag.Get("yaml")).To(Equal("-"))
|
||||
})
|
||||
|
||||
It("applies runtime download concurrency to configurable materializers", func() {
|
||||
materializer := &configurableApplicationArtifactMaterializer{}
|
||||
appConfig := NewApplicationConfig(WithModelArtifactMaterializer(materializer))
|
||||
concurrency := 4
|
||||
|
||||
appConfig.ApplyRuntimeSettings(&RuntimeSettings{ArtifactDownloadConcurrency: &concurrency})
|
||||
|
||||
Expect(appConfig.ArtifactDownloadConcurrency).To(Equal(4))
|
||||
Expect(materializer.concurrency).To(Equal(4))
|
||||
})
|
||||
})
|
||||
@@ -35,6 +35,7 @@ type ApplicationConfig struct {
|
||||
// network interfaces (e.g. eth0), filtering out docker0/veth noise.
|
||||
WebRTCICEInterfaces []string
|
||||
UploadLimitMB, Threads, ContextSize int
|
||||
ArtifactDownloadConcurrency int
|
||||
F16 bool
|
||||
Debug bool
|
||||
EnableTracing bool
|
||||
@@ -58,12 +59,12 @@ type ApplicationConfig struct {
|
||||
// gzip is skipped. 0 keeps middleware.DefaultCompressionMinLength.
|
||||
HTTPCompressionMinLength int
|
||||
PreloadJSONModels string
|
||||
PreloadModelsFromPath string
|
||||
CORSAllowOrigins string
|
||||
ApiKeys []string
|
||||
P2PToken string
|
||||
P2PNetworkID string
|
||||
Federated bool
|
||||
PreloadModelsFromPath string
|
||||
CORSAllowOrigins string
|
||||
ApiKeys []string
|
||||
P2PToken string
|
||||
P2PNetworkID string
|
||||
Federated bool
|
||||
|
||||
// ExternalBaseURL is the externally visible base URL of this instance
|
||||
// (scheme+host[:port]), set via LOCALAI_BASE_URL. When non-empty it is
|
||||
@@ -276,11 +277,12 @@ func NewApplicationConfig(o ...AppOption) *ApplicationConfig {
|
||||
// force-enables it). It's a small in-memory ring buffer; the Settings
|
||||
// toggle can still turn it off (a persisted false wins - see
|
||||
// loadRuntimeSettingsFromFile).
|
||||
EnableBackendLogging: true,
|
||||
AgentJobRetentionDays: 30, // Default: 30 days
|
||||
LRUEvictionMaxRetries: 30, // Default: 30 retries
|
||||
LRUEvictionRetryInterval: 1 * time.Second, // Default: 1 second
|
||||
ModelLoadFailureCooldown: 10 * time.Second, // Default: 10s base cooldown after a failed load
|
||||
EnableBackendLogging: true,
|
||||
ArtifactDownloadConcurrency: modelartifacts.DefaultDownloadConcurrency,
|
||||
AgentJobRetentionDays: 30, // Default: 30 days
|
||||
LRUEvictionMaxRetries: 30, // Default: 30 retries
|
||||
LRUEvictionRetryInterval: 1 * time.Second, // Default: 1 second
|
||||
ModelLoadFailureCooldown: 10 * time.Second, // Default: 10s base cooldown after a failed load
|
||||
// WatchDogInterval is intentionally left at the zero value here.
|
||||
// The startup loader applies a persisted runtime_settings.json value
|
||||
// only when the interval is still 0 (its "not set by env var"
|
||||
@@ -685,6 +687,15 @@ func WithModelArtifactMaterializer(materializer ArtifactMaterializer) AppOption
|
||||
}
|
||||
}
|
||||
|
||||
func WithArtifactDownloadConcurrency(concurrency int) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
if concurrency < 1 {
|
||||
concurrency = modelartifacts.DefaultDownloadConcurrency
|
||||
}
|
||||
o.ArtifactDownloadConcurrency = concurrency
|
||||
}
|
||||
}
|
||||
|
||||
// WithModelPreloadDisplay configures terminal rendering for model preload output.
|
||||
func WithModelPreloadDisplay(renderMode string, disableColor bool) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
@@ -1190,6 +1201,11 @@ func (o *ApplicationConfig) ApplyRuntimeSettings(settings *RuntimeSettings) (req
|
||||
xsysinfo.SetDefaultVRAMBudget(b)
|
||||
}
|
||||
}
|
||||
if settings.ArtifactDownloadConcurrency != nil {
|
||||
if configurable, ok := o.ModelArtifactMaterializer.(interface{ SetDownloadConcurrency(int) }); ok {
|
||||
configurable.SetDownloadConcurrency(o.ArtifactDownloadConcurrency)
|
||||
}
|
||||
}
|
||||
// Note: ApiKeys need env-merge handling (MergeAPIKeys) - done by the
|
||||
// caller, because the env-provided keys live on the startup config.
|
||||
return requireRestart
|
||||
|
||||
@@ -304,6 +304,20 @@ var BackendCapabilities = map[string]BackendCapability{
|
||||
AcceptsImages: true,
|
||||
Description: "SGLang — fast LLM inference with structured generation and optional vision",
|
||||
},
|
||||
// vllm-cpp serves two mutually exclusive engine handles from one backend:
|
||||
// a text engine, and MiniMax-H3's video+audio engine when the model config
|
||||
// declares the H3 checkpoint set. Both usecases are possible, and chat is
|
||||
// the default because a config that says nothing is a text model.
|
||||
//
|
||||
// AcceptsImages is the fl2va keyframe (start_image/end_image), the same
|
||||
// reason longcat-video declares it; the text path takes no image input.
|
||||
"vllm-cpp": {
|
||||
GRPCMethods: []GRPCMethod{MethodPredict, MethodPredictStream, MethodGenerateVideo},
|
||||
PossibleUsecases: []string{UsecaseChat, UsecaseCompletion, UsecaseVideo},
|
||||
DefaultUsecases: []string{UsecaseChat},
|
||||
AcceptsImages: true,
|
||||
Description: "vllm.cpp — the LocalAI team's C++20 port of vLLM; text generation plus MiniMax-H3 video+audio generation",
|
||||
},
|
||||
"vllm-omni": {
|
||||
GRPCMethods: []GRPCMethod{MethodPredict, MethodPredictStream, MethodGenerateImage, MethodGenerateVideo, MethodTTS},
|
||||
PossibleUsecases: []string{UsecaseChat, UsecaseCompletion, UsecaseImage, UsecaseVideo, UsecaseTTS, UsecaseVision},
|
||||
|
||||
@@ -38,6 +38,14 @@ var CacheTypeOptions = []FieldOption{
|
||||
{Value: "q4_1", Label: "Q4_1"},
|
||||
{Value: "q5_0", Label: "Q5_0"},
|
||||
{Value: "q5_1", Label: "Q5_1"},
|
||||
// TurboQuant KV-cache types — accepted by the turboquant and
|
||||
// buun-llama-cpp fork backends; stock llama-cpp will reject them at load.
|
||||
{Value: "turbo2", Label: "Turbo2 (TurboQuant)"},
|
||||
{Value: "turbo3", Label: "Turbo3 (TurboQuant)"},
|
||||
{Value: "turbo4", Label: "Turbo4 (TurboQuant)"},
|
||||
// Trellis-Coded Quantization variants — buun-llama-cpp only.
|
||||
{Value: "turbo2_tcq", Label: "Turbo2 TCQ (buun-llama-cpp)"},
|
||||
{Value: "turbo3_tcq", Label: "Turbo3 TCQ (buun-llama-cpp)"},
|
||||
}
|
||||
|
||||
var DiffusersPipelineOptions = []FieldOption{
|
||||
|
||||
@@ -816,6 +816,25 @@ func DefaultRegistry() map[string]FieldMetaOverride {
|
||||
AutocompleteProvider: "models:token_classify",
|
||||
Order: 201,
|
||||
},
|
||||
"pii.reversible_redactions": {
|
||||
Section: "pii",
|
||||
Label: "Reversible Redactions",
|
||||
Description: "Replace masked values with wrapped request-scoped tokens and restore them when the model returns those tokens. Supports streaming responses and never persists the substitution map.",
|
||||
Component: "toggle",
|
||||
Order: 202,
|
||||
},
|
||||
"pii.reversible_token_prefix": {
|
||||
Section: "pii",
|
||||
Label: "Reversible Token Prefix",
|
||||
Description: "Prefix for reversible redaction tokens. Defaults to [REDACTED:.",
|
||||
Order: 203,
|
||||
},
|
||||
"pii.reversible_token_suffix": {
|
||||
Section: "pii",
|
||||
Label: "Reversible Token Suffix",
|
||||
Description: "Suffix for reversible redaction tokens. Defaults to ].",
|
||||
Order: 204,
|
||||
},
|
||||
|
||||
// --- PII detection policy (on a token_classify detector model) ---
|
||||
"pii_detection.min_score": {
|
||||
|
||||
@@ -440,8 +440,18 @@ type PIIConfig struct {
|
||||
// model just opts in by listing detectors. Multiple detectors union
|
||||
// their hits; overlapping spans resolve to the strongest action.
|
||||
Detectors []string `yaml:"detectors,omitempty" json:"detectors,omitempty"`
|
||||
|
||||
// ReversibleRedactions replaces request PII with stable, request-scoped
|
||||
// tokens and restores those values when the wrapped tokens appear in the response.
|
||||
ReversibleRedactions bool `yaml:"reversible_redactions,omitempty" json:"reversible_redactions,omitempty"`
|
||||
ReversibleTokenPrefix string `yaml:"reversible_token_prefix,omitempty" json:"reversible_token_prefix,omitempty"`
|
||||
ReversibleTokenSuffix string `yaml:"reversible_token_suffix,omitempty" json:"reversible_token_suffix,omitempty"`
|
||||
}
|
||||
|
||||
func (c ModelConfig) PIIReversibleRedactions() bool { return c.PII.ReversibleRedactions }
|
||||
func (c ModelConfig) PIIReversibleTokenPrefix() string { return c.PII.ReversibleTokenPrefix }
|
||||
func (c ModelConfig) PIIReversibleTokenSuffix() string { return c.PII.ReversibleTokenSuffix }
|
||||
|
||||
// @Description Detection policy for a token-classification (NER) model
|
||||
// used as a PII detector. Lives on the detector model's own config so the
|
||||
// model is a self-describing policy unit: consuming models reference it by
|
||||
|
||||
@@ -33,15 +33,16 @@ type RuntimeSettings struct {
|
||||
LRUEvictionRetryInterval *string `json:"lru_eviction_retry_interval,omitempty"` // Interval between retries when waiting for busy models (e.g., 1s, 2s) (default: 1s)
|
||||
|
||||
// Performance settings
|
||||
Threads *int `json:"threads,omitempty"`
|
||||
ContextSize *int `json:"context_size,omitempty"`
|
||||
VRAMBudget *string `json:"vram_budget,omitempty"` // Cap VRAM for allocation ("80%" or "12GB"; "" = no cap)
|
||||
F16 *bool `json:"f16,omitempty"`
|
||||
Debug *bool `json:"debug,omitempty"`
|
||||
EnableTracing *bool `json:"enable_tracing,omitempty"`
|
||||
TracingMaxItems *int `json:"tracing_max_items,omitempty"`
|
||||
TracingMaxBodyBytes *int `json:"tracing_max_body_bytes,omitempty"` // Per-body cap in bytes; 0 disables the cap
|
||||
EnableBackendLogging *bool `json:"enable_backend_logging,omitempty"`
|
||||
Threads *int `json:"threads,omitempty"`
|
||||
ContextSize *int `json:"context_size,omitempty"`
|
||||
ArtifactDownloadConcurrency *int `json:"artifact_download_concurrency,omitempty"`
|
||||
VRAMBudget *string `json:"vram_budget,omitempty"` // Cap VRAM for allocation ("80%" or "12GB"; "" = no cap)
|
||||
F16 *bool `json:"f16,omitempty"`
|
||||
Debug *bool `json:"debug,omitempty"`
|
||||
EnableTracing *bool `json:"enable_tracing,omitempty"`
|
||||
TracingMaxItems *int `json:"tracing_max_items,omitempty"`
|
||||
TracingMaxBodyBytes *int `json:"tracing_max_body_bytes,omitempty"` // Per-body cap in bytes; 0 disables the cap
|
||||
EnableBackendLogging *bool `json:"enable_backend_logging,omitempty"`
|
||||
|
||||
// Security/CORS settings
|
||||
CORS *bool `json:"cors,omitempty"`
|
||||
|
||||
@@ -227,6 +227,15 @@ var runtimeSettingsFields = []fieldSpec{
|
||||
func(s *RuntimeSettings) **int { return &s.ContextSize },
|
||||
func(o *ApplicationConfig) int { return o.ContextSize },
|
||||
func(o *ApplicationConfig, v int) { o.ContextSize = v }),
|
||||
field("artifact_download_concurrency",
|
||||
func(s *RuntimeSettings) **int { return &s.ArtifactDownloadConcurrency },
|
||||
func(o *ApplicationConfig) int { return o.ArtifactDownloadConcurrency },
|
||||
func(o *ApplicationConfig, v int) {
|
||||
if v < 1 {
|
||||
v = 1
|
||||
}
|
||||
o.ArtifactDownloadConcurrency = v
|
||||
}),
|
||||
// VRAM budget: the cap string ("80%"/"12GB"/"" = uncapped). The live
|
||||
// side effect (xsysinfo.SetDefaultVRAMBudget) is post-processing in the
|
||||
// apply loop, not here - the row only owns the config member, matching
|
||||
|
||||
@@ -71,6 +71,7 @@ var _ = Describe("runtime settings registry", func() {
|
||||
src.LRUEvictionRetryInterval = 3 * time.Second
|
||||
src.Threads = 7
|
||||
src.ContextSize = 8192
|
||||
src.ArtifactDownloadConcurrency = 6
|
||||
src.VRAMBudget = "12GiB"
|
||||
src.F16 = true
|
||||
src.Debug = true
|
||||
|
||||
@@ -98,4 +98,9 @@ func (o *ApplicationConfig) ApplyRuntimeSettingsAtStartup(settings *RuntimeSetti
|
||||
xsysinfo.SetDefaultVRAMBudget(b)
|
||||
}
|
||||
}
|
||||
if settings.ArtifactDownloadConcurrency != nil {
|
||||
if configurable, ok := o.ModelArtifactMaterializer.(interface{ SetDownloadConcurrency(int) }); ok {
|
||||
configurable.SetDownloadConcurrency(o.ArtifactDownloadConcurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ func (i *LlamaCPPImporter) AdditionalBackends() []KnownBackendEntry {
|
||||
{Name: "ik-llama-cpp", Modality: "text", Description: "GGUF drop-in replacement for llama-cpp with ik-quants"},
|
||||
{Name: "turboquant", Modality: "text", Description: "GGUF drop-in replacement for llama-cpp with TurboQuant optimizations"},
|
||||
{Name: "vllm-cpp", Modality: "text", Description: "vLLM-style continuous-batching engine (vllm.cpp) consuming GGUF, by the LocalAI team"},
|
||||
{Name: "buun-llama-cpp", Modality: "text", Description: "GGUF drop-in replacement for llama-cpp with DFlash speculative decoding and TurboQuant/TCQ KV-cache quantization"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +137,7 @@ func (i *LlamaCPPImporter) Import(details Details) (gallery.ModelConfig, error)
|
||||
backend := "llama-cpp"
|
||||
if b, ok := preferencesMap["backend"].(string); ok {
|
||||
switch b {
|
||||
case "ik-llama-cpp", "turboquant", "vllm-cpp":
|
||||
case "ik-llama-cpp", "turboquant", "vllm-cpp", "buun-llama-cpp":
|
||||
backend = b
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,23 @@ var _ = Describe("LlamaCPPImporter", func() {
|
||||
Expect(modelConfig.Files[0].Filename).To(Equal("my-model.gguf"))
|
||||
})
|
||||
|
||||
It("swaps the emitted backend to buun-llama-cpp when preferred", func() {
|
||||
preferences := json.RawMessage(`{"backend": "buun-llama-cpp"}`)
|
||||
details := Details{
|
||||
URI: "https://example.com/my-model.gguf",
|
||||
Preferences: preferences,
|
||||
}
|
||||
|
||||
modelConfig, err := importer.Import(details)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: buun-llama-cpp"), fmt.Sprintf("Model config: %+v", modelConfig))
|
||||
Expect(modelConfig.ConfigFile).NotTo(ContainSubstring("backend: llama-cpp\n"), fmt.Sprintf("Model config: %+v", modelConfig))
|
||||
Expect(modelConfig.ConfigFile).To(ContainSubstring("model: my-model.gguf"), fmt.Sprintf("Model config: %+v", modelConfig))
|
||||
Expect(len(modelConfig.Files)).To(Equal(1))
|
||||
Expect(modelConfig.Files[0].Filename).To(Equal("my-model.gguf"))
|
||||
})
|
||||
|
||||
It("keeps backend: llama-cpp for unknown backend preferences", func() {
|
||||
// Unknown backend values must not leak into the emitted YAML —
|
||||
// we only honour the curated drop-in replacements.
|
||||
@@ -551,7 +568,7 @@ var _ = Describe("LlamaCPPImporter", func() {
|
||||
})
|
||||
|
||||
Context("AdditionalBackends", func() {
|
||||
It("advertises ik-llama-cpp, turboquant and vllm-cpp as drop-in replacements", func() {
|
||||
It("advertises all llama-cpp drop-in replacements", func() {
|
||||
entries := importer.AdditionalBackends()
|
||||
|
||||
names := make([]string, 0, len(entries))
|
||||
@@ -560,7 +577,7 @@ var _ = Describe("LlamaCPPImporter", func() {
|
||||
names = append(names, e.Name)
|
||||
byName[e.Name] = e
|
||||
}
|
||||
Expect(names).To(ConsistOf("ik-llama-cpp", "turboquant", "vllm-cpp"))
|
||||
Expect(names).To(ConsistOf("ik-llama-cpp", "turboquant", "vllm-cpp", "buun-llama-cpp"))
|
||||
|
||||
for _, name := range names {
|
||||
e := byName[name]
|
||||
|
||||
@@ -209,16 +209,15 @@ func VideoEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfi
|
||||
config.Backend = model.StableDiffusionGGMLBackend
|
||||
}
|
||||
|
||||
// Unset geometry is passed through as 0 so the BACKEND supplies its own
|
||||
// default canvas. Every video backend already does: stablediffusion-ggml
|
||||
// falls back to 512x512, diffusers to 1280x720, longcat-video to 832x480,
|
||||
// vllm-cpp to MiniMax-H3's trained 1344x768. Forcing 512x512 here made a
|
||||
// request that asked for nothing render at a size three of the four were
|
||||
// never trained at, and there is no size that is right for all of them.
|
||||
width := input.Width
|
||||
height := input.Height
|
||||
|
||||
if width == 0 {
|
||||
width = 512
|
||||
}
|
||||
if height == 0 {
|
||||
height = 512
|
||||
}
|
||||
|
||||
b64JSON := input.ResponseFormat == "b64_json"
|
||||
|
||||
tempDir := ""
|
||||
|
||||
@@ -11,6 +11,13 @@ test.describe('Settings - Backend Logging', () => {
|
||||
await expect(page.locator('text=Enable Backend Logging')).toBeVisible()
|
||||
})
|
||||
|
||||
test('artifact download concurrency is configurable', async ({ page }) => {
|
||||
const input = page.getByLabel('Artifact Download Concurrency')
|
||||
await expect(input).toBeVisible()
|
||||
await input.fill('4')
|
||||
await expect(input).toHaveValue('4')
|
||||
})
|
||||
|
||||
test('backend logging toggle can be toggled', async ({ page }) => {
|
||||
// Find the checkbox associated with backend logging
|
||||
const section = page.locator('div', { has: page.locator('text=Enable Backend Logging') })
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"activity": {
|
||||
"title": "Atividade",
|
||||
"supporting": "Instalações, downloads e remoções nesta instância.",
|
||||
"hide": "Ocultar",
|
||||
"moveToHistory": "Mover para o histórico",
|
||||
"moreCount": "mais {{count}}",
|
||||
"waitingForInstaller": "aguardando o instalador",
|
||||
"progressLabel": "Progresso de {{name}}",
|
||||
"toNode": "para {{node}}",
|
||||
"nodesDone": "{{done}} de {{total}} nós concluídos",
|
||||
"timeLeft": "restam {{value}}",
|
||||
"cancel": "Cancelar",
|
||||
"cancelLabel": "Cancelar {{name}}",
|
||||
"pause": "Pausar",
|
||||
"pauseLabel": "Pausar {{name}} e manter os dados baixados",
|
||||
"retry": "Tentar novamente",
|
||||
"retryLabel": "Tentar novamente {{name}}",
|
||||
"nodeCount": "{{count}} nós",
|
||||
"showNodes": "Mostrar {{count}} nós",
|
||||
"hideNodes": "Ocultar detalhes por nó",
|
||||
"node": {
|
||||
"done": "Concluído",
|
||||
"failed": "Falhou",
|
||||
"queued": "Na fila",
|
||||
"workerBusy": "Worker ocupado",
|
||||
"downloading": "Baixando"
|
||||
},
|
||||
"kind": {
|
||||
"model": "modelo",
|
||||
"backend": "backend"
|
||||
},
|
||||
"verb": {
|
||||
"installing": "Instalando {{kind}}",
|
||||
"installed": "{{kind}} instalado",
|
||||
"removed": "{{kind}} removido",
|
||||
"staged": "Modelo preparado",
|
||||
"failed": "Não foi possível instalar {{kind}}",
|
||||
"queued": "Na fila",
|
||||
"staging": "Preparando modelo",
|
||||
"removing": "Removendo {{kind}}",
|
||||
"failedRemoval": "Não foi possível remover {{kind}}",
|
||||
"failedStaging": "Não foi possível preparar o modelo"
|
||||
},
|
||||
"phase": {
|
||||
"resolving": "Resolvendo arquivos",
|
||||
"downloading": "Baixando",
|
||||
"verifying": "Verificando",
|
||||
"committing": "Finalizando",
|
||||
"persisting": "Salvando configuração"
|
||||
},
|
||||
"clearHistory": "Limpar histórico",
|
||||
"inProgress": "Em andamento",
|
||||
"needsAttention": "Requer atenção",
|
||||
"record": "Registro",
|
||||
"historyNote": "Mantém as últimas 50 operações. O histórico fica na memória e é reiniciado quando o LocalAI reinicia.",
|
||||
"emptyTitle": "Nenhuma operação desde a inicialização",
|
||||
"emptyBody": "Instalações de modelos e backends aparecem aqui enquanto são executadas e permanecem como registro após a conclusão. O histórico fica na memória, portanto é reiniciado quando o LocalAI reinicia.",
|
||||
"browseModels": "Procurar modelos",
|
||||
"viewInModels": "Ver em Modelos",
|
||||
"viewInBackends": "Ver em Backends",
|
||||
"rowInstalled": "instalado em {{duration}}",
|
||||
"rowFailed": "falhou: {{error}}",
|
||||
"rowCancelled": "cancelado",
|
||||
"rowRemoved": "removido",
|
||||
"retryFailed": "Falha ao tentar novamente: {{message}}",
|
||||
"filter": {
|
||||
"all": "Todas",
|
||||
"models": "Modelos",
|
||||
"backends": "Backends",
|
||||
"cluster": "Cluster"
|
||||
},
|
||||
"summaryRunning_one": "{{count}} operação em andamento.",
|
||||
"summaryRunning_other": "{{count}} operações em andamento.",
|
||||
"summaryFailed_one": "{{count}} operação requer atenção.",
|
||||
"summaryFailed_other": "{{count}} operações requerem atenção.",
|
||||
"summaryQuiet_one": "Nada em execução. {{count}} operação desde a inicialização.",
|
||||
"summaryQuiet_other": "Nada em execução. {{count}} operações desde a inicialização.",
|
||||
"summaryIdle": "Nada em execução.",
|
||||
"emptyFiltered": "Nenhuma operação corresponde a este filtro.",
|
||||
"showAll": "Mostrar tudo",
|
||||
"rowInstalledPlain": "instalado"
|
||||
},
|
||||
"manage": {
|
||||
"title": "Sistema",
|
||||
"subtitle": "Gerenciar modelos e backends instalados"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
"subtitle": "Configurar as configurações de runtime do LocalAI",
|
||||
"saved": "Configurações salvas com sucesso",
|
||||
"saveFailed": "Falha ao salvar: {{message}}",
|
||||
"loadFailed": "Falha ao carregar as configurações: {{message}}",
|
||||
"sections": {
|
||||
"branding": "Identidade visual",
|
||||
"watchdog": "Watchdog",
|
||||
"memory": "Memória",
|
||||
"backends": "Backends",
|
||||
"performance": "Desempenho",
|
||||
"tracing": "Rastreamento",
|
||||
"api": "API e CORS",
|
||||
"p2p": "P2P",
|
||||
"galleries": "Galerias",
|
||||
"apikeys": "Chaves de API",
|
||||
"agents": "Trabalhos de Agentes",
|
||||
"agentpool": "Pool de Agentes",
|
||||
"assistant": "Assistente LocalAI",
|
||||
"distributed": "Distribuído",
|
||||
"responses": "Respostas"
|
||||
}
|
||||
},
|
||||
"backends": {
|
||||
"title": "Gerenciamento de Backends",
|
||||
"subtitle": "Descubra e instale backends de IA para potencializar seus modelos"
|
||||
},
|
||||
"backendLogs": {
|
||||
"title": "Logs de Backends",
|
||||
"subtitle": "Ver logs dos backends em execução",
|
||||
"empty": "Nenhum log disponível"
|
||||
},
|
||||
"traces": {
|
||||
"title": "Rastreamentos",
|
||||
"subtitle": "Ver requisições, respostas e operações de backend registradas"
|
||||
},
|
||||
"nodes": {
|
||||
"title": "Nós Distribuídos",
|
||||
"subtitle": "Gerenciar nós workers de backend e de agentes"
|
||||
},
|
||||
"scheduling": {
|
||||
"title": "Agendamento",
|
||||
"subtitle": "Regras de posicionamento de modelos e réplicas no cluster"
|
||||
},
|
||||
"p2p": {
|
||||
"title": "Computação de IA Distribuída",
|
||||
"subtitle": "Escale suas cargas de trabalho de IA por vários dispositivos com distribuição ponto a ponto"
|
||||
},
|
||||
"users": {
|
||||
"title": "Usuários",
|
||||
"subtitle": "Gerenciar usuários registrados, funções e convites"
|
||||
},
|
||||
"usage": {
|
||||
"title": "Uso",
|
||||
"subtitle": "Estatísticas de uso de tokens da API",
|
||||
"sources": {
|
||||
"tab": "Origens",
|
||||
"mixTitle": "Mix de origens",
|
||||
"ribbonAria": "{{apikey}}% chaves de API, {{web}}% Web UI, {{legacy}}% Legado",
|
||||
"topSources": "Principais origens ao longo do tempo",
|
||||
"searchPlaceholder": "Buscar por nome ou prefixo",
|
||||
"sortBy": "Ordenar",
|
||||
"sortTokens": "Tokens",
|
||||
"sortRequests": "Requisições",
|
||||
"sortLastUsed": "Último uso",
|
||||
"sortName": "Nome",
|
||||
"sortUser": "Usuário",
|
||||
"webUI": "Web UI",
|
||||
"legacy": "Legado",
|
||||
"revoked": "revogada",
|
||||
"filteredTo": "Filtrado por: {{name}}",
|
||||
"clearFilter": "Limpar filtro",
|
||||
"other": "Outros ({{count}})",
|
||||
"noTrafficShort": "Nenhuma requisição neste período.",
|
||||
"noKeysYet": "Assim que houver requisições, você as verá detalhadas aqui.",
|
||||
"createKey": "Criar sua primeira chave de API",
|
||||
"truncatedWarning": "Mostrando as 200 principais chaves. Aplique um filtro para reduzir ainda mais."
|
||||
}
|
||||
},
|
||||
"explorer": {
|
||||
"title": "Explorador",
|
||||
"subtitle": "Navegar por arquivos e configurações"
|
||||
},
|
||||
"operate": {
|
||||
"overview": {
|
||||
"title": "Visão geral",
|
||||
"subtitle": "Tudo o que está em execução nesta instalação e tudo o que precisa de uma decisão.",
|
||||
"attention": {
|
||||
"heading": "Requer atenção",
|
||||
"clear": "Nada requer atenção. Os backends estão atualizados, nenhuma operação falhou e todos os nós estão saudáveis.",
|
||||
"backendUpdate": "Atualização disponível: {{from}} → {{to}}"
|
||||
},
|
||||
"sections": {
|
||||
"heading": "Seções",
|
||||
"runtime": "Runtime",
|
||||
"runtimeSummary": "{{backends}} backends · {{models}} modelos · {{updates}} atualizações · {{running}} em execução",
|
||||
"cluster": "Cluster",
|
||||
"clusterSummary": "{{nodes}} nós",
|
||||
"observability": "Observabilidade",
|
||||
"observabilitySummary": "Uso e rastreamentos",
|
||||
"administration": "Administração",
|
||||
"administrationSummary": "Usuários, middleware e configurações · {{memory}} de memória em uso",
|
||||
"clusterSingle": "Nó único",
|
||||
"observabilityCounted": "{{requests}} requisições · {{errors}} falhas · p95 {{p95}} ms"
|
||||
},
|
||||
"headline": {
|
||||
"requests": "Requisições · {{hours}}h",
|
||||
"errors": "Requisições com falha",
|
||||
"p95": "latência p95",
|
||||
"quiet": "Nenhuma requisição atendida nesta janela ainda.",
|
||||
"host": "Memória do host"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"title": "Agentes",
|
||||
"subtitle": "Gerenciar agentes de IA autônomos",
|
||||
"actions": {
|
||||
"agentHub": "Central de Agentes",
|
||||
"import": "Importar",
|
||||
"createAgent": "Criar Agente",
|
||||
"edit": "Editar",
|
||||
"chat": "Conversar",
|
||||
"export": "Exportar",
|
||||
"delete": "Excluir",
|
||||
"pause": "Pausar",
|
||||
"resume": "Retomar"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nome",
|
||||
"status": "Status",
|
||||
"events": "Eventos",
|
||||
"actions": "Ações",
|
||||
"eventsTooltip": "{{count}} eventos - Clique para ver"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar agentes...",
|
||||
"summary_one": "{{shown}} de {{total}} agente",
|
||||
"summary_other": "{{shown}} de {{total}} agentes"
|
||||
},
|
||||
"empty": {
|
||||
"noConfigured": "Nenhum agente configurado",
|
||||
"noConfiguredText": "Crie um agente para começar com fluxos de trabalho autônomos de IA.",
|
||||
"browseHub": "Não sabe por onde começar? Explore a <1>Central de Agentes</1> para encontrar configurações de agentes prontas que você pode importar.",
|
||||
"noMatching": "Nenhum agente correspondente",
|
||||
"noMatchingText": "Nenhum agente corresponde a \"{{query}}\""
|
||||
},
|
||||
"sections": {
|
||||
"yourAgents": "Seus Agentes",
|
||||
"otherUsersAgents": "Agentes de Outros Usuários"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Excluir Agente",
|
||||
"message": "Excluir o agente \"{{name}}\"? Esta ação não pode ser desfeita.",
|
||||
"confirm": "Excluir"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Falha ao carregar agentes: {{message}}",
|
||||
"deleted": "Agente \"{{name}}\" excluído",
|
||||
"deleteFailed": "Falha ao excluir agente: {{message}}",
|
||||
"paused": "Agente \"{{name}}\" pausado",
|
||||
"resumed": "Agente \"{{name}}\" retomado",
|
||||
"pauseFailed": "Falha ao pausar agente: {{message}}",
|
||||
"resumeFailed": "Falha ao retomar agente: {{message}}",
|
||||
"exported": "Agente \"{{name}}\" exportado",
|
||||
"exportFailed": "Falha ao exportar agente: {{message}}",
|
||||
"parseFailed": "Falha ao analisar o arquivo do agente: {{message}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"login": {
|
||||
"subtitle": "Entre para continuar",
|
||||
"registerSubtitle": "Crie uma conta",
|
||||
"createAdminSubtitle": "Crie sua conta de administrador",
|
||||
"tokenSubtitle": "Digite sua chave de API para continuar",
|
||||
"email": "E-mail",
|
||||
"emailPlaceholder": "voce@exemplo.com",
|
||||
"name": "Nome",
|
||||
"namePlaceholder": "Seu nome (opcional)",
|
||||
"password": "Senha",
|
||||
"passwordPlaceholder": "Digite a senha...",
|
||||
"newPasswordPlaceholder": "Pelo menos 12 caracteres",
|
||||
"confirmPassword": "Confirmar Senha",
|
||||
"confirmPasswordPlaceholder": "Repita a senha",
|
||||
"inviteCodeLabel": "Código de Convite",
|
||||
"inviteCodeOptional": " (opcional — pule a espera de aprovação)",
|
||||
"inviteCodePlaceholder": "Cole seu código de convite...",
|
||||
"tokenPlaceholder": "Digite a chave de API...",
|
||||
"tokenAltPlaceholder": "Digite o token de API...",
|
||||
"signIn": "Entrar",
|
||||
"signingIn": "Entrando...",
|
||||
"register": "Registrar",
|
||||
"creatingAccount": "Criando conta...",
|
||||
"createAdminAccount": "Criar Conta de Administrador",
|
||||
"signInWithGitHub": "Entrar com GitHub",
|
||||
"signInWithSSO": "Entrar com SSO",
|
||||
"loginWithToken": "Entrar com Token",
|
||||
"showTokenLogin": "Entrar com Token de API",
|
||||
"hideTokenLogin": "Ocultar login com token",
|
||||
"noAccount": "Não tem uma conta?",
|
||||
"hasAccount": "Já tem uma conta?",
|
||||
"or": "ou",
|
||||
"errors": {
|
||||
"loginFailed": "Falha no login",
|
||||
"registrationFailed": "Falha no registro",
|
||||
"invalidToken": "Token inválido",
|
||||
"passwordsDoNotMatch": "As senhas não coincidem",
|
||||
"enterToken": "Digite um token",
|
||||
"networkError": "Erro de rede",
|
||||
"inviteRequired": "Um código de convite válido é obrigatório para registrar"
|
||||
},
|
||||
"messages": {
|
||||
"registrationPending": "Registro bem-sucedido, aguardando aprovação."
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"title": "Conta",
|
||||
"subtitle": "Perfil, credenciais e chaves de API",
|
||||
"unavailable": "Conta indisponível",
|
||||
"unavailableText": "A autenticação deve estar habilitada para gerenciar sua conta.",
|
||||
"tabs": {
|
||||
"profile": "Perfil",
|
||||
"security": "Segurança",
|
||||
"apiKeys": "Chaves de API"
|
||||
},
|
||||
"profile": {
|
||||
"displayName": "Nome de exibição",
|
||||
"displayNameDescription": "Seu nome público de exibição",
|
||||
"avatarUrl": "URL do avatar",
|
||||
"avatarUrlDescription": "URL da sua foto de perfil",
|
||||
"avatarUrlPlaceholder": "https://exemplo.com/avatar.png",
|
||||
"save": "Salvar",
|
||||
"saving": "Salvando...",
|
||||
"updated": "Perfil atualizado",
|
||||
"updateFailed": "Falha ao atualizar o perfil: {{message}}"
|
||||
},
|
||||
"security": {
|
||||
"currentPassword": "Senha atual",
|
||||
"currentPasswordDescription": "Digite sua senha existente para verificar sua identidade",
|
||||
"currentPasswordPlaceholder": "Senha atual",
|
||||
"newPassword": "Nova senha",
|
||||
"newPasswordDescription": "Deve ter pelo menos 12 caracteres",
|
||||
"newPasswordPlaceholder": "Nova senha",
|
||||
"confirmPassword": "Confirmar senha",
|
||||
"confirmPasswordDescription": "Digite novamente sua nova senha",
|
||||
"confirmPasswordPlaceholder": "Confirme a nova senha",
|
||||
"changePassword": "Alterar senha",
|
||||
"changing": "Alterando...",
|
||||
"changed": "Senha alterada",
|
||||
"passwordsDoNotMatch": "As senhas não coincidem",
|
||||
"tooShort": "A nova senha deve ter pelo menos 12 caracteres",
|
||||
"oauthOnly": "O gerenciamento de senha não está disponível para contas {{provider}}."
|
||||
},
|
||||
"apiKeys": {
|
||||
"create": "Criar chave de API",
|
||||
"createDescription": "Gere uma chave para acesso programático",
|
||||
"namePlaceholder": "Nome da chave (ex.: meu-app)",
|
||||
"createButton": "Criar",
|
||||
"creating": "Criando...",
|
||||
"createdToast": "Chave de API criada",
|
||||
"createFailed": "Falha ao criar a chave de API: {{message}}",
|
||||
"loadFailed": "Falha ao carregar as chaves de API: {{message}}",
|
||||
"revoke": "Revogar",
|
||||
"revokeKey": "Revogar chave",
|
||||
"revokeTitle": "Revogar Chave de API",
|
||||
"revokeMessage": "Revogar a chave de API \"{{name}}\"? Esta ação não pode ser desfeita.",
|
||||
"revoked": "Chave de API revogada",
|
||||
"revokeFailed": "Falha ao revogar a chave de API: {{message}}",
|
||||
"copyNow": "Copie agora — esta chave não será mostrada novamente",
|
||||
"copiedToast": "Copiado para a área de transferência",
|
||||
"copyFailed": "Falha ao copiar",
|
||||
"empty": "Nenhuma chave de API ainda. Crie uma acima para obter acesso programático.",
|
||||
"lastUsed": "último uso em {{date}}"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Página Não Encontrada",
|
||||
"text": "Parece que esta página se perdeu. Vamos colocá-lo de volta no caminho certo.",
|
||||
"goHome": "Ir para a Página Inicial"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"activity": {
|
||||
"thought": "Pensamento",
|
||||
"tool": "Ferramenta",
|
||||
"result": "Resultado",
|
||||
"toolResult": "Resultado de {{name}}",
|
||||
"thinking": "Pensando..."
|
||||
},
|
||||
"header": {
|
||||
"manageModeTooltip": "Este chat pode instalar modelos, editar configurações e gerenciar backends conversando com o LocalAI.",
|
||||
"modelInfo": "Informações do modelo",
|
||||
"chatSettings": "Configurações do chat",
|
||||
"modelInfoTitle": "Informações do Modelo: {{model}}",
|
||||
"editConfig": "Editar configuração",
|
||||
"close": "Fechar"
|
||||
},
|
||||
"modelInfo": {
|
||||
"backend": "Backend",
|
||||
"modelFile": "Arquivo do modelo",
|
||||
"contextSize": "Tamanho do contexto",
|
||||
"threads": "Threads",
|
||||
"mcp": "MCP",
|
||||
"configured": "Configurado",
|
||||
"chatTemplate": "Modelo de conversa",
|
||||
"yes": "Sim",
|
||||
"gpuLayers": "Camadas GPU"
|
||||
},
|
||||
"context": {
|
||||
"label": "Contexto: {{percent}}%",
|
||||
"labelWithTokens": "Contexto: {{percent}}% ({{tokens}} tokens)"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações do Chat",
|
||||
"manageMode": "Modo de gerenciamento",
|
||||
"manageModeDesc": "Permita que este chat instale modelos, alterne backends e edite configurações conversando com o LocalAI.",
|
||||
"systemPrompt": "Instrução do sistema",
|
||||
"systemPromptPlaceholder": "Você é um assistente útil...",
|
||||
"temperature": "Temperatura",
|
||||
"topP": "Top P",
|
||||
"topK": "Top K",
|
||||
"contextSize": "Tamanho do contexto",
|
||||
"contextSizePlaceholder": "2048",
|
||||
"clearHistory": "Limpar histórico do chat"
|
||||
},
|
||||
"empty": {
|
||||
"manageTitle": "Gerencie o LocalAI conversando",
|
||||
"manageText": "Peça para instalar modelos, alternar backends, editar configurações ou verificar o status. O assistente resumirá as ações e aguardará sua confirmação antes de alterar qualquer coisa.",
|
||||
"startTitle": "Inicie uma conversa",
|
||||
"readyText": "Pronto para conversar com {{model}}",
|
||||
"selectModelText": "Selecione um modelo acima para começar",
|
||||
"suggestionsManage": [
|
||||
"O que está instalado?",
|
||||
"Instalar um modelo de chat",
|
||||
"Mostrar o status do sistema",
|
||||
"Atualizar um backend"
|
||||
],
|
||||
"suggestionsChat": [
|
||||
"Explique como isto funciona",
|
||||
"Ajude-me a escrever código",
|
||||
"Resumir um documento",
|
||||
"Gerar ideias"
|
||||
],
|
||||
"recent": "Recente",
|
||||
"noMessages": "Nenhuma mensagem ainda",
|
||||
"hintEnter": "Enter para enviar",
|
||||
"hintShiftEnter": "Shift+Enter para nova linha",
|
||||
"hintAttach": "Anexar arquivos"
|
||||
},
|
||||
"errors": {
|
||||
"viewTraces": "Ver rastreamentos para detalhes"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copiar",
|
||||
"edit": "Editar",
|
||||
"editMessage": "Editar mensagem",
|
||||
"save": "Salvar",
|
||||
"cancel": "Cancelar",
|
||||
"regenerate": "Regenerar",
|
||||
"branch": "Ramificar daqui",
|
||||
"jumpToLatest": "Ir para a mais recente"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "Transferindo modelo...",
|
||||
"transferringTo": "Transferindo o modelo para {{node}}..."
|
||||
},
|
||||
"tokens": {
|
||||
"perSec": "{{count}} tok/s",
|
||||
"peak": "Pico: {{count}} tok/s",
|
||||
"usage": "{{prompt}}p + {{completion}}c = {{total}}"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Mensagem...",
|
||||
"attachFile": "Anexar arquivo",
|
||||
"send": "Enviar mensagem",
|
||||
"stopGenerating": "Parar a geração",
|
||||
"canvasTitle": "Canvas — extrai blocos de código e mídia para um painel lateral para visualização, cópia e download",
|
||||
"canvasLabel": "Canvas",
|
||||
"openCanvas": "Abrir painel do canvas"
|
||||
},
|
||||
"deleteAllDialog": {
|
||||
"title": "Excluir Todos os Chats",
|
||||
"message": "Excluir todos os chats? Esta ação não pode ser desfeita.",
|
||||
"confirm": "Excluir todos"
|
||||
},
|
||||
"toasts": {
|
||||
"selectModel": "Selecione um modelo",
|
||||
"copied": "Copiado para a área de transferência",
|
||||
"copyFailed": "Não foi possível copiar para a área de transferência",
|
||||
"chatCopied": "Chat copiado para a área de transferência",
|
||||
"forked": "Novo chat criado"
|
||||
},
|
||||
"menu": {
|
||||
"trigger": "Chats",
|
||||
"triggerTitle": "Conversas (Ctrl/Cmd+K)",
|
||||
"search": "Buscar conversas...",
|
||||
"clearSearch": "Limpar busca",
|
||||
"noMatch": "Nenhuma conversa corresponde à sua busca",
|
||||
"noConversations": "Nenhuma conversa ainda",
|
||||
"rename": "Renomear",
|
||||
"duplicate": "Duplicar chat",
|
||||
"copyChat": "Copiar chat",
|
||||
"exportMarkdown": "Exportar como Markdown",
|
||||
"deleteChat": "Excluir chat",
|
||||
"newChat": "Novo chat",
|
||||
"clearAll": "Limpar tudo",
|
||||
"deleteAllTitle": "Excluir todas as conversas"
|
||||
},
|
||||
"message": {
|
||||
"you": "Você"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"title": "Base de Conhecimento",
|
||||
"subtitle": "Gerencie coleções de documentos para RAG de agentes",
|
||||
"newPlaceholder": "Nome da nova coleção...",
|
||||
"actions": {
|
||||
"create": "Criar",
|
||||
"creating": "Criando...",
|
||||
"details": "Detalhes",
|
||||
"reset": "Redefinir",
|
||||
"delete": "Excluir",
|
||||
"viewDetails": "Ver detalhes",
|
||||
"resetCollection": "Redefinir coleção",
|
||||
"deleteCollection": "Excluir coleção"
|
||||
},
|
||||
"sections": {
|
||||
"yourCollections": "Suas Coleções",
|
||||
"otherUsersCollections": "Coleções de Outros Usuários"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Nenhuma coleção ainda",
|
||||
"text": "As coleções permitem organizar documentos em bases de conhecimento que os agentes podem pesquisar usando RAG (Retrieval-Augmented Generation). Crie uma coleção acima para começar.",
|
||||
"noPersonal": "Você ainda não tem coleções."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Excluir Coleção",
|
||||
"message": "Excluir a coleção \"{{name}}\"? Isso removerá todas as entradas e não pode ser desfeito.",
|
||||
"confirm": "Excluir"
|
||||
},
|
||||
"resetDialog": {
|
||||
"title": "Redefinir Coleção",
|
||||
"message": "Redefinir a coleção \"{{name}}\"? Isso removerá todas as entradas, mas manterá a coleção.",
|
||||
"confirm": "Redefinir"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Falha ao carregar coleções: {{message}}",
|
||||
"created": "Coleção \"{{name}}\" criada",
|
||||
"createFailed": "Falha ao criar a coleção: {{message}}",
|
||||
"deleted": "Coleção \"{{name}}\" excluída",
|
||||
"deleteFailed": "Falha ao excluir a coleção: {{message}}",
|
||||
"reset": "Coleção \"{{name}}\" redefinida",
|
||||
"resetFailed": "Falha ao redefinir a coleção: {{message}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"unsaved": {
|
||||
"title": "Descartar alterações não salvas?",
|
||||
"message": "Você tem alterações não salvas que serão perdidas se você sair desta página.",
|
||||
"leave": "Sair"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Salvar",
|
||||
"saving": "Salvando...",
|
||||
"cancel": "Cancelar",
|
||||
"close": "Fechar",
|
||||
"confirm": "Confirmar",
|
||||
"delete": "Excluir",
|
||||
"edit": "Editar",
|
||||
"add": "Adicionar",
|
||||
"remove": "Remover",
|
||||
"create": "Criar",
|
||||
"update": "Atualizar",
|
||||
"refresh": "Atualizar",
|
||||
"reload": "Recarregar",
|
||||
"retry": "Tentar novamente",
|
||||
"search": "Buscar",
|
||||
"filter": "Filtrar",
|
||||
"clear": "Limpar",
|
||||
"reset": "Redefinir",
|
||||
"apply": "Aplicar",
|
||||
"back": "Voltar",
|
||||
"next": "Próximo",
|
||||
"previous": "Anterior",
|
||||
"open": "Abrir",
|
||||
"submit": "Enviar",
|
||||
"select": "Selecionar",
|
||||
"selectAll": "Selecionar tudo",
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado",
|
||||
"download": "Baixar",
|
||||
"upload": "Enviar",
|
||||
"import": "Importar",
|
||||
"export": "Exportar",
|
||||
"view": "Ver",
|
||||
"details": "Detalhes",
|
||||
"settings": "Configurações",
|
||||
"help": "Ajuda",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"loading": "Carregando..."
|
||||
},
|
||||
"status": {
|
||||
"loading": "Carregando...",
|
||||
"saving": "Salvando...",
|
||||
"saved": "Salvo",
|
||||
"ready": "Pronto",
|
||||
"running": "Em execução",
|
||||
"stopped": "Parado",
|
||||
"starting": "Iniciando...",
|
||||
"stopping": "Parando...",
|
||||
"pending": "Pendente",
|
||||
"active": "Ativo",
|
||||
"inactive": "Inativo",
|
||||
"enabled": "Habilitado",
|
||||
"disabled": "Desabilitado",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"error": "Erro",
|
||||
"success": "Sucesso",
|
||||
"warning": "Aviso",
|
||||
"info": "Informação",
|
||||
"empty": "Nenhum item",
|
||||
"none": "Nenhum",
|
||||
"unknown": "Desconhecido"
|
||||
},
|
||||
"dialogs": {
|
||||
"confirmDelete": {
|
||||
"title": "Confirmar exclusão",
|
||||
"message": "Tem certeza de que deseja excluir isto? Esta ação não pode ser desfeita.",
|
||||
"confirm": "Excluir",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"unsavedChanges": {
|
||||
"title": "Alterações não salvas",
|
||||
"message": "Você tem alterações não salvas. Deseja descartá-las?",
|
||||
"discard": "Descartar",
|
||||
"keepEditing": "Continuar editando"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"required": "Obrigatório",
|
||||
"optional": "Opcional",
|
||||
"name": "Nome",
|
||||
"description": "Descrição",
|
||||
"type": "Tipo",
|
||||
"value": "Valor",
|
||||
"search": "Buscar...",
|
||||
"selectPlaceholder": "Selecione uma opção...",
|
||||
"noMatch": "Nenhuma correspondência"
|
||||
},
|
||||
"time": {
|
||||
"now": "agora",
|
||||
"secondsAgo_one": "há {{count}} segundo",
|
||||
"secondsAgo_other": "há {{count}} segundos",
|
||||
"minutesAgo_one": "há {{count}} minuto",
|
||||
"minutesAgo_other": "há {{count}} minutos",
|
||||
"hoursAgo_one": "há {{count}} hora",
|
||||
"hoursAgo_other": "há {{count}} horas",
|
||||
"daysAgo_one": "há {{count}} dia",
|
||||
"daysAgo_other": "há {{count}} dias"
|
||||
},
|
||||
"units": {
|
||||
"bytes": "B",
|
||||
"kilobytes": "KB",
|
||||
"megabytes": "MB",
|
||||
"gigabytes": "GB",
|
||||
"terabytes": "TB"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"generic": "Algo deu errado",
|
||||
"network": "Erro de rede. Verifique sua conexão e tente novamente.",
|
||||
"unauthorized": "Você não está autorizado a executar esta ação.",
|
||||
"forbidden": "Acesso negado.",
|
||||
"notFound": "O recurso solicitado não foi encontrado.",
|
||||
"serverError": "Erro no servidor. Tente novamente mais tarde.",
|
||||
"loadFailed": "Falha ao carregar: {{message}}",
|
||||
"saveFailed": "Falha ao salvar: {{message}}",
|
||||
"deleteFailed": "Falha ao excluir: {{message}}",
|
||||
"updateFailed": "Falha ao atualizar: {{message}}",
|
||||
"createFailed": "Falha ao criar: {{message}}",
|
||||
"operationFailed": "Falha na operação: {{message}}",
|
||||
"invalidInput": "Entrada inválida. Verifique o formulário e tente novamente.",
|
||||
"tryAgain": "Tente novamente.",
|
||||
"contactAdmin": "Se o problema persistir, entre em contato com o administrador."
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"cluster": {
|
||||
"vram": "VRAM do cluster",
|
||||
"ram": "RAM do cluster",
|
||||
"nodesOnline": "{{healthy}}/{{total}} nós online"
|
||||
},
|
||||
"resourceGpu": "GPU",
|
||||
"resourceRam": "RAM",
|
||||
"greeting": {
|
||||
"morning": "Bom dia",
|
||||
"afternoon": "Boa tarde",
|
||||
"evening": "Boa noite",
|
||||
"night": "Trabalhando até tarde"
|
||||
},
|
||||
"statusLine": {
|
||||
"modelsLoaded_one": "{{count}} modelo carregado",
|
||||
"modelsLoaded_other": "{{count}} modelos carregados",
|
||||
"noModelsLoaded": "Nenhum modelo carregado",
|
||||
"nodes_one": "{{count}} nó",
|
||||
"nodes_other": "{{count}} nós",
|
||||
"loadedLabel": "Carregados",
|
||||
"nodesLabel": "Nós"
|
||||
},
|
||||
"assistant": {
|
||||
"title": "Gerencie o LocalAI conversando",
|
||||
"description": "Instale modelos, alterne backends, edite configurações e verifique o status conversando com o LocalAI.",
|
||||
"open": "Abrir assistente",
|
||||
"tooltip": "Gerencie o LocalAI conversando"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Mensagem...",
|
||||
"attachImage": "Anexar imagem",
|
||||
"attachAudio": "Anexar áudio",
|
||||
"attachFile": "Anexar arquivo",
|
||||
"enterToSend": "Enter para enviar",
|
||||
"selectModelFirst": "Selecione um modelo primeiro",
|
||||
"sendMessage": "Enviar mensagem",
|
||||
"selectModelToast": "Selecione um modelo primeiro"
|
||||
},
|
||||
"quickLinks": {
|
||||
"manageByChat": "Gerenciar por chat",
|
||||
"installedModels": "Modelos Instalados",
|
||||
"browseGallery": "Explorar Galeria",
|
||||
"importModel": "Importar Modelo",
|
||||
"documentation": "Documentação"
|
||||
},
|
||||
"loadedModels": {
|
||||
"heading": "Modelos ativos",
|
||||
"count_one": "{{count}} modelo carregado",
|
||||
"count_other": "{{count}} modelos carregados",
|
||||
"stop": "Parar modelo",
|
||||
"stopAll": "Parar todos",
|
||||
"serving": "Servindo"
|
||||
},
|
||||
"stopDialog": {
|
||||
"title": "Parar Modelo",
|
||||
"message": "Parar o modelo {{model}}?",
|
||||
"confirm": "Parar {{model}}",
|
||||
"stopAllTitle": "Parar Todos os Modelos",
|
||||
"stopAllMessage": "Parar todos os {{count}} modelos carregados?",
|
||||
"stopAllConfirm": "Parar todos",
|
||||
"stoppedToast": "Modelo {{model}} parado",
|
||||
"allStoppedToast": "Todos os modelos parados",
|
||||
"stopFailed": "Falha ao parar: {{message}}"
|
||||
},
|
||||
"wizard": {
|
||||
"getStarted": "Comece com {{name}}",
|
||||
"intro": "Instale seu primeiro modelo para começar. Explore a galeria ou importe o seu próprio.",
|
||||
"steps": {
|
||||
"step1Title": "Explore a Galeria de Modelos",
|
||||
"step1Body": "Encontre o modelo certo para suas necessidades em nossa coleção selecionada.",
|
||||
"step2Title": "Instale um Modelo",
|
||||
"step2Body": "Clique em instalar para baixar e configurá-lo automaticamente.",
|
||||
"step3Title": "Comece a Conversar",
|
||||
"step3Body": "Converse com seu modelo direto do navegador ou use a API."
|
||||
},
|
||||
"browseGallery": "Explorar Galeria de Modelos",
|
||||
"importModel": "Importar Modelo",
|
||||
"docs": "Documentação",
|
||||
"noModelsTitle": "Nenhum Modelo Disponível",
|
||||
"noModelsBody": "Ainda não há modelos instalados. Peça ao administrador para configurar modelos para você poder começar a conversar."
|
||||
},
|
||||
"starters": {
|
||||
"title": "Recomendados para o seu hardware",
|
||||
"tier": {
|
||||
"cpu": "Somente CPU",
|
||||
"gpu-small": "GPU",
|
||||
"gpu-mid": "GPU",
|
||||
"gpu-large": "GPU"
|
||||
},
|
||||
"cpuNote": "Nenhuma GPU detectada — estes modelos pequenos permanecem responsivos na CPU.",
|
||||
"gpuNote": "Selecionados para caber na sua VRAM disponível com espaço para o contexto.",
|
||||
"install": "Instalar",
|
||||
"installing": "Instalando",
|
||||
"installStarted": "Instalando {{model}}…",
|
||||
"installFailed": "Falha na instalação: {{message}}"
|
||||
},
|
||||
"connect": {
|
||||
"title": "Um endpoint, toda API",
|
||||
"subtitle": "O LocalAI oferece sua própria API completa — geração de imagem e vídeo, profundidade, detecção de objetos, reranking, áudio, reconhecimento facial e de voz, e voz em tempo real via WebRTC e WebSocket. Além disso, uma camada de compatibilidade permite que qualquer aplicativo criado para OpenAI, Anthropic, Ollama ou OpenAI Responses se comunique sem alterações.",
|
||||
"nativeTitle": "API nativa",
|
||||
"compatTitle": "Compatibilidade total",
|
||||
"apiReference": "Referência completa da API",
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado",
|
||||
"browse": "Explorar a API",
|
||||
"hide": "Ocultar endpoints",
|
||||
"dismiss": "Dispensar"
|
||||
},
|
||||
"jump": {
|
||||
"heading": "Continue de onde parou",
|
||||
"discover": "Descobrir",
|
||||
"discoverSummary": "Explore a galeria e instale modelos",
|
||||
"create": "Criar",
|
||||
"createSummary": "Abra uma sessão de chat, imagem ou voz",
|
||||
"operate": "Operar",
|
||||
"operateSummary": "{{models}} modelos configurados · nós, atividade e rastreamentos"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"title": "Importar Novo Modelo",
|
||||
"subtitle": {
|
||||
"simple": "Importe um modelo de uma URI — a detecção automática escolhe o backend.",
|
||||
"powerYaml": "Escreva a configuração YAML completa do modelo.",
|
||||
"powerPrefs": "Preferências de importação refinadas."
|
||||
},
|
||||
"actions": {
|
||||
"import": "Importar Modelo",
|
||||
"importing": "Importando...",
|
||||
"create": "Criar",
|
||||
"saving": "Salvando...",
|
||||
"browseHF": "Explorar modelos no HF",
|
||||
"addCustom": "Adicionar Personalizado",
|
||||
"copy": "Copiar"
|
||||
},
|
||||
"form": {
|
||||
"modelUri": "URI do modelo",
|
||||
"uriPlaceholder": "huggingface://TheBloke/Llama-2-7B-Chat-GGUF ou https://exemplo.com/modelo.gguf",
|
||||
"uriHint": "Digite a URI ou o caminho do arquivo do modelo que deseja importar",
|
||||
"supportedFormats": "Formatos de URI Suportados",
|
||||
"options": "Opções",
|
||||
"preferences": "Preferências (Opcional)",
|
||||
"commonPreferences": "Preferências Comuns",
|
||||
"customPreferences": "Preferências Personalizadas",
|
||||
"customKeyValueHint": "Adicione pares de chave-valor personalizados para configuração avançada.",
|
||||
"preferenceKey": "Chave da preferência da linha {{index}}",
|
||||
"preferenceValue": "Valor da preferência da linha {{index}}",
|
||||
"removePref": "Remover esta preferência",
|
||||
"key": "Chave",
|
||||
"value": "Valor",
|
||||
"backend": "Backend",
|
||||
"backendAuto": "Detecção automática (com base na URI)",
|
||||
"backendLoading": "Carregando backends…",
|
||||
"backendSearch": "Buscar backends...",
|
||||
"backendHint": "Force um backend específico. Deixe vazio para detectar automaticamente pela URI. Itens marcados como \"seleção manual\" não são detectáveis automaticamente — escolha-os você mesmo se souber o que o modelo precisa.",
|
||||
"backendErrorHint": "Não foi possível carregar a lista de backends — apenas detecção automática.",
|
||||
"backendNotInstalled": "Este backend ainda não está instalado. Enviar a importação fará o download dele primeiro.",
|
||||
"modelName": "Nome do Modelo",
|
||||
"modelNamePlaceholder": "Deixe vazio para usar o nome do arquivo",
|
||||
"modelNameHint": "Nome personalizado para o modelo. Se vazio, o nome do arquivo será usado.",
|
||||
"description": "Descrição",
|
||||
"descriptionPlaceholder": "Deixe vazio para usar a descrição padrão",
|
||||
"descriptionHint": "Descrição personalizada para o modelo.",
|
||||
"quantizations": "Quantizações",
|
||||
"quantizationsPlaceholder": "q4_k_m,q4_k_s,q3_k_m (separados por vírgula)",
|
||||
"quantizationsHint": "Quantizações preferidas (separadas por vírgula). Deixe vazio para o padrão (q4_k_m).",
|
||||
"mmprojQuantizations": "Quantizações MMProj",
|
||||
"mmprojQuantizationsPlaceholder": "fp16,fp32 (separados por vírgula)",
|
||||
"mmprojQuantizationsHint": "Quantizações MMProj preferidas. Deixe vazio para o padrão (fp16).",
|
||||
"embeddings": "Embeddings",
|
||||
"embeddingsHint": "Habilite o suporte a embeddings para este modelo.",
|
||||
"modelType": "Tipo do Modelo",
|
||||
"modelTypePlaceholder": "AutoModelForCausalLM (para o backend transformers)",
|
||||
"modelTypeHint": "Tipo de modelo para o backend transformers. Exemplos: AutoModelForCausalLM, SentenceTransformer, Mamba.",
|
||||
"pipelineType": "Tipo de Pipeline",
|
||||
"pipelineTypeHint": "Tipo de pipeline para o backend diffusers.",
|
||||
"schedulerType": "Tipo de Scheduler",
|
||||
"schedulerTypePlaceholder": "k_dpmpp_2m (opcional)",
|
||||
"schedulerTypeHint": "Tipo de scheduler para o backend diffusers. Exemplos: k_dpmpp_2m, euler_a, ddim.",
|
||||
"enableParameters": "Parâmetros Habilitados",
|
||||
"enableParametersPlaceholder": "negative_prompt,num_inference_steps (separados por vírgula)",
|
||||
"enableParametersHint": "Parâmetros habilitados para o backend diffusers (separados por vírgula).",
|
||||
"cuda": "CUDA",
|
||||
"cudaHint": "Habilite o suporte a CUDA para aceleração por GPU.",
|
||||
"yamlEditor": "Editor de Configuração YAML",
|
||||
"manualPick": "seleção manual",
|
||||
"manualPickTooltip": "A detecção automática não roteará para este backend. Escolha-o aqui se você souber que é o que deseja."
|
||||
},
|
||||
"modality": {
|
||||
"text": "LLM de texto",
|
||||
"asr": "Reconhecimento de fala",
|
||||
"tts": "Texto para fala",
|
||||
"image": "Imagem / Vídeo",
|
||||
"video": "Geração de vídeo",
|
||||
"embeddings": "Embeddings",
|
||||
"reranker": "Rerankers",
|
||||
"detection": "Detecção de objetos",
|
||||
"vad": "Detecção de atividade de voz",
|
||||
"other": "Outro"
|
||||
},
|
||||
"powerTabs": {
|
||||
"ariaLabel": "Aba do modo avançado",
|
||||
"preferences": "Preferências",
|
||||
"yaml": "YAML"
|
||||
},
|
||||
"switchDialog": {
|
||||
"title": "Manter suas preferências personalizadas?",
|
||||
"body": "Alternar para o modo Simples oculta as preferências além de backend, nome e descrição. Elas ainda serão enviadas quando você importar.",
|
||||
"cancel": "Cancelar",
|
||||
"discard": "Descartar e alternar",
|
||||
"keep": "Manter e alternar"
|
||||
},
|
||||
"estimate": {
|
||||
"title": "Requisitos estimados",
|
||||
"download": "Download: {{size}}",
|
||||
"vram": "VRAM: {{vram}}"
|
||||
},
|
||||
"toasts": {
|
||||
"noUri": "Digite uma URI de modelo",
|
||||
"noYaml": "Digite a configuração YAML",
|
||||
"started": "Importação iniciada! Acompanhando o progresso...",
|
||||
"startedWithMeta": "Importação iniciada! Acompanhando o progresso... ({{meta}})",
|
||||
"imported": "Modelo importado com sucesso!",
|
||||
"importedYaml": "Configuração do modelo importada com sucesso!",
|
||||
"importFailed": "Falha na importação: {{message}}",
|
||||
"startImportFailed": "Falha ao iniciar a importação: {{message}}",
|
||||
"backendsLoadFailed": "Não foi possível carregar a lista de backends — usando apenas detecção automática",
|
||||
"modalityClearedBackend": "Seleção de backend limpa — ela não estava no grupo {{label}}.",
|
||||
"copied": "Copiado para a área de transferência"
|
||||
},
|
||||
"uriFormats": {
|
||||
"huggingface": {
|
||||
"title": "HuggingFace",
|
||||
"standard": "Formato HuggingFace padrão",
|
||||
"short": "Formato HuggingFace curto",
|
||||
"fullUrl": "URL HuggingFace completa"
|
||||
},
|
||||
"http": {
|
||||
"title": "URLs HTTP/HTTPS",
|
||||
"direct": "Download direto de qualquer URL HTTPS"
|
||||
},
|
||||
"local": {
|
||||
"title": "Arquivos Locais",
|
||||
"filePath": "Caminho de arquivo local (absoluto)",
|
||||
"directYaml": "Arquivo YAML de configuração local direto"
|
||||
},
|
||||
"oci": {
|
||||
"title": "Registro OCI",
|
||||
"registry": "Registro de contêineres OCI",
|
||||
"tarball": "Arquivo tarball OCI local"
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama",
|
||||
"model": "Formato de modelo do Ollama"
|
||||
},
|
||||
"yaml": {
|
||||
"title": "Arquivos de Configuração YAML",
|
||||
"remote": "Arquivo de configuração YAML remoto",
|
||||
"local": "Arquivo de configuração YAML local"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Imagens",
|
||||
"video": "Vídeo",
|
||||
"tts": "TTS",
|
||||
"sound": "Som",
|
||||
"transform": "Transformar",
|
||||
"threed": "3D",
|
||||
"overview": "Visão geral"
|
||||
},
|
||||
"overview": {
|
||||
"eyebrow": "{{ready}} de {{total}} modalidades prontas",
|
||||
"title": "Studio",
|
||||
"subtitle": "Gere imagens, vídeos, 3D, fala e som com os modelos desta máquina.",
|
||||
"canMake": "O que você pode criar",
|
||||
"running": "Em execução agora",
|
||||
"recent": "Saídas recentes",
|
||||
"noModel": "Nenhum modelo instalado",
|
||||
"install": "Instalar um modelo",
|
||||
"ready": "Pronto",
|
||||
"seconds": "{{seconds}}s",
|
||||
"describe": {
|
||||
"images": "Texto para imagem, imagem para imagem, imagens de referência",
|
||||
"video": "Texto para vídeo e imagem para vídeo",
|
||||
"threed": "Reconstrução de malha a partir de imagem",
|
||||
"tts": "Texto para fala usando sua biblioteca de vozes",
|
||||
"sound": "Música e efeitos sonoros a partir de um prompt",
|
||||
"transform": "Separação, aprimoramento e conversão de voz"
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"create": "Criar",
|
||||
"voice": "Voz",
|
||||
"transform": "Transformar"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Geração de Imagens",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descreva a imagem que deseja gerar...",
|
||||
"negativePrompt": "Prompt Negativo",
|
||||
"negativePromptPlaceholder": "O que evitar...",
|
||||
"size": "Tamanho",
|
||||
"count": "Quantidade (1-4)",
|
||||
"advanced": "Configurações Avançadas",
|
||||
"imageInputs": "Entradas de Imagem",
|
||||
"steps": "Etapas",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Semente",
|
||||
"seedPlaceholder": "Aleatória",
|
||||
"sourceImage": "Imagem de Origem (img2img)",
|
||||
"refImages": "Imagens de Referência",
|
||||
"refImagesAdded_one": "{{count}} imagem adicionada",
|
||||
"refImagesAdded_other": "{{count}} imagens adicionadas"
|
||||
},
|
||||
"actions": {
|
||||
"view": "Ver",
|
||||
"generate": "Gerar",
|
||||
"generating": "Gerando..."
|
||||
},
|
||||
"empty": "As imagens geradas aparecerão aqui",
|
||||
"toasts": {
|
||||
"noPrompt": "Digite um prompt",
|
||||
"noModel": "Selecione um modelo",
|
||||
"noResults": "Nenhuma imagem gerada"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Geração de Vídeo",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descreva o vídeo que deseja gerar...",
|
||||
"duration": "Duração (segundos)",
|
||||
"fps": "FPS",
|
||||
"size": "Tamanho",
|
||||
"advanced": "Configurações Avançadas",
|
||||
"seed": "Semente",
|
||||
"seedPlaceholder": "Aleatória",
|
||||
"frames": "Quadros",
|
||||
"referenceMedia": "Mídia de referência",
|
||||
"startImage": "Imagem inicial",
|
||||
"endImage": "Imagem final",
|
||||
"avatarAudio": "Áudio do avatar"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Gerar",
|
||||
"generating": "Gerando..."
|
||||
},
|
||||
"empty": "O vídeo gerado aparecerá aqui",
|
||||
"toasts": {
|
||||
"noPrompt": "Digite um prompt",
|
||||
"noModel": "Selecione um modelo",
|
||||
"noResults": "Nenhum vídeo gerado"
|
||||
}
|
||||
},
|
||||
"threed": {
|
||||
"title": "Geração 3D",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"image": "Imagem de entrada",
|
||||
"quality": "Qualidade",
|
||||
"quality_auto": "Automática (melhor disponível)",
|
||||
"quality_coarse": "Pré-visualização grosseira (rápida)",
|
||||
"quality_512": "512³ fino",
|
||||
"quality_1024": "Cascata 1024³ (lenta)",
|
||||
"background": "Fundo",
|
||||
"background_auto": "Remover automaticamente o fundo sólido",
|
||||
"background_keep": "Manter original",
|
||||
"background_black": "Remover preto",
|
||||
"background_white": "Remover branco",
|
||||
"advanced": "Configurações Avançadas",
|
||||
"steps": "Etapas de forma",
|
||||
"textureSteps": "Etapas de material",
|
||||
"guidance": "Orientação",
|
||||
"seed": "Semente",
|
||||
"seedPlaceholder": "Aleatória"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Gerar",
|
||||
"generating": "Gerando...",
|
||||
"remesh": "Aplicar remalhamento",
|
||||
"remeshing": "Aplicando remalhamento...",
|
||||
"showOriginal": "Mostrar original",
|
||||
"download": "Baixar GLB"
|
||||
},
|
||||
"remesh": {
|
||||
"title": "Remalhamento hermético para impressão",
|
||||
"detail": "Detalhe do remalhamento",
|
||||
"coarser": "Mais grosseiro · mais rápido",
|
||||
"finer": "Mais fino · mais lento",
|
||||
"hint": "O detalhe controla as menores características preservadas. O deslocamento de envolvimento segue automaticamente.",
|
||||
"ready": "Pré-visualizando o modelo remalhado hermético. Esta é a versão que será baixada."
|
||||
},
|
||||
"viewer": {
|
||||
"wireframe": "Wireframe",
|
||||
"autoRotate": "Rotação automática",
|
||||
"noWebgl": "WebGL2 não está disponível neste navegador — baixe o GLB para vê-lo em outro lugar.",
|
||||
"contextLost": "A visualização 3D perdeu o contexto da GPU — recarregue a página para restaurá-la.",
|
||||
"stats": "{{verts}} vértices · {{tris}} triângulos",
|
||||
"hint": "arrastar: girar · pinça/roda: zoom · dois dedos/clique direito: mover · duplo clique: redefinir"
|
||||
},
|
||||
"empty": "O modelo 3D gerado aparecerá aqui",
|
||||
"toasts": {
|
||||
"noImage": "Forneça uma imagem de entrada",
|
||||
"noModel": "Selecione um modelo",
|
||||
"noResults": "Nenhum modelo gerado"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Texto para Fala",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"voice": "Voz",
|
||||
"voicePlaceholder": "Falante opcional ou ID de voz",
|
||||
"input": "Texto",
|
||||
"inputPlaceholder": "Digite o texto para sintetizar..."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Gerar",
|
||||
"generating": "Gerando..."
|
||||
},
|
||||
"empty": "O áudio gerado aparecerá aqui",
|
||||
"toasts": {
|
||||
"noText": "Digite o texto",
|
||||
"noModel": "Selecione um modelo",
|
||||
"generated": "Fala gerada",
|
||||
"generateFailed": "Falha na geração"
|
||||
},
|
||||
"voiceLibrary": {
|
||||
"cloningReady": "Clonagem de voz",
|
||||
"loading": "Carregando vozes salvas…",
|
||||
"modelDefault": "Usar padrão do modelo",
|
||||
"empty": "Nenhuma voz salva ainda.",
|
||||
"create": "Criar uma",
|
||||
"manage": "Gerenciar Biblioteca de Vozes",
|
||||
"namedVoiceHint": "Este modelo não aceita perfis de áudio de referência. Você ainda pode digitar um falante nomeado suportado pelo seu backend."
|
||||
}
|
||||
},
|
||||
"voiceLibrary": {
|
||||
"title": "Biblioteca de Vozes",
|
||||
"subtitle": "Crie e gerencie vozes de referência reutilizáveis para cada modelo instalado que suporta clonagem de voz.",
|
||||
"loading": "Carregando vozes salvas…",
|
||||
"listLabel": "Perfis de voz salvos",
|
||||
"status": {
|
||||
"ready": "Pronto"
|
||||
},
|
||||
"summary": {
|
||||
"label": "Status da biblioteca de vozes",
|
||||
"profiles": "vozes salvas",
|
||||
"modelsReady_one": "{{count}} modelo compatível pronto",
|
||||
"modelsReady_other": "{{count}} modelos compatíveis prontos",
|
||||
"noModels": "Nenhum modelo compatível instalado"
|
||||
},
|
||||
"search": {
|
||||
"label": "Buscar perfis de voz",
|
||||
"placeholder": "Buscar vozes, idiomas ou transcrições"
|
||||
},
|
||||
"filters": {
|
||||
"language": "Filtrar por idioma",
|
||||
"allLanguages": "Todos os idiomas"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Sua biblioteca de vozes está vazia",
|
||||
"body": "Grave ou envie um clipe de referência autorizado, adicione a transcrição exata e reuse-o nos modelos de TTS compatíveis."
|
||||
},
|
||||
"noResults": {
|
||||
"title": "Nenhuma voz corresponde a estes filtros",
|
||||
"body": "Tente uma busca diferente ou mostre todos os idiomas."
|
||||
},
|
||||
"detail": {
|
||||
"eyebrow": "Voz selecionada",
|
||||
"emptyTitle": "Selecione uma voz para inspecioná-la",
|
||||
"emptyBody": "Áudio de referência, transcrição, compatibilidade e detalhes de consentimento aparecerão aqui.",
|
||||
"referenceAudio": "Áudio de referência",
|
||||
"transcript": "Transcrição de referência"
|
||||
},
|
||||
"metadata": {
|
||||
"language": "Idioma",
|
||||
"languageUnknown": "Não especificado",
|
||||
"duration": "Duração",
|
||||
"sampleRate": "Taxa de amostragem",
|
||||
"created": "Criado em"
|
||||
},
|
||||
"consent": {
|
||||
"confirmed": "Consentimento confirmado",
|
||||
"confirmedAt": "Confirmado em {{date}}"
|
||||
},
|
||||
"modelSetup": {
|
||||
"title": "Instalar um modelo de clonagem de voz",
|
||||
"body": "Esta voz está pronta, mas precisa de um modelo que aceite áudio de referência. Escolha um verificado por este servidor.",
|
||||
"loading": "Encontrando modelos compatíveis em suas galerias…",
|
||||
"backendUnknown": "Backend selecionado durante a instalação",
|
||||
"install": "Instalar",
|
||||
"installing": "Instalando…",
|
||||
"loadFailed": "Recomendações compatíveis não puderam ser carregadas.",
|
||||
"noneAvailable": "Nenhum modelo compatível não instalado está disponível nas galerias configuradas.",
|
||||
"capabilityNote": "As recomendações vêm dos metadados de capacidade do modelo, não de uma lista de modelos do lado do navegador."
|
||||
},
|
||||
"api": {
|
||||
"title": "Uso da API e modelos compatíveis",
|
||||
"summary": "Use esta voz fora do navegador ou verifique o suporte do modelo",
|
||||
"body": "Envie a URI de voz estável do perfil com cada requisição de fala. O LocalAI resolve o áudio de referência privado e a transcrição no momento da geração; o YAML do modelo não é modificado.",
|
||||
"compatibleModels": "Modelos compatíveis instalados",
|
||||
"noInstalledModels": "Nenhum ainda. Instale um dos modelos verificados pelo servidor acima antes de enviar esta requisição.",
|
||||
"curlExample": "Exemplo com cURL",
|
||||
"copy": "Copiar requisição",
|
||||
"endpointNote": "Os mesmos campos de modelo, entrada e voz também funcionam com POST /tts."
|
||||
},
|
||||
"actions": {
|
||||
"create": "Criar voz",
|
||||
"createFirst": "Crie sua primeira voz",
|
||||
"retry": "Tentar novamente",
|
||||
"clearFilters": "Limpar filtros",
|
||||
"useInTTS": "Usar em Texto para Fala",
|
||||
"delete": "Excluir voz",
|
||||
"installModel": "Instalar um modelo compatível",
|
||||
"browseModels": "Explorar todos os modelos"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Excluir esta voz?",
|
||||
"message": "“{{name}}” e seu áudio de referência serão removidos permanentemente. O YAML dos modelos existentes não é alterado.",
|
||||
"deleting": "Excluindo…"
|
||||
},
|
||||
"toasts": {
|
||||
"deleted": "Voz {{name}} excluída",
|
||||
"installStarted": "Instalando {{name}}. O progresso está disponível em Operações.",
|
||||
"installFailed": "Não foi possível iniciar a instalação: {{message}}",
|
||||
"apiCopied": "Requisição de API copiada",
|
||||
"copyFailed": "Não foi possível copiar a requisição de API"
|
||||
}
|
||||
},
|
||||
"voiceCreate": {
|
||||
"eyebrow": "Biblioteca de Vozes",
|
||||
"title": "Criar uma voz reutilizável",
|
||||
"subtitle": "Adicione um clipe de referência limpo e sua transcrição exata. O LocalAI cuida dos parâmetros de clonagem específicos do backend no momento da geração.",
|
||||
"sections": {
|
||||
"reference": {
|
||||
"title": "Áudio de referência",
|
||||
"body": "Grave no navegador ou envie um clipe existente. A fala deve ser limpa, natural e de um único falante."
|
||||
},
|
||||
"details": {
|
||||
"title": "Identidade e transcrição",
|
||||
"body": "Dê aos administradores um nome reconhecível e transcreva o clipe exatamente, incluindo hesitações e pontuação."
|
||||
},
|
||||
"consent": {
|
||||
"title": "Autorização",
|
||||
"body": "A clonagem de voz pode ser sensível. Confirme a permissão antes que esta referência seja salva."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"label": "Referência de voz",
|
||||
"normalizing": "Preparando um PCM WAV seguro…",
|
||||
"preview": "Referência normalizada",
|
||||
"durationError": "O áudio de referência deve ter entre 1 segundo e 2 minutos.",
|
||||
"decodeError": "Este navegador não conseguiu decodificar o arquivo de áudio selecionado.",
|
||||
"qualityReady": "Duração recomendada",
|
||||
"qualityHint": "6–30 segundos geralmente produzem um clone mais forte"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Nome da voz",
|
||||
"namePlaceholder": "ex.: Narrador de documentário",
|
||||
"language": "Idioma (opcional)",
|
||||
"languagePlaceholder": "ex.: pt-BR",
|
||||
"description": "Descrição (opcional)",
|
||||
"descriptionPlaceholder": "Tom, origem ou uso pretendido",
|
||||
"transcript": "Transcrição exata",
|
||||
"transcriptPlaceholder": "Digite cada palavra falada no clipe de referência…",
|
||||
"transcriptHint": "Corresponda à gravação exatamente. Não reescreva a gramática nem remova pausas e hesitações."
|
||||
},
|
||||
"consent": {
|
||||
"title": "Confirmo que esta voz pode ser clonada",
|
||||
"body": "Tenho a permissão do falante ou outra base legal para armazenar e usar esta gravação para fala sintetizada."
|
||||
},
|
||||
"readiness": {
|
||||
"title": "Prontidão do perfil",
|
||||
"body": "Uma voz fica disponível em Texto para Fala quando todos os itens obrigatórios estão completos.",
|
||||
"audio": "Referência PCM-WAV válida",
|
||||
"quality": "Janela de qualidade de 6–30 segundos",
|
||||
"name": "Nome de voz reconhecível",
|
||||
"transcript": "Transcrição de referência exata",
|
||||
"consent": "Autorização confirmada"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privada por padrão. ",
|
||||
"body": "O áudio de referência é armazenado no diretório de dados do LocalAI com permissões de arquivo restritas e nunca é exposto como um caminho de pasta de modelo."
|
||||
},
|
||||
"actions": {
|
||||
"back": "Voltar para a biblioteca",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Salvar voz",
|
||||
"saving": "Salvando…"
|
||||
},
|
||||
"toasts": {
|
||||
"created": "{{name}} está pronta para uso"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Geração de Som",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descreva o som que deseja gerar...",
|
||||
"instrumental": "Instrumental",
|
||||
"bpm": "BPM",
|
||||
"duration": "Duração (segundos)",
|
||||
"language": "Idioma",
|
||||
"timesignature": "Fórmula de Compasso",
|
||||
"keyscale": "Tom/Escala",
|
||||
"vocalLanguage": "Idioma vocal",
|
||||
"vocalLanguagePlaceholder": "ex.: Português",
|
||||
"caption": "Legenda",
|
||||
"lyrics": "Letra",
|
||||
"lyricsPlaceholder": "Letra para geração vocal",
|
||||
"simple": "Simples",
|
||||
"advanced": "Avançado",
|
||||
"seed": "Semente",
|
||||
"seedPlaceholder": "Aleatória",
|
||||
"thinkMode": "Modo de pensamento"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Gerar",
|
||||
"generating": "Gerando..."
|
||||
},
|
||||
"empty": "O áudio gerado aparecerá aqui",
|
||||
"toasts": {
|
||||
"noPrompt": "Digite um prompt",
|
||||
"noModel": "Selecione um modelo",
|
||||
"generateFailed": "Falha na geração"
|
||||
}
|
||||
},
|
||||
"audioTransform": {
|
||||
"title": "Transformação de Áudio",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"audio": "Áudio (obrigatório)",
|
||||
"reference": "Referência (opcional)",
|
||||
"referenceHelp": "Sinal de loopback / extremidade distante para cancelamento de eco, falante alvo para conversão de voz. Deixe vazio para transformação incondicional.",
|
||||
"advancedParameters": "Parâmetros avançados",
|
||||
"advancedParametersHelp": "específicos do backend (uma chave=valor por linha, ex.: noise_gate=true)",
|
||||
"advancedParametersPlaceholder": "# Opcional. Para LocalVQE:\n# noise_gate=true\n# noise_gate_threshold_dbfs=-50"
|
||||
},
|
||||
"input": {
|
||||
"upload": "Enviar",
|
||||
"uploadDescription": "Solte um arquivo aqui ou",
|
||||
"uploadBrowse": "navegue",
|
||||
"record": "Gravar",
|
||||
"startRecording": "Iniciar gravação",
|
||||
"stop": "Parar",
|
||||
"encoding": "Codificando...",
|
||||
"clear": "Limpar",
|
||||
"microphoneUnavailable": "A captura de microfone não está disponível neste navegador.",
|
||||
"echoNotice": "Os navegadores costumam aplicar seu próprio cancelamento de eco WebRTC e supressão de ruído por padrão. Isso geralmente resulta em desempenho pior do que executar o LocalVQE no áudio bruto.",
|
||||
"echoTest": "Teste de eco (grave o microfone enquanto reproduz a referência)",
|
||||
"stopEchoTest": "Parar teste de eco"
|
||||
},
|
||||
"result": {
|
||||
"audio": "Áudio",
|
||||
"reference": "Referência",
|
||||
"inputSpectrum": "Espectro de entrada",
|
||||
"outputSpectrum": "Espectro de saída",
|
||||
"outputSpectrumHint": "Transforme para comparar a atenuação",
|
||||
"output": "Saída"
|
||||
},
|
||||
"actions": {
|
||||
"transform": "Transformar",
|
||||
"processing": "Processando..."
|
||||
},
|
||||
"empty": "Escolha um arquivo de áudio (e referência opcional) para transformar"
|
||||
},
|
||||
"talk": {
|
||||
"title": "Falar",
|
||||
"subtitle": "Conversa de voz em tempo real",
|
||||
"actions": {
|
||||
"start": "Iniciar sessão",
|
||||
"stop": "Encerrar sessão",
|
||||
"connecting": "Conectando...",
|
||||
"muted": "Mudo",
|
||||
"mute": "Silenciar",
|
||||
"unmute": "Reativar som"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"voice": "Voz",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Idioma",
|
||||
"languagePlaceholder": "pt",
|
||||
"instructions": "Instruções",
|
||||
"instructionsPlaceholder": "Defina a persona do assistente..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Ocioso",
|
||||
"connecting": "Conectando...",
|
||||
"listening": "Ouvindo...",
|
||||
"speaking": "Falando...",
|
||||
"ended": "Sessão encerrada"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Selecione um modelo primeiro",
|
||||
"connectFailed": "Falha ao conectar: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "Histórico",
|
||||
"empty": "Nenhum histórico ainda",
|
||||
"deleteEntry": "Excluir entrada",
|
||||
"clear": "Limpar histórico",
|
||||
"clearTitle": "Limpar todo o histórico",
|
||||
"clearMessage": "Remover todas as entradas do histórico? Esta ação não pode ser desfeita.",
|
||||
"clearConfirm": "Limpar",
|
||||
"cleared": "Histórico limpo"
|
||||
},
|
||||
"request": {
|
||||
"heading": "Requisição",
|
||||
"copyCurl": "Copiar como curl",
|
||||
"copied": "Copiado"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"title": {
|
||||
"add": "Adicionar Modelo",
|
||||
"edit": "Editor de Modelo"
|
||||
},
|
||||
"subtitle": {
|
||||
"chooseModelType": "Escolha um tipo de modelo para começar",
|
||||
"newModel": "Novo modelo"
|
||||
},
|
||||
"actions": {
|
||||
"backTo": "Voltar para {{page}}",
|
||||
"system": "Sistema",
|
||||
"templates": "Modelos",
|
||||
"createModel": "Criar Modelo",
|
||||
"saveChanges": "Salvar Alterações",
|
||||
"saving": "Salvando...",
|
||||
"saved": "Salvo",
|
||||
"switchWarning": "Salve ou descarte as alterações antes de alternar as abas.",
|
||||
"discardAndSwitch": "Descartar e Alternar"
|
||||
},
|
||||
"tabs": {
|
||||
"interactive": "Interativo",
|
||||
"yaml": "YAML",
|
||||
"yamlDescription": "Edite o YAML diretamente. O nome do modelo deve ser definido no YAML para que a criação funcione."
|
||||
},
|
||||
"forms": {
|
||||
"modelName": {
|
||||
"label": "Nome do Modelo",
|
||||
"placeholder": "meu-nome-de-modelo",
|
||||
"hint": "Use apenas letras, números, hifens, sublinhados e pontos."
|
||||
},
|
||||
"empty": {
|
||||
"nav": "Use a barra de busca acima para adicionar campos",
|
||||
"title": "Nenhum campo configurado",
|
||||
"text": "Use a barra de busca acima para encontrar e adicionar campos de configuração."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"title": "Descobrir",
|
||||
"subtitle": "Explore e instale modelos de IA da galeria",
|
||||
"models": "Modelos",
|
||||
"recommended": {
|
||||
"title": "Recomendados para o seu hardware",
|
||||
"cpuNote": "Nenhuma GPU detectada - modelos pequenos que permanecem responsivos na CPU.",
|
||||
"gpuNote": "Dimensionados para caber na sua VRAM disponível com espaço para o contexto.",
|
||||
"install": "Instalar",
|
||||
"installing": "Instalando",
|
||||
"installStarted": "Instalando {{model}}…",
|
||||
"installFailed": "Falha na instalação: {{message}}",
|
||||
"dismiss": "Dispensar recomendações",
|
||||
"summary": "{{n}} modelos sugeridos",
|
||||
"bestFit": "Melhor ajuste",
|
||||
"alternative": "Também serve"
|
||||
},
|
||||
"stats": {
|
||||
"available": "Disponíveis",
|
||||
"installed": "Instalados"
|
||||
},
|
||||
"actions": {
|
||||
"addModel": "Adicionar Modelo",
|
||||
"importModel": "Importar Modelo",
|
||||
"install": "Instalar",
|
||||
"reinstall": "Reinstalar",
|
||||
"delete": "Excluir"
|
||||
},
|
||||
"filters": {
|
||||
"all": "Todos",
|
||||
"llm": "Chat",
|
||||
"image": "Imagem",
|
||||
"video": "Vídeo",
|
||||
"threed": "3D",
|
||||
"multimodal": "Multimodal",
|
||||
"vision": "Visão",
|
||||
"tts": "TTS",
|
||||
"stt": "STT",
|
||||
"diarization": "Diarização",
|
||||
"soundClassification": "Classificação de Som",
|
||||
"soundGen": "Som",
|
||||
"audioTransform": "FX de Áudio",
|
||||
"realtimeAudio": "Áudio em Tempo Real",
|
||||
"embedding": "Embeddings",
|
||||
"rerank": "Rerank",
|
||||
"detection": "Detecção",
|
||||
"vad": "VAD",
|
||||
"ner": "NER",
|
||||
"fitsGpu": "Cabe na GPU",
|
||||
"collapseVariants": "Uma linha por modelo",
|
||||
"allBackends": "Todos os Backends",
|
||||
"searchBackends": "Buscar backends...",
|
||||
"contextSize": "Contexto:",
|
||||
"useCaseLabel": "Filtrar por caso de uso",
|
||||
"unavailableForBackend": "Não disponível para o backend selecionado",
|
||||
"someSelected": "{{count}} selecionados",
|
||||
"refineLabel": "Refinar"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar modelos...",
|
||||
"clearFilters": "Limpar filtros"
|
||||
},
|
||||
"table": {
|
||||
"modelName": "Nome do Modelo",
|
||||
"description": "Descrição",
|
||||
"backend": "Backend",
|
||||
"sizeVram": "Tamanho / VRAM",
|
||||
"status": "Status",
|
||||
"actions": "Ações",
|
||||
"size": "Tamanho: {{size}}",
|
||||
"vram": "VRAM: {{vram}}",
|
||||
"fits": "Cabe",
|
||||
"mayNotFit": "Pode não caber",
|
||||
"trustRemoteCode": "Confiar em Código Remoto",
|
||||
"installing": "Instalando",
|
||||
"installingPct": "Instalando · {{percent}}%",
|
||||
"installed": "Instalado",
|
||||
"notInstalled": "Não Instalado"
|
||||
},
|
||||
"detail": {
|
||||
"description": "Descrição",
|
||||
"gallery": "Galeria",
|
||||
"backend": "Backend",
|
||||
"size": "Tamanho",
|
||||
"vram": "VRAM",
|
||||
"license": "Licença",
|
||||
"tags": "Tags",
|
||||
"links": "Links",
|
||||
"warning": "Aviso",
|
||||
"files": "Arquivos",
|
||||
"fitsGpu": "Cabe na GPU",
|
||||
"mayNotFitGpu": "Pode não caber na GPU",
|
||||
"requiresTrustRemoteCode": "Requer Confiança em Código Remoto",
|
||||
"fileCount_one": "{{count}} arquivo",
|
||||
"fileCount_other": "{{count}} arquivos",
|
||||
"filename": "Nome do arquivo",
|
||||
"uri": "URI",
|
||||
"sha256": "SHA256",
|
||||
"backToAll": "Todos os modelos",
|
||||
"vramAt": "VRAM em {{context}}",
|
||||
"headroom": "Margem de sobra"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Nenhum modelo encontrado",
|
||||
"withFilters": "Nenhum modelo corresponde à sua busca ou filtros atuais.",
|
||||
"collapsedVariantsHint": "Construções alternativas que outra entrada já oferece estão ocultas. Desative \"Uma linha por modelo\" para incluí-las.",
|
||||
"noFilters": "A galeria de modelos está vazia."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Excluir Modelo",
|
||||
"message": "Excluir o modelo {{model}}?",
|
||||
"confirm": "Excluir {{model}}",
|
||||
"deletingToast": "Excluindo {{model}}..."
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "Falha ao carregar modelos: {{message}}",
|
||||
"installFailed": "Falha na instalação: {{message}}",
|
||||
"deleteFailed": "Falha ao excluir: {{message}}"
|
||||
},
|
||||
"selector": {
|
||||
"loading": "Carregando modelos...",
|
||||
"selectModel": "Selecionar modelo...",
|
||||
"searchPlaceholder": "Buscar modelos...",
|
||||
"noModels": "Nenhum modelo disponível"
|
||||
},
|
||||
"variants": {
|
||||
"title": "Variantes",
|
||||
"chooseVariant": "Escolha uma variante",
|
||||
"auto": "Automática",
|
||||
"autoSelected": "Selecionada automaticamente",
|
||||
"base": "Construção base",
|
||||
"doesNotFit": "Não cabe",
|
||||
"unknownSize": "Tamanho desconhecido",
|
||||
"unknownBackend": "Backend desconhecido",
|
||||
"loading": "Carregando variantes...",
|
||||
"installVariant": "Instalar {{variant}}",
|
||||
"quantizationTitle": "Formato dos pesos",
|
||||
"unknownQuantization": "Formato desconhecido",
|
||||
"showDetails": "Mostrar detalhes completos de {{variant}}",
|
||||
"hideDetails": "Ocultar detalhes completos de {{variant}}",
|
||||
"detailsLoading": "Carregando detalhes...",
|
||||
"detailsUnavailable": "Os detalhes de {{variant}} não puderam ser carregados.",
|
||||
"features": {
|
||||
"dflash": "Mais rápido: DFlash",
|
||||
"mtp": "Mais rápido: MTP"
|
||||
}
|
||||
},
|
||||
"rail": {
|
||||
"sortLabel": "Ordenar modelos",
|
||||
"downloadingPct": "baixando {{percent}}%",
|
||||
"tooLarge": "{{size}} · muito grande",
|
||||
"fitsSize": "{{size}} · cabe",
|
||||
"previousPage": "Página anterior",
|
||||
"nextPage": "Próxima página",
|
||||
"showingCount": "{{shown}} de {{total}}",
|
||||
"sizing": "medindo…"
|
||||
},
|
||||
"groups": {
|
||||
"text": "Texto e raciocínio",
|
||||
"vision": "Visão",
|
||||
"audio": "Fala e áudio",
|
||||
"visual": "Imagem e vídeo",
|
||||
"other": "Todo o resto"
|
||||
},
|
||||
"chart": {
|
||||
"title": "VRAM por tamanho de contexto",
|
||||
"available": "{{vram}} disponíveis",
|
||||
"barTitle": "o contexto {{context}} precisa de {{vram}}",
|
||||
"fitsEverywhere": "Executa em qualquer tamanho de contexto neste host.",
|
||||
"fitsNowhere": "Não caberá neste host em nenhum tamanho de contexto.",
|
||||
"fitsUpTo": "Cabe em contextos de até {{context}}."
|
||||
},
|
||||
"shelves": {
|
||||
"hostLabel": "Seu host",
|
||||
"heroWithGpu": "{{vram}} de memória GPU, {{count}} modelos na galeria.",
|
||||
"heroNoGpu": "{{count}} modelos na galeria.",
|
||||
"heroHint": "Escolha qualquer item à esquerda para ver seu tamanho, variantes e se ele executará aqui.",
|
||||
"browsing": "Explorando",
|
||||
"pickHint": "Selecione um modelo para ver seus detalhes.",
|
||||
"heroWithRam": "{{ram}} de memória do sistema, {{count}} modelos na galeria.",
|
||||
"byUseCase": "Ou comece por um caso de uso",
|
||||
"pickText": "Chat, raciocínio, embeddings",
|
||||
"pickVision": "Leia imagens e documentos",
|
||||
"pickAudio": "Fala de entrada e saída",
|
||||
"pickVisual": "Gere imagens e vídeos"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"appName": "LocalAI",
|
||||
"openMenu": "Abrir menu",
|
||||
"closeMenu": "Fechar menu",
|
||||
"primaryNavigation": "Navegação principal",
|
||||
"switchToLightMode": "Alternar para o modo claro",
|
||||
"switchToDarkMode": "Alternar para o modo escuro",
|
||||
"expandSidebar": "Expandir barra lateral",
|
||||
"collapseSidebar": "Recolher barra lateral",
|
||||
"changeLanguage": "Alterar idioma",
|
||||
"logout": "Sair",
|
||||
"accountSettings": "Configurações da conta",
|
||||
"account": "Conta",
|
||||
"accountFor": "Conta: {{name}}",
|
||||
"sections": {
|
||||
"create": "Criar",
|
||||
"recognition": "Reconhecimento",
|
||||
"build": "Construir",
|
||||
"operate": "Operar"
|
||||
},
|
||||
"operate": {
|
||||
"inference": "Inferência",
|
||||
"cluster": "Cluster",
|
||||
"observability": "Observabilidade",
|
||||
"access": "Acesso",
|
||||
"system": "Sistema",
|
||||
"activity": "Atividade",
|
||||
"runtime": "Runtime",
|
||||
"administration": "Administração"
|
||||
},
|
||||
"items": {
|
||||
"home": "Início",
|
||||
"discover": "Descobrir",
|
||||
"chat": "Chat",
|
||||
"studio": "Studio",
|
||||
"talk": "Falar",
|
||||
"fineTune": "Ajuste Fino",
|
||||
"quantize": "Quantizar",
|
||||
"faces": "Rostos",
|
||||
"voices": "Vozes",
|
||||
"jobs": "Trabalhos",
|
||||
"operate": "Admin",
|
||||
"host": "Host",
|
||||
"audioTransform": "Transformação de Áudio",
|
||||
"faceRecognition": "Reconhecimento Facial",
|
||||
"voiceRecognition": "Reconhecimento de Voz",
|
||||
"agents": "Agentes",
|
||||
"skills": "Habilidades",
|
||||
"memory": "Memória",
|
||||
"mcpJobs": "Trabalhos de CI MCP",
|
||||
"usage": "Uso",
|
||||
"users": "Usuários",
|
||||
"middleware": "Middleware",
|
||||
"backends": "Backends",
|
||||
"voiceLibrary": "Biblioteca de Vozes",
|
||||
"traces": "Rastreamentos",
|
||||
"nodes": "Nós",
|
||||
"scheduling": "Agendamento",
|
||||
"swarm": "Swarm",
|
||||
"system": "Sistema",
|
||||
"settings": "Configurações",
|
||||
"api": "API",
|
||||
"activity": "Atividade",
|
||||
"overview": "Visão geral"
|
||||
},
|
||||
"footer": {
|
||||
"github": "GitHub",
|
||||
"documentation": "Documentação",
|
||||
"author": "Autor",
|
||||
"copyright": "© 2023-{{year}} {{author}}"
|
||||
},
|
||||
"console": {
|
||||
"automation": "Automação",
|
||||
"training": "Treinamento",
|
||||
"expandNavigation": "Expandir navegação de {{section}}",
|
||||
"collapseNavigation": "Recolher navegação de {{section}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"title": "Habilidades",
|
||||
"subtitle": "Gerenciar habilidades de agentes (instruções e recursos reutilizáveis)",
|
||||
"unavailable": {
|
||||
"subtitle": "O serviço de habilidades não está disponível ou o índice está sendo reconstruído. Tente novamente em um momento.",
|
||||
"retry": "Tentar novamente"
|
||||
},
|
||||
"actions": {
|
||||
"newSkill": "Nova habilidade",
|
||||
"createSkill": "Criar habilidade",
|
||||
"import": "Importar",
|
||||
"importing": "Importando...",
|
||||
"gitRepos": "Repositórios Git",
|
||||
"edit": "Editar",
|
||||
"delete": "Excluir",
|
||||
"export": "Exportar",
|
||||
"sync": "Sincronizar",
|
||||
"addRepo": "Adicionar repositório",
|
||||
"adding": "Adicionando...",
|
||||
"remove": "Remover",
|
||||
"enable": "Habilitar",
|
||||
"disable": "Desabilitar"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar habilidades..."
|
||||
},
|
||||
"git": {
|
||||
"title": "Repositórios Git",
|
||||
"description": "Adicione repositórios Git para obter habilidades. As habilidades aparecerão na lista após a sincronização.",
|
||||
"urlPlaceholder": "https://github.com/usuário/repo ou git@github.com:usuário/repo.git",
|
||||
"noRepos": "Nenhum repositório Git configurado. Adicione um acima.",
|
||||
"disabled": "Desabilitado",
|
||||
"removeRepo": "Remover repositório"
|
||||
},
|
||||
"card": {
|
||||
"noDescription": "Sem descrição",
|
||||
"readOnly": "Somente leitura",
|
||||
"editTitle": "Editar habilidade",
|
||||
"deleteTitle": "Excluir habilidade",
|
||||
"exportTitle": "Exportar como .tar.gz"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Nenhuma habilidade encontrada",
|
||||
"text": "Crie uma habilidade ou importe uma para começar.",
|
||||
"noPersonal": "Você ainda não tem habilidades."
|
||||
},
|
||||
"sections": {
|
||||
"yourSkills": "Suas Habilidades",
|
||||
"otherUsersSkills": "Habilidades de Outros Usuários"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Excluir Habilidade",
|
||||
"message": "Excluir a habilidade \"{{name}}\"? Esta ação não pode ser desfeita.",
|
||||
"confirm": "Excluir"
|
||||
},
|
||||
"removeRepoDialog": {
|
||||
"title": "Remover Repositório Git",
|
||||
"message": "Remover este repositório Git? As habilidades dele não estarão mais disponíveis.",
|
||||
"confirm": "Remover"
|
||||
},
|
||||
"toasts": {
|
||||
"loadFailed": "Falha ao carregar habilidades",
|
||||
"deleted": "Habilidade \"{{name}}\" excluída",
|
||||
"deleteFailed": "Falha ao excluir a habilidade",
|
||||
"exported": "Habilidade \"{{name}}\" exportada",
|
||||
"exportFailed": "Falha na exportação",
|
||||
"imported": "Habilidade importada de \"{{file}}\"",
|
||||
"importFailed": "Falha na importação",
|
||||
"loadReposFailed": "Falha ao carregar repositórios Git",
|
||||
"repoAdded": "Repositório Git adicionado e sincronizando",
|
||||
"addRepoFailed": "Falha ao adicionar o repositório",
|
||||
"synced": "Repositório sincronizado",
|
||||
"syncFailed": "Falha na sincronização",
|
||||
"toggled": "Repositório alternado",
|
||||
"toggleFailed": "Falha ao alternar",
|
||||
"removed": "Repositório removido",
|
||||
"removeFailed": "Falha na remoção"
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -12,6 +12,7 @@ export const SUPPORTED_LANGUAGES = [
|
||||
{ code: 'zh-CN', name: '简体中文', flag: 'ZH' },
|
||||
{ code: 'id', name: 'Bahasa Indonesia', flag: 'ID' },
|
||||
{ code: 'ko', name: '한국어', flag: 'KO' },
|
||||
{ code: 'pt-BR', name: 'Português (Brasil)', flag: 'BR' },
|
||||
]
|
||||
|
||||
export const NAMESPACES = [
|
||||
|
||||
@@ -394,6 +394,9 @@ export default function Settings() {
|
||||
<SettingRow label="Default Context Size" description="Default context window size for models">
|
||||
<input className="input col-w-120" type="number" value={settings.context_size ?? ''} onChange={(e) => update('context_size', parseInt(e.target.value) || 0)} placeholder="2048" />
|
||||
</SettingRow>
|
||||
<SettingRow label="Artifact Download Concurrency" description="Maximum artifact files downloaded at once. 1 downloads sequentially.">
|
||||
<input aria-label="Artifact Download Concurrency" className="input" type="number" min="1" style={{ width: 120 }} value={settings.artifact_download_concurrency ?? 1} onChange={(e) => update('artifact_download_concurrency', Math.max(1, parseInt(e.target.value) || 1))} />
|
||||
</SettingRow>
|
||||
<SettingRow label="VRAM Budget" description="Cap VRAM used for model allocation on this node. Percentage (e.g. 80%) or absolute (e.g. 12GB). Empty uses all detected VRAM.">
|
||||
<input className="input col-w-120" type="text" value={settings.vram_budget ?? ''} onChange={(e) => update('vram_budget', e.target.value)} placeholder="e.g. 80% or 12GB" />
|
||||
</SettingRow>
|
||||
|
||||
@@ -194,6 +194,16 @@ func RequestMiddleware(redactor *Redactor, store EventStore, adapter Adapter, fa
|
||||
|
||||
texts := adapter.Scan(parsed)
|
||||
updates := make([]ScannedText, 0, len(texts))
|
||||
prefix, suffix := defaultReversibleTokenPrefix, defaultReversibleTokenSuffix
|
||||
if cfg, ok := rawCfg.(responsePIIConfig); ok {
|
||||
if cfg.PIIReversibleTokenPrefix() != "" {
|
||||
prefix = cfg.PIIReversibleTokenPrefix()
|
||||
}
|
||||
if cfg.PIIReversibleTokenSuffix() != "" {
|
||||
suffix = cfg.PIIReversibleTokenSuffix()
|
||||
}
|
||||
}
|
||||
pseudonyms := newPseudonymizer(prefix, suffix)
|
||||
var blocked bool
|
||||
var firstEventID string
|
||||
|
||||
@@ -259,7 +269,11 @@ func RequestMiddleware(redactor *Redactor, store EventStore, adapter Adapter, fa
|
||||
if res.Blocked {
|
||||
blocked = true
|
||||
}
|
||||
updates = append(updates, ScannedText{Index: st.Index, Text: res.Redacted})
|
||||
redacted := res.Redacted
|
||||
if cfg, ok := rawCfg.(responsePIIConfig); ok && cfg.PIIReversibleRedactions() {
|
||||
redacted = pseudonyms.replace(st.Text, res.Spans)
|
||||
}
|
||||
updates = append(updates, ScannedText{Index: st.Index, Text: redacted})
|
||||
}
|
||||
|
||||
if blocked {
|
||||
@@ -279,7 +293,16 @@ func RequestMiddleware(redactor *Redactor, store EventStore, adapter Adapter, fa
|
||||
if firstEventID != "" {
|
||||
c.Set(ctxKeyPIIEventID, firstEventID)
|
||||
}
|
||||
return next(c)
|
||||
if len(pseudonyms.original) == 0 {
|
||||
return next(c)
|
||||
}
|
||||
writer := newRestoringWriter(c.Response().Writer, pseudonyms.original)
|
||||
c.Response().Writer = writer
|
||||
err := next(c)
|
||||
if finishErr := writer.Finish(); err == nil {
|
||||
err = finishErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +60,16 @@ func setRequestOnContext(req *fakeRequest) echo.MiddlewareFunc {
|
||||
type fakeModelPIIConfig struct {
|
||||
enabled bool
|
||||
detectors []string
|
||||
reverse bool
|
||||
prefix string
|
||||
suffix string
|
||||
}
|
||||
|
||||
func (f fakeModelPIIConfig) PIIIsEnabled() bool { return f.enabled }
|
||||
func (f fakeModelPIIConfig) PIIDetectors() []string { return f.detectors }
|
||||
func (f fakeModelPIIConfig) PIIIsEnabled() bool { return f.enabled }
|
||||
func (f fakeModelPIIConfig) PIIDetectors() []string { return f.detectors }
|
||||
func (f fakeModelPIIConfig) PIIReversibleRedactions() bool { return f.reverse }
|
||||
func (f fakeModelPIIConfig) PIIReversibleTokenPrefix() string { return f.prefix }
|
||||
func (f fakeModelPIIConfig) PIIReversibleTokenSuffix() string { return f.suffix }
|
||||
|
||||
func withModelConfig(cfg fakeModelPIIConfig) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
@@ -129,6 +135,58 @@ var _ = Describe("RequestMiddleware (NER)", func() {
|
||||
Expect(events[0].Direction).To(Equal(DirectionIn))
|
||||
})
|
||||
|
||||
It("restores distinct pseudonyms across streaming write boundaries", func() {
|
||||
body := &fakeRequest{Messages: []string{"Email alice@example.com or bob@example.com"}}
|
||||
mw := RequestMiddleware(&Redactor{}, store(), fakeAdapter(), nil,
|
||||
WithNERResolver(resolverFor(map[string]NERConfig{
|
||||
"privacy-filter": nerCfg(ActionMask,
|
||||
NEREntity{Group: "EMAIL", Start: 6, End: 23, Score: 0.95},
|
||||
NEREntity{Group: "EMAIL", Start: 27, End: 42, Score: 0.95}),
|
||||
})))
|
||||
e := echo.New()
|
||||
e.POST("/chat", func(c echo.Context) error {
|
||||
Expect(body.Messages[0]).To(Equal("Email [REDACTED:EMAIL_001] or [REDACTED:EMAIL_002]"))
|
||||
_, err := c.Response().Write([]byte(`data: {"delta":"EMAIL_001 and [REDACTED:EMAIL_0`))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = c.Response().Write([]byte(`01] and [REDACTED:EMAIL_002]"}` + "\n\n"))
|
||||
return err
|
||||
}, setRequestOnContext(body), withModelConfig(fakeModelPIIConfig{
|
||||
enabled: true, detectors: []string{"privacy-filter"}, reverse: true,
|
||||
}), mw)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/chat", strings.NewReader(`{}`))
|
||||
w := httptest.NewRecorder()
|
||||
e.ServeHTTP(w, req)
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(w.Body.String()).To(Equal("data: {\"delta\":\"EMAIL_001 and alice@example.com and bob@example.com\"}\n\n"))
|
||||
})
|
||||
|
||||
It("uses configured reversible redaction token delimiters", func() {
|
||||
body := &fakeRequest{Messages: []string{"Email alice@example.com"}}
|
||||
mw := RequestMiddleware(&Redactor{}, store(), fakeAdapter(), nil,
|
||||
WithNERResolver(resolverFor(map[string]NERConfig{
|
||||
"privacy-filter": nerCfg(ActionMask,
|
||||
NEREntity{Group: "EMAIL", Start: 6, End: 23, Score: 0.95}),
|
||||
})))
|
||||
e := echo.New()
|
||||
e.POST("/chat", func(c echo.Context) error {
|
||||
Expect(body.Messages[0]).To(Equal("Email <PII:EMAIL_001>"))
|
||||
_, err := c.Response().Write([]byte(`{"text":"<PII:EMAIL_001>"}`))
|
||||
return err
|
||||
}, setRequestOnContext(body), withModelConfig(fakeModelPIIConfig{
|
||||
enabled: true, detectors: []string{"privacy-filter"}, reverse: true,
|
||||
prefix: "<PII:", suffix: ">",
|
||||
}), mw)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/chat", strings.NewReader(`{}`))
|
||||
w := httptest.NewRecorder()
|
||||
e.ServeHTTP(w, req)
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(w.Body.String()).To(Equal(`{"text":"alice@example.com"}`))
|
||||
})
|
||||
|
||||
It("blocks (400) when a detected entity's action is block", func() {
|
||||
st := store()
|
||||
body := &fakeRequest{Messages: []string{"my password is hunter2 ok"}}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package pii
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type responsePIIConfig interface {
|
||||
PIIReversibleRedactions() bool
|
||||
PIIReversibleTokenPrefix() string
|
||||
PIIReversibleTokenSuffix() string
|
||||
}
|
||||
|
||||
const (
|
||||
defaultReversibleTokenPrefix = "[REDACTED:"
|
||||
defaultReversibleTokenSuffix = "]"
|
||||
)
|
||||
|
||||
type pseudonymizer struct {
|
||||
byValue map[string]string
|
||||
original map[string]string
|
||||
counts map[string]int
|
||||
prefix string
|
||||
suffix string
|
||||
}
|
||||
|
||||
func newPseudonymizer(prefix, suffix string) *pseudonymizer {
|
||||
return &pseudonymizer{
|
||||
byValue: map[string]string{},
|
||||
original: map[string]string{},
|
||||
counts: map[string]int{},
|
||||
prefix: prefix,
|
||||
suffix: suffix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pseudonymizer) replace(text string, spans []Span) string {
|
||||
var b strings.Builder
|
||||
last := 0
|
||||
for _, span := range spans {
|
||||
if span.Action != ActionMask || span.Start < last || span.End > len(text) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(text[last:span.Start])
|
||||
value := text[span.Start:span.End]
|
||||
token, ok := p.byValue[value]
|
||||
if !ok {
|
||||
group := pseudonymGroup(span.Pattern)
|
||||
p.counts[group]++
|
||||
token = fmt.Sprintf("%s%s_%03d%s", p.prefix, group, p.counts[group], p.suffix)
|
||||
p.byValue[value] = token
|
||||
p.original[token] = value
|
||||
}
|
||||
b.WriteString(token)
|
||||
last = span.End
|
||||
}
|
||||
b.WriteString(text[last:])
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func pseudonymGroup(pattern string) string {
|
||||
if i := strings.LastIndexByte(pattern, ':'); i >= 0 {
|
||||
pattern = pattern[i+1:]
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, r := range strings.ToUpper(pattern) {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
b.WriteRune(r)
|
||||
} else {
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
if b.Len() == 0 {
|
||||
return "PII"
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type restoringWriter struct {
|
||||
http.ResponseWriter
|
||||
pending string
|
||||
replacements map[string]string
|
||||
}
|
||||
|
||||
func newRestoringWriter(w http.ResponseWriter, originals map[string]string) *restoringWriter {
|
||||
replacements := make(map[string]string, len(originals))
|
||||
for token, original := range originals {
|
||||
encoded, _ := json.Marshal(original)
|
||||
replacements[token] = string(encoded[1 : len(encoded)-1])
|
||||
}
|
||||
return &restoringWriter{ResponseWriter: w, replacements: replacements}
|
||||
}
|
||||
|
||||
func (w *restoringWriter) Write(data []byte) (int, error) {
|
||||
w.pending += string(data)
|
||||
ready, pending := w.splitReady(w.replace(w.pending))
|
||||
w.pending = pending
|
||||
if ready != "" {
|
||||
if _, err := w.ResponseWriter.Write([]byte(ready)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (w *restoringWriter) Flush() {
|
||||
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *restoringWriter) Finish() error {
|
||||
if w.pending == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := w.ResponseWriter.Write([]byte(w.replace(w.pending)))
|
||||
w.pending = ""
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *restoringWriter) replace(s string) string {
|
||||
for token, original := range w.replacements {
|
||||
s = strings.ReplaceAll(s, token, original)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (w *restoringWriter) splitReady(s string) (string, string) {
|
||||
keep := 0
|
||||
for token := range w.replacements {
|
||||
limit := min(len(token)-1, len(s))
|
||||
for n := 1; n <= limit; n++ {
|
||||
if strings.HasSuffix(s, token[:n]) && n > keep {
|
||||
keep = n
|
||||
}
|
||||
}
|
||||
}
|
||||
return s[:len(s)-keep], s[len(s)-keep:]
|
||||
}
|
||||
@@ -172,7 +172,7 @@ LocalAI supports various types of backends:
|
||||
- **Text-to-Speech Backends**: For speech synthesis (e.g., piper, Kokoro, VibeVoice, Qwen3-TTS, [NeMo-Speech.cpp]({{%relref "features/nemo-speech-cpp" %}}), [audio.cpp]({{%relref "features/audio-cpp" %}}))
|
||||
- **Sound Generation Backends**: For music and audio generation (e.g., ACE-Step, [audio.cpp]({{%relref "features/audio-cpp" %}}))
|
||||
- **Sound Classification Backends**: For sound-event classification / audio tagging - identifying everyday sounds like baby cry, glass breaking, alarms (e.g., ced.cpp)
|
||||
- **Image & Video Generation Backends**: For diffusion and audio-conditioned avatar models (e.g., stable-diffusion.cpp, diffusers, vLLM-Omni, [LongCat-Video]({{%relref "features/video-generation" %}}))
|
||||
- **Image & Video Generation Backends**: For diffusion and audio-conditioned avatar models (e.g., stable-diffusion.cpp, diffusers, vLLM-Omni, [LongCat-Video]({{%relref "features/video-generation" %}}), [vllm.cpp / MiniMax-H3]({{%relref "features/video-generation" %}}))
|
||||
- **3D Generation Backends**: For image-to-3D mesh generation ([trellis2.cpp]({{%relref "features/3d-generation" %}}) — Microsoft TRELLIS.2, producing GLB assets with PBR textures)
|
||||
- **Vision & Detection Backends**: For object detection, segmentation, depth, and face/voice recognition (e.g., rf-detr.cpp, locate-anything.cpp, sam3.cpp, insightface)
|
||||
- **Audio Processing Backends**: For voice activity detection and audio enhancement (e.g., Silero VAD, LocalVQE, [audio.cpp]({{%relref "features/audio-cpp" %}}))
|
||||
|
||||
@@ -49,6 +49,7 @@ You can configure these settings via the web UI or through environment variables
|
||||
|
||||
- **Threads**: Number of threads used for parallel computation (recommended: number of physical cores)
|
||||
- **Context Size**: Default context size for models (default: `512`)
|
||||
- **Artifact Download Concurrency**: Maximum number of artifact files downloaded at once. `1` downloads sequentially (default: `1`)
|
||||
- **F16**: Enable GPU acceleration using 16-bit floating point
|
||||
- **VRAM Budget**: Cap on VRAM used for model allocation (for example `80%` or `12GB`; empty means no cap). See [VRAM Management]({{%relref "advanced/vram-management" %}})
|
||||
|
||||
@@ -138,6 +139,7 @@ The `runtime_settings.json` file follows this structure:
|
||||
"lru_eviction_retry_interval": "1s",
|
||||
"threads": 8,
|
||||
"context_size": 2048,
|
||||
"artifact_download_concurrency": 4,
|
||||
"f16": false,
|
||||
"debug": false,
|
||||
"cors": true,
|
||||
@@ -223,4 +225,3 @@ If P2P is not starting:
|
||||
2. Check network connectivity
|
||||
3. Ensure the P2P network ID matches across nodes (if using federated mode)
|
||||
4. Review logs for P2P-related errors
|
||||
|
||||
@@ -685,6 +685,83 @@ The `cache_type_k` / `cache_type_v` fields map to llama.cpp's `-ctk` / `-ctv` fl
|
||||
- [Tracked branch: `feature/turboquant-kv-cache`](https://github.com/TheTom/llama-cpp-turboquant/tree/feature/turboquant-kv-cache)
|
||||
|
||||
|
||||
### buun-llama-cpp (DFlash speculative decoding + TurboQuant/TCQ KV-cache)
|
||||
|
||||
[buun-llama-cpp](https://github.com/spiritbuun/buun-llama-cpp) is a fork-of-a-fork: spiritbuun forked `TheTom/llama-cpp-turboquant` (the `turboquant` backend above) and added two independent features on top:
|
||||
|
||||
1. **DFlash** — a block-diffusion speculative decoding scheme that uses a dedicated drafter model (new `DFlashDraftModel` GGUF architecture). On a target/drafter pair it emits a block of tokens per speculation step and can be combined with tree-structured verification ("DDTree") for multi-branch draft expansion.
|
||||
2. **TCQ (Trellis-Coded Quantization)** — two additional KV-cache types (`turbo2_tcq`, `turbo3_tcq`) on top of the TurboQuant `turbo2` / `turbo3` / `turbo4` already shipped by the parent fork, delivering 10–44% KL reduction over scalar quantization at 2–3 bits per value.
|
||||
|
||||
Like `turboquant`, this backend shares LocalAI's stock `llama-cpp` gRPC server sources — so any GGUF model that runs on `llama-cpp` also runs on `buun-llama-cpp`. Pick it over `turboquant` specifically when you want DFlash speculative decoding or the newer TCQ KV-cache variants.
|
||||
|
||||
#### Features
|
||||
|
||||
- Drop-in GGUF compatibility with upstream `llama.cpp`.
|
||||
- DFlash block-diffusion speculative decoding (CUDA/Metal; no CPU fallback).
|
||||
- TurboQuant KV-cache types (`turbo2`, `turbo3`, `turbo4`) inherited from the parent `turboquant` fork, plus buun-exclusive `turbo2_tcq` and `turbo3_tcq` variants.
|
||||
- Same feature surface as `llama-cpp`: text generation, embeddings, tool calls, multimodal via mmproj.
|
||||
- Available on CPU (AVX/AVX2/AVX512/fallback), NVIDIA CUDA 12/13, AMD ROCm/HIP, Intel SYCL f32/f16, Vulkan, and NVIDIA L4T — but note that DFlash and `turbo*` KV types have no CPU fallback and error at model-load on CPU-only builds.
|
||||
|
||||
#### Setup
|
||||
|
||||
`buun-llama-cpp` ships as a separate container image in the LocalAI backend gallery. Install it like any other backend:
|
||||
|
||||
```bash
|
||||
local-ai backends install buun-llama-cpp
|
||||
```
|
||||
|
||||
Or pick a specific flavor for your hardware (example tags: `cpu-buun-llama-cpp`, `cuda12-buun-llama-cpp`, `cuda13-buun-llama-cpp`, `rocm-buun-llama-cpp`, `intel-sycl-f16-buun-llama-cpp`, `vulkan-buun-llama-cpp`).
|
||||
|
||||
#### YAML configuration — TCQ KV-cache
|
||||
|
||||
To run a model with TurboQuant/TCQ quantized KV-cache, set the backend and pick a `turbo*` cache type:
|
||||
|
||||
```yaml
|
||||
name: my-model
|
||||
backend: buun-llama-cpp
|
||||
parameters:
|
||||
model: file.gguf
|
||||
# Accepted values for the two fork-aware backends include the stock llama.cpp
|
||||
# types (f16, f32, q8_0, q4_0, q4_1, q5_0, q5_1), the TurboQuant types
|
||||
# (turbo2, turbo3, turbo4), and the buun-only TCQ variants (turbo2_tcq,
|
||||
# turbo3_tcq). turbo3 / turbo4 / turbo*_tcq auto-enable flash_attention.
|
||||
cache_type_k: turbo3
|
||||
cache_type_v: turbo3_tcq
|
||||
context_size: 8192
|
||||
```
|
||||
|
||||
#### YAML configuration — DFlash speculative decoding
|
||||
|
||||
DFlash requires a **dedicated drafter model** in the new `DFlashDraftModel` GGUF architecture. At time of writing the only known public target/drafter pair is [`z-lab/Qwen3.5-27B`](https://huggingface.co/z-lab/Qwen3.5-27B) + [`z-lab/Qwen3.5-27B-DFlash`](https://huggingface.co/z-lab/Qwen3.5-27B-DFlash).
|
||||
|
||||
```yaml
|
||||
name: qwen3-dflash
|
||||
backend: buun-llama-cpp
|
||||
parameters:
|
||||
# Target model (quantized as usual)
|
||||
model: Qwen3.5-27B-Q4_K_M.gguf
|
||||
# Drafter model produced by buun's convert_hf_to_gguf.py from the
|
||||
# DFlashDraftModel checkpoint. Resolved relative to the models path.
|
||||
draft_model: Qwen3.5-27B-DFlash.gguf
|
||||
options:
|
||||
# Switches the speculative pipeline from the default draft-model mode to
|
||||
# DFlash (block-diffusion). Required to activate the DFlash code path.
|
||||
- spec_type:dflash
|
||||
# Optional tuning:
|
||||
# - tree_budget:0 # 0 = flat DFlash; >0 = DDTree verification budget
|
||||
# - draft_topk:1 # drafter top-K per position (1 = argmax)
|
||||
# - spec_n_max:16 # cap on draft tokens per speculation step
|
||||
```
|
||||
|
||||
Under the hood LocalAI wires `draft_model` through to the grpc-server's `params.speculative.mparams_dft.path`, and `spec_type:dflash` is forwarded through the options passthrough to buun's `common_speculative_type_from_name("dflash")`. The `tree_budget` and `draft_topk` options are buun-exclusive; they reference struct fields that only exist in buun's fork, so they're surfaced on this backend only (passing them to stock `llama-cpp` is a no-op).
|
||||
|
||||
#### Reference
|
||||
|
||||
- [spiritbuun/buun-llama-cpp](https://github.com/spiritbuun/buun-llama-cpp)
|
||||
- [TCQ paper / dataset](https://huggingface.co/datasets/spiritbuun/turboquant-tcq-kv-cache) — *"Closing the Gap: Trellis-Coded Quantization for KV Cache at 2-3 Bits"*
|
||||
- DFlash target/drafter pair: [`z-lab/Qwen3.5-27B`](https://huggingface.co/z-lab/Qwen3.5-27B) + [`z-lab/Qwen3.5-27B-DFlash`](https://huggingface.co/z-lab/Qwen3.5-27B-DFlash)
|
||||
|
||||
|
||||
### vLLM
|
||||
|
||||
[vLLM](https://github.com/vllm-project/vllm) is a fast and easy-to-use library for LLM inference.
|
||||
|
||||
@@ -6,7 +6,7 @@ url = "/features/video-generation/"
|
||||
aliases = ["/features/longcat-video/"]
|
||||
+++
|
||||
|
||||
LocalAI can generate videos from text prompts and optional image or audio conditioning via the `/video` endpoint. Supported backends include `diffusers`, `stablediffusion`, `vllm-omni`, and the dedicated `longcat-video` backend.
|
||||
LocalAI can generate videos from text prompts and optional image or audio conditioning via the `/video` endpoint. Supported backends include `diffusers`, `stablediffusion`, `vllm-omni`, `vllm-cpp` (MiniMax-H3, which generates video **and** audio together), and the dedicated `longcat-video` backend.
|
||||
|
||||
## API
|
||||
|
||||
@@ -25,8 +25,8 @@ The request body is JSON with the following fields:
|
||||
| `start_image` | `string` | No | | Starting image as base64 string or URL |
|
||||
| `end_image` | `string` | No | | Ending image for guided generation |
|
||||
| `audio` | `string` | No | | Audio conditioning as base64, a data URI, or URL |
|
||||
| `width` | `int` | No | 512 | Video width in pixels |
|
||||
| `height` | `int` | No | 512 | Video height in pixels |
|
||||
| `width` | `int` | No | backend | Video width in pixels; omit it to get the model's own default canvas |
|
||||
| `height` | `int` | No | backend | Video height in pixels; omit it to get the model's own default canvas |
|
||||
| `num_frames` | `int` | No | | Number of frames |
|
||||
| `fps` | `int` | No | | Frames per second |
|
||||
| `seconds` | `string` | No | | Duration in seconds |
|
||||
@@ -279,6 +279,158 @@ With distillation enabled, Avatar uses eight inference steps and fixed text/audi
|
||||
- **Out of memory while loading**: use BF16 on unified-memory hardware, close other GPU workloads, or reduce model concurrency. INT8 is not guaranteed to reduce peak load memory.
|
||||
- **Slow first request**: the backend and checkpoints are downloaded and loaded on demand; subsequent requests reuse the loaded pipeline.
|
||||
|
||||
## MiniMax-H3 (vllm.cpp)
|
||||
|
||||
The `vllm-cpp` backend — LocalAI's own C++ port of vLLM — also serves MiniMax-H3, which generates **video and audio jointly**. The clip comes back as an MP4 with a real AAC track rather than a silent render.
|
||||
|
||||
| Gallery model | Upstream checkpoint | Inputs | Output |
|
||||
|---------------|---------------------|--------|--------|
|
||||
| `minimax-h3-fl2va-q4` | `MiniMaxAI/MiniMax-H3`, Q4_K_M FL2VA partition | text, optional start/end frame | video with generated audio |
|
||||
|
||||
```bash
|
||||
local-ai models install minimax-h3-fl2va-q4
|
||||
```
|
||||
|
||||
{{% notice warning %}}
|
||||
This is a large, slow model. The five weight files total roughly 40 GB, and generation was measured at about 176 seconds per denoise step at the default 1344x768 canvas on a 20-SM device — so the 50-step default is a **multi-hour** request, not a multi-second one. Nothing in the path imposes a deadline, but plan for a long-running HTTP call, and use a CUDA host.
|
||||
{{% /notice %}}
|
||||
|
||||
### Ask for the sound
|
||||
|
||||
The model generates picture and sound from the same prompt, so a prompt that only describes what is *seen* produces room tone and ambience. To get speech, say that the character talks and put the words in the prompt:
|
||||
|
||||
```text
|
||||
It is TALKING to the camera: its mouth moves clearly in sync with its speech,
|
||||
in a dry, deadpan tone.
|
||||
|
||||
It says, clearly and audibly: "Michael scheduled another all-hands.
|
||||
It is about the printer. Again."
|
||||
|
||||
Audio: a single clear voice, close-miked, with quiet room tone underneath.
|
||||
```
|
||||
|
||||
### Geometry and clip length
|
||||
|
||||
The trained canvas is **1344x768 at 124 frames and 24 fps**, about 5.2 seconds, and that is what the gallery entry defaults to. Two rules the engine enforces:
|
||||
|
||||
- The canvas is truncated onto a 32-pixel grid.
|
||||
- The frame count sits on a **17n+5** grid (…, 90, 107, 124, 141, …). A count off the grid is rounded up, and LocalAI logs the value it actually rendered.
|
||||
|
||||
The trained clip range is roughly 124 to 362 frames (about 5 to 15 seconds).
|
||||
|
||||
### Text-to-video with sound
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/video \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "minimax-h3-fl2va-q4",
|
||||
"prompt": "A cyan llama mascot in a grey office chair, talking to the camera. It says, clearly and audibly: \"the printer is down again\". Audio: one clear close-miked voice.",
|
||||
"num_frames": 124,
|
||||
"step": 50,
|
||||
"seed": 42
|
||||
}'
|
||||
```
|
||||
|
||||
### First-frame conditioning
|
||||
|
||||
`start_image` pins the supplied image as frame 0 (H3's `fl2va` task); `end_image` pins the last frame. LocalAI converts the upload to the binary PPM at the exact output canvas that the engine requires, using `ffmpeg`, so PNG and JPEG uploads work. When no `width`/`height` is given, the canvas is derived from the image's aspect on a 768-pixel short edge.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/video \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"model\": \"minimax-h3-fl2va-q4\",
|
||||
\"prompt\": \"the subject turns toward the camera and starts speaking\",
|
||||
\"start_image\": \"$(base64 --wrap=0 portrait.png)\"
|
||||
}"
|
||||
```
|
||||
|
||||
### Partitions: what this checkpoint will and will not do
|
||||
|
||||
MiniMax-H3 ships as two DiT partitions, and the gallery entry installs **FL2VA**, which serves `t2va` (text only) and `fl2va` (first/last frame). Reference conditioning — a whole reference image, a reference clip, or reference audio — belongs to the separate **Ref2VA** checkpoint.
|
||||
|
||||
This matters because the mismatch does not fail cleanly upstream: a reference passed to an FL2VA DiT renders for hours and returns a coloured lattice over the frame. The backend refuses the combination up front instead, naming the partition. The community quantisations strip the release metadata and the two DiTs are byte-structurally identical, so the partition is *declared* in the model config (`video_partition`) rather than detected.
|
||||
|
||||
### ffmpeg is required on the host
|
||||
|
||||
The engine writes frames and a WAV and composes the `ffmpeg` command line; LocalAI runs it. That process boundary is deliberate upstream, so the backend image ships no `ffmpeg`: install one on the host, or point `options: [ffmpeg:/path/to/ffmpeg]` at a binary. Without it, generation succeeds and the mux fails with a message saying so.
|
||||
|
||||
### MiniMax-H3 model configuration
|
||||
|
||||
```yaml
|
||||
name: minimax-h3-fl2va-q4
|
||||
backend: vllm-cpp
|
||||
cuda: true
|
||||
known_usecases:
|
||||
- video
|
||||
known_input_modalities:
|
||||
- text
|
||||
- image
|
||||
known_output_modalities:
|
||||
- video
|
||||
options:
|
||||
- video_encoder:minimax-h3/qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf
|
||||
- video_tokenizer:minimax-h3/tokenizer.json
|
||||
- video_vae:minimax-h3/video_vae.safetensors
|
||||
- video_vae_config:minimax-h3/video_vae_config.json
|
||||
- audio_vae:minimax-h3/audio_vae.safetensors
|
||||
- audio_vae_config:minimax-h3/audio_vae_config.json
|
||||
- video_partition:fl2va
|
||||
- video_device:cuda
|
||||
- video_dequant_bf16:true
|
||||
- video_width:1344
|
||||
- video_height:768
|
||||
- video_num_frames:124
|
||||
parameters:
|
||||
model: minimax-h3/MiniMax-H3-FL2VA-Q4_K_M.gguf
|
||||
```
|
||||
|
||||
`parameters.model` is the DiT. H3 is a checkpoint *set* rather than one model directory, so the encoder, the tokenizer and the two VAEs are named in `options`. Relative paths resolve against the models directory.
|
||||
|
||||
#### Load options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `video_encoder` | — | H3 text encoder (GGUF or a bf16 shard directory). Required unless `video_prompt_embeds` is set |
|
||||
| `video_tokenizer` | — | `tokenizer.json` for the encoder |
|
||||
| `video_vae` | — | Video VAE weights (`.safetensors`). Required |
|
||||
| `video_vae_config` | `config.json` beside the weights | Carries `latents_mean` / `latents_std` and `clip_length` / `token_drop`; the decode is wrong without it |
|
||||
| `audio_vae` | — | Audio VAE weights. Required |
|
||||
| `audio_vae_config` | `config.json` beside the weights | As above, for audio |
|
||||
| `video_prompt_embeds` | — | Pre-computed f32 conditioning, as an alternative to an encoder |
|
||||
| `video_partition` | `fl2va` | `fl2va` or `ref2va`; must match the DiT you installed |
|
||||
| `video_device` | `cpu`, or `cuda` when the config sets `cuda: true` | `cpu` or `cuda` |
|
||||
| `video_dequant_bf16` | `false` | Dequantise and stream the DiT as bf16; what the Q4_K_M GGUF arm wants |
|
||||
| `video_fp4_resident` | `false` | NVFP4 on CUDA: keep FP4 packed and use the Marlin W4A16 GEMM |
|
||||
| `video_width` / `video_height` | 1344 / 768 in the gallery entry | Default canvas when the request omits it |
|
||||
| `video_num_frames` | 124 in the gallery entry | Default clip length when the request omits it |
|
||||
| `video_steps` | engine default (50) | Default denoise steps when the request omits it |
|
||||
| `video_workdir` | a temporary directory | Where `frame_%06d.ppm` and `audio.wav` land. Set it to keep every run's frames |
|
||||
| `video_crf` | 18 | x264 CRF for the mux |
|
||||
| `ffmpeg` | `ffmpeg` from `PATH` | The mux binary |
|
||||
|
||||
#### Per-request parameters
|
||||
|
||||
The `/video` request's `params` object accepts string values. Unknown keys are rejected rather than ignored, so a typo does not cost you a multi-hour render of the wrong thing.
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `noise_aug` | Keyframe pinning strength; the default is 1.0 |
|
||||
| `ref_image` | `ref2va` only: one whole reference image, as a binary PPM |
|
||||
| `ref_video` | `ref2va` only: a directory of `frame_%06d.ppm` |
|
||||
| `crf` | Per-request x264 CRF override |
|
||||
|
||||
`negative_prompt`, `cfg_scale` and `fps` have no MiniMax-H3 equivalent: H3 has no negative prompt or CFG scale, and it renders at a fixed frame rate that the audio track is synchronised to. Setting them is logged and ignored rather than silently honoured.
|
||||
|
||||
### MiniMax-H3 troubleshooting
|
||||
|
||||
- **`ffmpeg not found`**: install ffmpeg on the host or set `options: [ffmpeg:<path>]`. The frames and WAV are already rendered; only the mux failed.
|
||||
- **`the FL2VA checkpoint serves t2va and fl2va only`**: you passed a reference image, clip or audio to the FL2VA DiT. Use `start_image` for first-frame conditioning, or install a Ref2VA checkpoint.
|
||||
- **`video_partition must be "fl2va" or "ref2va"`**: the config declares something else.
|
||||
- **`unknown params key`**: `params` accepts only the four keys above.
|
||||
- **Text inside the frame comes out malformed**: this is the model's weakest area. Composite logos and signage in afterwards.
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Status Code | Description |
|
||||
|
||||
@@ -165,15 +165,33 @@ pii:
|
||||
enabled: true # default-on for cloud-proxy; explicit for audit
|
||||
detectors:
|
||||
- privacy-filter-multilingual
|
||||
reversible_redactions: true # restore request PII if the model echoes its wrapped token
|
||||
reversible_token_prefix: "[REDACTED:" # optional; this is the default
|
||||
reversible_token_suffix: "]" # optional; this is the default
|
||||
```
|
||||
|
||||
`reversible_redactions` enables bijective, request-scoped replacement. Each
|
||||
masked value is sent to the backend as a stable wrapped token such as
|
||||
`[REDACTED:EMAIL_001]` instead of a generic redaction marker. If the model includes that token in
|
||||
its response, LocalAI restores the original value before returning JSON or SSE
|
||||
to the caller. The substitution map exists only for that request and is never
|
||||
logged or persisted. Leave the option unset (the default) for irreversible
|
||||
`[REDACTED:...]` masking.
|
||||
|
||||
The prefix and suffix reduce collisions with ordinary model output and can be
|
||||
customized with `reversible_token_prefix` and `reversible_token_suffix`.
|
||||
Reversible redactions provide less confidentiality than irreversible masking:
|
||||
any third party that can observe both the redacted request and restored response
|
||||
may be able to infer the original values.
|
||||
|
||||
Multiple detectors **union** their detections; overlapping spans resolve to
|
||||
the strongest action (`block` > `mask` > `allow`). A configured detector
|
||||
that can't be loaded **fails the request closed** (HTTP 503,
|
||||
`error.type=pii_ner_unavailable`) rather than silently skipping the check.
|
||||
The same NER path runs on the [MITM proxy]({{< relref "mitm-proxy.md" >}})
|
||||
request body for intercepted hosts. Response/output redaction is out of
|
||||
scope for now.
|
||||
request body for intercepted hosts. Reversible response restoration currently
|
||||
applies to LocalAI API routes; the MITM proxy keeps its own output-redaction
|
||||
policy.
|
||||
|
||||
### Instance-wide default detector
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ Complete reference for all LocalAI command-line interface (CLI) parameters and e
|
||||
| `--localai-config-dir` | `BASEPATH/configuration` | Directory for dynamic loading of certain configuration files (currently runtime_settings.json, api_keys.json, and external_backends.json). See [Runtime Settings]({{%relref "features/runtime-settings" %}}) for web-based configuration. | `$LOCALAI_CONFIG_DIR` |
|
||||
| `--localai-config-dir-poll-interval` | | Time duration to poll the LocalAI Config Dir if your system has broken fsnotify events (example: `1m`) | `$LOCALAI_CONFIG_DIR_POLL_INTERVAL` |
|
||||
| `--models-config-file` | | YAML file containing a list of model backend configs (alias: `--config-file`) | `$LOCALAI_MODELS_CONFIG_FILE`, `$CONFIG_FILE` |
|
||||
| `--artifact-download-concurrency` | `1` | How many files of a model artifact to download at once. `1` downloads sequentially. Raising it helps artifacts split into many files on a fast link, at the cost of more concurrent load on the models volume. Whole files only — a single file is never split, so resume and per-file checksum verification are unaffected | `$LOCALAI_ARTIFACT_DOWNLOAD_CONCURRENCY` |
|
||||
|
||||
## Backend Flags
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ All backends listed here can be installed on demand from the [Backend Gallery]({
|
||||
| [llama.cpp](https://github.com/ggerganov/llama.cpp) | LLM inference in C/C++. Supports LLaMA, Mamba, RWKV, Falcon, Starcoder, GPT-2, [and many others](https://github.com/ggerganov/llama.cpp?tab=readme-ov-file#description) | GPT, Functions | yes | yes | CPU, CUDA 12/13, ROCm, Intel SYCL, Vulkan, Metal, Jetson L4T |
|
||||
| [ik_llama.cpp](https://github.com/ikawrakow/ik_llama.cpp) | Hard fork of llama.cpp optimized for CPU/hybrid CPU+GPU with IQK quants, custom quant mixes, and MLA for DeepSeek | GPT | yes | yes | CPU (AVX2+) |
|
||||
| [turboquant](https://github.com/TheTom/llama-cpp-turboquant) | llama.cpp fork adding the TurboQuant KV-cache quantization scheme | GPT | yes | yes | CPU, CUDA 12/13, ROCm, Intel SYCL, Vulkan, Jetson L4T |
|
||||
| [buun-llama-cpp](https://github.com/spiritbuun/buun-llama-cpp) | llama.cpp fork with DFlash block-diffusion speculative decoding and TurboQuant/TCQ KV-cache quantization (2–3 bits per value). Accelerated paths are CUDA/Metal only. | GPT, Functions | yes | yes | CUDA, Metal (CPU fallback for non-turbo/non-DFlash only) |
|
||||
| [ds4](https://github.com/antirez/ds4) | DeepSeek V4 Flash single-model inference engine, optimized for Metal and CUDA | GPT | no | yes | CPU, CUDA 12/13, Metal, Jetson L4T |
|
||||
| [vllm.cpp](https://github.com/mudler/vllm.cpp) | From-scratch C++20 port of vLLM by the LocalAI team: paged KV cache, continuous batching, prefix caching, safetensors + GGUF, engine-enforced structured output, no Python at inference | GPT, Functions | no | yes | CPU, CUDA 12/13 (Blackwell-family), Vulkan, Metal, Jetson L4T (GB10) |
|
||||
| [vLLM](https://github.com/vllm-project/vllm) | Fast LLM serving with PagedAttention; GPTQ/AWQ/FP8 quantization | GPT, Functions, Multimodal | no | yes | CUDA 12/13, ROCm, Intel SYCL, Jetson L4T |
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "v4.8.1"
|
||||
"version": "v4.8.2"
|
||||
}
|
||||
+96
-1
@@ -2089,7 +2089,7 @@
|
||||
files:
|
||||
- filename: ds4flash.gguf
|
||||
uri: https://huggingface.co/unsloth/DeepSeek-V4-Flash-GGUF
|
||||
sha256: a9aadd5a1921708c97aecaf29e6b3d5c0aa252aadc3b706d1281f68361bd52b9
|
||||
sha256: 492a2af8558781d9c494815ab7df5a47114ad1d836e4e0a13a3e8f0e5e791bff
|
||||
- name: "qwopus3.6-35b-a3b-coder-mtp"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
@@ -8497,6 +8497,101 @@
|
||||
- vae/**
|
||||
parameters:
|
||||
model: meituan-longcat/LongCat-Video-Avatar-1.5
|
||||
- name: minimax-h3-fl2va-q4
|
||||
url: github:mudler/LocalAI/gallery/virtual.yaml@master
|
||||
urls:
|
||||
- https://huggingface.co/MiniMaxAI/MiniMax-H3
|
||||
- https://huggingface.co/realrebelai/MiniMax-H3_GGUFs
|
||||
- https://huggingface.co/lilcheaty/MiniMax-H3-NVFP4
|
||||
- https://github.com/mudler/vllm.cpp
|
||||
description: |
|
||||
MiniMax-H3 served by vllm.cpp, LocalAI's own C++ port of vLLM. It generates
|
||||
video AND audio jointly from a text prompt, so a clip comes back as an MP4
|
||||
with a real soundtrack rather than a silent render: ask for speech in the
|
||||
prompt and the model lip-syncs it.
|
||||
|
||||
This is the Q4_K_M quantisation of the FL2VA partition, which serves
|
||||
text-to-video (t2va) and first/last-frame conditioning (fl2va). Reference
|
||||
conditioning (ref2va) is a different checkpoint and is refused by this one.
|
||||
|
||||
Roughly 40 GB of weights across five files, plus the two VAE configs that
|
||||
carry the latent statistics. The default canvas is 1344x768
|
||||
at 124 frames and 24 fps, about 5.2 seconds. Generation is slow — measured
|
||||
at roughly 176 s per denoise step at that canvas on a 20-SM device, so the
|
||||
50-step default is a multi-hour job. Muxing the finished frames needs
|
||||
ffmpeg on the host.
|
||||
license: other
|
||||
icon: https://huggingface.co/MiniMaxAI/MiniMax-H3/resolve/main/assets/minimax-h3.png
|
||||
tags:
|
||||
- text-to-video
|
||||
- image-to-video
|
||||
- video-generation
|
||||
- audio-video-generation
|
||||
- minimax-h3
|
||||
- vllm-cpp
|
||||
- cuda
|
||||
- gpu
|
||||
- dgx-spark
|
||||
last_checked: "2026-08-09"
|
||||
overrides:
|
||||
backend: vllm-cpp
|
||||
# The video engine has no auto device slot; this is what selects CUDA over
|
||||
# the CPU queue, which for a multi-hour render is not a small difference.
|
||||
cuda: true
|
||||
known_usecases:
|
||||
- video
|
||||
known_input_modalities:
|
||||
- text
|
||||
- image
|
||||
known_output_modalities:
|
||||
- video
|
||||
options:
|
||||
# MiniMax-H3 is a checkpoint SET, not one model directory: parameters.model
|
||||
# is the DiT and the rest of the set is named here.
|
||||
- video_encoder:minimax-h3/qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf
|
||||
- video_tokenizer:minimax-h3/tokenizer.json
|
||||
- video_vae:minimax-h3/video_vae.safetensors
|
||||
- video_vae_config:minimax-h3/video_vae_config.json
|
||||
- audio_vae:minimax-h3/audio_vae.safetensors
|
||||
- audio_vae_config:minimax-h3/audio_vae_config.json
|
||||
# The community GGUF strips the release metadata and the FL2VA and Ref2VA
|
||||
# DiTs are byte-structurally identical, so the partition must be declared.
|
||||
# This is the FL2VA checkpoint: t2va and fl2va, never ref2va.
|
||||
- video_partition:fl2va
|
||||
- video_device:cuda
|
||||
# Q4_K_M streams up as bf16; keep-quant is for the NVFP4 arm.
|
||||
- video_dequant_bf16:true
|
||||
# H3's trained canvas and clip length. Frame counts sit on a 17n+5 grid.
|
||||
- video_width:1344
|
||||
- video_height:768
|
||||
- video_num_frames:124
|
||||
parameters:
|
||||
model: minimax-h3/MiniMax-H3-FL2VA-Q4_K_M.gguf
|
||||
files:
|
||||
- filename: minimax-h3/MiniMax-H3-FL2VA-Q4_K_M.gguf
|
||||
sha256: 5e8fa6e960d5fbd547390ceec63fcead275435d8f3bd2466a8a2cbd8c2e361e3
|
||||
uri: huggingface://realrebelai/MiniMax-H3_GGUFs/MiniMax-H3-FL2VA-Q4_K_M.gguf
|
||||
- filename: minimax-h3/qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf
|
||||
sha256: 1bf75e038c5895b97b6ea16cc1e3d32076254b06ec3df10657650d86dc82279e
|
||||
uri: huggingface://realrebelai/MiniMax-H3_GGUFs/qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf
|
||||
- filename: minimax-h3/video_vae.safetensors
|
||||
sha256: 7c1f131492e7eddacaac9069a61b81bdd39de5cc96561e677c5eab1cdce5e522
|
||||
uri: huggingface://lilcheaty/MiniMax-H3-NVFP4/vae/minimax_h3_video_vae_fp16.safetensors
|
||||
- filename: minimax-h3/audio_vae.safetensors
|
||||
sha256: 37dddc2f3e6d5d5139d823d5ea283bbf304dadcb885b1ccda818aa13dade5ea2
|
||||
uri: huggingface://MiniMaxAI/MiniMax-H3/FL2VA/audio_vae/model.safetensors
|
||||
# Each VAE config carries its per-channel latents_mean / latents_std and the
|
||||
# temporal clip_length / token_drop. The decode is wrong without them, so
|
||||
# they ship with the weights rather than being optional extras.
|
||||
- filename: minimax-h3/video_vae_config.json
|
||||
sha256: 3edd2cdd1ebc823c868be55ef917e1b3b8a398fde4d3150dae44a3bf05d9f627
|
||||
uri: huggingface://MiniMaxAI/MiniMax-H3/FL2VA/video_vae/config.json
|
||||
- filename: minimax-h3/audio_vae_config.json
|
||||
sha256: d8f3bcc62e23c7e9806970fa63cca6139c06faa3797cf9c94034f60db8512771
|
||||
uri: huggingface://MiniMaxAI/MiniMax-H3/FL2VA/audio_vae/config.json
|
||||
- filename: minimax-h3/tokenizer.json
|
||||
sha256: a5d85b6dcc535e6b93115a9ef287e6132fdbf30270da6218194ba742261173c7
|
||||
uri: huggingface://MiniMaxAI/MiniMax-H3/FL2VA/tokenizer/tokenizer.json
|
||||
- name: vllm-omni-qwen3-omni-30b
|
||||
url: github:mudler/LocalAI/gallery/virtual.yaml@master
|
||||
urls:
|
||||
|
||||
@@ -2,8 +2,10 @@ package downloader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/mudler/xlog"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// FileTask describes one download operation and an optional post-download
|
||||
@@ -23,23 +25,72 @@ type FileTask struct {
|
||||
// The helper centralizes the shared download path so callers only provide
|
||||
// source/destination metadata and any post-download hook they need.
|
||||
func DownloadFilesWithContext(ctx context.Context, tasks []FileTask, status func(string, string, string, float64), opts ...DownloadOption) error {
|
||||
for i := range tasks {
|
||||
task := tasks[i]
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
taskOpts := append([]DownloadOption{}, opts...)
|
||||
taskOpts = append(taskOpts, task.Options...)
|
||||
if err := downloadTaskWithRetry(ctx, task, status, taskOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
if task.AfterDownload != nil {
|
||||
if err := task.AfterDownload(task.Destination); err != nil {
|
||||
return err
|
||||
}
|
||||
return DownloadFilesWithConcurrency(ctx, tasks, status, 1, opts...)
|
||||
}
|
||||
|
||||
// DownloadFilesWithConcurrency runs up to concurrency downloads at once. A
|
||||
// concurrency of one or less keeps the original sequential path, so callers that
|
||||
// have not opted in are byte-for-byte unaffected: tasks still run in slice order
|
||||
// and the first failure still returns before any later task starts.
|
||||
//
|
||||
// Only whole files run in parallel. A single file is never split, so the
|
||||
// .partial resume machinery and the per-file SHA check in downloadTaskWithRetry
|
||||
// keep working untouched.
|
||||
//
|
||||
// The status callback is serialized, because it belongs to the caller and the
|
||||
// sequential path gave it an implicit guarantee of never being entered twice at
|
||||
// once. AfterDownload is deliberately *not* serialized: it does the per-file
|
||||
// verify-and-promote work that parallelism is meant to overlap, so hooks must be
|
||||
// safe to run concurrently with each other.
|
||||
func DownloadFilesWithConcurrency(ctx context.Context, tasks []FileTask, status func(string, string, string, float64), concurrency int, opts ...DownloadOption) error {
|
||||
if concurrency < 1 {
|
||||
concurrency = 1
|
||||
}
|
||||
|
||||
if status != nil && concurrency > 1 {
|
||||
var statusMutex sync.Mutex
|
||||
unsynchronized := status
|
||||
status = func(fileName, current, total string, percent float64) {
|
||||
statusMutex.Lock()
|
||||
defer statusMutex.Unlock()
|
||||
unsynchronized(fileName, current, total, percent)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
// errgroup.WithContext cancels the derived context on the first error, which
|
||||
// is what stops in-flight transfers instead of letting them run to
|
||||
// completion, and Wait reports that first error rather than the
|
||||
// context.Canceled the siblings observe.
|
||||
group, groupCtx := errgroup.WithContext(ctx)
|
||||
group.SetLimit(concurrency)
|
||||
|
||||
for i := range tasks {
|
||||
task := tasks[i]
|
||||
if err := groupCtx.Err(); err != nil {
|
||||
break
|
||||
}
|
||||
group.Go(func() error {
|
||||
if err := groupCtx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
taskOpts := append([]DownloadOption{}, opts...)
|
||||
taskOpts = append(taskOpts, task.Options...)
|
||||
if err := downloadTaskWithRetry(groupCtx, task, status, taskOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
if task.AfterDownload != nil {
|
||||
return task.AfterDownload(task.Destination)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := group.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
// A caller-cancelled context with no task in flight leaves the group clean,
|
||||
// so report the cancellation the sequential loop would have reported.
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// downloadTaskWithRetry fetches one file, retrying transient failures. Without
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package downloader_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/mudler/LocalAI/pkg/downloader"
|
||||
)
|
||||
|
||||
var _ = Describe("DownloadFilesWithConcurrency", func() {
|
||||
// slowServer holds every request open until it has seen `hold` of them at
|
||||
// once, or the client gives up. A sequential executor can never satisfy a
|
||||
// hold above one, so this doubles as proof that parallelism really happens
|
||||
// rather than just being configured.
|
||||
slowServer := func(delay time.Duration) (*httptest.Server, *int32) {
|
||||
var inFlight int32
|
||||
var peak int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
current := atomic.AddInt32(&inFlight, 1)
|
||||
for {
|
||||
observed := atomic.LoadInt32(&peak)
|
||||
if current <= observed || atomic.CompareAndSwapInt32(&peak, observed, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(delay)
|
||||
atomic.AddInt32(&inFlight, -1)
|
||||
_, _ = w.Write([]byte("payload"))
|
||||
}))
|
||||
return server, &peak
|
||||
}
|
||||
|
||||
tasksFor := func(server *httptest.Server, dir string, count int) []downloader.FileTask {
|
||||
tasks := make([]downloader.FileTask, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
tasks = append(tasks, downloader.FileTask{
|
||||
URI: downloader.URI(fmt.Sprintf("%s/file-%d", server.URL, i)),
|
||||
Destination: filepath.Join(dir, fmt.Sprintf("file-%d.bin", i)),
|
||||
FileIndex: i,
|
||||
TotalFiles: count,
|
||||
})
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
It("overlaps transfers up to the limit and no further", func() {
|
||||
server, peak := slowServer(60 * time.Millisecond)
|
||||
DeferCleanup(server.Close)
|
||||
|
||||
tasks := tasksFor(server, GinkgoT().TempDir(), 8)
|
||||
err := downloader.DownloadFilesWithConcurrency(context.Background(), tasks, nil, 3)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(*peak).To(BeNumerically(">", 1), "downloads never overlapped, so the limit was not applied")
|
||||
Expect(*peak).To(BeNumerically("<=", 3), "more transfers ran at once than the configured limit")
|
||||
})
|
||||
|
||||
It("keeps a concurrency of one strictly sequential", func() {
|
||||
server, peak := slowServer(10 * time.Millisecond)
|
||||
DeferCleanup(server.Close)
|
||||
|
||||
tasks := tasksFor(server, GinkgoT().TempDir(), 5)
|
||||
err := downloader.DownloadFilesWithConcurrency(context.Background(), tasks, nil, 1)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(*peak).To(Equal(int32(1)), "a limit of one must never overlap transfers")
|
||||
})
|
||||
|
||||
It("treats a non-positive concurrency as sequential", func() {
|
||||
server, peak := slowServer(10 * time.Millisecond)
|
||||
DeferCleanup(server.Close)
|
||||
|
||||
tasks := tasksFor(server, GinkgoT().TempDir(), 4)
|
||||
err := downloader.DownloadFilesWithConcurrency(context.Background(), tasks, nil, 0)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(*peak).To(Equal(int32(1)))
|
||||
})
|
||||
|
||||
It("reports the first hook error and stops starting new work", func() {
|
||||
server, _ := slowServer(0)
|
||||
DeferCleanup(server.Close)
|
||||
|
||||
var started int32
|
||||
tasks := tasksFor(server, GinkgoT().TempDir(), 24)
|
||||
for i := range tasks {
|
||||
index := i
|
||||
tasks[i].AfterDownload = func(string) error {
|
||||
atomic.AddInt32(&started, 1)
|
||||
if index == 0 {
|
||||
return fmt.Errorf("verification failed for shard %d", index)
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err := downloader.DownloadFilesWithConcurrency(context.Background(), tasks, nil, 2)
|
||||
|
||||
Expect(err).To(MatchError(ContainSubstring("verification failed for shard 0")))
|
||||
Expect(atomic.LoadInt32(&started)).To(BeNumerically("<", int32(len(tasks))),
|
||||
"the executor kept starting work after a failure instead of cancelling")
|
||||
})
|
||||
|
||||
It("returns the caller's cancellation rather than running the plan", func() {
|
||||
server, _ := slowServer(0)
|
||||
DeferCleanup(server.Close)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
var ran int32
|
||||
tasks := tasksFor(server, GinkgoT().TempDir(), 3)
|
||||
for i := range tasks {
|
||||
tasks[i].AfterDownload = func(string) error {
|
||||
atomic.AddInt32(&ran, 1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err := downloader.DownloadFilesWithConcurrency(ctx, tasks, nil, 4)
|
||||
|
||||
Expect(err).To(MatchError(context.Canceled))
|
||||
Expect(atomic.LoadInt32(&ran)).To(BeZero())
|
||||
})
|
||||
|
||||
It("serializes the status callback so callers need no locking of their own", func() {
|
||||
server, _ := slowServer(5 * time.Millisecond)
|
||||
DeferCleanup(server.Close)
|
||||
|
||||
// A deliberately unsynchronized counter: if the executor let two
|
||||
// callbacks in at once, -race would flag this write.
|
||||
unguarded := 0
|
||||
|
||||
tasks := tasksFor(server, GinkgoT().TempDir(), 6)
|
||||
err := downloader.DownloadFilesWithConcurrency(context.Background(), tasks, func(string, string, string, float64) {
|
||||
unguarded++
|
||||
}, 4)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(unguarded).To(BeNumerically(">", 0))
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -51,6 +52,12 @@ const (
|
||||
// the backend download the same repo in-band.
|
||||
DefaultLockWait = 30 * time.Minute
|
||||
|
||||
// DefaultDownloadConcurrency keeps materialization sequential unless an
|
||||
// operator opts in. Parallel transfers help a repo of many small shards on a
|
||||
// fast link, but they multiply memory and disk pressure on the shared models
|
||||
// volume, so the safe default is the behaviour this package already had.
|
||||
DefaultDownloadConcurrency = 1
|
||||
|
||||
initialLockRetryInterval = 100 * time.Millisecond
|
||||
maxLockRetryInterval = 5 * time.Second
|
||||
)
|
||||
@@ -65,6 +72,9 @@ type Manager struct {
|
||||
// the process run that created it, and outliving that run is precisely what
|
||||
// it must not do.
|
||||
writerID string
|
||||
// downloadConcurrency bounds how many of a snapshot's files transfer at
|
||||
// once. One means the sequential behaviour this package shipped with.
|
||||
downloadConcurrency atomic.Int64
|
||||
}
|
||||
|
||||
type ManagerOption func(*Manager)
|
||||
@@ -100,6 +110,25 @@ func WithLockWait(wait time.Duration) ManagerOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithDownloadConcurrency bounds how many of a snapshot's files are fetched at
|
||||
// once. Values below one mean sequential, which is the default: a shared models
|
||||
// volume is often the bottleneck rather than the network, so raising this is a
|
||||
// deployment decision rather than something to assume.
|
||||
func WithDownloadConcurrency(concurrency int) ManagerOption {
|
||||
return func(manager *Manager) {
|
||||
manager.SetDownloadConcurrency(concurrency)
|
||||
}
|
||||
}
|
||||
|
||||
// SetDownloadConcurrency updates the limit used by future file download
|
||||
// batches. Values below one select the safe sequential default.
|
||||
func (m *Manager) SetDownloadConcurrency(concurrency int) {
|
||||
if concurrency < 1 {
|
||||
concurrency = DefaultDownloadConcurrency
|
||||
}
|
||||
m.downloadConcurrency.Store(int64(concurrency))
|
||||
}
|
||||
|
||||
func NewManager(resolver SnapshotResolver, options ...ManagerOption) *Manager {
|
||||
manager := &Manager{
|
||||
resolver: resolver,
|
||||
@@ -107,6 +136,7 @@ func NewManager(resolver SnapshotResolver, options ...ManagerOption) *Manager {
|
||||
lockWait: DefaultLockWait,
|
||||
writerID: newWriterID(),
|
||||
}
|
||||
manager.SetDownloadConcurrency(DefaultDownloadConcurrency)
|
||||
for _, option := range options {
|
||||
option(manager)
|
||||
}
|
||||
@@ -376,7 +406,11 @@ func (m *Manager) materializeLocked(ctx context.Context, modelsPath string, spec
|
||||
// read this manifest, and getting its order or contents wrong would make a
|
||||
// corrupt tree look valid.
|
||||
manifest := Manifest{Version: ManifestVersion, Artifact: spec, Files: make([]ManifestFile, len(snapshot.Files))}
|
||||
completedBytes := int64(0)
|
||||
// completedBytes is atomic because WithDownloadConcurrency lets several
|
||||
// AfterDownload hooks add to it while other files' progress callbacks read
|
||||
// it. Each hook still writes its own manifest.Files slot, so the manifest
|
||||
// stays in snapshot order no matter which file finishes first.
|
||||
completedBytes := new(atomic.Int64)
|
||||
skippedFiles := 0
|
||||
skippedBytes := int64(0)
|
||||
tasks := make([]downloader.FileTask, 0, len(snapshot.Files))
|
||||
@@ -398,7 +432,7 @@ func (m *Manager) materializeLocked(ctx context.Context, modelsPath string, spec
|
||||
snapshotAbs := filepath.Join(layout.Partial, filepath.FromSlash(snapshotRel))
|
||||
if entry, ok := reuseMaterializedFile(snapshotAbs, file); ok {
|
||||
manifest.Files[taskIndex] = entry
|
||||
completedBytes += file.Size
|
||||
completedBytes.Add(file.Size)
|
||||
skippedFiles++
|
||||
skippedBytes += file.Size
|
||||
continue
|
||||
@@ -419,7 +453,7 @@ func (m *Manager) materializeLocked(ctx context.Context, modelsPath string, spec
|
||||
Phase: PhaseDownloading,
|
||||
Artifact: spec.Name,
|
||||
File: file.Path,
|
||||
CurrentBytes: completedBytes + event.Written,
|
||||
CurrentBytes: completedBytes.Load() + event.Written,
|
||||
TotalBytes: totalBytes,
|
||||
CompletedFiles: taskIndex,
|
||||
TotalFiles: len(snapshot.Files),
|
||||
@@ -431,7 +465,7 @@ func (m *Manager) materializeLocked(ctx context.Context, modelsPath string, spec
|
||||
Phase: PhaseVerifying,
|
||||
Artifact: spec.Name,
|
||||
File: file.Path,
|
||||
CurrentBytes: completedBytes + file.Size,
|
||||
CurrentBytes: completedBytes.Load() + file.Size,
|
||||
TotalBytes: totalBytes,
|
||||
CompletedFiles: taskIndex,
|
||||
TotalFiles: len(snapshot.Files),
|
||||
@@ -454,7 +488,7 @@ func (m *Manager) materializeLocked(ctx context.Context, modelsPath string, spec
|
||||
return err
|
||||
}
|
||||
manifest.Files[taskIndex] = entry
|
||||
completedBytes += file.Size
|
||||
completedBytes.Add(file.Size)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -471,7 +505,7 @@ func (m *Manager) materializeLocked(ctx context.Context, modelsPath string, spec
|
||||
"remaining_files", len(tasks),
|
||||
"total_files", len(snapshot.Files))
|
||||
}
|
||||
if err := downloader.DownloadFilesWithContext(ctx, tasks, nil); err != nil {
|
||||
if err := downloader.DownloadFilesWithConcurrency(ctx, tasks, nil, int(m.downloadConcurrency.Load())); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := root.RemoveAll(".downloads"); err != nil {
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package modelartifacts_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
hfapi "github.com/mudler/LocalAI/pkg/huggingface-api"
|
||||
"github.com/mudler/LocalAI/pkg/modelartifacts"
|
||||
)
|
||||
|
||||
var _ = Describe("artifact materialization with bounded download concurrency", func() {
|
||||
// shardedSnapshot serves `count` distinct files and reports the peak number
|
||||
// of simultaneous requests, so a test can tell configured concurrency from
|
||||
// actual concurrency.
|
||||
shardedSnapshot := func(count int, delay time.Duration) (hfapi.Snapshot, *httptest.Server, *int32) {
|
||||
bodies := make(map[string][]byte, count)
|
||||
files := make([]hfapi.SnapshotFile, 0, count)
|
||||
|
||||
var inFlight, peak int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
current := atomic.AddInt32(&inFlight, 1)
|
||||
for {
|
||||
observed := atomic.LoadInt32(&peak)
|
||||
if current <= observed || atomic.CompareAndSwapInt32(&peak, observed, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(delay)
|
||||
atomic.AddInt32(&inFlight, -1)
|
||||
_, _ = w.Write(bodies[r.URL.Path])
|
||||
}))
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
// Later shards are served first-come, so give them descending delays
|
||||
// as well: completion order ends up unrelated to snapshot order,
|
||||
// which is exactly what the manifest must survive.
|
||||
body := []byte(fmt.Sprintf("shard-%02d-bytes", i))
|
||||
urlPath := fmt.Sprintf("/shard-%02d", i)
|
||||
bodies[urlPath] = body
|
||||
sum := sha256.Sum256(body)
|
||||
files = append(files, hfapi.SnapshotFile{
|
||||
Path: fmt.Sprintf("shards/model-%02d.safetensors", i),
|
||||
Size: int64(len(body)),
|
||||
LFSOID: hex.EncodeToString(sum[:]),
|
||||
URL: server.URL + urlPath,
|
||||
})
|
||||
}
|
||||
|
||||
return hfapi.Snapshot{
|
||||
Endpoint: "https://huggingface.co", Repo: "owner/sharded",
|
||||
RequestedRevision: "main", ResolvedRevision: "0123456789abcdef0123456789abcdef01234567",
|
||||
Files: files,
|
||||
}, server, &peak
|
||||
}
|
||||
|
||||
spec := modelartifacts.Spec{Source: modelartifacts.Source{Type: "huggingface", Repo: "owner/sharded"}}
|
||||
|
||||
It("records the manifest in snapshot order regardless of completion order", func() {
|
||||
snapshot, server, peak := shardedSnapshot(12, 40*time.Millisecond)
|
||||
DeferCleanup(server.Close)
|
||||
|
||||
manager := modelartifacts.NewManager(&fakeSnapshotResolver{snapshot: snapshot},
|
||||
modelartifacts.WithDownloadConcurrency(4))
|
||||
modelsPath := GinkgoT().TempDir()
|
||||
|
||||
result, err := manager.Ensure(context.Background(), modelsPath, spec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(*peak).To(BeNumerically(">", 1), "files never overlapped, so this proves nothing about ordering")
|
||||
Expect(*peak).To(BeNumerically("<=", 4))
|
||||
|
||||
Expect(result.Manifest.Files).To(HaveLen(len(snapshot.Files)))
|
||||
for i, file := range result.Manifest.Files {
|
||||
Expect(file.Path).To(Equal(snapshot.Files[i].Path),
|
||||
"manifest entry %d is out of snapshot order", i)
|
||||
Expect(file.SHA256).To(HaveLen(64))
|
||||
}
|
||||
|
||||
// Every shard must also be on disk, not merely recorded.
|
||||
for _, file := range snapshot.Files {
|
||||
onDisk := filepath.Join(modelsPath, filepath.FromSlash(result.RelativePath), filepath.FromSlash(file.Path))
|
||||
info, statErr := os.Stat(onDisk)
|
||||
Expect(statErr).NotTo(HaveOccurred())
|
||||
Expect(info.Size()).To(Equal(file.Size))
|
||||
}
|
||||
})
|
||||
|
||||
It("produces the same manifest sequentially and concurrently", func() {
|
||||
sequentialSnapshot, sequentialServer, _ := shardedSnapshot(8, 0)
|
||||
DeferCleanup(sequentialServer.Close)
|
||||
sequential, err := modelartifacts.NewManager(&fakeSnapshotResolver{snapshot: sequentialSnapshot}).
|
||||
Ensure(context.Background(), GinkgoT().TempDir(), spec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
concurrentSnapshot, concurrentServer, _ := shardedSnapshot(8, 0)
|
||||
DeferCleanup(concurrentServer.Close)
|
||||
concurrent, err := modelartifacts.NewManager(&fakeSnapshotResolver{snapshot: concurrentSnapshot},
|
||||
modelartifacts.WithDownloadConcurrency(8)).
|
||||
Ensure(context.Background(), GinkgoT().TempDir(), spec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(concurrent.Manifest.Files).To(Equal(sequential.Manifest.Files))
|
||||
})
|
||||
|
||||
It("applies live concurrency updates to subsequent materializations", func() {
|
||||
snapshot, server, peak := shardedSnapshot(8, 40*time.Millisecond)
|
||||
DeferCleanup(server.Close)
|
||||
manager := modelartifacts.NewManager(&fakeSnapshotResolver{snapshot: snapshot})
|
||||
manager.SetDownloadConcurrency(4)
|
||||
|
||||
_, err := manager.Ensure(context.Background(), GinkgoT().TempDir(), spec)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(*peak).To(BeNumerically(">", 1))
|
||||
Expect(*peak).To(BeNumerically("<=", 4))
|
||||
})
|
||||
|
||||
It("still resumes past files an interrupted pass already completed", func() {
|
||||
snapshot, server, _ := shardedSnapshot(6, 0)
|
||||
DeferCleanup(server.Close)
|
||||
|
||||
var requests atomic.Int32
|
||||
counting := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests.Add(1)
|
||||
server.Config.Handler.ServeHTTP(w, r)
|
||||
}))
|
||||
DeferCleanup(counting.Close)
|
||||
for i := range snapshot.Files {
|
||||
snapshot.Files[i].URL = counting.URL + snapshot.Files[i].URL[len(server.URL):]
|
||||
}
|
||||
|
||||
manager := modelartifacts.NewManager(&fakeSnapshotResolver{snapshot: snapshot},
|
||||
modelartifacts.WithDownloadConcurrency(3))
|
||||
modelsPath := GinkgoT().TempDir()
|
||||
|
||||
first, err := manager.Ensure(context.Background(), modelsPath, spec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(requests.Load()).To(Equal(int32(len(snapshot.Files))))
|
||||
|
||||
// A committed artifact is served from cache without touching the network.
|
||||
second, err := manager.Ensure(context.Background(), modelsPath, first.Spec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(second.CacheHit).To(BeTrue())
|
||||
Expect(requests.Load()).To(Equal(int32(len(snapshot.Files))))
|
||||
})
|
||||
|
||||
It("fails the whole materialization when a shard cannot be verified", func() {
|
||||
snapshot, server, _ := shardedSnapshot(6, 0)
|
||||
DeferCleanup(server.Close)
|
||||
// Corrupt one shard's expected digest: the download succeeds, the
|
||||
// per-file SHA check does not.
|
||||
snapshot.Files[3].LFSOID = hex.EncodeToString(make([]byte, 32))
|
||||
|
||||
manager := modelartifacts.NewManager(&fakeSnapshotResolver{snapshot: snapshot},
|
||||
modelartifacts.WithDownloadConcurrency(3))
|
||||
modelsPath := GinkgoT().TempDir()
|
||||
|
||||
_, err := manager.Ensure(context.Background(), modelsPath, spec)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
// Nothing may be published under the final path when a shard failed.
|
||||
entries, readErr := os.ReadDir(filepath.Join(modelsPath, ".artifacts", "huggingface"))
|
||||
if readErr == nil {
|
||||
for _, entry := range entries {
|
||||
_, statErr := os.Stat(filepath.Join(modelsPath, ".artifacts", "huggingface", entry.Name(), "manifest.json"))
|
||||
Expect(statErr).To(HaveOccurred(), "a failed materialization published a manifest")
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -78,6 +78,11 @@ export function inferBackendPath(item) {
|
||||
// via a thin wrapper Makefile. Changes to either dir should retrigger it.
|
||||
return `backend/cpp/turboquant/`;
|
||||
}
|
||||
if (item.dockerfile.endsWith("buun-llama-cpp")) {
|
||||
// buun-llama-cpp is a llama.cpp fork that reuses backend/cpp/llama-cpp
|
||||
// sources via a thin wrapper Makefile. Changes to either dir retrigger it.
|
||||
return `backend/cpp/buun-llama-cpp/`;
|
||||
}
|
||||
if (item.dockerfile.endsWith("bonsai")) {
|
||||
// bonsai is a llama.cpp fork that reuses backend/cpp/llama-cpp sources
|
||||
// via a thin wrapper Makefile. Changes to either dir should retrigger it.
|
||||
@@ -152,7 +157,7 @@ export function backendChanged(backend, pathPrefix, changedFiles) {
|
||||
|
||||
// Fork backends reuse backend/cpp/llama-cpp sources via thin wrappers;
|
||||
// changes to either directory must retrigger their pipelines.
|
||||
return (backend === "turboquant" || backend === "bonsai") &&
|
||||
return (backend === "turboquant" || backend === "buun-llama-cpp" || backend === "bonsai") &&
|
||||
changedFiles.some(file => file.startsWith("backend/cpp/llama-cpp/"));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user