diff --git a/tests/tools/test_music_gen_force_instrumental.py b/tests/tools/test_music_gen_force_instrumental.py new file mode 100644 index 00000000..0a62a4ae --- /dev/null +++ b/tests/tools/test_music_gen_force_instrumental.py @@ -0,0 +1,87 @@ +"""Regression for the force_instrumental mandate (REVIEW §8 #8). + +``skills/creative/music-gen-usage.md`` mandates that video background music +ALWAYS be generated instrumental-only (vocals collide with narration). +``music_gen`` historically never sent the kwarg, so ElevenLabs could return +vocal tracks. This test pins the payload to include ``force_instrumental=True`` +by default and to honor an explicit opt-out. +""" + +from __future__ import annotations + +import sys +import types + +import pytest + +from tools.audio.music_gen import MusicGen + + +class _FakeResponse: + def __init__(self, content: bytes = b"audio") -> None: + self.content = content + + def raise_for_status(self) -> None: # noqa: D401 - stub + return None + + +def _install_fake_requests(captured: dict) -> types.ModuleType: + """Install a stub ``requests`` module that records the JSON payload.""" + fake = types.ModuleType("requests") + + def fake_post(url, headers=None, json=None, timeout=None): # noqa: ANN001 + captured["url"] = url + captured["headers"] = headers + captured["payload"] = json + return _FakeResponse() + + fake.post = fake_post + sys.modules["requests"] = fake + return fake + + +def test_force_instrumental_is_sent_true_by_default(monkeypatch, tmp_path): + monkeypatch.setenv("ELEVENLABS_API_KEY", "test-key") + captured: dict = {} + _install_fake_requests(captured) + # Route the output to tmp_path so the test never writes music_output.mp3 + # into the repo root (the default output_path). + out = tmp_path / "bg.mp3" + try: + MusicGen()._generate( + {"prompt": "gentle ambient", "duration_seconds": 10, "output_path": str(out)}, + "test-key", + ) + finally: + sys.modules.pop("requests", None) + + assert "force_instrumental" in captured["payload"], "force_instrumental kwarg was never sent" + assert captured["payload"]["force_instrumental"] is True + assert captured["payload"]["music_length_ms"] == 10_000 + + +def test_explicit_vocal_opt_out_is_respected(monkeypatch, tmp_path): + monkeypatch.setenv("ELEVENLABS_API_KEY", "test-key") + captured: dict = {} + _install_fake_requests(captured) + out = tmp_path / "vocals.mp3" + try: + MusicGen()._generate( + { + "prompt": "lead vocal pop", + "duration_seconds": 10, + "force_instrumental": False, + "output_path": str(out), + }, + "test-key", + ) + finally: + sys.modules.pop("requests", None) + + assert captured["payload"]["force_instrumental"] is False + + +def test_schema_defaults_force_instrumental_to_true(): + props = MusicGen().input_schema["properties"]["force_instrumental"] + assert props["type"] == "boolean" + assert props["default"] is True diff --git a/tools/audio/music_gen.py b/tools/audio/music_gen.py index 7b0e3e65..94776b00 100644 --- a/tools/audio/music_gen.py +++ b/tools/audio/music_gen.py @@ -69,6 +69,17 @@ class MusicGen(BaseTool): ), }, "output_path": {"type": "string"}, + "force_instrumental": { + "type": "boolean", + "default": True, + "description": ( + "Whether to generate instrumental-only music (no vocals). " + "Defaults to True — the music-gen-usage mandate is to always " + "set force_instrumental=true for video background music, since " + "vocals collide with narration/dialogue. Set False only for " + "explicitly vocal-led pieces." + ), + }, }, } @@ -147,6 +158,11 @@ class MusicGen(BaseTool): payload = { "prompt": prompt, "music_length_ms": int(duration * 1000), + # music-gen-usage mandate: always set force_instrumental=true for + # video background music (vocals collide with narration). The input + # schema defaults this to True, so callers get the mandate by + # default; they may opt out only by passing force_instrumental=False. + "force_instrumental": bool(inputs.get("force_instrumental", True)), } response = requests.post(