mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-12 22:33:54 -04:00
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>
This commit is contained in:
1 parent
783556bc93
commit
a15780858e
9 files changed
+198
-3
No files matched your search
@@ -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())
|
||||
@@ -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
|
||||
@@ -903,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:
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user