feat(sound): expand Simple prompts with chat (#12772)

* feat(sound): expand simple prompts with chat models

Allow callers to select an installed chat model for sound prompt
expansion. Validate its JSON before audio generation and preserve
explicit instrumental and language controls. Keep backend defaults
when expansion is omitted, including native Python ACE-Step sampling.

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

* feat(sound): map expansion by loaded audio family

Map neutral expanded fields where the resolved audio family is known.
Keep unmarked requests unchanged and reject unsupported expansion before
inference. Clear YuE2 and HeartMuLa transcripts to prevent lyric fallback.

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

* feat(sound): choose a prompt expansion model

Require an explicit Simple choice and remember it separately from chat.
Clear unavailable choices instead of silently switching language models.
Preserve the choice in history and restore it when editing a past result.
Keep Backend default and Advanced requests compatible with existing APIs.

Document family support and align the published optional request schema.
Verify selection, request fields, persistence and history in the browser.

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

* fix(sound): authorize both generation models

Check the resolved audio model and explicit expansion model before
loading either. Permission lookup failures stop generation.

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

* fix(sound): enforce JSON during prompt expansion

Constrain expansion output with the existing JSON grammar before decoding.
Require a llama-cpp chat model so unsupported backends cannot ignore it.
Keep strict field validation and fail before generating audio.

Assisted-by: Codex:gpt-6
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>
This commit is contained in:
mudler-agentandEttore Di Giacinto authored and GitHub committed 2026-10-19 18:16:20 +02:00
1 parent e517470554
commit ae8092e1c6
27 files changed
+1164 -21

No files matched your search

+1 -1
View File
@@ -395,7 +395,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
if not request.dst:
return backend_pb2.Result(success=False, message="request.dst is required")
use_simple = bool(request.text)
use_simple = bool(request.text) and not request.expanded
if use_simple:
payload = {
"sample_query": request.text or "",
@@ -0,0 +1,51 @@
# SPDX-License-Identifier: MIT
"""Test the Sound request adapter without importing model dependencies."""
import ast
from pathlib import Path
from types import SimpleNamespace
import unittest
class SoundExpansionTests(unittest.TestCase):
def setUp(self):
source = ast.parse(Path(__file__).with_name("backend.py").read_text())
service = next(node for node in source.body
if isinstance(node, ast.ClassDef) and node.name == "BackendServicer")
method = next(node for node in service.body
if isinstance(node, ast.FunctionDef) and node.name == "SoundGeneration")
self.calls = []
namespace = {
"backend_pb2": SimpleNamespace(Result=lambda **fields: SimpleNamespace(**fields)),
"_generate_audio_sync": lambda owner, payload, dst: self.calls.append(payload),
}
exec(compile(ast.Module(body=[method], type_ignores=[]), "backend.py", "exec"), namespace)
self.generate = namespace["SoundGeneration"]
def request(self, **fields):
values = dict(dst="output.wav", text="original description", expanded=False,
language="it", instrumental=True, caption="piano", lyrics="",
think=False, src="", bpm=120, keyscale="", timesignature="", duration=0)
values.update(fields)
values["HasField"] = lambda name: name in {"instrumental", "bpm"}
return SimpleNamespace(**values)
def test_omission_preserves_native_sample_query(self):
self.generate(None, self.request(), None)
self.assertEqual(self.calls[0]["sample_query"], "original description")
self.assertTrue(self.calls[0]["sample_mode"])
def test_expanded_uses_structured_caption_and_empty_lyrics(self):
self.generate(None, self.request(expanded=True, text=""), None)
self.assertEqual(self.calls[0]["prompt"], "piano")
self.assertEqual(self.calls[0]["lyrics"], "")
self.assertEqual(self.calls[0]["bpm"], 120)
self.assertNotIn("sample_query", self.calls[0])
def test_marker_prevents_native_expansion_even_with_stale_text(self):
self.generate(None, self.request(expanded=True), None)
self.assertEqual(self.calls[0]["prompt"], "piano")
self.assertNotIn("sample_query", self.calls[0])
if __name__ == "__main__":
unittest.main()