Compare commits

...
Author SHA1 Message Date
localai-org-maint-bot 67db320ee6 fix(audio-cpp): forward voice reference transcripts
Saved voices send ref_text, but Fish Audio requires reference_text.
Derive the canonical parameter while preserving explicit overrides.
Both TTS modes use the shared builder.

Add regression cases and document the parameter alias.

Assisted-by: Codex:gpt-6
2026-09-12 16:06:11 +00:00
Ettore Di Giacinto 8321234275 fix(agents): pass pool limits to the LocalAGI agent pool
The LocalAGI bump in #11985 changed state.NewAgentPool to take a
SkillsProvider and a PoolLimits value. The call site here was not
updated, so master stopped compiling and every Go job went red.

Pass the limits explicitly, mirroring LocalAGI's own defaults, so the
pool prunes conversation dumps and scheduler run history instead of
growing without end.

Assisted-by: Claude Code:claude-opus-5 [Bash] [Edit]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-12 08:15:23 +00:00
Ettore Di Giacinto 64628da9dd chore(deps): bump llvmlite 0.43→0.49, numba 0.60→0.67, numpy>=2.5.2 in transformers
Merge dependabot PRs #11639, #11640, #11641 together:
- #11639: llvmlite 0.43.0 → 0.49.0 (alone fails: numba 0.60 requires <0.44)
- #11640: numba 0.60.0 → 0.67.0 (alone fails: requires llvmlite >=0.49)
- #11641: numpy >=2.0.0 → >=2.5.2 (was blocked by scipy 1.15.1 <2.5, now
  resolved since scipy was bumped to 1.18.0 in #11399)

Also bump outetts backend to match (same numba/llvmlite conflict).

Signed-off-by: localai-org-maint-bot <bot@localai.io>
Assisted-by: MAKI:GLM5.2 [gh]
2026-09-11 23:34:59 +00:00
Ettore Di Giacinto 1dfa25e681 fix(qwen-tts): limit CUDA compiler threads
The CUDA 13 FlashAttention build still exhausts hosted-runner memory with a single ninja worker because nvcc can compile multiple threads internally. Limit nvcc to one thread for that profile and guard the setting in the backend test script.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 23:19:28 +00:00
Ettore Di Giacinto 43c54bc9ba fix(llama-cpp): stop a generation whose stream is gone
grpc::ServerWriter::Write() returns false once the peer is gone, and
PredictStream ignored that result at every call site. The handler kept
pulling decoded tokens and writing them into a dead stream, so the
llama.cpp slot stayed busy until the generation ended on its own terms.

A model configured with max_tokens 0 and a large context ends on its own
terms only at the context limit. On a 35B model at ~41 t/s a 120k context
is about fifty minutes, and a slot held that long is a slot every other
request for that model queues behind. Two abandoned requests were enough
to make a node with free VRAM and a healthy control plane serve nothing:
new requests timed out waiting for a slot, each timeout abandoned another
generation, and the node fell further behind the longer it ran.

Track the peer instead. The first failed write retires it for good, since
a stream never recovers, and the RPC's own cancellation flag folds into
the same predicate so the loop has one condition to test. Returning early
is what frees the slot: ~server_response_reader() posts
SERVER_TASK_TYPE_CANCEL for whatever is still decoding.

TTSStream already checked Write(); this brings PredictStream in line.
Cancellation stays cooperative and is checked between decoded results, so
a batch already in flight may finish before the request stops.

Assisted-by: Claude:claude-opus-5
2026-09-11 23:13:32 +00:00
Adira Denis Muhando 869000ceb7 fix(distributed): exclude pinned models from cluster eviction and idle scale-down
pinned: true was only honoured by the per-node watchdog. Every distributed
eviction path was pinned-blind: the router's LRU eviction (EvictLRU,
evictLRUAndFreeNode) and the replica reconciler's idle scale-down would
happily unload a pinned model — and since eviction is gated on
in_flight = 0, a pinned model became eviction-eligible the instant each
response completed. Under capacity pressure that surfaces as the backend
being freed immediately after every request (#11101).

Wire the model config loader into the router and reconciler through a new
PinnedModelResolver seam (mirroring ConcurrencyConflictResolver):

- EvictLRU passes the pinned set into FindLRUModel's query so the
  next-oldest unpinned model is selected instead of the attempt failing
- evictLRUAndFreeNode filters pinned models inside its locked selection
- scaleDownIdle skips pinned models entirely: trimming to the floor still
  means requests beyond the survivor's capacity pay a cold reload

Deliberate teardown (admin unload, model delete, node drain) intentionally
still applies to pinned models, as does dead-row reaping (state correction,
not eviction).

Regression specs verified to fail with the exclusion disabled.

Addresses the cluster-side eviction gap in #11101

Assisted-by: Claude Code:claude-fable-5 [Claude Code]
Signed-off-by: Adira Denis Muhando <dennisadira@gmail.com>
2026-09-11 23:08:26 +00:00
dependabot[bot] 40c4d5990e chore(deps): bump scipy in /backend/python/transformers
Bumps [scipy](https://github.com/scipy/scipy) from 1.15.1 to 1.18.0.
- [Release notes](https://github.com/scipy/scipy/releases)
- [Commits](https://github.com/scipy/scipy/compare/v1.15.1...v1.18.0)

---
updated-dependencies:
- dependency-name: scipy
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-11 23:06:18 +00:00
localai-org-maint-bot 7dc617c7c1 fix(transformers): use Python 3.12
SciPy 1.18 requires Python 3.12 or newer. Keep this backend on a
compatible portable Python for Linux and macOS builds.

Assisted-by: Codex:gpt-5.6 [Codex]
(cherry picked from commit 70bf6d4a3a)
2026-09-11 23:05:33 +00:00
81041d8b1b chore(deps): bump LocalAGI and localrecall to v0.6.5 (#11985)
* chore(deps): bump github.com/mudler/localrecall to v0.6.5

Picks up two Postgres engine fixes: the RRF fusion no longer scores
every hybrid-search candidate 0 through integer division, and the
search_vector text config is no longer pinned to 'simple' for the life
of the process after one transient lookup failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANmTgdYikmzBq67jVJ1CgX

* chore(deps): bump github.com/mudler/LocalAGI to d93d478

Picks up mudler/LocalAGI#493, which bumps localrecall to v0.6.5 there
too, so the direct pin in this module and the version arriving through
LocalAGI agree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANmTgdYikmzBq67jVJ1CgX

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 01:05:13 +02:00
dependabot[bot] 1f3d89fc4a chore(deps): update transformers requirement
Updates the requirements on [transformers](https://github.com/huggingface/transformers) to permit the latest version.
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v5.15.0...v5.15.1)

---
updated-dependencies:
- dependency-name: transformers
  dependency-version: 5.15.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-11 22:51:12 +00:00
Ettore Di Giacinto c9acb41903 fix(gallery): use tokenizer templates for Gemma
Let the model-provided tokenizer template format Gemma conversations instead of maintaining a shared inline prompt template.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 22:44:39 +00:00
Ettore Di Giacinto db12122329 fix(reasoning): ignore preclosed prompt markers
Do not seed streaming reasoning state when the latest prompt thinking marker is already followed by its matching closing marker. This keeps direct Gemma 4 output in content when its template disables thinking with a preclosed channel.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 22:44:39 +00:00
Ettore Di Giacinto bbd5488871 fix(distributed): make backend.stop report what it stopped
backend.stop was the one lifecycle subject a worker never answered. The
controller published and returned nil as soon as the local publish
succeeded, so a stop that killed nothing, and a stop that failed
outright, were indistinguishable from one that worked.

The unload endpoint calls model.unload and then StopBackend. Only the
first is acknowledged, so the endpoint answered 200 while the backend
kept running and held its VRAM, and its own "backend stop failed" branch
could never run. The worker logged the failure and nobody saw it.

Give the subject a reply. The worker now enumerates the process keys it
terminated and reports any per-process error, so StopBackend fails when
the stop failed. Resolving to nothing stays a success: stopping a backend
that is not running leaves the caller in the state it asked for, and
eviction paths stop already-gone models routinely. The empty list is what
says nothing matched, and ReportsStoppedProcesses is what makes that
emptiness trustworthy, the same way BackendDeleteReply handles it.

A worker built before this reply still receives the request and still
stops the backend, it only stays silent, so a timeout degrades to the old
assumption rather than failing every stop on a fleet mid-upgrade. Only
silence degrades: a transport error is still reported, because
UnloadRemoteModel skips its registry cleanup for a node it could not
reach and needs to keep hearing about that.

Assisted-by: Claude:claude-opus-5 golangci-lint
2026-09-11 22:44:11 +00:00
Ettore Di Giacinto 51c0a44bca gallery: apply PR #11574
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:43:46 +00:00
Ettore Di Giacinto d385a0bf8e gallery: apply PR #11494
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:43:42 +00:00
Ettore Di Giacinto a398c559b3 fix(gallery): persist inference defaults under parameters
Gallery installs merged family defaults at the YAML root and only re-marshaled them on the artifact path. Persist the defaults in the loader-visible parameters map for every install path while preserving authored overrides.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 22:43:10 +00:00
Ettore Di Giacinto 14103f1d78 gallery: apply PR #11983
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:56 +00:00
Ettore Di Giacinto 81f6898b69 gallery: apply PR #11960
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:52 +00:00
Ettore Di Giacinto c99d0d44cd gallery: apply PR #11958
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:48 +00:00
Ettore Di Giacinto ada207679a gallery: apply PR #11951
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:43 +00:00
Ettore Di Giacinto f528bf07dd gallery: apply PR #11949
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:38 +00:00
Ettore Di Giacinto c7ff1aabf0 gallery: apply PR #11947
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:33 +00:00
Ettore Di Giacinto 8b003539db gallery: apply PR #11946
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:29 +00:00
Ettore Di Giacinto d95b5d787f gallery: apply PR #11943
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:24 +00:00
Ettore Di Giacinto 9f100e9062 gallery: apply PR #11930
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:19 +00:00
Ettore Di Giacinto 08a87788b4 gallery: apply PR #11927
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:15 +00:00
Ettore Di Giacinto ab191912e9 gallery: apply PR #11926
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:11 +00:00
Ettore Di Giacinto 16fb9e81bd gallery: apply PR #11923
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:06 +00:00
Ettore Di Giacinto 075d0d3fa4 gallery: apply PR #11922
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:37:00 +00:00
Ettore Di Giacinto 9401844c6b gallery: apply PR #11913
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:36:55 +00:00
Ettore Di Giacinto 43cf7ed77f gallery: apply PR #11909
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:36:50 +00:00
Ettore Di Giacinto 29347bad67 gallery: apply PR #11905
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:36:46 +00:00
Ettore Di Giacinto 5a62ed1614 gallery: apply PR #11900
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:36:40 +00:00
Ettore Di Giacinto b9902148ae gallery: apply PR #11873
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:36:35 +00:00
Ettore Di Giacinto d81d7f821e gallery: apply PR #11841
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:36:30 +00:00
Ettore Di Giacinto c70e18392e gallery: apply PR #11832
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:36:26 +00:00
Ettore Di Giacinto 9e86521698 gallery: apply PR #11705
Assisted-by: localai-org-maint-bot:glm5.2 [gh]
2026-09-11 22:36:21 +00:00
Ettore Di Giacintoandxingyifeng df70ff311d feat: add FunASR speech recognition backend (#10090)
Adds FunASR/SenseVoice as a Python backend for speech-to-text with
support for CPU, CUDA 12/13, ROCm, Intel SYCL, L4T, and Apple MPS.

Co-authored-by: xingyifeng <xingyifeng@users.noreply.github.com>
2026-09-11 22:06:26 +00:00
Ettore Di Giacinto 6983477a71 fix(agent-ui): keep chat open for status
Agent Status replaced the chat route and unmounted its EventSource. Any response still in flight could then disappear from the conversation.\n\nOpen status in a separate tab so the chat keeps its live connection until the response completes.\n\nAssisted-by: Codex:gpt-5 [eslint]

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:56:08 +00:00
Ettore Di Giacinto db09452d54 feat(tts): support multi-reference personalities
Saved profiles previously resolved to one audio path and transcript, so
cloning backends could not use several examples of one personality.

Store ordered audio and transcript pairs while preserving the legacy
first-reference fields. Fish Speech and audio.cpp receive all pairs,
including on distributed workers. Other backends retain their
single-reference behavior.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:56:08 +00:00
Ettore Di Giacinto ddda0436a3 fix(ui): exclude navigation from the TTS mock
The API mock also matched navigation to /app/tts and returned a WAV download instead of the React page. Let non-POST requests reach the test server.

Assisted-by: Codex:gpt-5 [Playwright]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:56:02 +00:00
Ettore Di Giacinto 1bcbff586b fix(ui): target the TTS input in history tests
TTS instructions add a second textarea to the page. Target the speech input by its placeholder so the history test does not depend on the page having one textarea.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:56:02 +00:00
Ettore Di Giacinto 919b5c96fa feat(ui): add per-request TTS instructions
Let studio users guide speech delivery for backends that support request instructions. Blank guidance stays out of requests and media history.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:56:02 +00:00
Ettore Di Giacinto d55474a149 feat(audio): list available TTS voices
Clients cannot discover the named voices that an installed TTS model accepts without consulting backend-specific documentation. Expose voice metadata through the audio API and let custom model configs declare their own catalog.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:55:21 +00:00
Ettore Di Giacinto 21e81434b3 fix(llama-cpp): stage model load diagnostics
The generated gRPC source tree omitted the new header and test. Every llama.cpp-derived backend therefore failed when grpc-server.cpp included the missing header.

Assisted-by: Codex:gpt-5.6 [systematic-debugging]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:55:14 +00:00
Ettore Di Giacinto a6519182db fix(llama-cpp): explain tensor count mismatch
llama.cpp reports the same tensor-count error for unsupported model layouts and damaged GGUF files. Add a focused hint so operators can update the backend or verify the model without losing the upstream diagnostic.

Assisted-by: Codex:gpt-5.6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:55:14 +00:00
Ettore Di Giacinto cce7a9e441 fix(fish-speech): resolve relocated source
The editable install records the backend build path, which does not exist after LocalAI relocates the packaged backend. Add the runtime source directory to PYTHONPATH so inference modules remain importable.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:55:10 +00:00
Ettore Di Giacinto f5eb09f329 fix(audio-cpp): bundle rocRoller for ROCm
ROCm 7.2 links rocBLAS consumers to librocroller.so.1. Add that runtime family to the ROCm bundle so packaged backends resolve the dependency.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:53:43 +00:00
Ettore Di Giacinto bfcf1e37ec fix(audio-cpp): install rocBLAS headers
The HIP build reaches ggml configuration and requires the rocBLAS CMake package. Install its development package with the existing hipBLAS dependency.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:53:43 +00:00
Ettore Di Giacinto db0b00a5ac fix(audio-cpp): normalize HIP target list
audio.cpp forwards GPU_TARGETS to CMake as a semicolon-delimited list. The comma-delimited LocalAI value was treated as one invalid HIP architecture during configuration.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:53:43 +00:00
Ettore Di Giacinto 70be2c49a1 fix(audio-cpp): install hipBLAS headers
The ROCm builder lacks the CMake package metadata that ggml requires. Install the development package only for hipBLAS builds.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:53:43 +00:00
Ettore Di Giacinto 878b99384f feat(audio-cpp): add ROCm backend image
The pinned audio.cpp revision supports HIP, but LocalAI neither builds a ROCm image nor accepts its backend option. AMD hosts therefore fall back to the CPU image.

Build and publish the HIP variant, connect it to AMD capability selection, and accept both upstream HIP names.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:53:43 +00:00
Ettore Di Giacinto a15780858e feat(diffusers): add AudioLDM2 generation
Expose diffusers audio pipelines through the existing sound-generation RPC. AudioLDM2 can now return PCM WAV output from the model gallery without a separate backend.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:53:37 +00:00
Ettore Di Giacinto 783556bc93 feat(prefixcache): index reported KV residency
Add a NATS event contract and exact-residency provider for backend KV cache reports. Keep guessed request observations as the default routing source while maintaining the reported index for future producers.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:53:33 +00:00
Ettore Di Giacinto f884d4b456 fix(mlx-video): use supported Python on Darwin
The pinned mlx-video package requires Python 3.11 or newer, while an empty PYTHON_VERSION selected the backend helper default of 3.10. Pin the available 3.11.13 portable runtime for the Darwin package build.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:52:41 +00:00
Ettore Di Giacinto 21df4a120a feat(mlx): add Apple Silicon video backend
Add a Darwin-only MLX-Video backend for LTX-2 and converted Wan checkpoints, expose it through the existing video API, and wire packaging, discovery, tests, docs, and an example.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:52:41 +00:00
Ettore Di Giacinto 522644e4b6 docs: add RKLLM Rockchip NPU integration
Document how to connect LocalAI to the upstream RKLLM OpenAI-compatible server through cloud-proxy, including supported SoCs and current limitations.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:49:51 +00:00
Ettore Di Giacinto f5ea6e4eab fix(fish-speech): use CUDA toolkit ptxas
Prefer an explicitly configured Triton assembler, otherwise use the executable ptxas from CUDA_HOME so torch.compile can target GPU architectures newer than Triton bundled tooling.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:48:54 +00:00
Ettore Di Giacinto f8e228e0ba fix(fish-speech): support CUDA 13 on arm64
Keep the relocated upstream source importable, select CUDA 13 PyTorch wheels instead of the aarch64 CPU fallback, and decode reference audio without torchcodec, which has no Linux arm64 wheels.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:48:07 +00:00
Ettore Di Giacinto 3535675fd4 fix(whisper-medusa): pin available portable Python
The standalone Python release used by libbackend does not publish a 3.11.18 artifact. Pin Whisper-Medusa to the available 3.11.13 build and cover the generated download URL.

Assisted-by: Codex:gpt-5 [systematic-debugging]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:48:07 +00:00
Ettore Di Giacinto cec62c3dd3 feat(backends): add Whisper-Medusa transcription
Add a dedicated Python gRPC backend for aiola Whisper-Medusa checkpoints, including mono 16 kHz preprocessing, bounded clip validation, CPU/CUDA 12 images, backend gallery metadata, and user documentation.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:48:07 +00:00
Ettore Di Giacinto a64fc865ba fix(distributed): aggregate prefix cache pressure
Broadcast forced-disturb events and successful scale-up resets so every frontend shares the same rolling autoscale signal. Deduplicate NATS echoes, expose an origin-only Prometheus counter, and document cluster behavior.

Closes #10083

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:48:07 +00:00
Ettore Di Giacinto 3e4a44be9d fix(diffusers): forward original config for single files
Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:47:55 +00:00
Ettore Di Giacinto 419375baae fix(config): register original diffusers config field
Expose original_config_file through the model configuration metadata registry so registry completeness checks and generated configuration surfaces stay in sync.

Assisted-by: Codex:gpt-5 [systematic-debugging]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:47:12 +00:00
Ettore Di Giacinto a9f8062dd5 feat(nodes): add pluggable routing pipeline
Route prefix-cache replica selection through composable filters, weighted scorers, and a replaceable picker. Preserve the existing load guard and deterministic selection policy while exposing per-model scorer weights through scheduling configuration.

Assisted-by: Codex:gpt-5 [go-vet]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:47:08 +00:00
Ettore Di Giacinto 155804e144 docs: clarify model configuration precedence
Assisted-by: Codex:gpt-5 [Codex]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 21:40:10 +00:00
Andreas Egliandlocalai-org-maint-bot 9803034e21 fix(faster-whisper): manually install ctranslate2 with rocm support before installing other dependencies (#9839)
* fix(faster-whisper): manually install ctranslate2 with rocm support before installing other dependencies

Signed-off-by: Andreas Egli <github@kharan.ch>

* feat(faster-whisper): wire version into bump-deps workflow

Assisted-by: opencode:gpt-5.5
Signed-off-by: Andreas Egli <github@kharan.ch>

---------

Signed-off-by: Andreas Egli <github@kharan.ch>
Co-authored-by: localai-org-maint-bot <bot-opensource@localaisrl.com>
2026-09-11 18:11:51 +02:00
9bd7d17ff6 [model-config] feat: add environment variables support for backends (#10721)
* feat: add environment variables support for backends in model configurations

- Add field to model configuration to pass environment variables to backend processes
- Update backend options and model configuration handling
- Add documentation for environment variables configuration with examples including CUDA_VISIBLE_DEVICES

Assisted-by: qwen-agentworld-35b-a3b
Signed-off-by: nold <nold42@pm.me>

* fix(test):  Test environment variables configuration parsing from YAML

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: nold <Nold360@users.noreply.github.com>

---------

Signed-off-by: nold <nold42@pm.me>
Signed-off-by: nold <Nold360@users.noreply.github.com>
Co-authored-by: nold <nold42@pm.me>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-09-11 18:02:08 +02:00
Shayan khan 864db8e3d3 fix(backends): honor enable_thinking=false in mlx and vllm-omni (#11962)
The mlx, mlx-vlm, mlx-distributed and vllm-omni backends only forwarded
enable_thinking when the metadata value was "true". A "false" value never
reached apply_chat_template, so requests with thinking disabled (for
example a realtime pipeline with disable_thinking: true) still used the
chat template default. Apply the same coerce that #11715 added to sglang
and vllm.

Assisted-by: Claude:claude-opus-5

Signed-off-by: devv-shayan <shayankhanx1x@gmail.com>
2026-09-11 18:01:51 +02:00
Leoy aeac5e86dc fix(auth): bypass API-key auth for CORS preflight (OPTIONS) requests (#11113)
* fix(auth): bypass API-key auth for CORS preflight (OPTIONS) requests

When API-key auth is enabled, a browser making a cross-origin API call
first sends an OPTIONS CORS preflight, which cannot carry credentials by
HTTP spec. The auth middleware is registered (app.go:324) before the CORS
middleware (app.go:337-347), so the preflight hit auth first and returned
401 before the CORS middleware could answer it, blocking the actual call.

Bypass auth for OPTIONS so the request reaches the CORS middleware, which
answers the preflight with 200 + headers. Real API requests (GET/POST/etc.)
still require auth. Regression test added (red on master, green on branch).

Refs #4576

Signed-off-by: supermario_leo <leo.stack@outlook.com>

* fix(auth): exempt CORS preflights via publicRouteRegistry instead of middleware bypass

Route the global OPTIONS exemption through publicRouteRegistry
(OPTIONS on every path, replacing the OPTIONS-under-/api/auth/ rule
it subsumes) instead of a hardcoded method check inside Middleware,
so "which requests skip auth" has one mechanism. Preflights now flow
through the same authenticate-then-public-rules path as other public
routes, which also lets a credentialed OPTIONS request keep its user
context.

Update the route-coverage allowlist and the near-prefix lookalike
table for the new semantics (OPTIONS is public on every path by
design; near-prefix privacy stays pinned by the non-OPTIONS entries),
and fix the authentication docs' exempt-route enumeration, which
still described OPTIONS as an /api/auth/-only exemption.

Signed-off-by: supermario_leo <leo.stack@outlook.com>

---------

Signed-off-by: supermario_leo <leo.stack@outlook.com>
2026-09-11 17:57:09 +02:00
Som Samantray d27fb3202c fix(whisper): honour positional listen address argument (#11652)
* fix(whisper): honour positional listen address argument

The whisper backend parsed its gRPC listen address exclusively through
Go's flag package, while run.sh forwards launcher arguments verbatim.
A bare positional address was silently dropped by flag.Parse(), so the
server always bound the default localhost:50051 instead of the port its
caller allocated — LocalAI then failed to reach it with a misleading
'error reading from server: EOF'.

Fall back to the first positional argument when no explicit -addr value
was given, keeping the default for no-argument launches.

Fixes #11623

Assisted-by: ox-alpha:ox-alpha [go test]

Signed-off-by: Som Samantray <som.samantray@gmail.com>

* fix(whisper): track explicit -addr via flag.Visit and adopt Ginkgo test style

Review follow-up:
- Detect an explicitly set -addr with flag.FlagSet.Visit instead of
  comparing against the default sentinel, so '-addr localhost:50051'
  plus a positional argument keeps the flag value.
- Treat an explicitly empty -addr as unset rather than binding the
  empty address (OS-chosen port on all interfaces).
- Rewrite addr_test.go as Ginkgo v2 specs per .agents/coding-style.md;
  stdlib t.Run/t.Errorf are forbidden by .golangci.yml forbidigo.

Assisted-by: ox-alpha:ox-alpha [go test]

Signed-off-by: Som Samantray <som.samantray@gmail.com>

---------

Signed-off-by: Som Samantray <som.samantray@gmail.com>
2026-09-11 17:31:43 +02:00
QiuLG e588d463dc fix(ui): preserve percent signs in route parameters (#11883)
fix(ui): preserve decoded route parameters

React Router already decodes dynamic path segments before exposing them through useParams. Decoding those values again crashes pages for names containing a literal percent sign and mutates escape-like substrings.

Use route parameters as-is, encode the model editor API path at the outbound boundary, and cover all affected pages with Playwright.

Fixes #11882

Assisted-by: Codex:gpt-5 eslint playwright

Signed-off-by: QiuLG <l237455523@outlook.com>
2026-09-11 16:54:56 +02:00
Abdullah Mansour | عبد الله منصور acc7284526 fix(openresponses): support Codex WebSocket warm-up (#11608)
* fix(openresponses): support Codex WebSocket warmup

Assisted-by: ChatGPT:GPT-5.6-Sol golangci-lint
Signed-off-by: Abdullah Mansour <abdullahmansour.marketing@gmail.com>

* docs(openresponses): document WebSocket responses

Assisted-by: Codex:GPT-5.6-Sol gh Docker golangci-lint
Signed-off-by: Abdullah Mansour <abdullahmansour.marketing@gmail.com>

* fix(openresponses): harden WebSocket response lifecycle

Ensure response ownership, continuation storage, error sequencing, and connection-local resource limits remain correct across HTTP and WebSocket transports.

Assisted-by: Codex:GPT-5.6-Sol [gh] [Docker] [golangci-lint]
Signed-off-by: Abdullah Mansour <abdullahmansour.marketing@gmail.com>

---------

Signed-off-by: Abdullah Mansour <abdullahmansour.marketing@gmail.com>
2026-09-11 16:54:42 +02:00
localai-org-maint-botandlocalai-org-maint-bot c0993e580a feat(gallery): add Huihui Qwen3.8 Flash Next (#11921)
Add the published UD-Q4_K_XL shards and BF16 vision projector.
Document installation and the default context size.

Assisted-by: Codex:gpt-6

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-09-11 15:27:31 +02:00
localai-org-maint-botandlocalai-org-maint-bot 6fb52d3cb0 feat(gallery): add Gemma 4 E4B HauhauCS variants (#11903)
Add Q4_K_M and Q5_K_M builds with the vision projector and an install
example. Pin downloads to the verified Hugging Face revision.

Assisted-by: Codex:gpt-6

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-09-11 15:27:17 +02:00
1e5b8aeca5 feat(gallery): add Ornith 1.5 35B variants (#11625)
* feat(gallery): add Ornith 1.5 35B variants

Add the supported APEX, MTP, and official Q8_0 builds for the multimodal Ornith 1.5 35B model.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(gallery): restore Ornith 1.5 entries

The 35B insertion split the existing 9B entry and nested its remaining
fields under the final 35B file. Restore the 9B fields before the new
35B entries so the gallery index parses correctly.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-09-11 15:26:50 +02:00
localai-org-maint-botandEttore Di Giacinto b845468a11 fix(ds4): resolve Hugging Face repository imports (#11340)
fix(ds4): resolve repository imports to GGUF

Select a concrete DeepSeek V4 Flash GGUF from Hugging Face metadata while honoring quantization preference order. Reject unresolved repository imports instead of emitting an invalid bare URI.

Assisted-by: Codex:gpt-5

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-11 14:45:03 +02:00
a1c79b517b docs(faq): note OPENAI_BASE_URL env var for pointing clients at LocalAI (#11310)
* docs(faq): note OPENAI_BASE_URL env var for pointing clients at LocalAI

The "Can I use it with a Discord bot, or XXX?" answer says a client can be
pointed at the LocalAI endpoint, but not how. Most OpenAI clients pick the
endpoint up from OPENAI_BASE_URL, and clients that insist on a non-empty API
key accept any placeholder value while api_keys is unset.

Assisted-by: Claude:claude-opus-5
Signed-off-by: seven7763 <seven7763@users.noreply.github.com>

* docs(faq): use Hugo notice shortcode per coding-style

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: seven7763 <seven7763@users.noreply.github.com>

---------

Signed-off-by: seven7763 <seven7763@users.noreply.github.com>
Co-authored-by: seven7763 <seven7763@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-11 14:44:18 +02:00
dependabot[bot] bcf196da3e chore(deps): bump vllm from 0.28.0 to 0.29.0 in /backend/python/vllm (#11961)
Bumps [vllm](https://github.com/vllm-project/vllm) from 0.28.0 to 0.29.0.
- [Release notes](https://github.com/vllm-project/vllm/releases)
- [Changelog](https://github.com/vllm-project/vllm/blob/main/RELEASE.md)
- [Commits](https://github.com/vllm-project/vllm/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: vllm
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-11 13:16:12 +02:00
dependabot[bot] 50fc09848b chore(deps): bump securego/gosec from 2.27.1 to 2.29.0 (#11854)
Bumps [securego/gosec](https://github.com/securego/gosec) from 2.27.1 to 2.29.0.
- [Release notes](https://github.com/securego/gosec/releases)
- [Commits](https://github.com/securego/gosec/compare/v2.27.1...v2.29.0)

---
updated-dependencies:
- dependency-name: securego/gosec
  dependency-version: 2.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-11 13:13:54 +02:00
Matt Van HornandMatt Van Horn 8a0548714d fix: prefer the Intel diffusers venv SYCL runtime at startup (#11971)
Fixes #11895

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-09-11 12:13:03 +02:00
localai-org-maint-botandmudler a8ff0c3a6e chore: ⬆️ Update leejet/stable-diffusion.cpp to b68d58624d227682eb4b95ef8bcf569cd1311eb5 (#11965)
⬆️ Update leejet/stable-diffusion.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-09-11 08:33:29 +02:00
localai-org-maint-botandmudler 78015014a5 chore: ⬆️ Update ggml-org/llama.cpp to df03399b885831b2a1603b3abb0d8c156808e363 (#11966)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-09-11 08:33:12 +02:00
localai-org-maint-botandmudler 463a5e019a chore: ⬆️ Update ikawrakow/ik_llama.cpp to 3bb386eb68ffee0a5dc7db21da0735d594929eeb (#11967)
⬆️ Update ikawrakow/ik_llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-09-11 08:32:59 +02:00
localai-org-maint-botandmudler fe6ca3738d chore(model-gallery): ⬆️ update checksum (#11968)
⬆️ Checksum updates in gallery/index.yaml

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-09-11 08:32:47 +02:00
localai-org-maint-botandmudler 878ca9966e chore: ⬆️ Update 0xShug0/audio.cpp to efb04233dab73aeee4b2912042a90e7b36329061 (#11964)
⬆️ Update 0xShug0/audio.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-09-11 08:32:35 +02:00
localai-org-maint-botandmudler dec5cdf19b chore: ⬆️ Update vllm-project/vllm cu130 wheel to 0.29.0 (#11954)
⬆️ Update vllm-project/vllm cu130 wheel

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-09-10 17:05:36 +02:00
localai-org-maint-botandmudler bf405c003d chore: ⬆️ Update ggml-org/llama.cpp to 434ddbbc0e30522e897670681e503b797c12b7c1 (#11953)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-09-10 17:05:20 +02:00
localai-org-maint-botandmudler d60aaa171d chore: ⬆️ Update ikawrakow/ik_llama.cpp to 3e416d7f5a9d4cc3195e8171dbf891541ca59c6a (#11952)
⬆️ Update ikawrakow/ik_llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-09-10 15:21:47 +02:00
localai-org-maint-botandmudler 1816013ebd chore: ⬆️ Update 0xShug0/audio.cpp to fa5aaac9266a98c68f8a5c9fcd1ba6ff65875416 (#11955)
⬆️ Update 0xShug0/audio.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-09-10 08:58:59 +02:00
dependabot[bot] 428898373a chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#11957)
Bumps the npm_and_yarn group with 2 updates in the /core/http/react-ui directory: [hono](https://github.com/honojs/hono) and [js-yaml](https://github.com/nodeca/js-yaml).


Updates `hono` from 4.12.34 to 4.13.5
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.34...v4.13.5)

Updates `js-yaml` from 3.14.2 to 3.15.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/3.15.2/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.2...3.15.2)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.13.5
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: js-yaml
  dependency-version: 3.15.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-10 08:58:38 +02:00
243 changed files with 10136 additions and 550 deletions

No files matched your search

+155 -3
View File
@@ -166,6 +166,19 @@ include:
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: ''
cuda-major-version: ""
cuda-minor-version: ""
platforms: 'linux/amd64'
tag-latest: 'auto'
tag-suffix: '-cpu-whisper-medusa'
runs-on: 'ubuntu-latest'
base-image: "ubuntu:24.04"
skip-drivers: 'true'
backend: "whisper-medusa"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: ''
cuda-major-version: ""
cuda-minor-version: ""
@@ -354,6 +367,19 @@ include:
dockerfile: "./backend/Dockerfile.python"
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-funasr'
runs-on: 'ubuntu-latest'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
backend: "funasr"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: 'cublas'
cuda-major-version: "12"
cuda-minor-version: "8"
@@ -626,6 +652,19 @@ include:
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: 'cublas'
cuda-major-version: "12"
cuda-minor-version: "1"
platforms: 'linux/amd64'
tag-latest: 'auto'
tag-suffix: '-gpu-nvidia-cuda-12-whisper-medusa'
runs-on: 'ubuntu-latest'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
backend: "whisper-medusa"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: 'cublas'
cuda-major-version: "12"
cuda-minor-version: "8"
@@ -1069,6 +1108,19 @@ include:
dockerfile: "./backend/Dockerfile.python"
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-funasr'
runs-on: 'ubuntu-latest'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
backend: "funasr"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: 'cublas'
cuda-major-version: "13"
cuda-minor-version: "0"
@@ -1404,6 +1456,19 @@ include:
backend: "qwen-asr"
dockerfile: "./backend/Dockerfile.python"
context: "./"
- build-type: 'l4t'
cuda-major-version: "13"
cuda-minor-version: "0"
platforms: 'linux/arm64'
tag-latest: 'auto'
tag-suffix: '-nvidia-l4t-cuda-13-arm64-funasr'
runs-on: 'ubuntu-24.04-arm'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
ubuntu-version: '2404'
backend: "funasr"
dockerfile: "./backend/Dockerfile.python"
context: "./"
- build-type: 'l4t'
cuda-major-version: "13"
cuda-minor-version: "0"
@@ -2388,6 +2453,19 @@ include:
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: 'hipblas'
cuda-major-version: ""
cuda-minor-version: ""
platforms: 'linux/amd64'
tag-latest: 'auto'
tag-suffix: '-gpu-rocm-hipblas-funasr'
runs-on: 'ubuntu-latest'
base-image: "rocm/dev-ubuntu-24.04:7.2.1"
skip-drivers: 'false'
backend: "funasr"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: 'hipblas'
cuda-major-version: ""
cuda-minor-version: ""
@@ -2668,6 +2746,19 @@ include:
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2204'
- build-type: 'l4t'
cuda-major-version: "12"
cuda-minor-version: "0"
platforms: 'linux/arm64'
tag-latest: 'auto'
tag-suffix: '-nvidia-l4t-funasr'
runs-on: 'ubuntu-24.04-arm'
base-image: "nvcr.io/nvidia/l4t-jetpack:r36.4.0"
skip-drivers: 'true'
backend: "funasr"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2204'
- build-type: 'l4t'
cuda-major-version: "12"
cuda-minor-version: "0"
@@ -2877,6 +2968,19 @@ include:
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: 'intel'
cuda-major-version: ""
cuda-minor-version: ""
platforms: 'linux/amd64'
tag-latest: 'auto'
tag-suffix: '-gpu-intel-funasr'
runs-on: 'ubuntu-latest'
base-image: "intel/oneapi-basekit:2025.3.0-0-devel-ubuntu24.04"
skip-drivers: 'false'
backend: "funasr"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: 'intel'
cuda-major-version: ""
cuda-minor-version: ""
@@ -3208,9 +3312,10 @@ include:
# consumed. Same reason CUDA needs its toolkit in base-image rather than in a
# builder image: this is the ds4 shape, not the llama-cpp one.
#
# No ROCm entry: upstream has no HIP configuration. No CUDA arm64 or L4T
# entry: upstream documents and validates CUDA on x86 only. Darwin/Metal is in
# the includeDarwin matrix below, built by scripts/build/audio-cpp-darwin.sh.
# ROCm uses upstream's HIP backend and the project-wide ROCm 7.2.1 base. No
# CUDA arm64 or L4T entry: upstream documents and validates CUDA on x86 only.
# Darwin/Metal is in the includeDarwin matrix below, built by
# scripts/build/audio-cpp-darwin.sh.
#
# No vulkan entry either, though Dockerfile.audio-cpp and the backend Makefile
# both handle BUILD_TYPE=vulkan for local builds. Every other vulkan backend
@@ -3278,6 +3383,19 @@ include:
dockerfile: "./backend/Dockerfile.audio-cpp"
context: "./"
ubuntu-version: '2404'
- build-type: 'hipblas'
cuda-major-version: ""
cuda-minor-version: ""
platforms: 'linux/amd64'
tag-latest: 'auto'
tag-suffix: '-gpu-rocm-hipblas-audio-cpp'
runs-on: 'ubuntu-latest'
base-image: "rocm/dev-ubuntu-24.04:7.2.1"
skip-drivers: 'false'
backend: "audio-cpp"
dockerfile: "./backend/Dockerfile.audio-cpp"
context: "./"
ubuntu-version: '2404'
- build-type: ''
cuda-major-version: ""
cuda-minor-version: ""
@@ -6071,6 +6189,20 @@ include:
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: ''
cuda-major-version: ""
cuda-minor-version: ""
platforms: 'linux/amd64'
platform-tag: 'amd64'
tag-latest: 'auto'
tag-suffix: '-cpu-funasr'
runs-on: 'ubuntu-latest'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
backend: "funasr"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: ''
cuda-major-version: ""
cuda-minor-version: ""
@@ -6085,6 +6217,20 @@ include:
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: ''
cuda-major-version: ""
cuda-minor-version: ""
platforms: 'linux/arm64'
platform-tag: 'arm64'
tag-latest: 'auto'
tag-suffix: '-cpu-funasr'
runs-on: 'ubuntu-24.04-arm'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
backend: "funasr"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: ''
cuda-major-version: ""
cuda-minor-version: ""
@@ -6324,6 +6470,9 @@ includeDarwin:
- backend: "mlx"
tag-suffix: "-metal-darwin-arm64-mlx"
build-type: "mps"
- backend: "mlx-video"
tag-suffix: "-metal-darwin-arm64-mlx-video"
build-type: "mps"
- backend: "chatterbox"
tag-suffix: "-metal-darwin-arm64-chatterbox"
build-type: "mps"
@@ -6460,6 +6609,9 @@ includeDarwin:
- backend: "qwen-asr"
tag-suffix: "-metal-darwin-arm64-qwen-asr"
build-type: "mps"
- backend: "funasr"
tag-suffix: "-metal-darwin-arm64-funasr"
build-type: "mps"
- backend: "nemo"
tag-suffix: "-metal-darwin-arm64-nemo"
build-type: "mps"
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# Bump the CTranslate2 ROCm Python wheel release pin used by faster-whisper.
set -xe
REPO=$1 # OpenNMT/CTranslate2
FILE=$2 # backend/python/faster-whisper/install.sh
VAR=$3 # CTRANSLATE2_VERSION (used for output file names so the workflow can read them)
if [ -z "$FILE" ] || [ -z "$REPO" ] || [ -z "$VAR" ]; then
echo "usage: $0 <repo> <install-script> <var-name>" >&2
exit 1
fi
LATEST_RELEASE=$(curl -sS -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPO/releases/latest")
LATEST_TAG=$(python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'])" <<< "$LATEST_RELEASE")
set +e
CURRENT_VERSION=$(grep -m1 "^${VAR}=" "$FILE" | cut -d= -f2 | sed -E 's/^\$\{[^:]+:-([^}]+)\}$/\1/')
CTRANSLATE2_ROCM_WHEEL_OS=$(grep -m1 '^CTRANSLATE2_ROCM_WHEEL_OS=' "$FILE" | cut -d= -f2 | sed -E 's/^\$\{[^:]+:-([^}]+)\}$/\1/')
set -e
if [ -z "$CURRENT_VERSION" ]; then
echo "Could not find $VAR in $FILE."
exit 0
fi
if [ -z "$CTRANSLATE2_ROCM_WHEEL_OS" ]; then
echo "Could not find CTRANSLATE2_ROCM_WHEEL_OS in $FILE."
exit 0
fi
ASSET_NAME="rocm-python-wheels-${CTRANSLATE2_ROCM_WHEEL_OS}.zip"
LATEST_RELEASE="$LATEST_RELEASE" python3 - "$ASSET_NAME" <<'PY'
import json
import os
import sys
asset_name = sys.argv[1]
release = json.loads(os.environ["LATEST_RELEASE"])
assets = {asset.get("name") for asset in release.get("assets", [])}
if asset_name not in assets:
raise SystemExit(f"Could not find release asset {asset_name!r}")
PY
sed -i "$FILE" -e "s/^${VAR}=.*/${VAR}=\${${VAR}:-${LATEST_TAG}}/"
echo "Changes: https://github.com/$REPO/compare/${CURRENT_VERSION}...${LATEST_TAG}" >> "${VAR}_message.txt"
echo "${LATEST_TAG}" >> "${VAR}_commit.txt"
+33
View File
@@ -170,6 +170,39 @@ jobs:
body: ${{ steps.bump.outputs.message }}
signoff: true
bump-ctranslate2-rocm-wheel:
# CTranslate2's ROCm wheels are published as release assets, so the
# faster-whisper hipblas install path pins the release tag used in the URL.
if: github.repository == 'mudler/LocalAI'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Bump CTranslate2 ROCm wheel pin 🔧
id: bump
run: |
bash .github/bump_ctranslate2_rocm_wheel.sh OpenNMT/CTranslate2 backend/python/faster-whisper/install.sh CTRANSLATE2_VERSION
{
echo 'message<<EOF'
cat "CTRANSLATE2_VERSION_message.txt"
echo EOF
} >> "$GITHUB_OUTPUT"
{
echo 'commit<<EOF'
cat "CTRANSLATE2_VERSION_commit.txt"
echo EOF
} >> "$GITHUB_OUTPUT"
rm -rfv CTRANSLATE2_VERSION_message.txt CTRANSLATE2_VERSION_commit.txt
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.UPDATE_BOT_TOKEN }}
push-to-fork: ci-forks/LocalAI
commit-message: ':arrow_up: Update OpenNMT/CTranslate2 ROCm wheel'
title: 'chore: :arrow_up: Update OpenNMT/CTranslate2 ROCm wheel to `${{ steps.bump.outputs.commit }}`'
branch: "update/CTRANSLATE2_VERSION"
body: ${{ steps.bump.outputs.message }}
signoff: true
bump-vllm-wheel:
# vLLM's cu130 wheel comes from a per-tag index URL (no /latest/ alias),
# so the cublas13 requirements file pins both a URL segment and a version
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
if: ${{ !github.repository.fork && github.actor != 'dependabot[bot]' }}
- name: Run Gosec Security Scanner
if: ${{ !github.repository.fork && github.actor != 'dependabot[bot]' }}
uses: securego/gosec@v2.27.1
uses: securego/gosec@v2.29.0
with:
# we let the report trigger content trigger a failure using the GitHub Security features.
# backend/go/supertonic is excluded: it vendors upstream supertone-inc/supertonic
+19 -1
View File
@@ -1,5 +1,7 @@
# Disable parallel execution for backend builds
.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
.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/mlx-video 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
.NOTPARALLEL: backends/whisper-medusa
.NOTPARALLEL: backends/funasr
GOCMD=go
GOTEST=$(GOCMD) test
@@ -612,6 +614,7 @@ prepare-test-extra: protogen-python
$(MAKE) -C backend/python/vllm
$(MAKE) -C backend/python/vllm-omni
$(MAKE) -C backend/python/longcat-video
$(MAKE) -C backend/python/mlx-video
$(MAKE) -C backend/python/sglang
$(MAKE) -C backend/python/vibevoice
$(MAKE) -C backend/python/liquid-audio
@@ -621,9 +624,11 @@ prepare-test-extra: protogen-python
$(MAKE) -C backend/python/fish-speech
$(MAKE) -C backend/python/faster-qwen3-tts
$(MAKE) -C backend/python/qwen-asr
$(MAKE) -C backend/python/funasr
$(MAKE) -C backend/python/nemo
$(MAKE) -C backend/python/voxcpm
$(MAKE) -C backend/python/faster-whisper
$(MAKE) -C backend/python/whisper-medusa
$(MAKE) -C backend/python/whisperx
$(MAKE) -C backend/python/ace-step
$(MAKE) -C backend/python/trl
@@ -644,6 +649,7 @@ test-extra: prepare-test-extra
$(MAKE) -C backend/python/vllm test
$(MAKE) -C backend/python/vllm-omni test
$(MAKE) -C backend/python/longcat-video test
$(MAKE) -C backend/python/mlx-video test
$(MAKE) -C backend/python/vibevoice test
$(MAKE) -C backend/python/liquid-audio test
$(MAKE) -C backend/python/moonshine test
@@ -652,9 +658,11 @@ test-extra: prepare-test-extra
$(MAKE) -C backend/python/fish-speech test
$(MAKE) -C backend/python/faster-qwen3-tts test
$(MAKE) -C backend/python/qwen-asr test
$(MAKE) -C backend/python/funasr test
$(MAKE) -C backend/python/nemo test
$(MAKE) -C backend/python/voxcpm test
$(MAKE) -C backend/python/faster-whisper test
$(MAKE) -C backend/python/whisper-medusa test
$(MAKE) -C backend/python/whisperx test
$(MAKE) -C backend/python/ace-step test
$(MAKE) -C backend/python/trl test
@@ -1258,6 +1266,10 @@ backends/mlx:
BACKEND=mlx $(MAKE) build-darwin-python-backend
./local-ai backends install "ocifile://$(abspath ./backend-images/mlx.tar)"
backends/mlx-video:
BACKEND=mlx-video $(MAKE) build-darwin-python-backend
./local-ai backends install "ocifile://$(abspath ./backend-images/mlx-video.tar)"
backends/diffuser-darwin:
BACKEND=diffusers $(MAKE) build-darwin-python-backend
./local-ai backends install "ocifile://$(abspath ./backend-images/diffusers.tar)"
@@ -1344,6 +1356,7 @@ BACKEND_RERANKERS = rerankers|python|.|false|true
BACKEND_TRANSFORMERS = transformers|python|.|false|true
BACKEND_OUTETTS = outetts|python|.|false|true
BACKEND_FASTER_WHISPER = faster-whisper|python|.|false|true
BACKEND_WHISPER_MEDUSA = whisper-medusa|python|.|false|true
BACKEND_COQUI = coqui|python|.|false|true
BACKEND_RFDETR = rfdetr|python|.|false|true
BACKEND_INSIGHTFACE = insightface|python|.|false|true
@@ -1365,6 +1378,7 @@ BACKEND_QWEN_TTS = qwen-tts|python|.|false|true
BACKEND_FISH_SPEECH = fish-speech|python|.|false|true
BACKEND_FASTER_QWEN3_TTS = faster-qwen3-tts|python|.|false|true
BACKEND_QWEN_ASR = qwen-asr|python|.|false|true
BACKEND_FUNASR = funasr|python|.|false|true
BACKEND_NEMO = nemo|python|.|false|true
BACKEND_VOXCPM = voxcpm|python|.|false|true
BACKEND_WHISPERX = whisperx|python|.|false|true
@@ -1435,6 +1449,7 @@ $(eval $(call generate-docker-build-target,$(BACKEND_RERANKERS)))
$(eval $(call generate-docker-build-target,$(BACKEND_TRANSFORMERS)))
$(eval $(call generate-docker-build-target,$(BACKEND_OUTETTS)))
$(eval $(call generate-docker-build-target,$(BACKEND_FASTER_WHISPER)))
$(eval $(call generate-docker-build-target,$(BACKEND_WHISPER_MEDUSA)))
$(eval $(call generate-docker-build-target,$(BACKEND_COQUI)))
$(eval $(call generate-docker-build-target,$(BACKEND_RFDETR)))
$(eval $(call generate-docker-build-target,$(BACKEND_INSIGHTFACE)))
@@ -1456,6 +1471,7 @@ $(eval $(call generate-docker-build-target,$(BACKEND_QWEN_TTS)))
$(eval $(call generate-docker-build-target,$(BACKEND_FISH_SPEECH)))
$(eval $(call generate-docker-build-target,$(BACKEND_FASTER_QWEN3_TTS)))
$(eval $(call generate-docker-build-target,$(BACKEND_QWEN_ASR)))
$(eval $(call generate-docker-build-target,$(BACKEND_FUNASR)))
$(eval $(call generate-docker-build-target,$(BACKEND_NEMO)))
$(eval $(call generate-docker-build-target,$(BACKEND_VOXCPM)))
$(eval $(call generate-docker-build-target,$(BACKEND_WHISPERX)))
@@ -1485,6 +1501,8 @@ 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-whisper-medusa
docker-build-backends: docker-build-funasr
########################################################
### Mock Backend for E2E Tests
+13 -9
View File
@@ -6,13 +6,14 @@ ARG APT_PORTS_MIRROR=""
# ASR, VAD, diarization, source separation and music generation, wrapped as a
# LocalAI gRPC backend.
#
# BASE_IMAGE is ubuntu:24.04 for cpu and vulkan builds, or
# nvidia/cuda:<ver>-devel-ubuntu24.04 for cublas builds; both ship apt and
# Ubuntu Noble packages, and the CUDA base additionally provides
# /usr/local/cuda. BUILD_TYPE selects the engine backend in the Makefile:
# "" = portable CPU with all ggml CPU variants, "cublas" ->
# -DENGINE_ENABLE_CUDA=ON, "vulkan" -> -DENGINE_ENABLE_VULKAN=ON. Darwin
# (Metal) builds bypass this Dockerfile entirely.
# BASE_IMAGE is ubuntu:24.04 for cpu and vulkan builds,
# nvidia/cuda:<ver>-devel-ubuntu24.04 for cublas builds, or
# rocm/dev-ubuntu-24.04:<ver> for hipblas builds. All ship apt and Ubuntu Noble
# packages; the GPU bases also provide their toolkits. BUILD_TYPE selects the
# engine backend in the Makefile: "" = portable CPU with all ggml CPU variants,
# "cublas" -> -DENGINE_ENABLE_CUDA=ON, "hipblas" -> -DENGINE_ENABLE_HIP=ON,
# and "vulkan" -> -DENGINE_ENABLE_VULKAN=ON. Darwin (Metal) builds bypass this
# Dockerfile entirely.
#
# Upstream needs GCC 13 or newer, which ubuntu:24.04 and the CUDA 12/13
# devel-ubuntu24.04 images all provide.
@@ -62,7 +63,7 @@ ENV BUILD_TYPE=${BUILD_TYPE} \
APT_MIRROR=${APT_MIRROR} \
APT_PORTS_MIRROR=${APT_PORTS_MIRROR} \
DEBIAN_FRONTEND=noninteractive \
PATH=/usr/local/cuda/bin:${PATH}
PATH=/opt/rocm/bin:/usr/local/cuda/bin:${PATH}
WORKDIR /build
@@ -73,7 +74,7 @@ WORKDIR /build
# fallback of its own.
#
# BUILD_TYPE=vulkan additionally needs the loader headers and glslc; both are in
# Noble. The CUDA toolkit for BUILD_TYPE=cublas comes from BASE_IMAGE.
# Noble. The CUDA and ROCm toolkits come from their matching BASE_IMAGE.
RUN --mount=type=bind,source=.docker/apt-mirror.sh,target=/usr/local/sbin/apt-mirror \
sh /usr/local/sbin/apt-mirror && \
apt-get update && \
@@ -83,6 +84,9 @@ RUN --mount=type=bind,source=.docker/apt-mirror.sh,target=/usr/local/sbin/apt-mi
if [ "${BUILD_TYPE}" = "vulkan" ]; then \
apt-get install -y --no-install-recommends libvulkan-dev glslc; \
fi && \
if [ "${BUILD_TYPE}" = "hipblas" ]; then \
apt-get install -y --no-install-recommends hipblas-dev rocblas-dev; \
fi && \
if [ "${TARGETARCH}" = "arm64" ]; then \
apt-get install -y --no-install-recommends gcc-14 g++-14; \
fi && \
+1 -1
View File
@@ -47,7 +47,7 @@ The backend system provides language-specific Dockerfiles that handle the build
- **mlx**: Apple Silicon optimization
- **diffusers**: Stable Diffusion models
- **longcat-video**: CUDA text/image-to-video and speech-driven avatar generation
- **Audio**: coqui, faster-whisper, kitten-tts
- **Audio**: coqui, faster-whisper, funasr, kitten-tts
- **Vision**: mlx-vlm, rfdetr
- **Specialized**: rerankers, chatterbox, kokoro
+4
View File
@@ -499,6 +499,10 @@ message ModelOptions {
// applied verbatim to the backend's engine constructor (e.g. vLLM AsyncEngineArgs).
// Unknown keys produce an error at LoadModel time.
string EngineArgs = 73;
string OriginalConfigFile = 77;
// EnvVars carries environment variables to be passed to the backend process.
map<string, string> EnvVars = 76;
// Proxy carries the cloud-proxy backend's per-model configuration.
// Empty for non-proxy backends.
+11 -1
View File
@@ -9,7 +9,7 @@
# recipe is a make target (not a prepare.sh) so 'make purge && make' is a clean
# rebuild and so the bump bot can see the pin.
AUDIO_CPP_VERSION?=05e508a70e3600b01454c647cdb122133ba8e64c
AUDIO_CPP_VERSION?=efb04233dab73aeee4b2912042a90e7b36329061
AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
@@ -77,6 +77,16 @@ endif
ifeq ($(BUILD_TYPE),cublas)
CMAKE_ARGS += -DENGINE_ENABLE_CUDA=ON "-DCMAKE_CUDA_ARCHITECTURES=$(CUDA_ARCHITECTURES)"
else ifeq ($(BUILD_TYPE),hipblas)
ROCM_HOME ?= /opt/rocm
ROCM_PATH ?= /opt/rocm
export CXX=$(ROCM_HOME)/llvm/bin/clang++
export CC=$(ROCM_HOME)/llvm/bin/clang
AMDGPU_TARGETS ?= gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1151,gfx1200,gfx1201
# audio.cpp forwards GPU_TARGETS to CMake's semicolon-delimited HIP list.
comma := ,
HIP_TARGETS := $(subst $(comma),;,$(AMDGPU_TARGETS))
CMAKE_ARGS += -DENGINE_ENABLE_HIP=ON "-DGPU_TARGETS=$(HIP_TARGETS)"
else ifeq ($(BUILD_TYPE),vulkan)
CMAKE_ARGS += -DENGINE_ENABLE_VULKAN=ON
else ifeq ($(UNAME_S),Darwin)
+9 -1
View File
@@ -22,7 +22,8 @@ bool voice_is_reference_file(const std::string &voice) {
RequestShape build_tts_shape(const backend::TTSRequest &request) {
RequestShape shape;
shape.has_voice_reference = voice_is_reference_file(request.voice());
shape.has_voice_reference = voice_is_reference_file(request.voice()) ||
request.params().find("multi_reference_cond") != request.params().end();
// !empty() as well as has_instructions(), and it must match the guard in
// build_tts_request: a request whose instructions are an empty string
// carries no style condition, so telling routing to prefer VoiceDesign for
@@ -134,6 +135,13 @@ build_tts_request(const backend::TTSRequest &request,
task.options["language"] = request.language();
}
// Saved voice profiles send ref_text; Fish Audio reads reference_text.
// Derive the alias before copying params so an explicit canonical key wins.
const auto reference_text = request.params().find("ref_text");
if (reference_text != request.params().end()) {
task.options["reference_text"] = reference_text->second;
}
// LAST, so an explicit params entry wins over anything derived above. That
// matters for "caption": a caller who sets params[caption] has named the
// exact string they want, and it must not be overwritten by `instructions`.
@@ -128,6 +128,14 @@ static void test_tts_shape() {
check(!shape.has_voice_reference,
"shape: a preset name is not a voice reference");
}
{
backend::TTSRequest request;
(*request.mutable_params())["multi_reference_cond"] =
R"([{"audio":"one.wav","text":"one"}])";
const auto shape = build_tts_shape(request);
check(shape.has_voice_reference,
"shape: multi-reference conditioning is a voice reference");
}
{
backend::TTSRequest request;
request.set_voice(dir.string());
@@ -367,6 +375,40 @@ static void test_tts_language_and_params() {
"tts params: an explicit param overrides the derived caption");
}
static void test_tts_reference_transcript() {
backend::TTSRequest request;
request.set_text("New speech to generate.");
request.set_voice("reference.wav");
(*request.mutable_params())["ref_text"] = "The saved voice transcript.";
const auto task = build_tts_request(request, clip(24000, 1));
check(option_or(task.options, "reference_text", "") ==
"The saved voice transcript.",
"tts reference: saved transcript reaches Fish Audio's option");
check(option_or(task.options, "ref_text", "") ==
"The saved voice transcript.",
"tts reference: original transcript parameter is preserved");
check(task.text_input->text == "New speech to generate.",
"tts reference: transcript does not replace synthesis text");
(*request.mutable_params())["reference_text"] = "Explicit transcript.";
const auto explicit_task = build_tts_request(request, clip(24000, 1));
check(option_or(explicit_task.options, "reference_text", "") ==
"Explicit transcript.",
"tts reference: explicit canonical parameter wins over alias");
(*request.mutable_params())["reference_text"] = "";
const auto empty_task = build_tts_request(request, clip(24000, 1));
check(has_key(empty_task.options, "reference_text") &&
empty_task.options.at("reference_text").empty(),
"tts reference: explicit empty canonical parameter is preserved");
request.mutable_params()->clear();
const auto missing_task = build_tts_request(request, clip(24000, 1));
check(!has_key(missing_task.options, "reference_text"),
"tts reference: no transcript is invented when none was supplied");
}
static void test_sound_generation_minimal() {
backend::SoundGenerationRequest request;
request.set_text("a distant thunderstorm");
@@ -554,6 +596,7 @@ int main() {
test_tts_empty_language_is_not_a_language();
test_tts_clip_and_instructions();
test_tts_language_and_params();
test_tts_reference_transcript();
test_sound_generation_minimal();
test_sound_generation_full();
test_transform_text_absent();
+4 -1
View File
@@ -103,6 +103,9 @@ engine::core::BackendType parse_backend_type(const std::string &value) {
if (value == "cuda") {
return engine::core::BackendType::Cuda;
}
if (value == "hip" || value == "rocm") {
return engine::core::BackendType::Hip;
}
if (value == "vulkan") {
return engine::core::BackendType::Vulkan;
}
@@ -116,7 +119,7 @@ engine::core::BackendType parse_backend_type(const std::string &value) {
return engine::core::BackendType::Cpu;
}
throw ConfigError("audio-cpp: unknown backend option '" + value +
"'. Known backends: cpu, cuda, vulkan, metal, best");
"'. Known backends: cpu, cuda, hip, rocm, vulkan, metal, best");
}
std::filesystem::path executable_directory() {
+12
View File
@@ -59,6 +59,12 @@ bool starts_with(const std::string &value, const std::string &prefix) {
value.compare(0, prefix.size(), prefix) == 0;
}
bool is_known_backend(const std::string &value) {
return value == "cpu" || value == "cuda" || value == "hip" ||
value == "rocm" || value == "vulkan" || value == "metal" ||
value == "best";
}
} // namespace
ParsedOptions parse_model_options(const std::vector<std::string> &entries) {
@@ -107,6 +113,12 @@ ParsedOptions parse_model_options(const std::vector<std::string> &entries) {
} else if (key == "task") {
parsed.options.task = value;
} else if (key == "backend") {
if (!is_known_backend(value)) {
parsed.error = "audio-cpp: unknown backend option '" + value +
"'. Known backends: cpu, cuda, hip, rocm, "
"vulkan, metal, best";
return parsed;
}
parsed.options.backend = value;
} else if (key == "model_spec_override") {
parsed.options.model_spec_override = value;
+1 -1
View File
@@ -17,7 +17,7 @@ struct ModelOptions {
std::string family;
// Pins the audio.cpp task, overriding RPC-based routing. Empty means route.
std::string task;
// ggml backend: cpu, cuda, vulkan, metal, best.
// ggml backend: cpu, cuda, hip (or rocm), vulkan, metal, best.
std::string backend = "cpu";
int device = 0;
// True once a `device:` entry has been seen. 0 is both the default and a
@@ -75,6 +75,11 @@ static void test_scalar_options() {
check(parse_model_options({"live_idle_timeout_ms:0"}).options.live_idle_timeout_ms == 0,
"an explicit 0 turns the live idle limit off rather than reverting to "
"the default");
check(parse_model_options({"backend:hip"}).error.empty(),
"HIP backend option is accepted");
check(parse_model_options({"backend:rocm"}).error.empty(),
"ROCm backend alias is accepted");
}
// Values containing colons must survive: split on the FIRST colon only.
@@ -124,6 +129,8 @@ static void test_errors() {
"negative device is rejected");
check(!parse_model_options({"threads:x"}).error.empty(),
"non-numeric threads is rejected");
check(!parse_model_options({"backend:unknown"}).error.empty(),
"unknown compute backend is rejected before model loading");
// Values too large for int must be rejected, not silently wrapped into a
// negative device index that then reaches the ggml backend selector.
+1 -1
View File
@@ -1,5 +1,5 @@
IK_LLAMA_VERSION?=1a2a8604a6c6c6413c06bf9adfc2f64329af4366
IK_LLAMA_VERSION?=3bb386eb68ffee0a5dc7db21da0735d594929eeb
LLAMA_REPO?=https://github.com/ikawrakow/ik_llama.cpp
CMAKE_ARGS?=
+11
View File
@@ -125,4 +125,15 @@ if(LLAMA_GRPC_BUILD_TESTS)
target_include_directories(thread_params_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(thread_params_test PRIVATE cxx_std_17)
add_test(NAME thread_params_test COMMAND thread_params_test)
add_executable(model_load_error_test model_load_error_test.cpp model_load_error.h)
target_include_directories(model_load_error_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(model_load_error_test PRIVATE cxx_std_17)
add_test(NAME model_load_error_test COMMAND model_load_error_test)
# Dead-stream tracker (standard library only).
add_executable(stream_peer_test stream_peer_test.cpp stream_peer.h)
target_include_directories(stream_peer_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(stream_peer_test PRIVATE cxx_std_17)
add_test(NAME stream_peer_test COMMAND stream_peer_test)
endif()
+1 -1
View File
@@ -1,5 +1,5 @@
LLAMA_VERSION?=f3f1a8f2760f28325a5ec20c05b171e5b7c83a29
LLAMA_VERSION?=df03399b885831b2a1603b3abb0d8c156808e363
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
CMAKE_ARGS?=
+25 -8
View File
@@ -53,9 +53,11 @@
#include "arg.h"
#include "chat-auto-parser.h"
#include "llama_compat.h" // fork-skew switches, generated by prepare.sh
#include "model_load_error.h"
#include "thread_params.h"
#include "message_content.h"
#include "passthrough_options.h"
#include "stream_peer.h"
#include "tts_request_options.h"
#include <getopt.h>
#include <grpcpp/ext/proto_server_reflection_plugin.h>
@@ -1628,7 +1630,8 @@ public:
{
std::lock_guard<std::mutex> lock(error_capture_data.error_mutex);
if (!error_capture_data.captured_error.empty()) {
error_msg += ". Error: " + error_capture_data.captured_error;
error_msg += ". Error: " +
localai::model_load_error_with_hint(error_capture_data.captured_error);
} else {
error_msg += ". Model file may not exist or be invalid.";
}
@@ -2266,6 +2269,11 @@ public:
// such concept, so there is nothing to emit — the real tokens arrive in
// the loop below. Feeding this null into build_reply_from_json would
// throw (uncaught) and surface as a generic RPC error.
// A write that returns false means the peer is gone for good. Track it
// so the loop below stops decoding instead of feeding a dead stream —
// see stream_peer.h for why that matters to everyone else's requests.
llama_grpc::StreamPeer peer;
if (first_res_json.is_null()) {
// skip the begin-of-stream marker
} else if (first_res_json.is_array()) {
@@ -2278,17 +2286,21 @@ public:
if (!is_role_init) {
attach_chat_deltas(reply, first_result.get());
}
writer->Write(reply);
peer.observe_write(writer->Write(reply));
if (peer.gone()) {
break;
}
}
} else {
auto reply = build_reply_from_json(first_res_json, first_result.get());
attach_chat_deltas(reply, first_result.get());
writer->Write(reply);
peer.observe_write(writer->Write(reply));
}
// Process subsequent results
while (rd.has_next()) {
if (context->IsCancelled()) {
peer.observe_cancelled(context->IsCancelled());
if (peer.gone()) {
break;
}
@@ -2309,17 +2321,22 @@ public:
if (!is_role_init) {
attach_chat_deltas(reply, result.get());
}
writer->Write(reply);
peer.observe_write(writer->Write(reply));
if (peer.gone()) {
break;
}
}
} else {
auto reply = build_reply_from_json(res_json, result.get());
attach_chat_deltas(reply, result.get());
writer->Write(reply);
peer.observe_write(writer->Write(reply));
}
}
// Check if context was cancelled during processing
if (context->IsCancelled()) {
// Returning here is what releases the slot: ~server_response_reader()
// posts SERVER_TASK_TYPE_CANCEL for whatever is still decoding.
peer.observe_cancelled(context->IsCancelled());
if (peer.gone()) {
return grpc::Status(grpc::StatusCode::CANCELLED, "Request cancelled by client");
}
+24
View File
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
#pragma once
#include <string>
namespace localai {
inline std::string model_load_error_with_hint(const std::string& error) {
const std::string mismatch = "wrong number of tensors; expected ";
const std::string got = ", got ";
const std::string::size_type mismatch_pos = error.find(mismatch);
if (mismatch_pos == std::string::npos ||
error.find(got, mismatch_pos + mismatch.size()) == std::string::npos) {
return error;
}
return error +
" Hint: the model may be incompatible with this llama.cpp backend "
"or the GGUF file may be corrupt. Try a newer compatible backend "
"and verify or re-download the model file.";
}
} // namespace localai
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
#include "model_load_error.h"
#include <cassert>
#include <string>
int main() {
const std::string issue_error =
"llama_model_load: error loading model: done_getting_tensors: wrong number of tensors; expected 2131, got 720; "
"llama_model_load_from_file_impl: failed to load model";
const std::string issue_result = localai::model_load_error_with_hint(issue_error);
assert(issue_result.compare(0, issue_error.size(), issue_error) == 0);
assert(issue_result.find("incompatible") != std::string::npos);
assert(issue_result.find("corrupt") != std::string::npos);
const std::string generic_error =
"wrong number of tensors; expected 42, got 17";
const std::string generic_result = localai::model_load_error_with_hint(generic_error);
assert(generic_result.compare(0, generic_error.size(), generic_error) == 0);
assert(generic_result.size() > generic_error.size());
const std::string unrelated_error = "failed to open GGUF file";
assert(localai::model_load_error_with_hint(unrelated_error) == unrelated_error);
}
+8
View File
@@ -45,6 +45,10 @@ done
cp -r CMakeLists.txt llama.cpp/tools/grpc-server/
cp -r grpc-server.cpp llama.cpp/tools/grpc-server/
# Model-load diagnostics (included by grpc-server.cpp) and their standalone
# regression test.
cp -r model_load_error.h llama.cpp/tools/grpc-server/
cp -r model_load_error_test.cpp llama.cpp/tools/grpc-server/
# Shared message-reconstruction helpers (included by grpc-server.cpp) and their
# unit test (compiled only when -DLLAMA_GRPC_BUILD_TESTS=ON).
cp -r message_content.h llama.cpp/tools/grpc-server/
@@ -63,6 +67,10 @@ cp -r thread_params_test.cpp llama.cpp/tools/grpc-server/
# test (run via backend/cpp/run-unit-tests.sh; also buildable under ctest).
cp -r parent_watch.h llama.cpp/tools/grpc-server/
cp -r parent_watch_test.cpp llama.cpp/tools/grpc-server/
# Dead-stream tracker (included by grpc-server.cpp) and its standalone unit
# test (run via backend/cpp/run-unit-tests.sh; also buildable under ctest).
cp -r stream_peer.h llama.cpp/tools/grpc-server/
cp -r stream_peer_test.cpp llama.cpp/tools/grpc-server/
cp -rfv llama.cpp/vendor/nlohmann/json.hpp llama.cpp/tools/grpc-server/
cp -rfv llama.cpp/vendor/cpp-httplib/httplib.h llama.cpp/tools/grpc-server/
+44
View File
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
#pragma once
namespace llama_grpc {
// Tracks whether a server-streaming RPC still has somewhere to send tokens.
//
// grpc::ServerWriter::Write() returns false once the peer is gone, and a
// stream never recovers afterwards. Ignoring that result is not harmless: the
// handler goes on draining decoded tokens into a dead stream, so the llama.cpp
// slot stays busy for the rest of the request's token budget. A model config
// with no max_tokens and a large context turns that into tens of minutes per
// abandoned request, and the slots are exactly what every other request queues
// behind.
//
// Returning as soon as the peer is gone is what frees the slot: the handler's
// server_response_reader then goes out of scope and its destructor posts
// SERVER_TASK_TYPE_CANCEL for whatever is still decoding.
class StreamPeer {
public:
// Records the outcome of a Write(). Once a write has failed the peer stays
// gone -- a later write cannot succeed on a broken stream.
void observe_write(bool ok) noexcept {
if (!ok) {
gone_ = true;
}
}
// Folds in the RPC's own cancellation flag, so callers have a single
// predicate to test rather than two that can disagree.
void observe_cancelled(bool cancelled) noexcept {
if (cancelled) {
gone_ = true;
}
}
bool gone() const noexcept { return gone_; }
bool alive() const noexcept { return !gone_; }
private:
bool gone_ = false;
};
} // namespace llama_grpc
@@ -0,0 +1,67 @@
#include "stream_peer.h"
#include <cstdio>
namespace {
int failures = 0;
void check(bool condition, const char *what) {
if (!condition) {
std::fprintf(stderr, "FAIL: %s\n", what);
++failures;
}
}
} // namespace
int main() {
{
llama_grpc::StreamPeer peer;
check(peer.alive(), "a fresh peer is alive");
check(!peer.gone(), "a fresh peer is not gone");
}
{
llama_grpc::StreamPeer peer;
peer.observe_write(true);
peer.observe_write(true);
check(peer.alive(), "successful writes keep the peer alive");
}
{
llama_grpc::StreamPeer peer;
peer.observe_write(false);
check(peer.gone(), "a failed write marks the peer gone");
}
{
// The whole point of the guard: a stream never comes back, so a later
// success must not resurrect a peer an earlier failure retired.
llama_grpc::StreamPeer peer;
peer.observe_write(false);
peer.observe_write(true);
check(peer.gone(), "a failed write is sticky across later writes");
}
{
llama_grpc::StreamPeer peer;
peer.observe_cancelled(false);
check(peer.alive(), "an uncancelled RPC keeps the peer alive");
peer.observe_cancelled(true);
check(peer.gone(), "cancellation marks the peer gone");
}
{
llama_grpc::StreamPeer peer;
peer.observe_cancelled(true);
peer.observe_cancelled(false);
check(peer.gone(), "cancellation is sticky across later checks");
}
if (failures != 0) {
std::fprintf(stderr, "%d check(s) failed\n", failures);
return 1;
}
return 0;
}
+1 -1
View File
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# stablediffusion.cpp (ggml)
STABLEDIFFUSION_GGML_REPO?=https://github.com/leejet/stable-diffusion.cpp
STABLEDIFFUSION_GGML_VERSION?=d04e8950c1ec8d30248cbe996682b3182fb1adf6
STABLEDIFFUSION_GGML_VERSION?=b68d58624d227682eb4b95ef8bcf569cd1311eb5
CMAKE_ARGS+=-DGGML_MAX_NAME=128
+35
View File
@@ -0,0 +1,35 @@
package main
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// Specs for resolveAddr run under the suite bootstrap in gowhisper_test.go
// (TestWhisper); they need no native library, so they never skip.
var _ = Describe("resolveAddr", func() {
It("prefers an explicitly set -addr over a positional argument", func() {
Expect(resolveAddr("127.0.0.1:12345", true, []string{"127.0.0.1:59999"})).To(Equal("127.0.0.1:12345"))
})
It("keeps an explicit -addr equal to the default over a positional argument", func() {
Expect(resolveAddr(defaultAddr, true, []string{"127.0.0.1:59999"})).To(Equal(defaultAddr))
})
It("falls back to the positional argument when -addr is unset", func() {
Expect(resolveAddr(defaultAddr, false, []string{"127.0.0.1:59999"})).To(Equal("127.0.0.1:59999"))
})
It("keeps the default when -addr is unset and no positional argument is given", func() {
Expect(resolveAddr(defaultAddr, false, nil)).To(Equal(defaultAddr))
})
It("uses the first positional argument among several", func() {
Expect(resolveAddr(defaultAddr, false, []string{"127.0.0.1:59999", "extra"})).To(Equal("127.0.0.1:59999"))
})
It("treats an explicitly empty -addr as unset", func() {
Expect(resolveAddr("", true, []string{"127.0.0.1:59999"})).To(Equal("127.0.0.1:59999"))
Expect(resolveAddr("", true, nil)).To(Equal(defaultAddr))
})
})
+33 -3
View File
@@ -10,10 +10,31 @@ import (
grpc "github.com/mudler/LocalAI/pkg/grpc"
)
var (
addr = flag.String("addr", "localhost:50051", "the address to connect to")
const (
defaultAddr = "localhost:50051"
)
var (
addr = flag.String("addr", defaultAddr, "the address to listen on")
)
// resolveAddr picks the address the gRPC server binds to. An explicitly set
// -addr always wins. Launchers may hand us the listen address as a bare
// positional argument, which Go's flag package silently drops — honour it
// next so the server binds the port its caller actually allocated instead of
// the default one (#11623). An explicitly empty -addr counts as unset:
// binding the empty address would listen on an OS-chosen port on every
// interface instead of the one the caller allocated.
func resolveAddr(flagAddr string, addrSet bool, args []string) string {
if addrSet && flagAddr != "" {
return flagAddr
}
if len(args) > 0 {
return args[0]
}
return defaultAddr
}
type LibFuncs struct {
FuncPtr any
Name string
@@ -61,7 +82,16 @@ func main() {
flag.Parse()
if err := grpc.StartServer(*addr, &Whisper{}); err != nil {
// flag.Visit reports only flags that were explicitly set, so an -addr
// equal to the default is still distinguished from an untouched one.
addrSet := false
flag.Visit(func(f *flag.Flag) {
if f.Name == "addr" {
addrSet = true
}
})
if err := grpc.StartServer(resolveAddr(*addr, addrSet, flag.Args()), &Whisper{}); err != nil {
panic(err)
}
}
+210
View File
@@ -150,6 +150,7 @@
- audio-transcription
- CPU
- CUDA
- HIP
- Metal
# No vulkan key: the vulkan image would carry a Vulkan loader with no Mesa ICD
# (see the audio-cpp block in .github/backend-matrix.yml). Pointing a
@@ -161,6 +162,7 @@
nvidia: "cuda12-audio-cpp"
nvidia-cuda-12: "cuda12-audio-cpp"
nvidia-cuda-13: "cuda13-audio-cpp"
amd: "rocm-audio-cpp"
metal: "metal-audio-cpp"
metal-darwin-arm64: "metal-audio-cpp"
- &whispercpp
@@ -1060,6 +1062,21 @@
nvidia-l4t: "nvidia-l4t-mlx"
nvidia-l4t-cuda-12: "nvidia-l4t-mlx"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-mlx"
- &mlx-video
name: "mlx-video"
icon: https://avatars.githubusercontent.com/u/102832242?s=200&v=4
urls:
- https://github.com/Blaizzy/mlx-video
license: MIT
description: |
Generate videos with LTX-2 and converted Wan2.1/Wan2.2 checkpoints using
MLX on Apple Silicon.
tags:
- text-to-video
- image-to-video
- MLX
capabilities:
metal: "metal-mlx-video"
- &mlx-vlm
name: "mlx-vlm"
icon: https://avatars.githubusercontent.com/u/102832242?s=200&v=4
@@ -1194,6 +1211,7 @@
tags:
- image-generation
- video-generation
- sound-generation
- diffusion-models
license: apache-2.0
alias: "diffusers"
@@ -1518,6 +1536,25 @@
nvidia-cuda-12: "cuda12-faster-whisper"
nvidia-l4t: "nvidia-l4t-arm64-faster-whisper"
nvidia-l4t-cuda-12: "nvidia-l4t-arm64-faster-whisper"
- &whisper-medusa
description: |
Whisper-Medusa accelerates Whisper transcription by predicting multiple tokens per decoding step.
The upstream checkpoints use custom Transformers generation code and accept audio clips up to 30 seconds.
urls:
- https://github.com/aiola-lab/whisper-medusa
- https://huggingface.co/collections/aiola/whisper-medusa
tags:
- speech-to-text
- transcription
- Whisper
- Medusa
license: MIT
name: "whisper-medusa"
alias: "whisper-medusa"
capabilities:
default: "cpu-whisper-medusa"
nvidia: "cuda12-whisper-medusa"
nvidia-cuda-12: "cuda12-whisper-medusa"
- &moonshine
description: |
Moonshine is a fast, accurate, and efficient speech-to-text transcription model using ONNX Runtime.
@@ -1785,6 +1822,32 @@
nvidia-l4t-cuda-12: "nvidia-l4t-faster-qwen3-tts"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-faster-qwen3-tts"
icon: https://cdn-avatars.huggingface.co/v1/production/uploads/620760a26e3b7210c2ff1943/-s1gyJfvbE1RgO5iBeNOi.png
- &funasr
urls:
- https://github.com/modelscope/FunASR
description: |
FunASR is an industrial-grade speech recognition toolkit supporting 50+ languages.
Includes SenseVoice (170x realtime, emotion detection), Paraformer (highest Chinese
accuracy), and built-in VAD, punctuation restoration, and speaker diarization.
tags:
- speech-recognition
- ASR
- multilingual
license: mit
name: "funasr"
alias: "funasr"
capabilities:
nvidia: "cuda12-funasr"
intel: "intel-funasr"
amd: "rocm-funasr"
metal: "metal-funasr"
default: "cpu-funasr"
nvidia-cuda-13: "cuda13-funasr"
nvidia-cuda-12: "cuda12-funasr"
nvidia-l4t: "nvidia-l4t-funasr"
nvidia-l4t-cuda-12: "nvidia-l4t-funasr"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-funasr"
icon: https://avatars.githubusercontent.com/u/109454077
- &qwen-asr
urls:
- https://github.com/QwenLM/Qwen3-ASR
@@ -2101,6 +2164,7 @@
nvidia: "cuda12-audio-cpp-development"
nvidia-cuda-12: "cuda12-audio-cpp-development"
nvidia-cuda-13: "cuda13-audio-cpp-development"
amd: "rocm-audio-cpp-development"
metal: "metal-audio-cpp-development"
metal-darwin-arm64: "metal-audio-cpp-development"
- !!merge <<: *stablediffusionggml
@@ -2164,6 +2228,20 @@
uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-mlx"
mirrors:
- localai/localai-backends:latest-metal-darwin-arm64-mlx
- !!merge <<: *mlx-video
name: "metal-mlx-video"
uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-mlx-video"
mirrors:
- localai/localai-backends:latest-metal-darwin-arm64-mlx-video
- !!merge <<: *mlx-video
name: "metal-mlx-video-development"
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-mlx-video"
mirrors:
- localai/localai-backends:master-metal-darwin-arm64-mlx-video
- !!merge <<: *mlx-video
name: "mlx-video-development"
capabilities:
metal: "metal-mlx-video-development"
- !!merge <<: *mlx
name: "metal-mlx-development"
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-mlx"
@@ -6368,6 +6446,34 @@
uri: "quay.io/go-skynet/local-ai-backends:master-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts"
mirrors:
- localai/localai-backends:master-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts
## whisper-medusa
- !!merge <<: *whisper-medusa
name: "whisper-medusa-development"
capabilities:
default: "cpu-whisper-medusa-development"
nvidia: "cuda12-whisper-medusa-development"
nvidia-cuda-12: "cuda12-whisper-medusa-development"
- !!merge <<: *whisper-medusa
name: "cpu-whisper-medusa"
uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-whisper-medusa"
mirrors:
- localai/localai-backends:latest-cpu-whisper-medusa
- !!merge <<: *whisper-medusa
name: "cpu-whisper-medusa-development"
uri: "quay.io/go-skynet/local-ai-backends:master-cpu-whisper-medusa"
mirrors:
- localai/localai-backends:master-cpu-whisper-medusa
- !!merge <<: *whisper-medusa
name: "cuda12-whisper-medusa"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-12-whisper-medusa"
mirrors:
- localai/localai-backends:latest-gpu-nvidia-cuda-12-whisper-medusa
- !!merge <<: *whisper-medusa
name: "cuda12-whisper-medusa-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-12-whisper-medusa"
mirrors:
- localai/localai-backends:master-gpu-nvidia-cuda-12-whisper-medusa
## qwen-asr
- !!merge <<: *qwen-asr
name: "qwen-asr-development"
@@ -7054,6 +7160,16 @@
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-13-audio-cpp"
mirrors:
- localai/localai-backends:master-gpu-nvidia-cuda-13-audio-cpp
- !!merge <<: *audiocpp
name: "rocm-audio-cpp"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-rocm-hipblas-audio-cpp"
mirrors:
- localai/localai-backends:latest-gpu-rocm-hipblas-audio-cpp
- !!merge <<: *audiocpp
name: "rocm-audio-cpp-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-rocm-hipblas-audio-cpp"
mirrors:
- localai/localai-backends:master-gpu-rocm-hipblas-audio-cpp
- !!merge <<: *audiocpp
name: "metal-audio-cpp"
uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-audio-cpp"
@@ -7064,3 +7180,97 @@
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-audio-cpp"
mirrors:
- localai/localai-backends:master-metal-darwin-arm64-audio-cpp
## funasr
- !!merge <<: *funasr
name: "funasr-development"
capabilities:
nvidia: "cuda12-funasr-development"
intel: "intel-funasr-development"
amd: "rocm-funasr-development"
nvidia-l4t: "nvidia-l4t-funasr-development"
metal: "metal-funasr-development"
default: "cpu-funasr-development"
nvidia-cuda-13: "cuda13-funasr-development"
nvidia-cuda-12: "cuda12-funasr-development"
nvidia-l4t-cuda-12: "nvidia-l4t-funasr-development"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-funasr-development"
- !!merge <<: *funasr
name: "cpu-funasr"
uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-funasr"
mirrors:
- localai/localai-backends:latest-cpu-funasr
- !!merge <<: *funasr
name: "cpu-funasr-development"
uri: "quay.io/go-skynet/local-ai-backends:master-cpu-funasr"
mirrors:
- localai/localai-backends:master-cpu-funasr
- !!merge <<: *funasr
name: "cuda12-funasr"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-12-funasr"
mirrors:
- localai/localai-backends:latest-gpu-nvidia-cuda-12-funasr
- !!merge <<: *funasr
name: "cuda12-funasr-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-12-funasr"
mirrors:
- localai/localai-backends:master-gpu-nvidia-cuda-12-funasr
- !!merge <<: *funasr
name: "cuda13-funasr"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-13-funasr"
mirrors:
- localai/localai-backends:latest-gpu-nvidia-cuda-13-funasr
- !!merge <<: *funasr
name: "cuda13-funasr-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-13-funasr"
mirrors:
- localai/localai-backends:master-gpu-nvidia-cuda-13-funasr
- !!merge <<: *funasr
name: "intel-funasr"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-intel-funasr"
mirrors:
- localai/localai-backends:latest-gpu-intel-funasr
- !!merge <<: *funasr
name: "intel-funasr-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-intel-funasr"
mirrors:
- localai/localai-backends:master-gpu-intel-funasr
- !!merge <<: *funasr
name: "rocm-funasr"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-rocm-hipblas-funasr"
mirrors:
- localai/localai-backends:latest-gpu-rocm-hipblas-funasr
- !!merge <<: *funasr
name: "rocm-funasr-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-rocm-hipblas-funasr"
mirrors:
- localai/localai-backends:master-gpu-rocm-hipblas-funasr
- !!merge <<: *funasr
name: "nvidia-l4t-funasr"
uri: "quay.io/go-skynet/local-ai-backends:latest-nvidia-l4t-funasr"
mirrors:
- localai/localai-backends:latest-nvidia-l4t-funasr
- !!merge <<: *funasr
name: "nvidia-l4t-funasr-development"
uri: "quay.io/go-skynet/local-ai-backends:master-nvidia-l4t-funasr"
mirrors:
- localai/localai-backends:master-nvidia-l4t-funasr
- !!merge <<: *funasr
name: "cuda13-nvidia-l4t-arm64-funasr"
uri: "quay.io/go-skynet/local-ai-backends:latest-nvidia-l4t-cuda-13-arm64-funasr"
mirrors:
- localai/localai-backends:latest-nvidia-l4t-cuda-13-arm64-funasr
- !!merge <<: *funasr
name: "cuda13-nvidia-l4t-arm64-funasr-development"
uri: "quay.io/go-skynet/local-ai-backends:master-nvidia-l4t-cuda-13-arm64-funasr"
mirrors:
- localai/localai-backends:master-nvidia-l4t-cuda-13-arm64-funasr
- !!merge <<: *funasr
name: "metal-funasr"
uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-funasr"
mirrors:
- localai/localai-backends:latest-metal-darwin-arm64-funasr
- !!merge <<: *funasr
name: "metal-funasr-development"
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-funasr"
mirrors:
- localai/localai-backends:master-metal-darwin-arm64-funasr
+1
View File
@@ -20,6 +20,7 @@ The Python backends use a unified build system based on `libbackend.sh` that pro
### Audio & Speech
- **coqui** - Coqui TTS models
- **faster-whisper** - Fast Whisper speech recognition
- **funasr** - Local multilingual transcription with FunASR and SenseVoice
- **kitten-tts** - Lightweight TTS
- **mlx-audio** - Apple Silicon audio processing
- **chatterbox** - TTS model
+12
View File
@@ -563,6 +563,18 @@ function startBackend() {
echo "Added ${EDIR}/lib to LD_LIBRARY_PATH for GPU libraries"
fi
# XPU wheels carry a matching SYCL/Unified Runtime set. Prefer it over
# packaged oneAPI or host libraries, which may expose an older loader ABI.
if [ "$(uname -s)" = "Linux" ]; then
local sycl_runtime
for sycl_runtime in "${EDIR}/venv/lib"/libsycl.so*; do
if [ -f "${sycl_runtime}" ]; then
export LD_LIBRARY_PATH="${EDIR}/venv/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
break
fi
done
fi
if [ ! -z "${BACKEND_FILE:-}" ]; then
exec "${EDIR}/venv/bin/python" "${BACKEND_FILE}" "$@"
elif [ -e "${MY_DIR}/server.py" ]; then
+24
View File
@@ -0,0 +1,24 @@
import array
import sys
import wave
def write_pcm_wav(destination, samples, sampling_rate):
"""Write normalized floating-point audio samples as mono 16-bit PCM."""
pcm = array.array(
"h",
(
max(-32768, min(32767, round(float(sample) * 32768)))
for sample in samples
),
)
if pcm.itemsize != 2:
raise RuntimeError("16-bit PCM requires two-byte signed integers")
if sys.byteorder != "little":
pcm.byteswap()
with wave.open(destination, "wb") as output:
output.setnchannels(1)
output.setsampwidth(2)
output.setframerate(sampling_rate)
output.writeframes(pcm.tobytes())
+48
View File
@@ -26,6 +26,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
from grpc_auth import get_auth_interceptors
from model_utils import resolve_model_reference
from audio_utils import write_pcm_wav
# Import dynamic loader for pipeline discovery
@@ -35,6 +36,7 @@ from diffusers_dynamic_loader import (
get_available_pipelines,
load_diffusers_pipeline,
)
from load_options import single_file_load_kwargs
# Import specific items still needed for special cases and safety checker
from diffusers import DiffusionPipeline, ControlNetModel
@@ -479,6 +481,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
# Build kwargs for dynamic loading
load_kwargs = {"torch_dtype": torchType}
load_kwargs.update(
single_file_load_kwargs(request.OriginalConfigFile, from_single_file)
)
# Add variant if not loading from single file
if not from_single_file and variant:
@@ -899,6 +904,49 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
return backend_pb2.Result(message="Media generated", success=True)
def SoundGeneration(self, request, context):
if not request.dst:
return backend_pb2.Result(success=False, message="request.dst is required")
prompt = request.text or request.caption
if not prompt:
return backend_pb2.Result(success=False, message="request.text is required")
try:
generation_options = dict(self.options)
if "num_inference_steps" in generation_options:
generation_options["num_inference_steps"] = int(
generation_options["num_inference_steps"]
)
generation_options["prompt"] = prompt
if request.HasField("duration"):
generation_options["audio_length_in_s"] = request.duration
if request.HasField("temperature"):
generation_options["guidance_scale"] = request.temperature
generated = self.pipe(**generation_options)
if not hasattr(generated, "audios") or len(generated.audios) == 0:
return backend_pb2.Result(
success=False,
message="The diffusers pipeline returned no audio",
)
samples = generated.audios[0]
if hasattr(samples, "reshape"):
samples = samples.reshape(-1)
if hasattr(samples, "tolist"):
samples = samples.tolist()
sampling_rate = getattr(
getattr(getattr(self.pipe, "vae", None), "config", None),
"sampling_rate",
16000,
)
write_pcm_wav(request.dst, samples, sampling_rate)
return backend_pb2.Result(success=True, message="Sound generated successfully")
except Exception as err:
return backend_pb2.Result(success=False, message=f"SoundGeneration error: {err}")
def UpscaleImage(self, request, context):
try:
if not request.src:
+7
View File
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: MIT
def single_file_load_kwargs(original_config_file: str, from_single_file: bool) -> dict:
if from_single_file and original_config_file:
return {"original_config_file": original_config_file}
return {}
+64
View File
@@ -4,6 +4,9 @@ A test script to test the gRPC service and dynamic loader
import unittest
import subprocess
import time
import os
import tempfile
import wave
from unittest.mock import patch, MagicMock
# Import dynamic loader for testing (these don't need gRPC)
@@ -445,3 +448,64 @@ class TestDeviceSelection(unittest.TestCase):
def test_mps_overrides(self):
self.assertEqual(backend.select_device(False, None, True, False, True), "mps")
class TestWritePcmWav(unittest.TestCase):
def test_writes_clipped_float_samples_as_mono_pcm(self):
from audio_utils import write_pcm_wav
destination = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
destination.close()
try:
write_pcm_wav(destination.name, [0.0, 0.5, -0.5, 2.0], 16000)
with wave.open(destination.name, "rb") as generated:
self.assertEqual(generated.getframerate(), 16000)
self.assertEqual(generated.getnchannels(), 1)
self.assertEqual(generated.getsampwidth(), 2)
self.assertEqual(generated.getnframes(), 4)
self.assertEqual(
generated.readframes(4),
b"\x00\x00\x00@\x00\xc0\xff\x7f",
)
finally:
os.unlink(destination.name)
@unittest.skipUnless(GRPC_AVAILABLE, "gRPC modules not available")
class TestSoundGeneration(unittest.TestCase):
def test_maps_request_options_and_writes_pipeline_audio(self):
from backend import BackendServicer
service = BackendServicer.__new__(BackendServicer)
service.options = {"num_inference_steps": 200.0}
service.pipe = MagicMock()
service.pipe.return_value.audios = [[0.0, 0.5, -0.5]]
service.pipe.vae.config.sampling_rate = 16000
destination = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
destination.close()
try:
request = backend_pb2.SoundGenerationRequest(
text="ocean waves",
dst=destination.name,
duration=2.5,
temperature=0,
)
result = service.SoundGeneration(request, context=None)
self.assertTrue(result.success, result.message)
service.pipe.assert_called_once_with(
num_inference_steps=200,
prompt="ocean waves",
audio_length_in_s=2.5,
guidance_scale=0,
)
with wave.open(destination.name, "rb") as generated:
self.assertEqual(generated.getframerate(), 16000)
self.assertEqual(generated.getnframes(), 3)
finally:
os.unlink(destination.name)
@@ -0,0 +1,26 @@
# SPDX-License-Identifier: MIT
import unittest
from backend.python.diffusers.load_options import single_file_load_kwargs
class SingleFileLoadKwargsTest(unittest.TestCase):
def test_includes_original_config_for_single_file(self):
self.assertEqual(
single_file_load_kwargs("configs/v1-inference.yaml", True),
{"original_config_file": "configs/v1-inference.yaml"},
)
def test_omits_original_config_for_pretrained_model(self):
self.assertEqual(
single_file_load_kwargs("configs/v1-inference.yaml", False),
{},
)
def test_omits_empty_original_config(self):
self.assertEqual(single_file_load_kwargs("", True), {})
if __name__ == "__main__":
unittest.main()
+13
View File
@@ -26,4 +26,17 @@ if [ "x${BUILD_PROFILE}" == "xl4t12" ]; then
USE_PIP=true
fi
CTRANSLATE2_VERSION=${CTRANSLATE2_VERSION:-v4.7.1}
CTRANSLATE2_ROCM_WHEEL_OS=${CTRANSLATE2_ROCM_WHEEL_OS:-Linux}
CTRANSLATE2_ROCM_WHEEL_ARCHIVE="rocm-python-wheels-${CTRANSLATE2_ROCM_WHEEL_OS}.zip"
if [ "x${BUILD_PROFILE}" == "xhipblas" ]; then
ensureVenv
mkdir /tmp/ctranslate2-rocm
wget -O "/tmp/ctranslate2-rocm/${CTRANSLATE2_ROCM_WHEEL_ARCHIVE}" "https://github.com/OpenNMT/CTranslate2/releases/download/${CTRANSLATE2_VERSION}/${CTRANSLATE2_ROCM_WHEEL_ARCHIVE}"
unzip "/tmp/ctranslate2-rocm/${CTRANSLATE2_ROCM_WHEEL_ARCHIVE}" -d /tmp/ctranslate2-rocm/
python3 -m ensurepip
python3 -m pip install --no-dependencies --no-index --find-links=/tmp/ctranslate2-rocm/temp-linux/ ctranslate2
fi
installRequirements
+11 -3
View File
@@ -1,18 +1,26 @@
.PHONY: fish-speech test-source-preparation
fish-speech: test-source-preparation
.PHONY: fish-speech test-source-preparation test-runtime-import
fish-speech: test-source-preparation test-runtime-import
bash install.sh
test-source-preparation:
bash prepare-source_test.sh
test-runtime-import:
bash run_test.sh
.PHONY: run
run: fish-speech
@echo "Running fish-speech..."
bash run.sh
@echo "fish-speech run."
.PHONY: test-unit
test-unit:
python3 -m unittest -v prepare_upstream_test.py
bash run_test.sh
.PHONY: test
test: fish-speech
test: fish-speech test-unit
@echo "Testing fish-speech..."
bash test.sh
@echo "fish-speech tested."
+20 -2
View File
@@ -326,11 +326,29 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
max_new_tokens = self.options.get("max_new_tokens", 1024)
chunk_length = self.options.get("chunk_length", 200)
# Build references list for voice cloning
# Build references list for voice cloning. Saved LocalAI
# personalities use the same ordered JSON shape as audio.cpp.
references = []
voice_name = request.voice if request.voice else None
if voice_name and os.path.isfile(voice_name):
multi_reference_cond = request.params.get("multi_reference_cond", "") if hasattr(request, "params") else ""
if multi_reference_cond:
reference_entries = json.loads(multi_reference_cond)
if not isinstance(reference_entries, list) or not reference_entries:
raise ValueError("multi_reference_cond must be a non-empty JSON array")
for entry in reference_entries:
if not isinstance(entry, dict) or not entry.get("audio") or not entry.get("text"):
raise ValueError("multi_reference_cond entries require audio and text")
ref_audio_path = self._get_ref_audio_path(entry["audio"])
with open(ref_audio_path, "rb") as f:
audio_bytes = f.read()
references.append(ServeReferenceAudio(audio=audio_bytes, text=entry["text"]))
print(
f"[INFO] Using {len(references)} per-request reference audios",
file=sys.stderr,
)
elif voice_name and os.path.isfile(voice_name):
ref_audio_path = self._get_ref_audio_path(voice_name)
with open(ref_audio_path, "rb") as f:
audio_bytes = f.read()
+7
View File
@@ -44,6 +44,13 @@ fi
# editable install. pyaudio is only used by the upstream playback client.
bash "${backend_dir}/prepare-source.sh" "${BUILD_TYPE:-}" "${FISH_SPEECH_DIR}/pyproject.toml"
# CUDA 13 has no torch 2.8 wheels, so fish-speech's exact upstream pin would
# make pip select the CPU-only aarch64 wheel from PyPI. Prepare the cloned tree
# before resolving it, and use soundfile for reference audio because torchcodec
# does not publish Linux aarch64 wheels.
python3 "${backend_dir}/prepare_upstream.py" "${FISH_SPEECH_DIR}" \
--cuda-major "${CUDA_MAJOR_VERSION:-}"
# Install fish-speech deps from source (without the package itself since we use PYTHONPATH)
ensureVenv
if [ "x${USE_PIP}" == "xtrue" ]; then
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
import argparse
from pathlib import Path
TORCH_28 = '"torch==2.8.0"'
TORCH_29 = '"torch==2.9.1"'
TORCHAUDIO_28 = '"torchaudio==2.8.0"'
TORCHAUDIO_29 = '"torchaudio==2.9.1"'
TORCHAUDIO_LOAD = (
" waveform, original_sr = "
"torchaudio.load(reference_audio, backend=self.backend)"
)
SOUNDFILE_LOAD = "\n".join(
(
" import soundfile as _sf",
" import torch as _torch",
"",
" data, original_sr = _sf.read(",
' reference_audio, dtype="float32", always_2d=True',
" )",
" waveform = _torch.from_numpy(data.T.copy())",
)
)
def patch_cuda13_dependencies(pyproject: Path) -> None:
content = pyproject.read_text()
if (
TORCH_28 not in content
and TORCHAUDIO_28 not in content
and TORCH_29 in content
and TORCHAUDIO_29 in content
):
return
if TORCH_28 not in content or TORCHAUDIO_28 not in content:
raise RuntimeError("fish-speech's torch 2.8 dependency pins have changed")
content = content.replace(TORCH_28, TORCH_29)
content = content.replace(TORCHAUDIO_28, TORCHAUDIO_29)
pyproject.write_text(content)
def patch_reference_loader(loader: Path) -> None:
content = loader.read_text()
if TORCHAUDIO_LOAD not in content and content.count(SOUNDFILE_LOAD) == 1:
return
if content.count(TORCHAUDIO_LOAD) != 1:
raise RuntimeError("fish-speech's torchaudio.load call has changed")
loader.write_text(content.replace(TORCHAUDIO_LOAD, SOUNDFILE_LOAD))
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path)
parser.add_argument("--cuda-major")
args = parser.parse_args()
if args.cuda_major == "13":
patch_cuda13_dependencies(args.source / "pyproject.toml")
patch_reference_loader(
args.source / "fish_speech/inference_engine/reference_loader.py"
)
if __name__ == "__main__":
main()
@@ -0,0 +1,136 @@
# SPDX-License-Identifier: MIT
import importlib.util
import sys
import tempfile
import types
import unittest
from pathlib import Path
MODULE_PATH = Path(__file__).with_name("prepare_upstream.py")
def load_prepare_upstream():
if not MODULE_PATH.exists():
raise AssertionError("prepare_upstream.py is missing")
spec = importlib.util.spec_from_file_location("prepare_upstream", MODULE_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class FakeAudioData:
@property
def T(self):
return self
def copy(self):
return "channels-first"
class PrepareUpstreamTests(unittest.TestCase):
def test_cuda13_dependencies_follow_available_pytorch_wheels(self):
prepare_upstream = load_prepare_upstream()
with tempfile.TemporaryDirectory() as tmp:
pyproject = Path(tmp) / "pyproject.toml"
pyproject.write_text(
'dependencies = [\n "torch==2.8.0",\n "torchaudio==2.8.0",\n]\n'
'stable = [\n "torch==2.8.0",\n "torchaudio",\n]\n'
)
prepare_upstream.patch_cuda13_dependencies(pyproject)
self.assertEqual(
pyproject.read_text(),
'dependencies = [\n "torch==2.9.1",\n "torchaudio==2.9.1",\n]\n'
'stable = [\n "torch==2.9.1",\n "torchaudio",\n]\n',
)
def test_reference_audio_uses_soundfile_without_torchcodec(self):
prepare_upstream = load_prepare_upstream()
with tempfile.TemporaryDirectory() as tmp:
loader = Path(tmp) / "reference_loader.py"
loader.write_text(
"class ReferenceLoader:\n"
" def load_audio(self, reference_audio):\n"
" waveform, original_sr = torchaudio.load(reference_audio, backend=self.backend)\n"
" return waveform, original_sr\n"
)
prepare_upstream.patch_reference_loader(loader)
calls = []
fake_soundfile = types.SimpleNamespace(
read=lambda source, **kwargs: (
calls.append((source, kwargs)) or FakeAudioData(),
24000,
)
)
fake_torch = types.SimpleNamespace(
from_numpy=lambda data: ("tensor", data),
)
previous_soundfile = sys.modules.get("soundfile")
previous_torch = sys.modules.get("torch")
sys.modules["soundfile"] = fake_soundfile
sys.modules["torch"] = fake_torch
try:
namespace = {"torchaudio": None}
exec(compile(loader.read_text(), str(loader), "exec"), namespace)
instance = namespace["ReferenceLoader"]()
instance.backend = "soundfile"
waveform, sample_rate = instance.load_audio("voice.wav")
finally:
if previous_soundfile is None:
del sys.modules["soundfile"]
else:
sys.modules["soundfile"] = previous_soundfile
if previous_torch is None:
del sys.modules["torch"]
else:
sys.modules["torch"] = previous_torch
self.assertEqual(waveform, ("tensor", "channels-first"))
self.assertEqual(sample_rate, 24000)
self.assertEqual(
calls,
[("voice.wav", {"dtype": "float32", "always_2d": True})],
)
def test_reference_loader_drift_fails_the_build(self):
prepare_upstream = load_prepare_upstream()
with tempfile.TemporaryDirectory() as tmp:
loader = Path(tmp) / "reference_loader.py"
loader.write_text("def load_audio():\n pass\n")
with self.assertRaisesRegex(RuntimeError, "torchaudio.load call"):
prepare_upstream.patch_reference_loader(loader)
def test_preparation_can_be_repeated(self):
prepare_upstream = load_prepare_upstream()
with tempfile.TemporaryDirectory() as tmp:
pyproject = Path(tmp) / "pyproject.toml"
pyproject.write_text(
'dependencies = ["torch==2.8.0", "torchaudio==2.8.0"]\n'
)
loader = Path(tmp) / "reference_loader.py"
loader.write_text(
"def load_audio(reference_audio):\n"
" waveform, original_sr = torchaudio.load(reference_audio, backend=self.backend)\n"
)
prepare_upstream.patch_cuda13_dependencies(pyproject)
prepare_upstream.patch_reference_loader(loader)
try:
prepare_upstream.patch_cuda13_dependencies(pyproject)
prepare_upstream.patch_reference_loader(loader)
except RuntimeError as err:
self.fail(f"preparation is not idempotent: {err}")
if __name__ == "__main__":
unittest.main()
@@ -1,3 +1,3 @@
--extra-index-url https://download.pytorch.org/whl/cu130
torch
torchaudio
torch==2.9.1+cu130
torchaudio==2.9.1
@@ -1,3 +1,3 @@
--extra-index-url https://download.pytorch.org/whl/cu130
torch
torchaudio
torch==2.9.1+cu130
torchaudio==2.9.1
+11 -2
View File
@@ -1,9 +1,18 @@
#!/bin/bash
backend_dir=$(dirname $0)
backend_dir=$(dirname "$(realpath "$0")")
if [ -d $backend_dir/common ]; then
source $backend_dir/common/libbackend.sh
else
source $backend_dir/../common/libbackend.sh
fi
startBackend $@
# Editable installs record their build-time absolute source path, which becomes
# stale when the backend is relocated under /backends at install time.
export PYTHONPATH="${backend_dir}/fish-speech-src${PYTHONPATH:+:${PYTHONPATH}}"
cuda_home=${CUDA_HOME:-/usr/local/cuda}
if [ -z "${TRITON_PTXAS_PATH:-}" ] && [ -x "$cuda_home/bin/ptxas" ]; then
export TRITON_PTXAS_PATH="$cuda_home/bin/ptxas"
fi
startBackend "$@"
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(dirname "$(realpath "$0")")
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT
mkdir -p "$WORK_DIR/runtime/common" "$WORK_DIR/runtime/fish-speech-src/fish_speech"
cp "$SCRIPT_DIR/run.sh" "$WORK_DIR/runtime/run.sh"
cat > "$WORK_DIR/runtime/fish-speech-src/fish_speech/inference_engine.py" <<'PY'
IMPORT_OK = True
PY
cat > "$WORK_DIR/runtime/common/libbackend.sh" <<'SH'
startBackend() {
python3 -c 'from fish_speech.inference_engine import IMPORT_OK; assert IMPORT_OK'
}
SH
bash "$WORK_DIR/runtime/run.sh"
echo "PASS: runtime launcher imports relocated fish-speech source"
+23
View File
@@ -0,0 +1,23 @@
.PHONY: funasr
funasr:
bash install.sh
.PHONY: run
run: funasr
@echo "Running funasr..."
bash run.sh
@echo "funasr run."
.PHONY: test
test: funasr
@echo "Testing funasr..."
bash test.sh
@echo "funasr tested."
.PHONY: protogen-clean
protogen-clean:
$(RM) backend_pb2_grpc.py backend_pb2.py
.PHONY: clean
clean: protogen-clean
rm -rf venv __pycache__
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""
gRPC backend for LocalAI wrapping FunASR (SenseVoice / Paraformer).
"""
from concurrent import futures
import time
import argparse
import signal
import sys
import os
import backend_pb2
import backend_pb2_grpc
import torch
import grpc
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
from grpc_auth import get_auth_interceptors
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
MAX_WORKERS = int(os.environ.get('PYTHON_GRPC_MAX_WORKERS', '1'))
class BackendServicer(backend_pb2_grpc.BackendServicer):
def Health(self, request, context):
return backend_pb2.Reply(message=bytes("OK", 'utf-8'))
def LoadModel(self, request, context):
from funasr import AutoModel
device = "cpu"
if request.CUDA and torch.cuda.is_available():
device = "cuda"
elif hasattr(torch, "xpu") and torch.xpu.is_available():
device = "xpu"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
device = "mps"
model_id = request.Model or "iic/SenseVoiceSmall"
candidate_devices = [device]
if device in ("xpu", "mps"):
candidate_devices.append("cpu")
for candidate_device in candidate_devices:
try:
print(
f"Loading FunASR model: {model_id} on {candidate_device}",
file=sys.stderr,
)
self.model = AutoModel(
model=model_id,
vad_model="fsmn-vad",
device=candidate_device,
disable_update=True,
)
print("FunASR model loaded successfully", file=sys.stderr)
break
except Exception as err:
if candidate_device != candidate_devices[-1]:
print(
f"[WARN] FunASR {candidate_device} initialization failed: "
f"{err}; retrying on cpu",
file=sys.stderr,
)
continue
print(f"[ERROR] LoadModel failed: {err}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
return backend_pb2.Result(success=False, message=str(err))
return backend_pb2.Result(message="Model loaded successfully", success=True)
def AudioTranscription(self, request, context):
from funasr.utils.postprocess_utils import rich_transcription_postprocess
result_segments = []
text = ""
try:
audio_path = request.dst
if not audio_path or not os.path.exists(audio_path):
print(f"Error: Audio file not found: {audio_path}", file=sys.stderr)
return backend_pb2.TranscriptResult(segments=[], text="")
language = None
if request.language and request.language.strip():
language = request.language.strip()
kwargs = {}
if language:
kwargs["language"] = language
results = self.model.generate(input=audio_path, **kwargs)
if not results:
return backend_pb2.TranscriptResult(segments=[], text="")
for idx, r in enumerate(results):
seg_text = r.get("text", "") if isinstance(r, dict) else str(r)
seg_text = rich_transcription_postprocess(seg_text)
text += seg_text
result_segments.append(backend_pb2.TranscriptSegment(
id=idx,
start=0,
end=0,
text=seg_text,
))
except Exception as err:
print(f"Error in AudioTranscription: {err}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
return backend_pb2.TranscriptResult(segments=[], text="")
return backend_pb2.TranscriptResult(segments=result_segments, text=text)
def serve(address):
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=MAX_WORKERS),
options=[
('grpc.max_message_length', 50 * 1024 * 1024),
('grpc.max_send_message_length', 50 * 1024 * 1024),
('grpc.max_receive_message_length', 50 * 1024 * 1024),
],
interceptors=get_auth_interceptors(),
)
backend_pb2_grpc.add_BackendServicer_to_server(BackendServicer(), server)
server.add_insecure_port(address)
server.start()
print("Server started. Listening on: " + address, file=sys.stderr)
def signal_handler(sig, frame):
print("Received termination signal. Shutting down...")
server.stop(0)
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
try:
while True:
time.sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
server.stop(0)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the gRPC server.")
parser.add_argument("--addr", default="localhost:50051", help="The address to bind the server to.")
args = parser.parse_args()
serve(args.addr)
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
set -e
EXTRA_PIP_INSTALL_FLAGS="--no-build-isolation"
backend_dir=$(dirname $0)
if [ -d $backend_dir/common ]; then
source $backend_dir/common/libbackend.sh
else
source $backend_dir/../common/libbackend.sh
fi
if [ "x${BUILD_PROFILE}" == "xintel" ]; then
EXTRA_PIP_INSTALL_FLAGS+=" --upgrade --index-strategy=unsafe-first-match"
fi
PYTHON_VERSION="3.12"
PYTHON_PATCH="12"
PY_STANDALONE_TAG="20251120"
installRequirements
@@ -0,0 +1,4 @@
--extra-index-url https://download.pytorch.org/whl/cpu
torch
torchaudio
funasr
@@ -0,0 +1,4 @@
--extra-index-url https://download.pytorch.org/whl/cu121
torch
torchaudio
funasr
@@ -0,0 +1,4 @@
--extra-index-url https://download.pytorch.org/whl/cu130
torch
torchaudio
funasr
@@ -0,0 +1,4 @@
--extra-index-url https://download.pytorch.org/whl/rocm7.0
torch
torchaudio
funasr
@@ -0,0 +1,4 @@
--extra-index-url https://download.pytorch.org/whl/xpu
torch
torchaudio
funasr
@@ -0,0 +1,4 @@
--extra-index-url https://pypi.jetson-ai-lab.io/jp6/cu129/
torch
torchaudio
funasr
@@ -0,0 +1,4 @@
--extra-index-url https://download.pytorch.org/whl/cu130
torch
torchaudio
funasr
@@ -0,0 +1,3 @@
torch==2.7.1
torchaudio==2.7.1
funasr
+5
View File
@@ -0,0 +1,5 @@
grpcio==1.71.0
protobuf
certifi
packaging==24.1
setuptools
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
backend_dir=$(dirname $0)
if [ -d $backend_dir/common ]; then
source $backend_dir/common/libbackend.sh
else
source $backend_dir/../common/libbackend.sh
fi
startBackend $@
+295
View File
@@ -0,0 +1,295 @@
import importlib
import os
import re
import sys
import types
import unittest
class _Reply:
def __init__(self, message=b""):
self.message = message
class _Result:
def __init__(self, message="", success=False):
self.message = message
self.success = success
class _TranscriptSegment:
def __init__(self, id=0, start=0, end=0, text=""):
self.id = id
self.start = start
self.end = end
self.text = text
class _TranscriptResult:
def __init__(self, segments=None, text=""):
self.segments = segments or []
self.text = text
class _FakeBackendServicer:
pass
class _FakeTorch:
cuda = types.SimpleNamespace(is_available=lambda: False)
xpu = types.SimpleNamespace(is_available=lambda: False)
backends = types.SimpleNamespace(mps=types.SimpleNamespace(is_available=lambda: False))
class _FakeAutoModel:
instances = []
attempts = []
fail_devices = set()
def __init__(self, **kwargs):
_FakeAutoModel.attempts.append(kwargs)
if kwargs.get("device") in _FakeAutoModel.fail_devices:
raise RuntimeError(f"unsupported device: {kwargs['device']}")
self.kwargs = kwargs
self.generate_calls = []
self.results = [{"text": "hello"}, {"text": " world"}]
_FakeAutoModel.instances.append(self)
def generate(self, **kwargs):
self.generate_calls.append(kwargs)
return self.results
def _install_stubs():
sys.modules["backend_pb2"] = types.SimpleNamespace(
Reply=_Reply,
Result=_Result,
TranscriptSegment=_TranscriptSegment,
TranscriptResult=_TranscriptResult,
)
sys.modules["backend_pb2_grpc"] = types.SimpleNamespace(
BackendServicer=_FakeBackendServicer,
add_BackendServicer_to_server=lambda *args, **kwargs: None,
)
sys.modules["grpc"] = types.SimpleNamespace(
RpcMethodHandler=object,
ServerInterceptor=object,
StatusCode=types.SimpleNamespace(UNAUTHENTICATED="UNAUTHENTICATED"),
aio=types.SimpleNamespace(ServerInterceptor=object),
server=lambda *args, **kwargs: None,
)
sys.modules["torch"] = _FakeTorch
funasr = types.ModuleType("funasr")
funasr.__path__ = []
funasr.AutoModel = _FakeAutoModel
funasr_utils = types.ModuleType("funasr.utils")
funasr_utils.__path__ = []
postprocess_utils = types.ModuleType("funasr.utils.postprocess_utils")
postprocess_utils.rich_transcription_postprocess = lambda text: re.sub(
r"<\|.*?\|>", "", text
)
sys.modules["funasr"] = funasr
sys.modules["funasr.utils"] = funasr_utils
sys.modules["funasr.utils.postprocess_utils"] = postprocess_utils
def _load_backend():
_install_stubs()
sys.modules.pop("backend", None)
_FakeAutoModel.instances.clear()
_FakeAutoModel.attempts.clear()
_FakeAutoModel.fail_devices.clear()
return importlib.import_module("backend")
class TestFunASRBackend(unittest.TestCase):
def test_torch_profiles_install_torchaudio(self):
backend_dir = os.path.dirname(__file__)
for profile in (
"cpu",
"cublas12",
"cublas13",
"hipblas",
"intel",
"l4t12",
"l4t13",
"mps",
):
with self.subTest(profile=profile):
requirements_path = os.path.join(
backend_dir, f"requirements-{profile}.txt"
)
with open(requirements_path, encoding="utf-8") as requirements_file:
requirements = {
line.strip().split("=", 1)[0]
for line in requirements_file
if line.strip() and not line.lstrip().startswith(("#", "--"))
}
self.assertIn("torchaudio", requirements)
def test_accelerator_profiles_use_current_pytorch_indexes(self):
backend_dir = os.path.dirname(__file__)
expected_indexes = {
"cublas13": "https://download.pytorch.org/whl/cu130",
"hipblas": "https://download.pytorch.org/whl/rocm7.0",
"intel": "https://download.pytorch.org/whl/xpu",
"l4t12": "https://pypi.jetson-ai-lab.io/jp6/cu129/",
"l4t13": "https://download.pytorch.org/whl/cu130",
}
for profile, expected_index in expected_indexes.items():
with self.subTest(profile=profile):
requirements_path = os.path.join(
backend_dir, f"requirements-{profile}.txt"
)
with open(requirements_path, encoding="utf-8") as requirements_file:
requirements = requirements_file.read()
self.assertIn(
f"--extra-index-url {expected_index}\n",
requirements,
)
def test_health_returns_ok(self):
backend = _load_backend()
servicer = backend.BackendServicer()
reply = servicer.Health(types.SimpleNamespace(), None)
self.assertEqual(reply.message, b"OK")
def test_load_model_uses_default_sensevoice_model_on_cpu(self):
backend = _load_backend()
servicer = backend.BackendServicer()
result = servicer.LoadModel(types.SimpleNamespace(Model="", CUDA=False), None)
self.assertTrue(result.success, result.message)
self.assertEqual(result.message, "Model loaded successfully")
self.assertEqual(_FakeAutoModel.instances[0].kwargs["model"], "iic/SenseVoiceSmall")
self.assertEqual(_FakeAutoModel.instances[0].kwargs["vad_model"], "fsmn-vad")
self.assertEqual(_FakeAutoModel.instances[0].kwargs["device"], "cpu")
self.assertTrue(_FakeAutoModel.instances[0].kwargs["disable_update"])
def test_load_model_uses_xpu_when_available(self):
backend = _load_backend()
servicer = backend.BackendServicer()
original_xpu = _FakeTorch.xpu
_FakeTorch.xpu = types.SimpleNamespace(is_available=lambda: True)
self.addCleanup(setattr, _FakeTorch, "xpu", original_xpu)
result = servicer.LoadModel(
types.SimpleNamespace(Model="iic/SenseVoiceSmall", CUDA=False), None
)
self.assertTrue(result.success, result.message)
self.assertEqual(_FakeAutoModel.instances[0].kwargs["device"], "xpu")
def test_load_model_prefers_requested_cuda_over_xpu_and_mps(self):
backend = _load_backend()
servicer = backend.BackendServicer()
original_cuda = _FakeTorch.cuda
original_xpu = _FakeTorch.xpu
original_mps = _FakeTorch.backends.mps
_FakeTorch.cuda = types.SimpleNamespace(is_available=lambda: True)
_FakeTorch.xpu = types.SimpleNamespace(is_available=lambda: True)
_FakeTorch.backends.mps = types.SimpleNamespace(is_available=lambda: True)
self.addCleanup(setattr, _FakeTorch, "cuda", original_cuda)
self.addCleanup(setattr, _FakeTorch, "xpu", original_xpu)
self.addCleanup(setattr, _FakeTorch.backends, "mps", original_mps)
result = servicer.LoadModel(
types.SimpleNamespace(Model="iic/SenseVoiceSmall", CUDA=True), None
)
self.assertTrue(result.success, result.message)
self.assertEqual(
[attempt["device"] for attempt in _FakeAutoModel.attempts], ["cuda"]
)
def test_load_model_retries_cpu_when_xpu_initialization_fails(self):
backend = _load_backend()
servicer = backend.BackendServicer()
original_xpu = _FakeTorch.xpu
_FakeTorch.xpu = types.SimpleNamespace(is_available=lambda: True)
_FakeAutoModel.fail_devices.add("xpu")
self.addCleanup(setattr, _FakeTorch, "xpu", original_xpu)
result = servicer.LoadModel(
types.SimpleNamespace(Model="iic/SenseVoiceSmall", CUDA=False), None
)
self.assertTrue(result.success, result.message)
self.assertEqual(
[attempt["device"] for attempt in _FakeAutoModel.attempts],
["xpu", "cpu"],
)
self.assertEqual(_FakeAutoModel.instances[0].kwargs["device"], "cpu")
def test_load_model_retries_cpu_when_mps_initialization_fails(self):
backend = _load_backend()
servicer = backend.BackendServicer()
original_mps = _FakeTorch.backends.mps
_FakeTorch.backends.mps = types.SimpleNamespace(is_available=lambda: True)
_FakeAutoModel.fail_devices.add("mps")
self.addCleanup(setattr, _FakeTorch.backends, "mps", original_mps)
result = servicer.LoadModel(
types.SimpleNamespace(Model="iic/SenseVoiceSmall", CUDA=False), None
)
self.assertTrue(result.success, result.message)
self.assertEqual(
[attempt["device"] for attempt in _FakeAutoModel.attempts],
["mps", "cpu"],
)
self.assertEqual(_FakeAutoModel.instances[0].kwargs["device"], "cpu")
def test_audio_transcription_passes_language_and_builds_segments(self):
backend = _load_backend()
servicer = backend.BackendServicer()
servicer.model = _FakeAutoModel()
audio_path = os.path.abspath(__file__)
request = types.SimpleNamespace(dst=audio_path, language=" zh ")
result = servicer.AudioTranscription(request, None)
self.assertEqual(result.text, "hello world")
self.assertEqual([segment.text for segment in result.segments], ["hello", " world"])
self.assertEqual(servicer.model.generate_calls, [{"input": audio_path, "language": "zh"}])
def test_audio_transcription_cleans_sensevoice_tags(self):
backend = _load_backend()
servicer = backend.BackendServicer()
servicer.model = _FakeAutoModel()
servicer.model.results = [
{"text": "<|zh|><|NEUTRAL|><|Speech|><|woitn|>hello"}
]
result = servicer.AudioTranscription(
types.SimpleNamespace(dst=os.path.abspath(__file__), language=""),
None,
)
self.assertEqual(result.text, "hello")
self.assertEqual([segment.text for segment in result.segments], ["hello"])
def test_audio_transcription_missing_file_returns_empty_result(self):
backend = _load_backend()
servicer = backend.BackendServicer()
servicer.model = _FakeAutoModel()
result = servicer.AudioTranscription(
types.SimpleNamespace(dst="/tmp/localai-funasr-missing.wav", language=""),
None,
)
self.assertEqual(result.text, "")
self.assertEqual(result.segments, [])
self.assertEqual(servicer.model.generate_calls, [])
if __name__ == "__main__":
unittest.main()
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
set -e
backend_dir=$(dirname $0)
if [ -d $backend_dir/common ]; then
source $backend_dir/common/libbackend.sh
else
source $backend_dir/../common/libbackend.sh
fi
runUnittests "$@"
+3 -2
View File
@@ -440,8 +440,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
kwargs["tools"] = json.loads(request.Tools)
except json.JSONDecodeError:
pass
if request.Metadata.get("enable_thinking", "").lower() == "true":
kwargs["enable_thinking"] = True
enable_thinking = request.Metadata.get("enable_thinking", "").lower()
if enable_thinking in ("true", "false"):
kwargs["enable_thinking"] = enable_thinking == "true"
try:
return self.tokenizer.apply_chat_template(messages, **kwargs)
except TypeError:
+34
View File
@@ -135,3 +135,37 @@ class TestSharedHelpers(unittest.TestCase):
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0]["name"], "get_weather")
self.assertEqual(calls[0]["arguments"], '{"location": "Paris"}')
class TestPreparePrompt(unittest.TestCase):
"""Server-less tests for BackendServicer._prepare_prompt."""
def test_forwards_enable_thinking(self):
from backend import BackendServicer
class Tok:
def __init__(self):
self.kwargs = None
def apply_chat_template(self, messages, **kwargs):
self.kwargs = kwargs
return "PROMPT"
def kwargs_for(metadata):
servicer = BackendServicer()
servicer.tokenizer = Tok()
req = types.SimpleNamespace(
Prompt="",
UseTokenizerTemplate=True,
Messages=[backend_pb2.Message(role="user", content="hi")],
Tools="",
Metadata=metadata,
)
self.assertEqual(servicer._prepare_prompt(req), "PROMPT")
return servicer.tokenizer.kwargs
self.assertIs(kwargs_for({"enable_thinking": "true"})["enable_thinking"], True)
# "false" used to be dropped, so thinking models kept reasoning
self.assertIs(kwargs_for({"enable_thinking": "false"})["enable_thinking"], False)
self.assertIs(kwargs_for({"enable_thinking": "FALSE"})["enable_thinking"], False)
self.assertNotIn("enable_thinking", kwargs_for({}))
+5
View File
@@ -0,0 +1,5 @@
__pycache__/
backend_pb2.py
backend_pb2_grpc.py
common/
venv/
+16
View File
@@ -0,0 +1,16 @@
.PHONY: all run test protogen-clean clean
all:
bash install.sh
run: all
bash run.sh
test:
bash test.sh
protogen-clean:
$(RM) backend_pb2.py backend_pb2_grpc.py
clean: protogen-clean
rm -rf __pycache__ venv
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
import argparse
import os
import platform
import signal
import subprocess
import sys
import traceback
from concurrent import futures
import grpc
import backend_pb2
import backend_pb2_grpc
from mlx_video_utils import build_generation_command, classify_model, parse_options
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "common"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from grpc_auth import get_auth_interceptors # noqa: E402
MAX_WORKERS = int(os.environ.get("PYTHON_GRPC_MAX_WORKERS", "1"))
class BackendServicer(backend_pb2_grpc.BackendServicer):
def __init__(self):
self.family = None
self.model = None
self.options = {}
def Health(self, request, context):
return backend_pb2.Reply(message=b"OK")
def LoadModel(self, request, context):
if platform.system() != "Darwin" or platform.machine() != "arm64":
return self._fail(
context,
grpc.StatusCode.FAILED_PRECONDITION,
"mlx-video requires macOS on Apple Silicon",
)
model = (
request.ModelFile
if request.ModelFile and os.path.isdir(request.ModelFile)
else request.Model
)
family = classify_model(model)
if family is None:
return self._fail(
context,
grpc.StatusCode.INVALID_ARGUMENT,
"mlx-video supports LTX-2 and converted Wan2.1/Wan2.2 MLX checkpoints",
)
if family == "wan" and not os.path.isdir(model):
return self._fail(
context,
grpc.StatusCode.INVALID_ARGUMENT,
"Wan models must be converted to MLX and configured as a local model directory",
)
self.family = family
self.model = model
self.options = parse_options(request.Options)
return backend_pb2.Result(message="mlx-video model configured", success=True)
def Free(self, request, context):
self.family = None
self.model = None
self.options = {}
return backend_pb2.Result(message="Model released", success=True)
def GenerateVideo(self, request, context):
if not self.family or not self.model:
return self._fail(
context, grpc.StatusCode.FAILED_PRECONDITION, "model is not loaded"
)
if not request.prompt.strip():
return self._fail(context, grpc.StatusCode.INVALID_ARGUMENT, "prompt is required")
if not request.dst:
return self._fail(
context,
grpc.StatusCode.INVALID_ARGUMENT,
"output destination is required",
)
process = None
try:
command = build_generation_command(
family=self.family,
model=self.model,
prompt=request.prompt,
destination=request.dst,
negative_prompt=request.negative_prompt,
start_image=request.start_image,
end_image=request.end_image,
audio=request.audio,
width=request.width,
height=request.height,
num_frames=request.num_frames,
fps=request.fps,
seed=request.seed,
steps=request.step,
cfg_scale=request.cfg_scale,
params={**self.options, **dict(request.params)},
)
os.makedirs(
os.path.dirname(request.dst) or ".", mode=0o750, exist_ok=True
)
process = subprocess.Popen(command)
if hasattr(context, "add_callback"):
context.add_callback(
lambda: process.poll() is None and process.terminate()
)
return_code = process.wait()
if return_code != 0:
raise RuntimeError(
f"upstream generator exited with status {return_code}"
)
if not os.path.isfile(request.dst) or os.path.getsize(request.dst) == 0:
raise RuntimeError("upstream generator did not create the requested video")
return backend_pb2.Result(message="Video generated successfully", success=True)
except ValueError as err:
return self._fail(context, grpc.StatusCode.INVALID_ARGUMENT, str(err))
except Exception as err:
print(f"mlx-video generation failed: {err}", file=sys.stderr)
traceback.print_exc()
return self._fail(
context,
grpc.StatusCode.INTERNAL,
f"mlx-video generation failed: {err}",
)
@staticmethod
def _fail(context, code, message):
context.set_code(code)
context.set_details(message)
return backend_pb2.Result(message=message, success=False)
def serve(address):
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=MAX_WORKERS),
options=[
("grpc.max_message_length", 64 * 1024 * 1024),
("grpc.max_send_message_length", 64 * 1024 * 1024),
("grpc.max_receive_message_length", 64 * 1024 * 1024),
],
interceptors=get_auth_interceptors(),
)
backend_pb2_grpc.add_BackendServicer_to_server(BackendServicer(), server)
server.add_insecure_port(address)
server.start()
print(f"MLX Video backend listening on {address}", file=sys.stderr)
def stop_server(signum, frame):
del signum, frame
server.stop(0)
signal.signal(signal.SIGINT, stop_server)
signal.signal(signal.SIGTERM, stop_server)
server.wait_for_termination()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the MLX Video gRPC backend")
parser.add_argument("--addr", default="localhost:50051")
serve(parser.parse_args().addr)
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
set -euo pipefail
USE_PIP=true
PYTHON_VERSION="3.11"
PYTHON_PATCH="13"
backend_dir=$(dirname "$0")
if [ -d "${backend_dir}/common" ]; then
source "${backend_dir}/common/libbackend.sh"
else
source "${backend_dir}/../common/libbackend.sh"
fi
installRequirements
+102
View File
@@ -0,0 +1,102 @@
# SPDX-License-Identifier: MIT
import os
import sys
def parse_options(options):
parsed = {}
for option in options:
key, separator, value = option.partition(":")
parsed[key] = value if separator else "true"
return parsed
def classify_model(model):
normalized = model.rstrip("/").lower()
name = os.path.basename(normalized)
if "ltx-2" in normalized or "ltx2" in normalized:
return "ltx2"
if "wan2" in name or "wan_2" in name:
return "wan"
return None
def _append(command, flag, value):
if value is not None and value != "":
command.extend([flag, str(value)])
def build_generation_command(
*,
family,
model,
prompt,
destination,
negative_prompt="",
start_image="",
end_image="",
audio="",
width=0,
height=0,
num_frames=0,
fps=0,
seed=0,
steps=0,
cfg_scale=0,
params=None,
):
params = params or {}
if audio:
raise ValueError("audio conditioning is not supported by mlx-video")
if family == "ltx2":
command = [sys.executable, "-m", "mlx_video.models.ltx_2.generate"]
_append(command, "--model-repo", model)
_append(command, "--prompt", prompt)
_append(command, "--output-path", destination)
_append(command, "--negative-prompt", negative_prompt)
_append(command, "--image", start_image)
_append(command, "--end-image", end_image)
_append(command, "--width", width or None)
_append(command, "--height", height or None)
_append(command, "--num-frames", num_frames or None)
_append(command, "--fps", fps or None)
_append(command, "--seed", seed if seed > 0 else None)
_append(command, "--steps", steps or None)
_append(command, "--cfg-scale", cfg_scale or None)
for key, flag in {
"pipeline": "--pipeline",
"tiling": "--tiling",
"image_strength": "--image-strength",
"end_image_strength": "--end-image-strength",
"text_encoder_repo": "--text-encoder-repo",
}.items():
_append(command, flag, params.get(key))
return command
if family == "wan":
if end_image:
raise ValueError("Wan models do not support end_image conditioning")
command = [sys.executable, "-m", "mlx_video.models.wan_2.generate"]
_append(command, "--model-dir", model)
_append(command, "--prompt", prompt)
_append(command, "--output-path", destination)
_append(command, "--negative-prompt", negative_prompt)
_append(command, "--image", start_image)
_append(command, "--width", width or None)
_append(command, "--height", height or None)
_append(command, "--num-frames", num_frames or None)
_append(command, "--seed", seed if seed > 0 else None)
_append(command, "--steps", steps or None)
_append(command, "--guide-scale", cfg_scale or None)
for key, flag in {
"scheduler": "--scheduler",
"shift": "--shift",
"tiling": "--tiling",
"trim_first_frames": "--trim-first-frames",
}.items():
_append(command, flag, params.get(key))
return command
raise ValueError(f"unsupported mlx-video model family: {family}")
@@ -0,0 +1 @@
git+https://github.com/Blaizzy/mlx-video.git@87db56a51758fefb748a359b90a5283bb8ba4837
@@ -0,0 +1,4 @@
grpcio==1.71.0
protobuf
certifi
setuptools
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
set -euo pipefail
backend_dir=$(dirname "$0")
if [ -d "${backend_dir}/common" ]; then
source "${backend_dir}/common/libbackend.sh"
else
source "${backend_dir}/../common/libbackend.sh"
fi
startBackend "$@"
+86
View File
@@ -0,0 +1,86 @@
# SPDX-License-Identifier: MIT
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mlx_video_utils import ( # noqa: E402
build_generation_command,
classify_model,
parse_options,
)
class MLXVideoUtilsTest(unittest.TestCase):
def test_parses_model_options_and_preserves_colons(self):
self.assertEqual(
parse_options(["pipeline:dev", "tiling:aggressive", "flag"]),
{"pipeline": "dev", "tiling": "aggressive", "flag": "true"},
)
def test_classifies_supported_model_families(self):
self.assertEqual(classify_model("Lightricks/LTX-2"), "ltx2")
self.assertEqual(classify_model("/models/Wan2.2-TI2V-5B-mlx"), "wan")
self.assertIsNone(classify_model("some-org/unrelated-model"))
def test_maps_ltx_request_without_invoking_a_shell(self):
command = build_generation_command(
family="ltx2",
model="prince-canuma/LTX-2.3-distilled-mlx",
prompt="waves; touch /tmp/nope",
destination="/tmp/out.mp4",
negative_prompt="blurry",
start_image="/tmp/start.png",
end_image="/tmp/end.png",
width=768,
height=512,
num_frames=49,
fps=24,
seed=7,
steps=12,
cfg_scale=3.5,
params={"pipeline": "dev", "tiling": "aggressive"},
)
self.assertEqual(command[:3], [sys.executable, "-m", "mlx_video.models.ltx_2.generate"])
self.assertIn("waves; touch /tmp/nope", command)
self.assertEqual(command[command.index("--model-repo") + 1], "prince-canuma/LTX-2.3-distilled-mlx")
self.assertEqual(command[command.index("--pipeline") + 1], "dev")
self.assertEqual(command[command.index("--cfg-scale") + 1], "3.5")
self.assertIn("--end-image", command)
def test_maps_wan_request_and_rejects_unsupported_audio(self):
command = build_generation_command(
family="wan",
model="/models/wan22-mlx",
prompt="a fox",
destination="/tmp/out.mp4",
width=1280,
height=704,
num_frames=81,
fps=16,
seed=9,
steps=20,
cfg_scale=4.0,
params={"scheduler": "unipc", "shift": "5.0"},
)
self.assertEqual(command[:3], [sys.executable, "-m", "mlx_video.models.wan_2.generate"])
self.assertEqual(command[command.index("--model-dir") + 1], "/models/wan22-mlx")
self.assertEqual(command[command.index("--guide-scale") + 1], "4.0")
self.assertNotIn("--fps", command)
with self.assertRaisesRegex(ValueError, "audio conditioning"):
build_generation_command(
family="wan",
model="/models/wan22-mlx",
prompt="a fox",
destination="/tmp/out.mp4",
audio="/tmp/input.wav",
)
if __name__ == "__main__":
unittest.main()
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
set -euo pipefail
backend_dir=$(dirname "$0")
if [ -d "${backend_dir}/common" ]; then
source "${backend_dir}/common/libbackend.sh"
else
source "${backend_dir}/../common/libbackend.sh"
fi
runUnittests
+3 -2
View File
@@ -376,8 +376,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
kwargs["tools"] = json.loads(request.Tools)
except json.JSONDecodeError:
pass
if request.Metadata.get("enable_thinking", "").lower() == "true":
kwargs["enable_thinking"] = True
enable_thinking = request.Metadata.get("enable_thinking", "").lower()
if enable_thinking in ("true", "false"):
kwargs["enable_thinking"] = enable_thinking == "true"
return kwargs
def _apply_template(self, request, messages, num_images, num_audios):
+2 -2
View File
@@ -432,8 +432,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
except json.JSONDecodeError:
pass
enable_thinking = request.Metadata.get("enable_thinking", "").lower()
if enable_thinking == "true":
kwargs["enable_thinking"] = True
if enable_thinking in ("true", "false"):
kwargs["enable_thinking"] = enable_thinking == "true"
try:
return self.tokenizer.apply_chat_template(messages, **kwargs)
+36
View File
@@ -340,4 +340,40 @@ class TestSharedHelpers(unittest.TestCase):
self.assertNotIn("<tool_call>", remaining)
class TestPreparePrompt(unittest.TestCase):
"""Server-less tests for BackendServicer._prepare_prompt."""
def test_forwards_enable_thinking(self):
from backend import BackendServicer
class Tok:
chat_template = "{{ messages }}"
def __init__(self):
self.kwargs = None
def apply_chat_template(self, messages, **kwargs):
self.kwargs = kwargs
return "PROMPT"
def kwargs_for(metadata):
servicer = BackendServicer()
servicer.tokenizer = Tok()
req = types.SimpleNamespace(
Prompt="",
UseTokenizerTemplate=True,
Messages=[backend_pb2.Message(role="user", content="hi")],
Tools="",
Metadata=metadata,
)
self.assertEqual(servicer._prepare_prompt(req), "PROMPT")
return servicer.tokenizer.kwargs
self.assertIs(kwargs_for({"enable_thinking": "true"})["enable_thinking"], True)
# "false" used to be dropped, so thinking models kept reasoning
self.assertIs(kwargs_for({"enable_thinking": "false"})["enable_thinking"], False)
self.assertIs(kwargs_for({"enable_thinking": "FALSE"})["enable_thinking"], False)
self.assertNotIn("enable_thinking", kwargs_for({}))
# Unit tests for ThreadSafeLRUPromptCache are in test_mlx_cache.py
+2 -2
View File
@@ -1,6 +1,6 @@
torch==2.7.1
llvmlite==0.43.0
numba==0.60.0
llvmlite==0.49.0
numba==0.67.0
accelerate
bitsandbytes
outetts
@@ -1,7 +1,7 @@
torch==2.7.1
accelerate
llvmlite==0.43.0
numba==0.60.0
llvmlite==0.49.0
numba==0.67.0
bitsandbytes
protobuf==6.33.5
outetts
@@ -1,7 +1,7 @@
--extra-index-url https://download.pytorch.org/whl/cu130
torch==2.9.0
llvmlite==0.43.0
numba==0.60.0
llvmlite==0.49.0
numba==0.67.0
bitsandbytes
outetts
protobuf==6.33.5
@@ -1,8 +1,8 @@
--extra-index-url https://download.pytorch.org/whl/rocm7.0
torch==2.10.0+rocm7.0
accelerate
llvmlite==0.43.0
numba==0.60.0
llvmlite==0.49.0
numba==0.67.0
bitsandbytes
outetts
protobuf==6.33.5
@@ -1,8 +1,8 @@
--extra-index-url https://download.pytorch.org/whl/xpu
torch
optimum[openvino]
llvmlite==0.43.0
numba==0.60.0
llvmlite==0.49.0
numba==0.67.0
bitsandbytes
outetts
protobuf==6.33.5
+7
View File
@@ -10,4 +10,11 @@ else
source $backend_dir/../common/libbackend.sh
fi
# CUDA 13 has no prebuilt FlashAttention wheel, so the fallback source build
# exceeds the CI runner's memory when ninja compiles multiple units at once.
if [ "x${BUILD_PROFILE}" = "xcublas13" ]; then
export MAX_JOBS="${MAX_JOBS:-1}"
export NVCC_THREADS="${NVCC_THREADS:-1}"
fi
installRequirements
@@ -0,0 +1,2 @@
ninja
flash-attn
+9
View File
@@ -2,6 +2,15 @@
set -e
backend_dir=$(dirname $0)
for cuda_version in 12 13; do
grep -qx "flash-attn" "$backend_dir/requirements-cublas${cuda_version}-after.txt"
done
grep -q 'BUILD_PROFILE.*cublas13' "$backend_dir/install.sh"
grep -q 'MAX_JOBS.*1' "$backend_dir/install.sh"
grep -q 'NVCC_THREADS.*1' "$backend_dir/install.sh"
if [ -d $backend_dir/common ]; then
source $backend_dir/common/libbackend.sh
else
+3
View File
@@ -1,6 +1,9 @@
#!/bin/bash
set -e
PYTHON_VERSION="3.12"
PYTHON_PATCH="11"
backend_dir=$(dirname $0)
if [ -d $backend_dir/common ]; then
source $backend_dir/common/libbackend.sh
@@ -1,8 +1,8 @@
torch==2.7.1
llvmlite==0.43.0
numba==0.60.0
llvmlite==0.49.0
numba==0.67.0
accelerate
transformers>=5.15.0
transformers>=5.15.1
bitsandbytes
sentence-transformers==5.7.0
diffusers
@@ -1,8 +1,8 @@
torch==2.7.1
accelerate
llvmlite==0.43.0
numba==0.60.0
transformers>=5.15.0
llvmlite==0.49.0
numba==0.67.0
transformers>=5.15.1
bitsandbytes
sentence-transformers==5.7.0
diffusers
@@ -1,8 +1,8 @@
--extra-index-url https://download.pytorch.org/whl/cu130
torch==2.9.0
llvmlite==0.43.0
numba==0.60.0
transformers>=5.15.0
llvmlite==0.49.0
numba==0.67.0
transformers>=5.15.1
bitsandbytes
sentence-transformers==5.7.0
diffusers
@@ -1,9 +1,9 @@
--extra-index-url https://download.pytorch.org/whl/rocm7.0
torch==2.10.0+rocm7.0
accelerate
transformers>=5.15.0
llvmlite==0.43.0
numba==0.60.0
transformers>=5.15.1
llvmlite==0.49.0
numba==0.67.0
bitsandbytes
sentence-transformers==5.7.0
diffusers
@@ -1,9 +1,9 @@
--extra-index-url https://download.pytorch.org/whl/xpu
torch
optimum[openvino]
llvmlite==0.43.0
numba==0.60.0
transformers>=5.15.0
llvmlite==0.49.0
numba==0.67.0
transformers>=5.15.1
bitsandbytes
sentence-transformers==5.7.0
diffusers
@@ -1,8 +1,8 @@
torch==2.7.1
llvmlite==0.43.0
numba==0.60.0
llvmlite==0.49.0
numba==0.67.0
accelerate
transformers>=5.15.0
transformers>=5.15.1
bitsandbytes
sentence-transformers==5.7.0
diffusers
+2 -2
View File
@@ -2,5 +2,5 @@ grpcio==1.83.0
protobuf==7.35.0
certifi
setuptools
scipy==1.15.1
numpy>=2.0.0
scipy==1.18.0
numpy>=2.5.2
+3 -2
View File
@@ -489,8 +489,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
except json.JSONDecodeError:
pass
try:
if request.Metadata.get("enable_thinking", "").lower() == "true":
template_kwargs["enable_thinking"] = True
enable_thinking = request.Metadata.get("enable_thinking", "").lower()
if enable_thinking in ("true", "false"):
template_kwargs["enable_thinking"] = enable_thinking == "true"
except Exception:
pass
try:
@@ -3,8 +3,8 @@
# on a cu130 host. Pull the cu130-flavoured wheel from vLLM's per-tag index
# instead — the cublas13 case in install.sh adds --index-strategy=unsafe-best-match
# so uv consults this index alongside PyPI.
--extra-index-url https://wheels.vllm.ai/0.28.0/cu130
--extra-index-url https://wheels.vllm.ai/0.29.0/cu130
# VERSION COUPLING: darwin/Apple-Silicon builds use vllm-metal (see install.sh),
# which pins this exact vLLM version. Bumping vllm here means coordinating with a
# vllm-metal release that supports the new version, or macOS/Metal builds break.
vllm==0.28.0
vllm==0.29.0
@@ -9,4 +9,4 @@
# memory architecture crash deterministically with an empty "Engine core init
# failed" set (mudler/LocalAI#10722). Leaving this unpinned let the L4T image
# drift onto whatever wheel was latest at build time.
vllm==0.28.0
vllm==0.29.0
+14
View File
@@ -0,0 +1,14 @@
.DEFAULT_GOAL := install
.PHONY: install
install:
bash install.sh
.PHONY: clean
clean:
$(RM) backend_pb2_grpc.py backend_pb2.py
rm -rf venv __pycache__
.PHONY: test
test:
python3 -m unittest test_unit.py
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""LocalAI gRPC backend for aiola Whisper-Medusa speech recognition."""
import argparse
from concurrent import futures
import os
import signal
import sys
import time
import backend_pb2
import backend_pb2_grpc
import grpc
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "common"))
from grpc_auth import get_auth_interceptors
from model_utils import resolve_model_reference
SAMPLE_RATE = 16000
MAX_DURATION_SECONDS = 30
MAX_WORKERS = int(os.environ.get("PYTHON_GRPC_MAX_WORKERS", "1"))
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
def _parse_options(raw_options):
options = {}
for option in raw_options:
if ":" not in option:
continue
key, value = option.split(":", 1)
try:
value = int(value)
except ValueError:
try:
value = float(value)
except ValueError:
pass
options[key] = value
return options
def _prepare_audio(path, torchaudio):
waveform, sample_rate = torchaudio.load(path)
if waveform.shape[0] > 1:
waveform = waveform.mean(dim=0, keepdim=True)
if sample_rate != SAMPLE_RATE:
waveform = torchaudio.transforms.Resample(sample_rate, SAMPLE_RATE)(waveform)
sample_rate = SAMPLE_RATE
return waveform, sample_rate
class BackendServicer(backend_pb2_grpc.BackendServicer):
def __init__(self):
self.model = None
self.processor = None
self.device = None
self.options = {}
def Health(self, request, context):
return backend_pb2.Reply(message=b"OK")
def LoadModel(self, request, context):
try:
import torch
from transformers import WhisperProcessor
from whisper_medusa import WhisperMedusaModel
if request.CUDA and not torch.cuda.is_available():
return backend_pb2.Result(success=False, message="CUDA is not available")
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
self.device = torch.device("mps")
self.options = _parse_options(request.Options)
model_path, local_only = resolve_model_reference(
request, "aiola/whisper-medusa-linear-libri"
)
self.model = WhisperMedusaModel.from_pretrained(
model_path, local_files_only=local_only
).to(self.device)
self.model.eval()
self.processor = WhisperProcessor.from_pretrained(
model_path, local_files_only=local_only
)
except Exception as err:
print(f"Whisper-Medusa model load failed: {err}", file=sys.stderr)
return backend_pb2.Result(success=False, message=str(err))
return backend_pb2.Result(success=True, message="Model loaded successfully")
def AudioTranscription(self, request, context):
if self.model is None or self.processor is None:
return backend_pb2.TranscriptResult(segments=[], text="")
try:
import torch
import torchaudio
waveform, sample_rate = _prepare_audio(request.dst, torchaudio)
duration = waveform.shape[-1] / sample_rate
if duration > MAX_DURATION_SECONDS:
raise ValueError(
f"Whisper-Medusa supports audio clips up to {MAX_DURATION_SECONDS} seconds"
)
language = request.language or str(self.options.get("language", "en"))
regulation_start = int(self.options.get("regulation_start", 140))
regulation_factor = float(self.options.get("regulation_factor", 1.01))
features = self.processor(
waveform.squeeze(), return_tensors="pt", sampling_rate=sample_rate
).input_features.to(self.device)
with torch.inference_mode():
output = self.model.generate(
features,
language=language,
exponential_decay_length_penalty=(
regulation_start,
regulation_factor,
),
)
text = self.processor.decode(output[0], skip_special_tokens=True).strip()
segment = backend_pb2.TranscriptSegment(
id=0,
start=0,
end=int(duration * 1_000_000_000),
text=text,
)
return backend_pb2.TranscriptResult(segments=[segment], text=text)
except Exception as err:
print(f"Whisper-Medusa transcription failed: {err}", file=sys.stderr)
return backend_pb2.TranscriptResult(segments=[], text="")
def serve(address):
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=MAX_WORKERS),
options=[
("grpc.max_send_message_length", 50 * 1024 * 1024),
("grpc.max_receive_message_length", 50 * 1024 * 1024),
],
interceptors=get_auth_interceptors(),
)
backend_pb2_grpc.add_BackendServicer_to_server(BackendServicer(), server)
server.add_insecure_port(address)
server.start()
print(f"Server started. Listening on: {address}", file=sys.stderr)
def stop_server(_signal, _frame):
server.stop(0)
sys.exit(0)
signal.signal(signal.SIGINT, stop_server)
signal.signal(signal.SIGTERM, stop_server)
try:
while True:
time.sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
server.stop(0)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the Whisper-Medusa backend")
parser.add_argument("--addr", default="localhost:50051")
serve(parser.parse_args().addr)
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -e
backend_dir=$(dirname "$0")
if [ -d "$backend_dir/common" ]; then
source "$backend_dir/common/libbackend.sh"
else
source "$backend_dir/../common/libbackend.sh"
fi
PYTHON_VERSION="3.11"
PYTHON_PATCH="13"
installRequirements
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
set -e
python3 -m grpc_tools.protoc -I../../ --python_out=. --grpc_python_out=. ../../backend.proto
@@ -0,0 +1,3 @@
--extra-index-url https://download.pytorch.org/whl/cpu
torch==2.2.2
torchaudio==2.2.2
@@ -0,0 +1,3 @@
--extra-index-url https://download.pytorch.org/whl/cu121
torch==2.2.2
torchaudio==2.2.2
Loaded 100 of 243 files, more files were not shown because too many files have changed in this diff. Show more