diff --git a/backend/worker/executor.py b/backend/worker/executor.py index acf92707..349cf281 100644 --- a/backend/worker/executor.py +++ b/backend/worker/executor.py @@ -208,9 +208,11 @@ class TaskExecutor: from services.text_normalization import normalize_for_tts text = normalize_for_tts(row.get("text") or "", row.get("language")) + seed = None if row.get("seed") is not None: import torch - torch.manual_seed(int(row["seed"])) + seed = int(row["seed"]) + torch.manual_seed(seed) kwargs = { "language": row.get("language") if row.get("language") != "Auto" else None, "ref_audio": row.get("ref_audio"), "ref_text": row.get("ref_text"), @@ -221,6 +223,11 @@ class TaskExecutor: "speed": float(row.get("speed") or 1.0), "denoise": True, "postprocess_output": True, } + if ( + getattr(backend, "supports_native_omnivoice_controls", False) + and seed is not None + ): + kwargs["seed"] = seed audio = backend.generate(text=text, **kwargs) preset = row.get("effect_preset") or "broadcast" if preset != "raw": @@ -332,7 +339,10 @@ class TaskExecutor: key: value for key, value in opts.to_manifest().items() if value is not None and key not in ("seed", "vary_repeats") } - if isinstance(backend, OmniVoiceBackend): + native_proxy = bool( + getattr(backend, "supports_native_omnivoice_controls", False) + ) + if isinstance(backend, OmniVoiceBackend) or native_proxy: extra.setdefault("num_step", 32) extra.setdefault("guidance_scale", 2.0) for key in ("emo_vector", "emo_text", "emo_alpha"): @@ -341,11 +351,13 @@ class TaskExecutor: def synth(text, index, speed=None): voice = voices[int(index)] base_seed = opts.seed if opts.seed is not None else voice.get("seed") + seed = None if base_seed is not None: import torch nonce = occurrence["value"] if opts.vary_repeats else 0 occurrence["value"] += 1 - torch.manual_seed(segment_seed(base_seed, text, nonce)) + seed = segment_seed(base_seed, text, nonce) + torch.manual_seed(seed) kwargs = { "language": language, "ref_audio": voice.get("ref_audio"), @@ -354,6 +366,8 @@ class TaskExecutor: "speed": float(speed) if speed else 1.0, **extra, } + if native_proxy and seed is not None: + kwargs["seed"] = seed return backend.generate(text, **kwargs) spans = [Span(voice_id=str(i), text=row.get("text", ""), diff --git a/docs/engines/omnivoice-subprocess.md b/docs/engines/omnivoice-subprocess.md index 00cedb5f..2356ef88 100644 --- a/docs/engines/omnivoice-subprocess.md +++ b/docs/engines/omnivoice-subprocess.md @@ -32,7 +32,7 @@ lower call overhead. ## Selecting it -- **Settings -> Engines**, or +- **Model Catalogue → Engines**, or - `OMNIVOICE_TTS_BACKEND=omnivoice-subprocess` The explicit engine is opt-in on CUDA, ROCm, and CPU. Apple Silicon gets the diff --git a/tests/test_audiobook_remote.py b/tests/test_audiobook_remote.py index 947f3af7..2c72506b 100644 --- a/tests/test_audiobook_remote.py +++ b/tests/test_audiobook_remote.py @@ -54,3 +54,30 @@ def test_audiobook_worker_marks_and_encodes_chapter(monkeypatch): ) assert len(audio) == 20 assert marked == [(100, "worker.executor.tts")] + + +def test_audiobook_worker_forwards_mps_proxy_quality_and_seed(): + import numpy as np + from services.audiobook import segment_seed + from worker.executor import TaskExecutor + + calls = [] + + class Backend: + sample_rate = 100 + supports_native_omnivoice_controls = True + + def generate(self, text, **kwargs): + calls.append((text, kwargs)) + return np.ones(20, dtype=np.float32) + + TaskExecutor._synthesize_audiobook( + Backend(), [{"text": "hello", "pause_ms_after": 0}], + [{"ref_text": None, "instruct": None, "seed": 42}], + {"ref_audio": [None], "expressive": {}, "watermark": False}, + ) + + _text, kwargs = calls[0] + assert kwargs["num_step"] == 32 + assert kwargs["guidance_scale"] == 2.0 + assert kwargs["seed"] == segment_seed(42, "hello") diff --git a/tests/test_worker_executor_residency.py b/tests/test_worker_executor_residency.py index 9ff6adcb..2b7caef2 100644 --- a/tests/test_worker_executor_residency.py +++ b/tests/test_worker_executor_residency.py @@ -83,6 +83,27 @@ class _FakeBackend: type(self).unloaded += 1 +def test_dub_worker_forwards_seed_into_mps_proxy(): + calls = [] + + class Proxy: + sample_rate = 24_000 + applies_own_mastering = False + supports_native_omnivoice_controls = True + + def generate(self, text, **kwargs): + calls.append((text, kwargs)) + import torch + + return torch.zeros(1, 240) + + TaskExecutor._synthesize_dub_segment(Proxy(), { + "text": "hello", "seed": 123, "effect_preset": "raw", + }) + + assert calls[0][1]["seed"] == 123 + + @pytest.mark.asyncio async def test_cancelling_execution_drains_the_blocking_engine_thread(monkeypatch): started = threading.Event()