mirror of
https://github.com/mudler/LocalAI.git
synced 2026-04-30 03:55:58 -04:00
* feat(voice-recognition): add /v1/voice/{verify,analyze,embed} + speaker-recognition backend
Audio analog to face recognition. Adds three gRPC RPCs
(VoiceVerify / VoiceAnalyze / VoiceEmbed), their Go service and HTTP
layers, a new FLAG_SPEAKER_RECOGNITION capability flag, and a Python
backend scaffold under backend/python/speaker-recognition/ wrapping
SpeechBrain ECAPA-TDNN with a parallel OnnxDirectEngine for
WeSpeaker / 3D-Speaker ONNX exports.
The kokoros Rust backend gets matching unimplemented trait stubs —
tonic's async_trait has no defaults, so adding an RPC without Rust
stubs breaks the build (same regression fixed by eb01c772 for face).
Swagger, /api/instructions, and the auth RouteFeatureRegistry /
APIFeatures list are updated so the endpoints surface everywhere a
client or admin UI looks.
Assisted-by: Claude:claude-opus-4-7
* feat(voice-recognition): add 1:N identify + register/forget endpoints
Mirrors the face-recognition register/identify/forget surface. New
package core/services/voicerecognition/ carries a Registry interface
and a local-store-backed implementation (same in-memory vector-store
plumbing facerecognition uses, separate instance so the embedding
spaces stay isolated).
Handlers under /v1/voice/{register,identify,forget} reuse
backend.VoiceEmbed to compute the probe vector, then delegate the
nearest-neighbour search to the registry. Default cosine-distance
threshold is tuned for ECAPA-TDNN on VoxCeleb (0.25, EER ~1.9%).
As with the face registry, the current backing is in-memory only — a
pgvector implementation is a future constructor-level swap.
Assisted-by: Claude:claude-opus-4-7
* feat(voice-recognition): gallery, docs, CI and e2e coverage
- backend/index.yaml: speaker-recognition backend entry + CPU and
CUDA-12 image variants (plus matching development variants).
- gallery/index.yaml: speechbrain-ecapa-tdnn (default) and
wespeaker-resnet34 model entries. The WeSpeaker SHA-256 is a
deliberate placeholder — the HF URI must be curl'd and its hash
filled in before the entry installs.
- docs/content/features/voice-recognition.md: API reference + quickstart,
mirrors the face-recognition docs.
- React UI: CAP_SPEAKER_RECOGNITION flag export (consumers follow face's
precedent — no dedicated tab yet).
- tests/e2e-backends: voice_embed / voice_verify / voice_analyze specs.
Helper resolveFaceFixture is reused as-is — the only thing face/voice
share is "download a file into workDir", so no need for a new helper.
- Makefile: docker-build-speaker-recognition + test-extra-backend-
speaker-recognition-{ecapa,all} targets. Audio fixtures default to
VCTK p225/p226 samples from HuggingFace.
- CI: test-extra.yml grows a tests-speaker-recognition-grpc job
mirroring insightface. backend.yml matrix gains CPU + CUDA-12 image
build entries — scripts/changed-backends.js auto-picks these up.
Assisted-by: Claude:claude-opus-4-7
* feat(voice-recognition): wire a working /v1/voice/analyze head
Adds AnalysisHead: a lazy-loading age / gender / emotion inference
wrapper that plugs into both SpeechBrainEngine and OnnxDirectEngine.
Defaults to two open-licence HuggingFace checkpoints:
- audeering/wav2vec2-large-robust-24-ft-age-gender (Apache 2.0) —
age regression + 3-way gender (female / male / child).
- superb/wav2vec2-base-superb-er (Apache 2.0) — 4-way emotion.
Both are optional and degrade gracefully when transformers or the
model can't be loaded — the engine raises NotImplementedError so the
gRPC layer returns 501 instead of a generic 500.
Emotion classes pass through from the model (neutral/happy/angry/sad
on the default checkpoint); the e2e test now accepts any non-empty
dominant gender so custom age_gender_model overrides don't fail it.
Adds transformers to the backend's CPU and CUDA-12 requirements.
Assisted-by: Claude:claude-opus-4-7
* fix(voice-recognition): pin real WeSpeaker ResNet34 ONNX SHA-256
Replaces the placeholder hash in gallery/index.yaml with the actual
SHA-256 (7bb2f06e…) of the upstream
Wespeaker/wespeaker-voxceleb-resnet34-LM ONNX at ~25MB. `local-ai
models install wespeaker-resnet34` now succeeds.
Assisted-by: Claude:claude-opus-4-7
* fix(voice-recognition): soundfile loader + honest analyze default
Two issues surfaced on first end-to-end smoke with the actual backend
image:
1. torchaudio.load in torchaudio 2.8+ requires the torchcodec package
for audio decoding. Switch SpeechBrainEngine._load_waveform to the
already-present soundfile (listed in requirements.txt) plus a numpy
linear resample to 16kHz. Drops a heavy ffmpeg-linked dep and the
codepath we never exercise (torchaudio's ffmpeg backend).
2. The AnalysisHead was defaulting to audeering/wav2vec2-large-robust-
24-ft-age-gender, but AutoModelForAudioClassification silently
mangles that checkpoint — it reports the age head weights as
UNEXPECTED and re-initialises the classifier head with random
values, so the "gender" output is noise and there is no age output
at all. Make age/gender opt-in instead (empty default; users wire
a cleanly-loadable Wav2Vec2ForSequenceClassification checkpoint via
age_gender_model: option). Emotion keeps its working Superb default.
Also broaden _infer_age_gender's tensor-shape handling and catch
runtime exceptions so a dodgy age/gender head never takes down the
whole analyze call.
Docs and README updated to match the new policy.
Verified with the branch-scoped gallery on localhost:
- voice/embed → 192-d ECAPA-TDNN vector
- voice/verify → same-clip dist≈6e-08 verified=true; cross-speaker
dist 0.76–0.99 verified=false (as expected)
- voice/register/identify/forget → round-trip works, 404 on unknown id
- voice/analyze → emotion populated, age/gender omitted (opt-in)
Assisted-by: Claude:claude-opus-4-7
* fix(voice-recognition): real CI audio fixtures + fixture-agnostic verify spec
Two issues surfaced after CI actually ran the speaker-recognition e2e
target (I'd curl-tested against a running server but hadn't run the
make target locally):
1. The default BACKEND_TEST_VOICE_AUDIO_* URLs pointed at
huggingface.co/datasets/CSTR-Edinburgh/vctk paths that return 404
(the dataset is gated). Swap them for the speechbrain test samples
served from github.com/speechbrain/speechbrain/raw/develop/ —
public, no auth, correct 16kHz mono format.
2. The VoiceVerify spec required d(file1,file2) < 0.4, assuming
file1/file2 were same-speaker. The speechbrain samples are three
different speakers (example1/2/5), and there is no easy un-gated
source of true same-speaker audio pairs (VoxCeleb/VCTK/LibriSpeech
are all license- or size-gated for CI use). Replace the ceiling
check with a relative-ordering assertion: d(pair) > d(same-clip)
for both file2 and file3 — that's enough to prove the embeddings
encode speaker info, and it works with any three non-identical
clips. Actual speaker ordering d(1,2) vs d(1,3) is logged but not
asserted.
Local run: 4/4 voice specs pass (Health, LoadModel, VoiceEmbed,
VoiceVerify) on the built backend image. 12 non-voice specs skipped
as expected.
Assisted-by: Claude:claude-opus-4-7
* fix(ci): checkout with submodules in the reusable backend_build workflow
The kokoros Rust backend build fails with
failed to read .../sources/Kokoros/kokoros/Cargo.toml: No such file
because the reusable backend_build.yml workflow's actions/checkout
step was missing `submodules: true`. Dockerfile.rust does `COPY .
/LocalAI`, and without the submodule files the subsequent `cargo
build` can't find the vendored Kokoros crate.
The bug pre-dates this PR — scripts/changed-backends.js only triggers
the kokoros image job when something under backend/rust/kokoros or
the shared proto changes, so master had been coasting past it. The
voice-recognition proto addition re-broke it.
Other checkouts in backend.yml (llama-cpp-darwin) and test-extra.yml
(insightface, kokoros, speaker-recognition) already pass
`submodules: true`; this brings the shared backend image builder in
line.
Assisted-by: Claude:claude-opus-4-7
260 lines
9.0 KiB
YAML
260 lines
9.0 KiB
YAML
---
|
|
name: 'build backend container images (reusable)'
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
base-image:
|
|
description: 'Base image'
|
|
required: true
|
|
type: string
|
|
build-type:
|
|
description: 'Build type'
|
|
default: ''
|
|
type: string
|
|
cuda-major-version:
|
|
description: 'CUDA major version'
|
|
default: "12"
|
|
type: string
|
|
cuda-minor-version:
|
|
description: 'CUDA minor version'
|
|
default: "1"
|
|
type: string
|
|
platforms:
|
|
description: 'Platforms'
|
|
default: ''
|
|
type: string
|
|
tag-latest:
|
|
description: 'Tag latest'
|
|
default: ''
|
|
type: string
|
|
tag-suffix:
|
|
description: 'Tag suffix'
|
|
default: ''
|
|
type: string
|
|
runs-on:
|
|
description: 'Runs on'
|
|
required: true
|
|
default: ''
|
|
type: string
|
|
backend:
|
|
description: 'Backend to build'
|
|
required: true
|
|
type: string
|
|
context:
|
|
description: 'Build context'
|
|
required: true
|
|
type: string
|
|
dockerfile:
|
|
description: 'Build Dockerfile'
|
|
required: true
|
|
type: string
|
|
skip-drivers:
|
|
description: 'Skip drivers'
|
|
default: 'false'
|
|
type: string
|
|
ubuntu-version:
|
|
description: 'Ubuntu version'
|
|
required: false
|
|
default: '2204'
|
|
type: string
|
|
amdgpu-targets:
|
|
description: 'AMD GPU targets for ROCm/HIP builds'
|
|
required: false
|
|
default: 'gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1151,gfx1200,gfx1201'
|
|
type: string
|
|
secrets:
|
|
dockerUsername:
|
|
required: false
|
|
dockerPassword:
|
|
required: false
|
|
quayUsername:
|
|
required: true
|
|
quayPassword:
|
|
required: true
|
|
|
|
jobs:
|
|
backend-build:
|
|
runs-on: ${{ inputs.runs-on }}
|
|
env:
|
|
quay_username: ${{ secrets.quayUsername }}
|
|
steps:
|
|
|
|
|
|
- name: Free Disk Space (Ubuntu)
|
|
if: inputs.runs-on == 'ubuntu-latest'
|
|
uses: jlumbroso/free-disk-space@main
|
|
with:
|
|
# this might remove tools that are actually needed,
|
|
# if set to "true" but frees about 6 GB
|
|
tool-cache: true
|
|
# all of these default to true, but feel free to set to
|
|
# "false" if necessary for your workflow
|
|
android: true
|
|
dotnet: true
|
|
haskell: true
|
|
large-packages: true
|
|
docker-images: true
|
|
swap-storage: true
|
|
|
|
- name: Force Install GIT latest
|
|
run: |
|
|
sudo apt-get update \
|
|
&& sudo apt-get install -y software-properties-common \
|
|
&& sudo apt-get update \
|
|
&& sudo add-apt-repository -y ppa:git-core/ppa \
|
|
&& sudo apt-get update \
|
|
&& sudo apt-get install -y git
|
|
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: true
|
|
|
|
- name: Release space from worker
|
|
if: inputs.runs-on == 'ubuntu-latest'
|
|
run: |
|
|
echo "Listing top largest packages"
|
|
pkgs=$(dpkg-query -Wf '${Installed-Size}\t${Package}\t${Status}\n' | awk '$NF == "installed"{print $1 "\t" $2}' | sort -nr)
|
|
head -n 30 <<< "${pkgs}"
|
|
echo
|
|
df -h
|
|
echo
|
|
sudo apt-get remove -y '^llvm-.*|^libllvm.*' || true
|
|
sudo apt-get remove --auto-remove android-sdk-platform-tools snapd || true
|
|
sudo apt-get purge --auto-remove android-sdk-platform-tools snapd || true
|
|
sudo rm -rf /usr/local/lib/android
|
|
sudo apt-get remove -y '^dotnet-.*|^aspnetcore-.*' || true
|
|
sudo rm -rf /usr/share/dotnet
|
|
sudo apt-get remove -y '^mono-.*' || true
|
|
sudo apt-get remove -y '^ghc-.*' || true
|
|
sudo apt-get remove -y '.*jdk.*|.*jre.*' || true
|
|
sudo apt-get remove -y 'php.*' || true
|
|
sudo apt-get remove -y hhvm powershell firefox monodoc-manual msbuild || true
|
|
sudo apt-get remove -y '^google-.*' || true
|
|
sudo apt-get remove -y azure-cli || true
|
|
sudo apt-get remove -y '^mongo.*-.*|^postgresql-.*|^mysql-.*|^mssql-.*' || true
|
|
sudo apt-get remove -y '^gfortran-.*' || true
|
|
sudo apt-get remove -y microsoft-edge-stable || true
|
|
sudo apt-get remove -y firefox || true
|
|
sudo apt-get remove -y powershell || true
|
|
sudo apt-get remove -y r-base-core || true
|
|
sudo apt-get autoremove -y
|
|
sudo apt-get clean
|
|
echo
|
|
echo "Listing top largest packages"
|
|
pkgs=$(dpkg-query -Wf '${Installed-Size}\t${Package}\t${Status}\n' | awk '$NF == "installed"{print $1 "\t" $2}' | sort -nr)
|
|
head -n 30 <<< "${pkgs}"
|
|
echo
|
|
sudo rm -rfv build || true
|
|
sudo rm -rf /usr/share/dotnet || true
|
|
sudo rm -rf /opt/ghc || true
|
|
sudo rm -rf "/usr/local/share/boost" || true
|
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY" || true
|
|
df -h
|
|
|
|
- name: Docker meta
|
|
id: meta
|
|
if: github.event_name != 'pull_request'
|
|
uses: docker/metadata-action@v6
|
|
with:
|
|
images: |
|
|
quay.io/go-skynet/local-ai-backends
|
|
localai/localai-backends
|
|
tags: |
|
|
type=ref,event=branch
|
|
type=semver,pattern={{raw}}
|
|
type=sha
|
|
flavor: |
|
|
latest=${{ inputs.tag-latest }}
|
|
suffix=${{ inputs.tag-suffix }},onlatest=true
|
|
|
|
- name: Docker meta for PR
|
|
id: meta_pull_request
|
|
if: github.event_name == 'pull_request'
|
|
uses: docker/metadata-action@v6
|
|
with:
|
|
images: |
|
|
quay.io/go-skynet/ci-tests
|
|
tags: |
|
|
type=ref,event=branch,suffix=${{ github.event.number }}-${{ inputs.backend }}-${{ inputs.build-type }}-${{ inputs.cuda-major-version }}-${{ inputs.cuda-minor-version }}
|
|
type=semver,pattern={{raw}},suffix=${{ github.event.number }}-${{ inputs.backend }}-${{ inputs.build-type }}-${{ inputs.cuda-major-version }}-${{ inputs.cuda-minor-version }}
|
|
type=sha,suffix=${{ github.event.number }}-${{ inputs.backend }}-${{ inputs.build-type }}-${{ inputs.cuda-major-version }}-${{ inputs.cuda-minor-version }}
|
|
flavor: |
|
|
latest=${{ inputs.tag-latest }}
|
|
suffix=${{ inputs.tag-suffix }},onlatest=true
|
|
## End testing image
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@master
|
|
with:
|
|
platforms: all
|
|
|
|
- name: Set up Docker Buildx
|
|
id: buildx
|
|
uses: docker/setup-buildx-action@master
|
|
|
|
- name: Login to DockerHub
|
|
if: github.event_name != 'pull_request'
|
|
uses: docker/login-action@v4
|
|
with:
|
|
username: ${{ secrets.dockerUsername }}
|
|
password: ${{ secrets.dockerPassword }}
|
|
|
|
- name: Login to Quay.io
|
|
if: ${{ env.quay_username != '' }}
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: quay.io
|
|
username: ${{ secrets.quayUsername }}
|
|
password: ${{ secrets.quayPassword }}
|
|
|
|
- name: Build and push
|
|
uses: docker/build-push-action@v7
|
|
if: github.event_name != 'pull_request'
|
|
with:
|
|
builder: ${{ steps.buildx.outputs.name }}
|
|
build-args: |
|
|
BUILD_TYPE=${{ inputs.build-type }}
|
|
SKIP_DRIVERS=${{ inputs.skip-drivers }}
|
|
CUDA_MAJOR_VERSION=${{ inputs.cuda-major-version }}
|
|
CUDA_MINOR_VERSION=${{ inputs.cuda-minor-version }}
|
|
BASE_IMAGE=${{ inputs.base-image }}
|
|
BACKEND=${{ inputs.backend }}
|
|
UBUNTU_VERSION=${{ inputs.ubuntu-version }}
|
|
AMDGPU_TARGETS=${{ inputs.amdgpu-targets }}
|
|
context: ${{ inputs.context }}
|
|
file: ${{ inputs.dockerfile }}
|
|
cache-from: type=gha
|
|
platforms: ${{ inputs.platforms }}
|
|
push: ${{ github.event_name != 'pull_request' }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
|
|
- name: Build and push (PR)
|
|
uses: docker/build-push-action@v7
|
|
if: github.event_name == 'pull_request'
|
|
with:
|
|
builder: ${{ steps.buildx.outputs.name }}
|
|
build-args: |
|
|
BUILD_TYPE=${{ inputs.build-type }}
|
|
SKIP_DRIVERS=${{ inputs.skip-drivers }}
|
|
CUDA_MAJOR_VERSION=${{ inputs.cuda-major-version }}
|
|
CUDA_MINOR_VERSION=${{ inputs.cuda-minor-version }}
|
|
BASE_IMAGE=${{ inputs.base-image }}
|
|
BACKEND=${{ inputs.backend }}
|
|
UBUNTU_VERSION=${{ inputs.ubuntu-version }}
|
|
AMDGPU_TARGETS=${{ inputs.amdgpu-targets }}
|
|
context: ${{ inputs.context }}
|
|
file: ${{ inputs.dockerfile }}
|
|
cache-from: type=gha
|
|
platforms: ${{ inputs.platforms }}
|
|
push: ${{ env.quay_username != '' }}
|
|
tags: ${{ steps.meta_pull_request.outputs.tags }}
|
|
labels: ${{ steps.meta_pull_request.outputs.labels }}
|
|
|
|
|
|
|
|
- name: job summary
|
|
run: |
|
|
echo "Built image: ${{ steps.meta.outputs.labels }}" >> $GITHUB_STEP_SUMMARY
|