606 lines
24 KiB
Python
606 lines
24 KiB
Python
"""Request-shape tests for current ElevenLabs endpoints, models and parameters. HTTP is mocked."""
|
|
import base64
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
import nodes
|
|
import utils
|
|
|
|
AUDIO = {"waveform": torch.zeros(1, 1, 1000), "sample_rate": 22050}
|
|
FAKE_DECODED = {"waveform": torch.zeros(1, 1, 100), "sample_rate": 44100}
|
|
|
|
|
|
class Capture:
|
|
"""Patches api_post/get_api_key/audio decoding and records the last request."""
|
|
|
|
def __init__(self, json_payload=None, content=b"FAKE"):
|
|
self.url = None
|
|
self.kwargs = None
|
|
self.decoded_format = None
|
|
resp = MagicMock(status_code=200, content=content)
|
|
resp.json = MagicMock(return_value=json_payload if json_payload is not None else {})
|
|
self.resp = resp
|
|
|
|
def _post(self, url, key, **kwargs):
|
|
self.url = url
|
|
self.kwargs = kwargs
|
|
return self.resp
|
|
|
|
def _decode(self, audio_bytes, fmt):
|
|
self.decoded_format = fmt
|
|
return FAKE_DECODED
|
|
|
|
def __enter__(self):
|
|
self._patches = [
|
|
patch("nodes.api_post", side_effect=self._post),
|
|
patch("nodes.get_api_key", return_value="k"),
|
|
patch("nodes.audio_bytes_to_comfy", side_effect=self._decode),
|
|
]
|
|
for p in self._patches:
|
|
p.start()
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
for p in self._patches:
|
|
p.stop()
|
|
|
|
@property
|
|
def body(self):
|
|
return self.kwargs["json"]
|
|
|
|
@property
|
|
def form(self):
|
|
return self.kwargs["data"]
|
|
|
|
|
|
def _optional_names(node_cls):
|
|
return list(node_cls.INPUT_TYPES()["optional"])
|
|
|
|
|
|
# --------- Models: shut down, new, limits ---------
|
|
|
|
def test_shut_down_models_removed():
|
|
assert "eleven_monolingual_v1" not in utils.TTS_MODELS
|
|
assert "eleven_multilingual_v1" not in utils.TTS_MODELS
|
|
assert "scribe_v1" not in utils.STT_MODELS
|
|
assert "eleven_monolingual_v1" not in nodes.ElevenLabsPro_CostEstimator._COST_PER_CHAR
|
|
|
|
|
|
def test_new_models_available():
|
|
assert "eleven_v4" in utils.TTS_MODELS
|
|
assert "eleven_v4" in utils.DIALOGUE_MODELS
|
|
assert "scribe_v2_medical" in utils.STT_MODELS
|
|
assert {"music_v1", "music_v2", "music_v2_5"} <= set(utils.MUSIC_MODELS)
|
|
assert utils.VOICE_DESIGN_MODELS == ["eleven_multilingual_ttv_v2", "eleven_ttv_v3"]
|
|
|
|
|
|
def test_defaults_unchanged():
|
|
assert nodes.ElevenLabsPro_TTS.INPUT_TYPES()["required"]["model"][1]["default"] == "eleven_v3"
|
|
assert nodes.ElevenLabsPro_STT.INPUT_TYPES()["required"]["model"][1]["default"] == "scribe_v2"
|
|
assert nodes.ElevenLabsPro_Music.INPUT_TYPES()["optional"]["model"][1]["default"] == "music_v1"
|
|
assert nodes.ElevenLabsPro_Dialogue.INPUT_TYPES()["required"]["model"][1]["default"] == "eleven_v3"
|
|
|
|
|
|
def test_char_limits_follow_docs():
|
|
utils.validate_text_length("x" * 10000, "eleven_v4")
|
|
with pytest.raises(ValueError):
|
|
utils.validate_text_length("x" * 10001, "eleven_v4")
|
|
with pytest.raises(ValueError):
|
|
utils.validate_text_length("x" * 30001, "eleven_flash_v2")
|
|
|
|
|
|
def test_tts_accepts_eleven_v4():
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_TTS().generate(api_key="k", text="hi", voice_id="vid", model="eleven_v4")
|
|
assert cap.body["model_id"] == "eleven_v4"
|
|
assert cap.url.endswith("/v1/text-to-speech/vid")
|
|
|
|
|
|
# --------- New widgets are appended after existing ones ---------
|
|
|
|
def test_new_inputs_are_appended_last():
|
|
assert _optional_names(nodes.ElevenLabsPro_TTS)[-1] == "apply_language_text_normalization"
|
|
assert _optional_names(nodes.ElevenLabsPro_TTSTimestamps)[:5] == [
|
|
"stability", "similarity_boost", "output_format", "seed", "enable_logging",
|
|
]
|
|
assert _optional_names(nodes.ElevenLabsPro_SFX)[-1] == "auto_duration"
|
|
assert _optional_names(nodes.ElevenLabsPro_STT)[-3:] == [
|
|
"transcript_edit", "use_multi_channel", "detect_speaker_roles",
|
|
]
|
|
assert _optional_names(nodes.ElevenLabsPro_Music)[-5:] == [
|
|
"finetune_id", "finetune_strength", "use_phonetic_names", "generation_mode", "lyrics_text",
|
|
]
|
|
assert _optional_names(nodes.ElevenLabsPro_VoiceClone)[-1] == "labels"
|
|
assert _optional_names(nodes.ElevenLabsPro_VoiceCreate) == ["voice_description", "labels"]
|
|
assert _optional_names(nodes.ElevenLabsPro_VoiceDesign)[0] == "preview_index"
|
|
assert _optional_names(nodes.ElevenLabsPro_Dialogue)[-4:] == [
|
|
"previous_text", "future_text", "use_pvc_as_ivc", "pronunciation_dictionary_locators",
|
|
]
|
|
assert nodes.ElevenLabsPro_STT.RETURN_NAMES[:3] == ("text", "language_code", "words_json")
|
|
|
|
|
|
def test_new_node_keys_registered():
|
|
for key in ("ElevenLabsPro_VoiceRemix", "ElevenLabsPro_ForcedAlignment",
|
|
"ElevenLabsPro_DialogueTimestamps", "ElevenLabsPro_MusicPlan"):
|
|
assert key in nodes.NODE_CLASS_MAPPINGS
|
|
assert key in nodes.NODE_DISPLAY_NAME_MAPPINGS
|
|
assert len(nodes.NODE_CLASS_MAPPINGS) == 30
|
|
|
|
|
|
# --------- Voice design / remix / create ---------
|
|
|
|
PREVIEWS = {
|
|
"previews": [
|
|
{"generated_voice_id": "gen1", "audio_base_64": base64.b64encode(b"mp3").decode()},
|
|
{"generated_voice_id": "gen2", "audio_base_64": base64.b64encode(b"mp3").decode()},
|
|
],
|
|
"text": "sample",
|
|
}
|
|
LONG_TEXT = "x" * 120
|
|
|
|
|
|
def test_voice_design_uses_design_endpoint():
|
|
with Capture(PREVIEWS) as cap:
|
|
audio, gen_id, all_ids = nodes.ElevenLabsPro_VoiceDesign().design(
|
|
api_key="k", text=LONG_TEXT, voice_description="a warm baritone narrator voice",
|
|
preview_index=1)
|
|
assert cap.url.endswith("/v1/text-to-voice/design")
|
|
assert cap.body == {"text": LONG_TEXT, "voice_description": "a warm baritone narrator voice"}
|
|
assert gen_id == "gen2"
|
|
assert all_ids == "gen1\ngen2"
|
|
assert audio is FAKE_DECODED
|
|
|
|
|
|
def test_voice_design_short_text_is_auto_generated():
|
|
with Capture(PREVIEWS) as cap:
|
|
nodes.ElevenLabsPro_VoiceDesign().design(
|
|
api_key="k", text="too short", voice_description="a warm baritone narrator voice")
|
|
assert cap.body["auto_generate_text"] is True
|
|
assert "text" not in cap.body
|
|
|
|
|
|
def test_voice_design_empty_text_raises_unless_auto():
|
|
node = nodes.ElevenLabsPro_VoiceDesign()
|
|
with pytest.raises(ValueError, match="Text is required"):
|
|
node.design(api_key="k", text="", voice_description="a warm baritone narrator voice")
|
|
with Capture(PREVIEWS) as cap:
|
|
node.design(api_key="k", text="", voice_description="a warm baritone narrator voice",
|
|
auto_generate_text=True)
|
|
assert cap.body["auto_generate_text"] is True
|
|
|
|
|
|
def test_voice_design_new_params():
|
|
with Capture(PREVIEWS) as cap:
|
|
nodes.ElevenLabsPro_VoiceDesign().design(
|
|
api_key="k", text=LONG_TEXT, voice_description="a warm baritone narrator voice",
|
|
model="eleven_ttv_v3", loudness=0.2, guidance_scale=7.0, seed=42,
|
|
should_enhance=True, reference_audio=AUDIO, prompt_strength=0.8)
|
|
body = cap.body
|
|
assert body["model_id"] == "eleven_ttv_v3"
|
|
assert body["loudness"] == 0.2
|
|
assert body["guidance_scale"] == 7.0
|
|
assert body["seed"] == 42
|
|
assert body["should_enhance"] is True
|
|
assert body["prompt_strength"] == 0.8
|
|
assert base64.b64decode(body["reference_audio_base64"])[:4] == b"RIFF"
|
|
|
|
|
|
def test_voice_design_requires_description():
|
|
with pytest.raises(ValueError, match="description"):
|
|
nodes.ElevenLabsPro_VoiceDesign().design(api_key="k", text=LONG_TEXT, voice_description=" ")
|
|
|
|
|
|
def test_voice_design_no_previews_raises():
|
|
with Capture({"previews": []}):
|
|
with pytest.raises(RuntimeError, match="No voice previews"):
|
|
nodes.ElevenLabsPro_VoiceDesign().design(
|
|
api_key="k", text=LONG_TEXT, voice_description="a warm baritone narrator voice")
|
|
|
|
|
|
def test_voice_remix_request():
|
|
with Capture(PREVIEWS) as cap:
|
|
audio, gen_id, all_ids = nodes.ElevenLabsPro_VoiceRemix().remix(
|
|
api_key="k", voice_id=" vid1 ", voice_description="make it older", text=LONG_TEXT,
|
|
loudness=-0.5, guidance_scale=4.0, seed=7)
|
|
assert cap.url.endswith("/v1/text-to-voice/vid1/remix")
|
|
assert cap.body == {
|
|
"text": LONG_TEXT, "voice_description": "make it older",
|
|
"loudness": -0.5, "guidance_scale": 4.0, "seed": 7,
|
|
}
|
|
assert gen_id == "gen1"
|
|
assert all_ids == "gen1\ngen2"
|
|
|
|
|
|
def test_voice_remix_default_guidance_not_sent():
|
|
with Capture(PREVIEWS) as cap:
|
|
nodes.ElevenLabsPro_VoiceRemix().remix(
|
|
api_key="k", voice_id="v", voice_description="make it older", text=LONG_TEXT)
|
|
assert "guidance_scale" not in cap.body
|
|
|
|
|
|
def test_voice_remix_requires_voice_id():
|
|
with pytest.raises(ValueError, match="voice_id"):
|
|
nodes.ElevenLabsPro_VoiceRemix().remix(
|
|
api_key="k", voice_id="", voice_description="x" * 10, text=LONG_TEXT)
|
|
|
|
|
|
def test_voice_create_uses_text_to_voice_endpoint():
|
|
with Capture({"voice_id": "vid_new"}) as cap:
|
|
vid, status = nodes.ElevenLabsPro_VoiceCreate().create(
|
|
api_key="k", generated_voice_id=" gen1 ", voice_name="N", create=True,
|
|
voice_description="A calm, warm narrator voice for audiobooks",
|
|
labels='{"accent": "british"}')
|
|
assert cap.url.endswith("/v1/text-to-voice")
|
|
assert cap.body == {
|
|
"voice_name": "N",
|
|
"voice_description": "A calm, warm narrator voice for audiobooks",
|
|
"generated_voice_id": "gen1",
|
|
"labels": {"accent": "british"},
|
|
}
|
|
assert vid == "vid_new"
|
|
|
|
|
|
def test_voice_create_short_description_raises():
|
|
with patch("nodes.api_post") as api_mock:
|
|
with pytest.raises(ValueError, match="20-1000"):
|
|
nodes.ElevenLabsPro_VoiceCreate().create(
|
|
api_key="k", generated_voice_id="g", voice_name="N", create=True,
|
|
voice_description="too short")
|
|
api_mock.assert_not_called()
|
|
|
|
|
|
def test_voice_create_dry_run_needs_no_description():
|
|
with patch("nodes.api_post") as api_mock:
|
|
_, status = nodes.ElevenLabsPro_VoiceCreate().create(
|
|
api_key="k", generated_voice_id="g", voice_name="N", create=False)
|
|
api_mock.assert_not_called()
|
|
assert "DRY-RUN" in status
|
|
|
|
|
|
def test_labels_must_be_json_object():
|
|
with pytest.raises(ValueError, match="valid JSON"):
|
|
nodes._parse_labels("{nope")
|
|
with pytest.raises(ValueError, match="JSON object"):
|
|
nodes._parse_labels("[1]")
|
|
assert nodes._parse_labels("") is None
|
|
|
|
|
|
def test_voice_clone_sends_labels():
|
|
with Capture({"voice_id": "v"}) as cap:
|
|
nodes.ElevenLabsPro_VoiceClone().clone(
|
|
api_key="k", voice_name="X", audio1=AUDIO, create=True, labels='{"gender": "female"}')
|
|
assert json.loads(cap.form["labels"]) == {"gender": "female"}
|
|
|
|
|
|
# --------- TTS / TTS with timestamps ---------
|
|
|
|
def test_tts_language_normalization_only_sent_when_enabled():
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_TTS().generate(api_key="k", text="hi", voice_id="v", model="eleven_v3")
|
|
assert "apply_language_text_normalization" not in cap.body
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_TTS().generate(
|
|
api_key="k", text="hi", voice_id="v", model="eleven_flash_v2_5",
|
|
language="Japanese (ja)", apply_language_text_normalization=True)
|
|
assert cap.body["apply_language_text_normalization"] is True
|
|
|
|
|
|
def test_tts_pronunciation_locators_still_sent():
|
|
locators = [{"pronunciation_dictionary_id": "d1", "version_id": "v1"}]
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_TTS().generate(
|
|
api_key="k", text="hi", voice_id="v", model="eleven_v3",
|
|
pronunciation_dictionary_locators=json.dumps(locators))
|
|
assert cap.body["pronunciation_dictionary_locators"] == locators
|
|
|
|
|
|
def test_tts_timestamps_decodes_audio_base64():
|
|
payload = {
|
|
"audio_base64": base64.b64encode(b"mp3bytes").decode(),
|
|
"alignment": {"characters": ["h"], "character_start_times_seconds": [0.0],
|
|
"character_end_times_seconds": [0.1]},
|
|
}
|
|
with Capture(payload) as cap:
|
|
audio, ts = nodes.ElevenLabsPro_TTSTimestamps().generate(
|
|
api_key="k", text="hi", voice_id="v", model="eleven_v3")
|
|
assert audio is FAKE_DECODED
|
|
assert json.loads(ts)["characters"] == ["h"]
|
|
assert cap.url.endswith("/v1/text-to-speech/v/with-timestamps")
|
|
|
|
|
|
def test_tts_timestamps_defaults_keep_request_unchanged():
|
|
with Capture({"audio_base64": ""}) as cap:
|
|
nodes.ElevenLabsPro_TTSTimestamps().generate(
|
|
api_key="k", text="hi", voice_id="v", model="eleven_multilingual_v2")
|
|
assert cap.body == {
|
|
"text": "hi",
|
|
"model_id": "eleven_multilingual_v2",
|
|
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75},
|
|
"apply_text_normalization": "auto",
|
|
}
|
|
|
|
|
|
def test_tts_timestamps_new_params():
|
|
with Capture({"audio_base64": ""}) as cap:
|
|
nodes.ElevenLabsPro_TTSTimestamps().generate(
|
|
api_key="k", text="hi", voice_id="v", model="eleven_v3",
|
|
style=0.3, speed=1.2, use_speaker_boost=False, language="French (fr)",
|
|
apply_text_normalization="on", previous_text=" before ", next_text=" after ")
|
|
body = cap.body
|
|
assert body["voice_settings"] == {
|
|
"stability": 0.5, "similarity_boost": 0.75,
|
|
"style": 0.3, "speed": 1.2, "use_speaker_boost": False,
|
|
}
|
|
assert body["language_code"] == "fr"
|
|
assert body["apply_text_normalization"] == "off" # eleven_v3 forces off
|
|
assert body["previous_text"] == "before"
|
|
assert body["next_text"] == "after"
|
|
|
|
|
|
# --------- SFX / audio isolation ---------
|
|
|
|
def test_sfx_auto_duration_omits_duration():
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_SFX().generate(api_key="k", text="thunder", auto_duration=True, duration=99.0)
|
|
assert "duration_seconds" not in cap.body
|
|
|
|
|
|
def test_sfx_default_sends_duration():
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_SFX().generate(api_key="k", text="thunder")
|
|
assert cap.body["duration_seconds"] == 5.0
|
|
|
|
|
|
def test_audio_isolation_sends_no_output_format_and_decodes_mp3():
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_AudioIsolation().isolate(api_key="k", audio=AUDIO, output_format="pcm_44100")
|
|
assert "params" not in cap.kwargs
|
|
assert cap.url.endswith("/v1/audio-isolation")
|
|
assert cap.decoded_format == "mp3_44100_128"
|
|
|
|
|
|
# --------- Speech to text ---------
|
|
|
|
STT_RESPONSE = {"text": "hello", "language_code": "eng", "words": [{"text": "hello"}]}
|
|
|
|
|
|
def test_stt_sends_tag_audio_events_explicitly():
|
|
with Capture(STT_RESPONSE) as cap:
|
|
nodes.ElevenLabsPro_STT().transcribe(api_key="k", audio=AUDIO, model="scribe_v2")
|
|
assert cap.form["tag_audio_events"] == "false"
|
|
with Capture(STT_RESPONSE) as cap:
|
|
nodes.ElevenLabsPro_STT().transcribe(
|
|
api_key="k", audio=AUDIO, model="scribe_v2", tag_audio_events=True)
|
|
assert cap.form["tag_audio_events"] == "true"
|
|
|
|
|
|
def test_stt_keyterms_sent_as_list():
|
|
with Capture(STT_RESPONSE) as cap:
|
|
nodes.ElevenLabsPro_STT().transcribe(
|
|
api_key="k", audio=AUDIO, model="scribe_v2", keyterms=" ComfyUI, ElevenLabs ,, Scribe ")
|
|
assert cap.form["keyterms"] == ["ComfyUI", "ElevenLabs", "Scribe"]
|
|
|
|
|
|
def test_stt_no_keyterms_not_sent():
|
|
with Capture(STT_RESPONSE) as cap:
|
|
nodes.ElevenLabsPro_STT().transcribe(api_key="k", audio=AUDIO, model="scribe_v2")
|
|
assert "keyterms" not in cap.form
|
|
for field in ("transcript_edit", "use_multi_channel", "multichannel_output_style",
|
|
"detect_speaker_roles"):
|
|
assert field not in cap.form
|
|
|
|
|
|
def test_stt_medical_model_passthrough():
|
|
with Capture(STT_RESPONSE) as cap:
|
|
nodes.ElevenLabsPro_STT().transcribe(api_key="k", audio=AUDIO, model="scribe_v2_medical")
|
|
assert cap.form["model_id"] == "scribe_v2_medical"
|
|
|
|
|
|
def test_stt_new_params_and_edited_text():
|
|
response = dict(STT_RESPONSE, edited_transcript={"kind": "transcript", "text": "Hello."})
|
|
with Capture(response) as cap:
|
|
text, lang, words, edited = nodes.ElevenLabsPro_STT().transcribe(
|
|
api_key="k", audio=AUDIO, model="scribe_v2", diarize=True,
|
|
transcript_edit=" add punctuation ", use_multi_channel=True, detect_speaker_roles=True)
|
|
assert cap.form["transcript_edit"] == "add punctuation"
|
|
assert cap.form["use_multi_channel"] == "true"
|
|
assert cap.form["multichannel_output_style"] == "combined"
|
|
assert cap.form["detect_speaker_roles"] == "true"
|
|
assert (text, lang, edited) == ("hello", "eng", "Hello.")
|
|
assert json.loads(words) == [{"text": "hello"}]
|
|
|
|
|
|
def test_stt_edit_error_returns_empty_edited_text():
|
|
response = dict(STT_RESPONSE, edited_transcript={
|
|
"kind": "error", "error_type": "edit_failed", "message": "could not edit"})
|
|
with Capture(response):
|
|
out = nodes.ElevenLabsPro_STT().transcribe(
|
|
api_key="k", audio=AUDIO, model="scribe_v2", transcript_edit="fix")
|
|
assert out[3] == ""
|
|
assert out[0] == "hello"
|
|
|
|
|
|
def test_stt_without_edit_returns_empty_edited_text():
|
|
with Capture(STT_RESPONSE):
|
|
out = nodes.ElevenLabsPro_STT().transcribe(api_key="k", audio=AUDIO, model="scribe_v2")
|
|
assert len(out) == 4
|
|
assert out[3] == ""
|
|
|
|
|
|
# --------- Forced alignment ---------
|
|
|
|
def test_forced_alignment_request_and_outputs():
|
|
payload = {
|
|
"characters": [],
|
|
"words": [{"text": "hi", "start": 0.0, "end": 0.4, "loss": 0.1}],
|
|
"loss": 0.25,
|
|
}
|
|
with Capture(payload) as cap:
|
|
words_json, loss = nodes.ElevenLabsPro_ForcedAlignment().align(
|
|
api_key="k", audio=AUDIO, text="hi")
|
|
assert cap.url.endswith("/v1/forced-alignment")
|
|
assert cap.form == {"text": "hi"}
|
|
assert cap.kwargs["files"]["file"][0] == "input.wav"
|
|
assert json.loads(words_json)[0]["end"] == 0.4
|
|
assert loss == 0.25
|
|
|
|
|
|
def test_forced_alignment_words_feed_subtitle_export():
|
|
words = json.dumps([{"text": "hello", "start": 0.0, "end": 0.5, "loss": 0.1},
|
|
{"text": "world", "start": 0.6, "end": 1.0, "loss": 0.1}])
|
|
out, = nodes.ElevenLabsPro_SubtitleExport().export(words, format="srt")
|
|
assert "hello world" in out
|
|
|
|
|
|
def test_forced_alignment_empty_text_raises():
|
|
with pytest.raises(ValueError, match="empty"):
|
|
nodes.ElevenLabsPro_ForcedAlignment().align(api_key="k", audio=AUDIO, text=" ")
|
|
|
|
|
|
# --------- Dialogue ---------
|
|
|
|
def test_dialogue_defaults_unchanged():
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_Dialogue().generate(api_key="k", text1="hi", voice_id1="v1")
|
|
assert cap.url.endswith("/v1/text-to-dialogue")
|
|
assert cap.body == {
|
|
"inputs": [{"text": "hi", "voice_id": "v1"}],
|
|
"model_id": "eleven_v3",
|
|
"settings": {"stability": 0.5},
|
|
"apply_text_normalization": "off",
|
|
}
|
|
assert cap.kwargs["params"] == {"output_format": "mp3_44100_192"}
|
|
|
|
|
|
def test_dialogue_eleven_v4_and_new_params():
|
|
locators = [{"pronunciation_dictionary_id": "d1", "version_id": "v1"}]
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_Dialogue().generate(
|
|
api_key="k", text1="hi", voice_id1="v1", model="eleven_v4",
|
|
previous_text=" before ", future_text=" after ", use_pvc_as_ivc=True,
|
|
pronunciation_dictionary_locators=json.dumps(locators), output_format="wav_44100")
|
|
assert cap.body["model_id"] == "eleven_v4"
|
|
assert cap.body["previous_text"] == "before"
|
|
assert cap.body["future_text"] == "after"
|
|
assert cap.body["use_pvc_as_ivc"] is True
|
|
assert cap.body["pronunciation_dictionary_locators"] == locators
|
|
assert cap.kwargs["params"] == {"output_format": "wav_44100"}
|
|
|
|
|
|
def test_dialogue_output_formats_cover_all_formats():
|
|
fmt = nodes.ElevenLabsPro_Dialogue.INPUT_TYPES()["optional"]["output_format"]
|
|
assert set(utils.OUTPUT_FORMATS) == set(fmt[0])
|
|
assert {"mp3_44100_192", "opus_48000_192"} <= set(fmt[0])
|
|
|
|
|
|
def test_dialogue_timestamps_request_and_outputs():
|
|
payload = {
|
|
"audio_base64": base64.b64encode(b"mp3").decode(),
|
|
"alignment": {"characters": ["h"], "character_start_times_seconds": [0.0],
|
|
"character_end_times_seconds": [0.1]},
|
|
"voice_segments": [{"voice_id": "v1", "start_time_seconds": 0.0, "end_time_seconds": 0.1,
|
|
"character_start_index": 0, "character_end_index": 1}],
|
|
}
|
|
with Capture(payload) as cap:
|
|
audio, ts, segments = nodes.ElevenLabsPro_DialogueTimestamps().generate(
|
|
api_key="k", text1="hi", voice_id1="v1")
|
|
assert cap.url.endswith("/v1/text-to-dialogue/with-timestamps")
|
|
assert audio is FAKE_DECODED
|
|
assert json.loads(ts)["characters"] == ["h"]
|
|
assert json.loads(segments)[0]["voice_id"] == "v1"
|
|
|
|
|
|
def test_dialogue_timestamps_shares_dialogue_inputs():
|
|
assert (nodes.ElevenLabsPro_DialogueTimestamps.INPUT_TYPES()
|
|
== nodes.ElevenLabsPro_Dialogue.INPUT_TYPES())
|
|
|
|
|
|
def test_dialogue_timestamps_missing_audio_returns_silence():
|
|
with Capture({"alignment": {}, "voice_segments": []}):
|
|
audio, ts, segments = nodes.ElevenLabsPro_DialogueTimestamps().generate(
|
|
api_key="k", text1="hi", voice_id1="v1")
|
|
assert audio["waveform"].shape[-1] > 0
|
|
assert json.loads(segments) == []
|
|
|
|
|
|
# --------- Music / composition plan ---------
|
|
|
|
def test_music_prompt_request_defaults():
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_Music().generate(api_key="k", prompt="synthwave", duration_seconds=12.0)
|
|
assert cap.body == {
|
|
"model_id": "music_v1",
|
|
"store_for_inpainting": False,
|
|
"sign_with_c2pa": False,
|
|
"prompt": "synthwave",
|
|
"music_length_ms": 12000,
|
|
"force_instrumental": False,
|
|
}
|
|
assert cap.kwargs["params"] == {"output_format": "mp3_44100_128"}
|
|
|
|
|
|
def test_music_plan_request_has_no_prompt_only_fields():
|
|
plan = {"positive_global_styles": ["pop"], "negative_global_styles": [], "sections": []}
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_Music().generate(
|
|
api_key="k", prompt="ignored", composition_plan=json.dumps(plan),
|
|
respect_sections_durations=False, lyrics_text="la la", generation_mode="loop",
|
|
use_phonetic_names=True)
|
|
assert cap.body["composition_plan"] == plan
|
|
assert cap.body["respect_sections_durations"] is False
|
|
for field in ("prompt", "music_length_ms", "force_instrumental", "lyrics_text",
|
|
"generation_mode", "use_phonetic_names"):
|
|
assert field not in cap.body
|
|
|
|
|
|
def test_music_new_models_and_prompt_params():
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_Music().generate(
|
|
api_key="k", prompt="synthwave", model="music_v2_5", output_format="mp3_48000_192",
|
|
finetune_id=" ft_1 ", finetune_strength=1.5, use_phonetic_names=True,
|
|
generation_mode="ambience", lyrics_text=" la la ")
|
|
body = cap.body
|
|
assert body["model_id"] == "music_v2_5"
|
|
assert body["finetune_id"] == "ft_1"
|
|
assert body["finetune_strength"] == 1.5
|
|
assert body["use_phonetic_names"] is True
|
|
assert body["generation_mode"] == "ambience"
|
|
assert body["lyrics_text"] == "la la"
|
|
assert cap.kwargs["params"] == {"output_format": "mp3_48000_192"}
|
|
|
|
|
|
def test_music_finetune_strength_ignored_without_finetune_id():
|
|
with Capture() as cap:
|
|
nodes.ElevenLabsPro_Music().generate(api_key="k", prompt="x", finetune_strength=1.5)
|
|
assert "finetune_id" not in cap.body
|
|
assert "finetune_strength" not in cap.body
|
|
|
|
|
|
def test_music_output_formats_include_48k_mp3():
|
|
fmt = nodes.ElevenLabsPro_Music.INPUT_TYPES()["optional"]["output_format"][0]
|
|
assert {"mp3_48000_128", "mp3_48000_192", "mp3_48000_240", "mp3_48000_320"} <= set(fmt)
|
|
assert set(utils.OUTPUT_FORMATS) <= set(fmt)
|
|
|
|
|
|
def test_music_plan_node_request():
|
|
plan = {"positive_global_styles": ["pop"], "negative_global_styles": [], "sections": []}
|
|
with Capture(plan) as cap:
|
|
out, = nodes.ElevenLabsPro_MusicPlan().plan(
|
|
api_key="k", prompt=" a pop song ", model="music_v2", duration_seconds=45.0)
|
|
assert cap.url.endswith("/v1/music/plan")
|
|
assert cap.body == {"prompt": "a pop song", "model_id": "music_v2", "music_length_ms": 45000}
|
|
assert json.loads(out) == plan
|
|
|
|
|
|
def test_music_plan_node_length_optional_and_validated():
|
|
with Capture({}) as cap:
|
|
nodes.ElevenLabsPro_MusicPlan().plan(api_key="k", prompt="x")
|
|
assert cap.body == {"prompt": "x", "model_id": "music_v1"}
|
|
with pytest.raises(ValueError, match="between 3 and 600"):
|
|
nodes.ElevenLabsPro_MusicPlan().plan(api_key="k", prompt="x", duration_seconds=2.0)
|
|
with pytest.raises(ValueError, match="empty"):
|
|
nodes.ElevenLabsPro_MusicPlan().plan(api_key="k", prompt=" ")
|