Skip to content

Commit aeba7e4

Browse files
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
1 parent 90b3585 commit aeba7e4

9 files changed

Lines changed: 198 additions & 3 deletions

File tree

backend/index.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,7 @@
11941194
tags:
11951195
- image-generation
11961196
- video-generation
1197+
- sound-generation
11971198
- diffusion-models
11981199
license: apache-2.0
11991200
alias: "diffusers"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import array
2+
import sys
3+
import wave
4+
5+
6+
def write_pcm_wav(destination, samples, sampling_rate):
7+
"""Write normalized floating-point audio samples as mono 16-bit PCM."""
8+
pcm = array.array(
9+
"h",
10+
(
11+
max(-32768, min(32767, round(float(sample) * 32768)))
12+
for sample in samples
13+
),
14+
)
15+
if pcm.itemsize != 2:
16+
raise RuntimeError("16-bit PCM requires two-byte signed integers")
17+
if sys.byteorder != "little":
18+
pcm.byteswap()
19+
20+
with wave.open(destination, "wb") as output:
21+
output.setnchannels(1)
22+
output.setsampwidth(2)
23+
output.setframerate(sampling_rate)
24+
output.writeframes(pcm.tobytes())

backend/python/diffusers/backend.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
2727
from grpc_auth import get_auth_interceptors
2828
from model_utils import resolve_model_reference
29+
from audio_utils import write_pcm_wav
2930

3031

3132
# Import dynamic loader for pipeline discovery
@@ -883,6 +884,49 @@ def GenerateImage(self, request, context):
883884

884885
return backend_pb2.Result(message="Media generated", success=True)
885886

887+
def SoundGeneration(self, request, context):
888+
if not request.dst:
889+
return backend_pb2.Result(success=False, message="request.dst is required")
890+
891+
prompt = request.text or request.caption
892+
if not prompt:
893+
return backend_pb2.Result(success=False, message="request.text is required")
894+
895+
try:
896+
generation_options = dict(self.options)
897+
if "num_inference_steps" in generation_options:
898+
generation_options["num_inference_steps"] = int(
899+
generation_options["num_inference_steps"]
900+
)
901+
generation_options["prompt"] = prompt
902+
if request.HasField("duration"):
903+
generation_options["audio_length_in_s"] = request.duration
904+
if request.HasField("temperature"):
905+
generation_options["guidance_scale"] = request.temperature
906+
907+
generated = self.pipe(**generation_options)
908+
if not hasattr(generated, "audios") or len(generated.audios) == 0:
909+
return backend_pb2.Result(
910+
success=False,
911+
message="The diffusers pipeline returned no audio",
912+
)
913+
914+
samples = generated.audios[0]
915+
if hasattr(samples, "reshape"):
916+
samples = samples.reshape(-1)
917+
if hasattr(samples, "tolist"):
918+
samples = samples.tolist()
919+
920+
sampling_rate = getattr(
921+
getattr(getattr(self.pipe, "vae", None), "config", None),
922+
"sampling_rate",
923+
16000,
924+
)
925+
write_pcm_wav(request.dst, samples, sampling_rate)
926+
return backend_pb2.Result(success=True, message="Sound generated successfully")
927+
except Exception as err:
928+
return backend_pb2.Result(success=False, message=f"SoundGeneration error: {err}")
929+
886930
def UpscaleImage(self, request, context):
887931
try:
888932
if not request.src:

backend/python/diffusers/test.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import unittest
55
import subprocess
66
import time
7+
import os
8+
import tempfile
9+
import wave
710
from unittest.mock import patch, MagicMock
811

912
# Import dynamic loader for testing (these don't need gRPC)
@@ -373,3 +376,64 @@ def test_options_merged_into_pipeline_kwargs(self):
373376
finally:
374377
os.unlink(src_file.name)
375378
os.unlink(dst_file.name)
379+
380+
381+
class TestWritePcmWav(unittest.TestCase):
382+
def test_writes_clipped_float_samples_as_mono_pcm(self):
383+
from audio_utils import write_pcm_wav
384+
385+
destination = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
386+
destination.close()
387+
388+
try:
389+
write_pcm_wav(destination.name, [0.0, 0.5, -0.5, 2.0], 16000)
390+
391+
with wave.open(destination.name, "rb") as generated:
392+
self.assertEqual(generated.getframerate(), 16000)
393+
self.assertEqual(generated.getnchannels(), 1)
394+
self.assertEqual(generated.getsampwidth(), 2)
395+
self.assertEqual(generated.getnframes(), 4)
396+
self.assertEqual(
397+
generated.readframes(4),
398+
b"\x00\x00\x00@\x00\xc0\xff\x7f",
399+
)
400+
finally:
401+
os.unlink(destination.name)
402+
403+
404+
@unittest.skipUnless(GRPC_AVAILABLE, "gRPC modules not available")
405+
class TestSoundGeneration(unittest.TestCase):
406+
def test_maps_request_options_and_writes_pipeline_audio(self):
407+
from backend import BackendServicer
408+
409+
service = BackendServicer.__new__(BackendServicer)
410+
service.options = {"num_inference_steps": 200.0}
411+
service.pipe = MagicMock()
412+
service.pipe.return_value.audios = [[0.0, 0.5, -0.5]]
413+
service.pipe.vae.config.sampling_rate = 16000
414+
415+
destination = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
416+
destination.close()
417+
418+
try:
419+
request = backend_pb2.SoundGenerationRequest(
420+
text="ocean waves",
421+
dst=destination.name,
422+
duration=2.5,
423+
temperature=0,
424+
)
425+
426+
result = service.SoundGeneration(request, context=None)
427+
428+
self.assertTrue(result.success, result.message)
429+
service.pipe.assert_called_once_with(
430+
num_inference_steps=200,
431+
prompt="ocean waves",
432+
audio_length_in_s=2.5,
433+
guidance_scale=0,
434+
)
435+
with wave.open(destination.name, "rb") as generated:
436+
self.assertEqual(generated.getframerate(), 16000)
437+
self.assertEqual(generated.getnframes(), 3)
438+
finally:
439+
os.unlink(destination.name)

core/config/backend_capabilities.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,10 @@ var BackendCapabilities = map[string]BackendCapability{
369369

370370
// --- Image/video generation backends ---
371371
"diffusers": {
372-
GRPCMethods: []GRPCMethod{MethodGenerateImage, MethodUpscaleImage, MethodGenerateVideo},
373-
PossibleUsecases: []string{UsecaseImage, UsecaseVideo},
372+
GRPCMethods: []GRPCMethod{MethodGenerateImage, MethodUpscaleImage, MethodGenerateVideo, MethodSoundGeneration},
373+
PossibleUsecases: []string{UsecaseImage, UsecaseVideo, UsecaseSoundGeneration},
374374
DefaultUsecases: []string{UsecaseImage},
375-
Description: "HuggingFace diffusers — Stable Diffusion, Flux, video generation",
375+
Description: "HuggingFace diffusers — image, video, and sound generation",
376376
},
377377
"longcat-video": {
378378
GRPCMethods: []GRPCMethod{MethodGenerateVideo},

core/config/backend_capabilities_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ var _ = Describe("BackendCapabilities", func() {
5757
})
5858

5959
var _ = Describe("GetBackendCapability", func() {
60+
It("advertises diffusers sound generation", func() {
61+
capability := GetBackendCapability("diffusers")
62+
Expect(capability.GRPCMethods).To(ContainElement(MethodSoundGeneration))
63+
Expect(capability.PossibleUsecases).To(ContainElement(UsecaseSoundGeneration))
64+
})
65+
6066
It("returns the capability for a known backend", func() {
6167
cap := GetBackendCapability("llama-cpp")
6268
Expect(cap).NotTo(BeNil())

docs/content/features/text-to-audio.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,31 @@ The `/v1/sound-generation` endpoint is compatible with the [ElevenLabs sound gen
302302

303303
Error responses: `400` for a missing or invalid model or request parameters, and `500` for a backend error during sound generation.
304304

305+
### AudioLDM 2
306+
307+
[AudioLDM 2](https://github.com/haoheliu/AudioLDM2) generates sound effects,
308+
music, and speech from a text description. Install the gallery model:
309+
310+
```bash
311+
local-ai models install audioldm2
312+
```
313+
314+
Generate a WAV file through the sound-generation endpoint:
315+
316+
```bash
317+
curl http://localhost:8080/v1/sound-generation \
318+
-H "Content-Type: application/json" \
319+
-d '{
320+
"model_id": "audioldm2",
321+
"text": "Waves breaking on a rocky beach during a distant thunderstorm",
322+
"duration_seconds": 10
323+
}' --output storm.wav
324+
```
325+
326+
AudioLDM 2 uses the `AudioLDM2Pipeline` from the diffusers backend. The
327+
`duration_seconds` field maps to the pipeline's `audio_length_in_s` option, and
328+
`prompt_influence` maps to `guidance_scale`.
329+
305330
#### Configuration
306331

307332
You can configure ACE-Step models with various options:

gallery/audioldm2.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
name: "audioldm2"
3+
4+
config_file: |
5+
backend: diffusers
6+
known_usecases:
7+
- sound_generation
8+
parameters:
9+
model: cvssp/audioldm2
10+
diffusers:
11+
pipeline_type: AudioLDM2Pipeline
12+
cuda: true
13+
options:
14+
- num_inference_steps:200
15+
- torch_dtype:fp16

gallery/index.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
---
2+
- name: audioldm2
3+
url: github:mudler/LocalAI/gallery/audioldm2.yaml@master
4+
urls:
5+
- https://huggingface.co/cvssp/audioldm2
6+
- https://github.com/haoheliu/AudioLDM2
7+
description: |
8+
AudioLDM 2 generates sound effects, music, and speech from natural-language
9+
descriptions through the diffusers backend and LocalAI sound-generation API.
10+
license: cc-by-nc-sa-4.0
11+
tags:
12+
- audio
13+
- sound-generation
14+
- text-to-audio
15+
- diffusers
16+
- gpu
17+
last_checked: "2026-08-13"
218
- &twil-lm3
319
name: "twil-lm3-q4"
420
variants:

0 commit comments

Comments
 (0)