From 49278b03a8bd8f199c5076ba41a52dd36c8b1154 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:10:37 +0000 Subject: [PATCH 1/4] feat(chatterbox): support Turbo and Nano variants Select the model at load time and filter incompatible generation controls. Keep the original model as the default and load Turbo lazily. Add CPU-only contract tests for selection, errors, and speech requests. Assisted-by: Codex:gpt-6 Signed-off-by: Ettore Di Giacinto --- backend/python/chatterbox/backend.py | 28 +++- backend/python/chatterbox/test_variants.py | 149 +++++++++++++++++++++ 2 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 backend/python/chatterbox/test_variants.py diff --git a/backend/python/chatterbox/backend.py b/backend/python/chatterbox/backend.py index 996d4e50b7ed..1e35819e0e37 100644 --- a/backend/python/chatterbox/backend.py +++ b/backend/python/chatterbox/backend.py @@ -177,9 +177,23 @@ def LoadModel(self, request, context): self.AudioPath = os.path.join(modelFileBase, request.AudioPath) try: print("Preparing models, please wait", file=sys.stderr) - if "multilingual" in self.options: - # remove key from options - del self.options["multilingual"] + selectors = {} + for name in ("multilingual", "turbo", "nano"): + value = self.options.pop(name, False) + if not isinstance(value, bool): + raise ValueError(f"{name} must be true or false") + selectors[name] = value + if sum(selectors.values()) > 1: + raise ValueError("Only one of multilingual, turbo, or nano can be true") + self.variant = next((name for name, enabled in selectors.items() if enabled), "original") + if self.variant in ("turbo", "nano"): + # Older installations can still load the original models. + from chatterbox.tts_turbo import ChatterboxTurboTTS + if self.variant == "nano": + self.model = ChatterboxTurboTTS.from_pretrained(device=device, nano=True) + else: + self.model = ChatterboxTurboTTS.from_pretrained(device=device) + elif self.variant == "multilingual": self.model = ChatterboxMultilingualTTS.from_pretrained(device=device) else: self.model = ChatterboxTTS.from_pretrained(device=device) @@ -213,6 +227,14 @@ def TTS(self, request, context): continue kwargs[key] = coerce_param_value(value) + # Selectors apply only at load time, including when supplied in params. + for name in ("multilingual", "turbo", "nano"): + kwargs.pop(name, None) + if self.variant in ("turbo", "nano"): + # These controls belong to the original model's sampler. + for name in ("cfg_weight", "exaggeration", "min_p"): + kwargs.pop(name, None) + # Check if text exceeds 250 characters # (chatterbox does not support long text) # https://github.com/resemble-ai/chatterbox/issues/60 diff --git a/backend/python/chatterbox/test_variants.py b/backend/python/chatterbox/test_variants.py new file mode 100644 index 000000000000..349f449a7443 --- /dev/null +++ b/backend/python/chatterbox/test_variants.py @@ -0,0 +1,149 @@ +# SPDX-License-Identifier: MIT +"""CPU-only backend contract tests; no model packages or weights required.""" +import importlib.util +from pathlib import Path +import sys +from types import ModuleType, SimpleNamespace +import unittest +from unittest.mock import MagicMock, patch + + +def load_backend(turbo_available=True): + modules = {} + for name in ("torch", "torchaudio", "chatterbox", "chatterbox.tts", + "chatterbox.mtl_tts", "backend_pb2", "backend_pb2_grpc", + "grpc", "grpc_auth"): + modules[name] = ModuleType(name) + modules["chatterbox"].__path__ = [] + modules["torch"].cuda = SimpleNamespace(is_available=lambda: False) + modules["torch"].backends = SimpleNamespace() + modules["torchaudio"].save = MagicMock() + modules["backend_pb2"].Result = SimpleNamespace + modules["backend_pb2_grpc"].BackendServicer = object + modules["grpc_auth"].get_auth_interceptors = MagicMock() + classes = {} + for variant, module, cls in ( + ("original", "chatterbox.tts", "ChatterboxTTS"), + ("multilingual", "chatterbox.mtl_tts", "ChatterboxMultilingualTTS"), + ("turbo", "chatterbox.tts_turbo", "ChatterboxTurboTTS"), + ): + classes[variant] = MagicMock() + classes[variant].from_pretrained.return_value.sr = 24000 + if variant != "turbo" or turbo_available: + modules.setdefault(module, ModuleType(module)) + setattr(modules[module], cls, classes[variant]) + if not turbo_available: + modules["chatterbox.tts_turbo"] = None + spec = importlib.util.spec_from_file_location( + "chatterbox_backend_test", Path(__file__).with_name("backend.py")) + backend = importlib.util.module_from_spec(spec) + with patch.dict(sys.modules, modules), patch.object(sys, "path", sys.path.copy()): + spec.loader.exec_module(backend) + return backend, modules, classes + + +class VariantTests(unittest.TestCase): + def setUp(self): + self.backend, self.modules, self.classes = load_backend() + self.module_patch = patch.dict(sys.modules, self.modules) + self.module_patch.start() + self.addCleanup(self.module_patch.stop) + self.service = self.backend.BackendServicer() + + def load(self, options=(), audio_path=""): + return self.service.LoadModel(SimpleNamespace( + CUDA=False, Options=options, AudioPath=audio_path, + ModelFile="/models/voice.yaml"), None) + + def speak(self, params=None, voice="", text="Hello"): + return self.service.TTS(SimpleNamespace( + text=text, dst="/tmp/output.wav", voice=voice, params=params or {}), None) + + def test_model_selection(self): + for options, variant, expected in ( + ([], "original", {"device": "cpu"}), + (["multilingual:true"], "multilingual", {"device": "cpu"}), + (["turbo:true"], "turbo", {"device": "cpu"}), + (["nano:true"], "turbo", {"device": "cpu", "nano": True}), + (["multilingual:false", "turbo:false", "nano:false"], "original", {"device": "cpu"}), + (["multilingual:false", "turbo:TRUE", "nano:false"], "turbo", {"device": "cpu"}), + ): + with self.subTest(options=options): + for cls in self.classes.values(): + cls.reset_mock() + result = self.load(options) + self.assertTrue(result.success, result.message) + self.classes[variant].from_pretrained.assert_called_once_with(**expected) + for other, cls in self.classes.items(): + if other != variant: + cls.from_pretrained.assert_not_called() + + def test_conflicting_variants_are_rejected(self): + for selectors in (("turbo", "nano"), ("multilingual", "turbo"), ("multilingual", "nano")): + with self.subTest(selectors=selectors): + result = self.load([f"{selector}:true" for selector in selectors]) + self.assertFalse(result.success) + self.assertIn("Only one", result.message) + for cls in self.classes.values(): + cls.from_pretrained.assert_not_called() + + def test_invalid_selector_is_rejected(self): + for selector in ("turbo", "nano", "multilingual"): + for value in ("yes", "1", ""): + with self.subTest(selector=selector, value=value): + result = self.load([f"{selector}:{value}"]) + self.assertFalse(result.success) + self.assertIn("true or false", result.message) + + def test_turbo_import_is_lazy(self): + backend, modules, classes = load_backend(turbo_available=False) + with patch.dict(sys.modules, modules): + self.service = backend.BackendServicer() + self.assertTrue(self.load().success) + self.assertFalse(self.load(["turbo:true"]).success) + classes["original"].from_pretrained.assert_called_once() + + def test_load_error_is_returned(self): + for variant in ("original", "multilingual", "turbo", "nano"): + cls = self.classes["turbo" if variant == "nano" else variant] + cls.from_pretrained.side_effect = RuntimeError("weights unavailable") + result = self.load([] if variant == "original" else [f"{variant}:true"]) + self.assertFalse(result.success) + self.assertIn("weights unavailable", result.message) + cls.from_pretrained.side_effect = None + + def test_generation_filters_selectors_and_unsupported_knobs(self): + for variant in ("original", "multilingual", "turbo", "nano"): + with self.subTest(variant=variant): + options = [] if variant == "original" else [f"{variant}:true"] + self.assertTrue(self.load(options + ["cfg_weight:0.4", "exaggeration:0.6", "min_p:0.1", "temperature:0.7"], "reference.wav").success) + result = self.speak({"turbo": "true", "nano": "true", "multilingual": "true", "cfg_weight": "0.8", "exaggeration": "0.9", "min_p": "0.2", "temperature": "0.5", "ref_text": "ignored"}) + self.assertTrue(result.success, getattr(result, "message", "")) + expected = {"temperature": 0.5, "audio_prompt_path": "/models/reference.wav"} + if variant in ("original", "multilingual"): + expected.update(cfg_weight=0.8, exaggeration=0.9, min_p=0.2) + self.service.model.generate.assert_called_with("Hello", **expected) + + def test_static_unsupported_knobs_are_filtered(self): + self.assertTrue(self.load(["nano:true", "cfg_weight:0.4", "exaggeration:0.6", "min_p:0.1"]).success) + self.assertTrue(self.speak().success) + self.service.model.generate.assert_called_once_with("Hello") + + def test_voice_override_is_preserved(self): + self.assertTrue(self.load(["turbo:true"], "reference.wav").success) + with patch.object(self.backend.os.path, "isfile", return_value=True): + self.assertTrue(self.speak(voice="/voices/alice.wav").success) + self.service.model.generate.assert_called_once_with("Hello", audio_prompt_path="/voices/alice.wav") + + def test_long_text_still_uses_chunks(self): + self.assertTrue(self.load(["nano:true"]).success) + text = "A sentence with several words. " * 20 + with patch.object(self.backend, "merge_audio_files") as merge: + self.assertTrue(self.speak(text=text).success) + chunks = self.backend.split_text_at_word_boundary(text) + self.assertEqual([call.args[0] for call in self.service.model.generate.call_args_list], chunks) + merge.assert_called_once() + + +if __name__ == "__main__": + unittest.main() From 325557e5894a3840b18511bb94bc52bcb255c62f Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:12:55 +0000 Subject: [PATCH 2/4] fix(chatterbox): preserve integer sampling options Parse integers before floats for static options and request parameters. Turbo and Nano require an integer top_k value during generation. Assisted-by: Codex:gpt-6 Signed-off-by: Ettore Di Giacinto --- backend/python/chatterbox/backend.py | 13 +++---------- backend/python/chatterbox/test_variants.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/backend/python/chatterbox/backend.py b/backend/python/chatterbox/backend.py index 1e35819e0e37..13ab93a75a43 100644 --- a/backend/python/chatterbox/backend.py +++ b/backend/python/chatterbox/backend.py @@ -44,10 +44,10 @@ def coerce_param_value(value): YAML options are coerced at load time. Non-string values pass through.""" if not isinstance(value, str): return value - if is_float(value): - return float(value) if is_int(value): return int(value) + if is_float(value): + return float(value) if value.lower() in ["true", "false"]: return value.lower() == "true" return value @@ -157,14 +157,7 @@ def LoadModel(self, request, context): if ":" not in opt: continue key, value = opt.split(":") - # if value is a number, convert it to the appropriate type - if is_float(value): - value = float(value) - elif is_int(value): - value = int(value) - elif value.lower() in ["true", "false"]: - value = value.lower() == "true" - self.options[key] = value + self.options[key] = coerce_param_value(value) self.AudioPath = None diff --git a/backend/python/chatterbox/test_variants.py b/backend/python/chatterbox/test_variants.py index 349f449a7443..c97e9320e134 100644 --- a/backend/python/chatterbox/test_variants.py +++ b/backend/python/chatterbox/test_variants.py @@ -124,6 +124,16 @@ def test_generation_filters_selectors_and_unsupported_knobs(self): expected.update(cfg_weight=0.8, exaggeration=0.9, min_p=0.2) self.service.model.generate.assert_called_with("Hello", **expected) + def test_top_k_remains_an_integer(self): + for variant in ("turbo", "nano"): + for params, expected in (({}, 100), ({"top_k": "50"}, 50)): + with self.subTest(variant=variant, params=params): + self.assertTrue(self.load([f"{variant}:true", "top_k:100"]).success) + self.assertTrue(self.speak(params).success) + top_k = self.service.model.generate.call_args.kwargs["top_k"] + self.assertIs(type(top_k), int) + self.assertEqual(top_k, expected) + def test_static_unsupported_knobs_are_filtered(self): self.assertTrue(self.load(["nano:true", "cfg_weight:0.4", "exaggeration:0.6", "min_p:0.1"]).success) self.assertTrue(self.speak().success) From 2d74f7a56d8a4acfb7c1c6f4cc533c8b708a71c2 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:13:13 +0000 Subject: [PATCH 3/4] build(chatterbox): pin Turbo and Nano support Pin the dependency fork with isolated Turbo and Nano inference. Match its Transformers requirement across build profiles and add pyloudnorm. Run variant tests in CI and document configuration and sampler limits. Assisted-by: Codex:gpt-6 Signed-off-by: Ettore Di Giacinto --- .github/workflows/lint.yml | 2 + backend/python/chatterbox/Makefile | 6 ++- backend/python/chatterbox/install.sh | 6 ++- .../python/chatterbox/requirements-cpu.txt | 4 +- .../chatterbox/requirements-cublas12.txt | 4 +- .../chatterbox/requirements-cublas13.txt | 2 +- .../chatterbox/requirements-hipblas.txt | 2 +- .../python/chatterbox/requirements-intel.txt | 4 +- .../python/chatterbox/requirements-l4t12.txt | 2 +- .../python/chatterbox/requirements-l4t13.txt | 2 +- .../python/chatterbox/requirements-mps.txt | 4 +- backend/python/chatterbox/requirements.txt | 4 +- docs/content/features/text-to-audio.md | 51 +++++++++++++++++++ 13 files changed, 77 insertions(+), 16 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 35493fe4b19e..84bbca453edc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -130,3 +130,5 @@ jobs: # ride along here instead of waiting on a multi-GB backend image build. - name: run shared python backend helper tests run: make test-python-helpers + - name: run Chatterbox variant tests + run: make -C backend/python/chatterbox test-unit diff --git a/backend/python/chatterbox/Makefile b/backend/python/chatterbox/Makefile index be9330f8eac9..e26a1ff2c3f6 100644 --- a/backend/python/chatterbox/Makefile +++ b/backend/python/chatterbox/Makefile @@ -14,10 +14,14 @@ test: chatterbox bash test.sh @echo "coqui tested." +.PHONY: test-unit +test-unit: + python3 -m unittest discover -s . -p 'test_variants.py' -v + .PHONY: protogen-clean protogen-clean: $(RM) backend_pb2_grpc.py backend_pb2.py .PHONY: clean clean: protogen-clean - rm -rf venv __pycache__ \ No newline at end of file + rm -rf venv __pycache__ diff --git a/backend/python/chatterbox/install.sh b/backend/python/chatterbox/install.sh index c72445f06013..eedcde6761a3 100755 --- a/backend/python/chatterbox/install.sh +++ b/backend/python/chatterbox/install.sh @@ -29,9 +29,11 @@ installRequirements # pip to backtrack through Jinja2/MarkupSafe/omegaconf/ruamel.yaml into Python-2-era # sdists that no longer build. We install chatterbox-tts itself with --no-deps and # list its real runtime deps in requirements-*.txt instead. +# Pin the fork so every backend build includes the same Turbo/Nano implementation. +CHATTERBOX_REV=b38c317b03834ef35828ae96c72ac8929b002a05 echo "Installing chatterbox-tts with --no-deps" if [ "x${USE_PIP}" == "xtrue" ]; then - pip install ${EXTRA_PIP_INSTALL_FLAGS:-} --no-deps "chatterbox-tts@git+https://git@github.com/mudler/chatterbox.git@faster" + pip install ${EXTRA_PIP_INSTALL_FLAGS:-} --no-deps "chatterbox-tts@git+https://git@github.com/mudler/chatterbox.git@${CHATTERBOX_REV}" else - uv pip install ${EXTRA_PIP_INSTALL_FLAGS:-} --no-deps "chatterbox-tts@git+https://git@github.com/mudler/chatterbox.git@faster" + uv pip install ${EXTRA_PIP_INSTALL_FLAGS:-} --no-deps "chatterbox-tts@git+https://git@github.com/mudler/chatterbox.git@${CHATTERBOX_REV}" fi diff --git a/backend/python/chatterbox/requirements-cpu.txt b/backend/python/chatterbox/requirements-cpu.txt index 3ad9332920e5..6e766dcc943f 100644 --- a/backend/python/chatterbox/requirements-cpu.txt +++ b/backend/python/chatterbox/requirements-cpu.txt @@ -3,7 +3,7 @@ accelerate torch torchaudio numpy>=1.24.0,<1.26.0 -transformers +transformers==4.46.3 # chatterbox-tts itself is installed with --no-deps in install.sh. # These are its real runtime deps, mirroring upstream's pyproject.toml # minus russian-text-stresser (whose ancient pins break the resolver). @@ -16,4 +16,4 @@ resemble-perth==1.0.1 conformer safetensors spacy-pkuseg -pykakasi==2.3.0 \ No newline at end of file +pykakasi==2.3.0 diff --git a/backend/python/chatterbox/requirements-cublas12.txt b/backend/python/chatterbox/requirements-cublas12.txt index 1ca1ad8d6211..ed94e3eb57be 100644 --- a/backend/python/chatterbox/requirements-cublas12.txt +++ b/backend/python/chatterbox/requirements-cublas12.txt @@ -1,7 +1,7 @@ --extra-index-url https://download.pytorch.org/whl/cu124 torch==2.6.0+cu124 torchaudio==2.6.0+cu124 -transformers<5 +transformers==4.46.3 numpy>=1.24.0,<1.26.0 # chatterbox-tts itself is installed with --no-deps in install.sh. # These are its real runtime deps, mirroring upstream's pyproject.toml @@ -16,4 +16,4 @@ conformer safetensors spacy-pkuseg pykakasi==2.3.0 -accelerate \ No newline at end of file +accelerate diff --git a/backend/python/chatterbox/requirements-cublas13.txt b/backend/python/chatterbox/requirements-cublas13.txt index bd7b80c6e083..0c24e1c3ea53 100644 --- a/backend/python/chatterbox/requirements-cublas13.txt +++ b/backend/python/chatterbox/requirements-cublas13.txt @@ -1,7 +1,7 @@ --extra-index-url https://download.pytorch.org/whl/cu130 torch torchaudio -transformers +transformers==4.46.3 numpy>=1.24.0,<1.26.0 # chatterbox-tts itself is installed with --no-deps in install.sh. # These are its real runtime deps, mirroring upstream's pyproject.toml diff --git a/backend/python/chatterbox/requirements-hipblas.txt b/backend/python/chatterbox/requirements-hipblas.txt index 6647da2cf4d9..90b68af471fb 100644 --- a/backend/python/chatterbox/requirements-hipblas.txt +++ b/backend/python/chatterbox/requirements-hipblas.txt @@ -1,7 +1,7 @@ --extra-index-url https://download.pytorch.org/whl/rocm7.0 torch==2.10.0+rocm7.0 torchaudio==2.10.0+rocm7.0 -transformers +transformers==4.46.3 numpy>=1.24.0,<1.26.0 # chatterbox-tts itself is installed with --no-deps in install.sh. # These are its real runtime deps, mirroring upstream's pyproject.toml diff --git a/backend/python/chatterbox/requirements-intel.txt b/backend/python/chatterbox/requirements-intel.txt index 6578cb1afaba..db622385b72e 100644 --- a/backend/python/chatterbox/requirements-intel.txt +++ b/backend/python/chatterbox/requirements-intel.txt @@ -1,7 +1,7 @@ --extra-index-url https://download.pytorch.org/whl/xpu torch torchaudio -transformers +transformers==4.46.3 numpy>=1.24.0,<1.26.0 # chatterbox-tts itself is installed with --no-deps in install.sh. # These are its real runtime deps, mirroring upstream's pyproject.toml @@ -19,4 +19,4 @@ pykakasi==2.3.0 accelerate oneccl_bind_pt==2.3.100+xpu optimum[openvino] -setuptools \ No newline at end of file +setuptools diff --git a/backend/python/chatterbox/requirements-l4t12.txt b/backend/python/chatterbox/requirements-l4t12.txt index 009934b5b0d6..194d4acdc5b8 100644 --- a/backend/python/chatterbox/requirements-l4t12.txt +++ b/backend/python/chatterbox/requirements-l4t12.txt @@ -1,7 +1,7 @@ --extra-index-url https://pypi.jetson-ai-lab.io/jp6/cu126/ torch torchaudio -transformers +transformers==4.46.3 numpy>=1.24.0,<1.26.0 # chatterbox-tts itself is installed with --no-deps in install.sh. # These are its real runtime deps, mirroring upstream's pyproject.toml diff --git a/backend/python/chatterbox/requirements-l4t13.txt b/backend/python/chatterbox/requirements-l4t13.txt index bd7b80c6e083..0c24e1c3ea53 100644 --- a/backend/python/chatterbox/requirements-l4t13.txt +++ b/backend/python/chatterbox/requirements-l4t13.txt @@ -1,7 +1,7 @@ --extra-index-url https://download.pytorch.org/whl/cu130 torch torchaudio -transformers +transformers==4.46.3 numpy>=1.24.0,<1.26.0 # chatterbox-tts itself is installed with --no-deps in install.sh. # These are its real runtime deps, mirroring upstream's pyproject.toml diff --git a/backend/python/chatterbox/requirements-mps.txt b/backend/python/chatterbox/requirements-mps.txt index 6aaa122c7f97..fe43b596aaec 100644 --- a/backend/python/chatterbox/requirements-mps.txt +++ b/backend/python/chatterbox/requirements-mps.txt @@ -2,7 +2,7 @@ torch torchaudio accelerate numpy>=1.24.0,<1.26.0 -transformers +transformers==4.46.3 # chatterbox-tts itself is installed with --no-deps in install.sh. # These are its real runtime deps, mirroring upstream's pyproject.toml # minus russian-text-stresser (whose ancient pins break the resolver). @@ -15,4 +15,4 @@ resemble-perth==1.0.1 conformer safetensors spacy-pkuseg -pykakasi==2.3.0 \ No newline at end of file +pykakasi==2.3.0 diff --git a/backend/python/chatterbox/requirements.txt b/backend/python/chatterbox/requirements.txt index 07d7d0706b17..6afa17588aee 100644 --- a/backend/python/chatterbox/requirements.txt +++ b/backend/python/chatterbox/requirements.txt @@ -3,4 +3,6 @@ protobuf certifi packaging setuptools<81 -poetry \ No newline at end of file +poetry +# Turbo and Nano normalize reference audio before cloning. +pyloudnorm diff --git a/docs/content/features/text-to-audio.md b/docs/content/features/text-to-audio.md index c8109ba57f62..20027a6bea70 100644 --- a/docs/content/features/text-to-audio.md +++ b/docs/content/features/text-to-audio.md @@ -287,6 +287,57 @@ You can use the env variable COQUI_LANGUAGE to set the language used by the coqu You can also use config files to configure tts models (see section below on how to use config files). +### Chatterbox + +The `chatterbox` backend supports the original, multilingual, Turbo, and Nano +variants. Select a variant with a model configuration option: + +| Variant | Option | +| --- | --- | +| Original | No selector | +| Multilingual | `multilingual:true` | +| Turbo | `turbo:true` | +| Nano | `nano:true` | + +Enable only one selector. A selector set to `false` does not select that variant. +The backend downloads the selected variant's weights when it first loads the model. +Changing `parameters.model` alone does not select a variant. + +Save this configuration in your models directory to use Nano: + +```yaml +name: chatterbox-nano +backend: chatterbox +parameters: + model: ResembleAI/chatterbox-nano +options: + - nano:true + - temperature:0.8 +tts: + audio_path: /models/reference.wav +``` + +Use a reference recording longer than five seconds for Turbo or Nano voice cloning. +The reference path must be accessible to the backend. +For Turbo, change the name and model to `chatterbox-turbo` and +`ResembleAI/chatterbox-turbo`, then replace `nano:true` with `turbo:true`. + +```sh +curl http://localhost:8080/v1/audio/speech \ + -H "Content-Type: application/json" \ + -d '{ + "model": "chatterbox-nano", + "input": "Hello! This is a voice cloning test.", + "response_format": "wav", + "params": {"temperature": "0.8"} + }' --output speech.wav +``` + +Turbo and Nano ignore `cfg_weight`, `exaggeration`, and `min_p`, including values +in per-request `params`. Other supported generation options, such as +`temperature`, remain available. Request parameters cannot change the loaded +variant; use separate model configurations to serve multiple variants. + ### Fish Speech Fish Speech models accept the `compile` backend option. Enabling it can improve From a06c06d9f1350100f4318a370a51efd8b24a3d3a Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:14:55 +0000 Subject: [PATCH 4/4] fix(chatterbox): coerce sampling controls by name Keep float controls as floats when values omit the decimal point. Only top_k uses integer conversion for its strict sampler contract. Assisted-by: Codex:gpt-6 Signed-off-by: Ettore Di Giacinto --- backend/python/chatterbox/backend.py | 10 ++++++---- backend/python/chatterbox/test_variants.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/backend/python/chatterbox/backend.py b/backend/python/chatterbox/backend.py index 13ab93a75a43..bb6e3b137836 100644 --- a/backend/python/chatterbox/backend.py +++ b/backend/python/chatterbox/backend.py @@ -38,13 +38,15 @@ def is_int(s): except ValueError: return False -def coerce_param_value(value): +def coerce_param_value(value, key=None): """Coerce a TTSRequest.params value (string on the wire) to the type the Chatterbox generate() kwargs expect (float/int/bool), matching how static YAML options are coerced at load time. Non-string values pass through.""" if not isinstance(value, str): return value - if is_int(value): + # Transformers requires top_k to be an int, but float controls such as + # temperature must remain floats even when written without a decimal point. + if key == "top_k" and is_int(value): return int(value) if is_float(value): return float(value) @@ -157,7 +159,7 @@ def LoadModel(self, request, context): if ":" not in opt: continue key, value = opt.split(":") - self.options[key] = coerce_param_value(value) + self.options[key] = coerce_param_value(value, key) self.AudioPath = None @@ -218,7 +220,7 @@ def TTS(self, request, context): for key, value in request.params.items(): if key == "ref_text": continue - kwargs[key] = coerce_param_value(value) + kwargs[key] = coerce_param_value(value, key) # Selectors apply only at load time, including when supplied in params. for name in ("multilingual", "turbo", "nano"): diff --git a/backend/python/chatterbox/test_variants.py b/backend/python/chatterbox/test_variants.py index c97e9320e134..52ba7f455fc1 100644 --- a/backend/python/chatterbox/test_variants.py +++ b/backend/python/chatterbox/test_variants.py @@ -134,6 +134,19 @@ def test_top_k_remains_an_integer(self): self.assertIs(type(top_k), int) self.assertEqual(top_k, expected) + def test_integral_float_controls_remain_floats(self): + for variant in ("turbo", "nano"): + for params, expected in (({}, 2.0), ({"temperature": "3", "repetition_penalty": "3"}, 3.0)): + with self.subTest(variant=variant, params=params): + self.assertTrue(self.load([ + f"{variant}:true", "temperature:2", "repetition_penalty:2", + ]).success) + self.assertTrue(self.speak(params).success) + kwargs = self.service.model.generate.call_args.kwargs + for name in ("temperature", "repetition_penalty"): + self.assertIs(type(kwargs[name]), float) + self.assertEqual(kwargs[name], expected) + def test_static_unsupported_knobs_are_filtered(self): self.assertTrue(self.load(["nano:true", "cfg_weight:0.4", "exaggeration:0.6", "min_p:0.1"]).success) self.assertTrue(self.speak().success)