mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-08-05 18:05:08 +08:00
Add Comfy Cloud Audio POC nodes
Amp-Thread-ID: https://ampcode.com/threads/T-019fd081-33af-7539-8c76-fd13e47ec584 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -20,6 +20,12 @@ ComfyCloudWorkflow = Literal[
|
||||
"video.ltx-2-3-first-last-frame.v1",
|
||||
"video.wan-2-2-14b-first-last-frame.v1",
|
||||
"video.scail-2-character-replacement.v1",
|
||||
"audio.ace-step-1-5-xl-turbo.v1",
|
||||
"audio.stable-audio-3-medium.v1",
|
||||
"audio.chatterbox-multilingual-voice-clone.v1",
|
||||
"audio.chatterbox-dialogue.v1",
|
||||
"audio.chatterbox-voice-conversion.v1",
|
||||
"audio.melbandroformer-stem-separation.v1",
|
||||
]
|
||||
|
||||
|
||||
@@ -45,6 +51,19 @@ class ComfyCloudWorkflowInputs(BaseModel):
|
||||
scene_prompt: str | None = Field(None)
|
||||
driving_subject: str | None = Field(None)
|
||||
reference_subject: str | None = Field(None)
|
||||
style_prompt: str | None = Field(None)
|
||||
lyrics: str | None = Field(None)
|
||||
bpm: int | None = Field(None)
|
||||
time_signature: str | None = Field(None)
|
||||
language: str | None = Field(None)
|
||||
key: str | None = Field(None)
|
||||
expand_prompt: bool | None = Field(None)
|
||||
category: str | None = Field(None)
|
||||
text: str | None = Field(None)
|
||||
exaggeration: float | None = Field(None)
|
||||
cfg_weight: float | None = Field(None)
|
||||
temperature: float | None = Field(None)
|
||||
script: str | None = Field(None)
|
||||
|
||||
|
||||
class ComfyCloudAssetInput(BaseModel):
|
||||
@@ -69,4 +88,5 @@ class ComfyCloudStatusResponse(BaseModel):
|
||||
status: str = Field(...)
|
||||
progress: float | None = Field(None)
|
||||
output_url: str | None = Field(None)
|
||||
output_urls: dict[str, str] | None = Field(None)
|
||||
error: str | None = Field(None)
|
||||
|
||||
@@ -13,6 +13,7 @@ from comfy_api_nodes.apis.comfy_cloud import (
|
||||
)
|
||||
from comfy_api_nodes.util import (
|
||||
ApiEndpoint,
|
||||
download_url_to_audio_input,
|
||||
download_url_to_image_tensor,
|
||||
download_url_to_video_output,
|
||||
get_number_of_images,
|
||||
@@ -566,6 +567,194 @@ class ComfyCloudSCAIL2CharacterReplacementNode(IO.ComfyNode):
|
||||
return await _run_video_workflow(cls, "video.scail-2-character-replacement.v1", ComfyCloudWorkflowInputs(scene_prompt=scene_prompt, driving_subject=driving_subject, reference_subject=reference_subject, reference_character_url=image_url, driving_video_url=video_url, seed=seed))
|
||||
|
||||
|
||||
_UINT32_MAX = 0xFFFFFFFF
|
||||
_ACE_LANGUAGES = ["ar", "az", "bg", "bn", "ca", "cs", "da", "de", "el", "en", "es", "fa", "fi", "fr", "he", "hi", "hr", "ht", "hu", "id", "is", "it", "ja", "ko", "la", "lt", "ms", "ne", "nl", "no", "pa", "pl", "pt", "ro", "ru", "sa", "sk", "sr", "sv", "sw", "ta", "te", "th", "tl", "tr", "uk", "ur", "vi", "yue", "zh", "unknown"]
|
||||
_ACE_KEYS = [f"{root} {mode}" for mode in ("major", "minor") for root in ("C", "C#", "Db", "D", "D#", "Eb", "E", "F", "F#", "Gb", "G", "G#", "Ab", "A", "A#", "Bb", "B")]
|
||||
_CHATTERBOX_LANGUAGES = ["Arabic (ar)", "Danish (da)", "German (de)", "Greek (el)", "English (en)", "Spanish (es)", "Finnish (fi)", "French (fr)", "Hebrew (he)", "Hindi (hi)", "Italian (it)", "Japanese (ja)", "Korean (ko)", "Malay (ms)", "Dutch (nl)", "Norwegian (no)", "Polish (pl)", "Portuguese (pt)", "Russian (ru)", "Swedish (sv)", "Swahili (sw)", "Turkish (tr)", "Chinese (zh)"]
|
||||
|
||||
|
||||
def _audio_schema(node_id: str, display_name: str, inputs: list[IO.Input], outputs: list[IO.Output] | None = None) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id=node_id,
|
||||
display_name=display_name,
|
||||
category="partner/audio/Comfy Cloud",
|
||||
inputs=inputs,
|
||||
outputs=outputs or [IO.Audio.Output()],
|
||||
hidden=[IO.Hidden.auth_token_comfy_org, IO.Hidden.api_key_comfy_org, IO.Hidden.unique_id],
|
||||
is_api_node=True,
|
||||
)
|
||||
|
||||
|
||||
def _audio_duration(audio: Input.Audio) -> float:
|
||||
return audio["waveform"].shape[-1] / audio["sample_rate"]
|
||||
|
||||
|
||||
def _validate_audio_duration(name: str, audio: Input.Audio, minimum: float, maximum: float) -> None:
|
||||
duration = _audio_duration(audio)
|
||||
if duration < minimum or duration > maximum:
|
||||
raise ValueError(f"{name} duration must be between {minimum:g} and {maximum:g} seconds.")
|
||||
|
||||
|
||||
async def _audio_asset(cls: type[IO.ComfyNode], name: str, audio: Input.Audio) -> dict[str, ComfyCloudAssetInput]:
|
||||
return {name: ComfyCloudAssetInput(type="AUDIO", url=await upload_audio_to_comfyapi(cls, audio))}
|
||||
|
||||
|
||||
async def _run_audio_workflow(cls: type[IO.ComfyNode], workflow: ComfyCloudWorkflow, inputs: ComfyCloudWorkflowInputs, output_names: tuple[str, ...] = ()) -> IO.NodeOutput:
|
||||
task = await sync_op(cls, _GENERATE_ENDPOINT, response_model=ComfyCloudGenerateResponse, data=ComfyCloudGenerateRequest(workflow=workflow, inputs=inputs))
|
||||
result = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=task.polling_url),
|
||||
response_model=ComfyCloudStatusResponse,
|
||||
status_extractor=lambda response: response.status,
|
||||
progress_extractor=lambda response: response.progress,
|
||||
cancel_endpoint=ApiEndpoint(path=task.cancel_url, method="POST"),
|
||||
)
|
||||
if output_names:
|
||||
if not result.output_urls or any(not result.output_urls.get(name) for name in output_names):
|
||||
detail = f": {result.error}" if result.error else ""
|
||||
raise RuntimeError(f"Comfy Cloud task {result.task_id} completed without all named output URLs{detail}")
|
||||
outputs = [await download_url_to_audio_input(result.output_urls[name], cls=cls) for name in output_names]
|
||||
return IO.NodeOutput(*outputs)
|
||||
if not result.output_url:
|
||||
detail = f": {result.error}" if result.error else ""
|
||||
raise RuntimeError(f"Comfy Cloud task {result.task_id} completed without an output URL{detail}")
|
||||
return IO.NodeOutput(await download_url_to_audio_input(result.output_url, cls=cls))
|
||||
|
||||
|
||||
class ComfyCloudACEStep15XLTurboNode(IO.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return _audio_schema(
|
||||
"ComfyCloudACEStep15XLTurboNode",
|
||||
"ACE-Step 1.5 XL Turbo",
|
||||
[
|
||||
_prompt_input("style_prompt"),
|
||||
IO.String.Input("lyrics", multiline=True, default=""),
|
||||
IO.Float.Input("duration_seconds", default=120, min=10, max=300, step=0.1),
|
||||
_seed_input(),
|
||||
IO.Int.Input("bpm", default=120, min=10, max=300),
|
||||
IO.Combo.Input("time_signature", options=["2", "3", "4", "6"], default="4"),
|
||||
IO.Combo.Input("language", options=_ACE_LANGUAGES, default="en"),
|
||||
IO.Combo.Input("key", options=_ACE_KEYS, default="E minor"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, style_prompt: str, lyrics: str, duration_seconds: float, seed: int, bpm: int, time_signature: str, language: str, key: str) -> IO.NodeOutput:
|
||||
validate_string(style_prompt, min_length=1, max_length=4096, field_name="style_prompt")
|
||||
validate_string(lyrics, min_length=0, max_length=20000, field_name="lyrics")
|
||||
return await _run_audio_workflow(cls, "audio.ace-step-1-5-xl-turbo.v1", ComfyCloudWorkflowInputs(style_prompt=style_prompt, lyrics=lyrics, duration_seconds=duration_seconds, seed=seed, bpm=bpm, time_signature=time_signature, language=language, key=key))
|
||||
|
||||
|
||||
class ComfyCloudStableAudio3MediumNode(IO.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return _audio_schema(
|
||||
"ComfyCloudStableAudio3MediumNode",
|
||||
"Stable Audio 3 Medium",
|
||||
[
|
||||
_prompt_input(),
|
||||
IO.Float.Input("duration_seconds", default=30, min=1, max=300, step=0.1),
|
||||
_seed_input(),
|
||||
IO.Boolean.Input("expand_prompt", default=True),
|
||||
IO.Combo.Input("category", options=["Music", "Instrument", "SFX", "One-shot"], default="Music"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, prompt: str, duration_seconds: float, seed: int, expand_prompt: bool, category: str) -> IO.NodeOutput:
|
||||
validate_string(prompt, min_length=1, max_length=4096)
|
||||
return await _run_audio_workflow(cls, "audio.stable-audio-3-medium.v1", ComfyCloudWorkflowInputs(prompt=prompt, duration_seconds=duration_seconds, seed=seed, expand_prompt=expand_prompt, category=category))
|
||||
|
||||
|
||||
class ComfyCloudChatterboxMultilingualVoiceCloneNode(IO.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return _audio_schema(
|
||||
"ComfyCloudChatterboxMultilingualVoiceCloneNode",
|
||||
"Chatterbox Multilingual Voice Clone",
|
||||
[
|
||||
_prompt_input("text"), IO.Audio.Input("voice_reference"),
|
||||
IO.Combo.Input("language", options=_CHATTERBOX_LANGUAGES, default="English (en)"),
|
||||
IO.Float.Input("exaggeration", default=0.5, min=0, max=2, step=0.05),
|
||||
IO.Float.Input("cfg_weight", default=0.5, min=0, max=1, step=0.05),
|
||||
IO.Float.Input("temperature", default=0.8, min=0.05, max=2, step=0.05),
|
||||
IO.Int.Input("seed", default=0, min=0, max=_UINT32_MAX, control_after_generate=True),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, text: str, voice_reference: Input.Audio, language: str, exaggeration: float, cfg_weight: float, temperature: float, seed: int) -> IO.NodeOutput:
|
||||
validate_string(text, min_length=1, max_length=5000, field_name="text")
|
||||
_validate_audio_duration("Voice reference", voice_reference, 1, 30)
|
||||
return await _run_audio_workflow(cls, "audio.chatterbox-multilingual-voice-clone.v1", ComfyCloudWorkflowInputs(text=text, assets=await _audio_asset(cls, "voice_reference", voice_reference), language=language, exaggeration=exaggeration, cfg_weight=cfg_weight, temperature=temperature, seed=seed))
|
||||
|
||||
|
||||
class ComfyCloudChatterboxDialogueNode(IO.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return _audio_schema(
|
||||
"ComfyCloudChatterboxDialogueNode",
|
||||
"Chatterbox Dialogue",
|
||||
[
|
||||
_prompt_input("script"), IO.Audio.Input("speaker_a_reference"), IO.Audio.Input("speaker_b_reference"),
|
||||
IO.Float.Input("exaggeration", default=0.5, min=0.25, max=2, step=0.05),
|
||||
IO.Float.Input("cfg_weight", default=0.5, min=0.2, max=1, step=0.05),
|
||||
IO.Float.Input("temperature", default=0.8, min=0.05, max=5, step=0.05),
|
||||
IO.Int.Input("seed", default=0, min=0, max=_UINT32_MAX, control_after_generate=True),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, script: str, speaker_a_reference: Input.Audio, speaker_b_reference: Input.Audio, exaggeration: float, cfg_weight: float, temperature: float, seed: int) -> IO.NodeOutput:
|
||||
validate_string(script, min_length=1, max_length=10000, field_name="script")
|
||||
if any(line.strip() and not line.strip().startswith(("SPEAKER A:", "SPEAKER B:", "SPEAKER C:", "SPEAKER D:")) for line in script.splitlines()):
|
||||
raise ValueError("Every nonblank utterance must start with SPEAKER A: through SPEAKER D:.")
|
||||
_validate_audio_duration("Speaker A reference", speaker_a_reference, 1, 30)
|
||||
_validate_audio_duration("Speaker B reference", speaker_b_reference, 1, 30)
|
||||
assets = {
|
||||
"speaker_a_reference": ComfyCloudAssetInput(type="AUDIO", url=await upload_audio_to_comfyapi(cls, speaker_a_reference)),
|
||||
"speaker_b_reference": ComfyCloudAssetInput(type="AUDIO", url=await upload_audio_to_comfyapi(cls, speaker_b_reference)),
|
||||
}
|
||||
return await _run_audio_workflow(cls, "audio.chatterbox-dialogue.v1", ComfyCloudWorkflowInputs(script=script, assets=assets, exaggeration=exaggeration, cfg_weight=cfg_weight, temperature=temperature, seed=seed))
|
||||
|
||||
|
||||
class ComfyCloudChatterboxVoiceConversionNode(IO.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return _audio_schema(
|
||||
"ComfyCloudChatterboxVoiceConversionNode",
|
||||
"Chatterbox Voice Conversion",
|
||||
[IO.Audio.Input("source_audio"), IO.Audio.Input("target_voice_reference"), IO.Int.Input("seed", default=0, min=0, max=_UINT32_MAX, control_after_generate=True)],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, source_audio: Input.Audio, target_voice_reference: Input.Audio, seed: int) -> IO.NodeOutput:
|
||||
_validate_audio_duration("Source audio", source_audio, 0.5, 300)
|
||||
_validate_audio_duration("Target voice reference", target_voice_reference, 1, 30)
|
||||
assets = {
|
||||
"source_audio": ComfyCloudAssetInput(type="AUDIO", url=await upload_audio_to_comfyapi(cls, source_audio)),
|
||||
"target_voice_reference": ComfyCloudAssetInput(type="AUDIO", url=await upload_audio_to_comfyapi(cls, target_voice_reference)),
|
||||
}
|
||||
return await _run_audio_workflow(cls, "audio.chatterbox-voice-conversion.v1", ComfyCloudWorkflowInputs(assets=assets, seed=seed))
|
||||
|
||||
|
||||
class ComfyCloudMelBandRoFormerStemSeparationNode(IO.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return _audio_schema(
|
||||
"ComfyCloudMelBandRoFormerStemSeparationNode",
|
||||
"MelBandRoFormer Stem Separation",
|
||||
[IO.Audio.Input("audio")],
|
||||
[IO.Audio.Output("vocals"), IO.Audio.Output("instruments")],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, audio: Input.Audio) -> IO.NodeOutput:
|
||||
_validate_audio_duration("Audio", audio, 0.5, 600)
|
||||
return await _run_audio_workflow(cls, "audio.melbandroformer-stem-separation.v1", ComfyCloudWorkflowInputs(assets=await _audio_asset(cls, "audio", audio)), ("vocals", "instruments"))
|
||||
|
||||
|
||||
class ComfyCloudExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||
@@ -586,6 +775,12 @@ class ComfyCloudExtension(ComfyExtension):
|
||||
ComfyCloudLTX23FirstLastFrameNode,
|
||||
ComfyCloudWan22FirstLastFrameNode,
|
||||
ComfyCloudSCAIL2CharacterReplacementNode,
|
||||
ComfyCloudACEStep15XLTurboNode,
|
||||
ComfyCloudStableAudio3MediumNode,
|
||||
ComfyCloudChatterboxMultilingualVoiceCloneNode,
|
||||
ComfyCloudChatterboxDialogueNode,
|
||||
ComfyCloudChatterboxVoiceConversionNode,
|
||||
ComfyCloudMelBandRoFormerStemSeparationNode,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -336,6 +336,104 @@ def test_download_cloud_audio_url_to_audio_input(monkeypatch):
|
||||
audio_decode.assert_called_once_with(downloaded)
|
||||
|
||||
|
||||
AUDIO_POC_NODES = [
|
||||
(nodes_comfy_cloud.ComfyCloudACEStep15XLTurboNode, "audio.ace-step-1-5-xl-turbo.v1", ["style_prompt", "lyrics", "duration_seconds", "seed", "bpm", "time_signature", "language", "key"]),
|
||||
(nodes_comfy_cloud.ComfyCloudStableAudio3MediumNode, "audio.stable-audio-3-medium.v1", ["prompt", "duration_seconds", "seed", "expand_prompt", "category"]),
|
||||
(nodes_comfy_cloud.ComfyCloudChatterboxMultilingualVoiceCloneNode, "audio.chatterbox-multilingual-voice-clone.v1", ["text", "voice_reference", "language", "exaggeration", "cfg_weight", "temperature", "seed"]),
|
||||
(nodes_comfy_cloud.ComfyCloudChatterboxDialogueNode, "audio.chatterbox-dialogue.v1", ["script", "speaker_a_reference", "speaker_b_reference", "exaggeration", "cfg_weight", "temperature", "seed"]),
|
||||
(nodes_comfy_cloud.ComfyCloudChatterboxVoiceConversionNode, "audio.chatterbox-voice-conversion.v1", ["source_audio", "target_voice_reference", "seed"]),
|
||||
(nodes_comfy_cloud.ComfyCloudMelBandRoFormerStemSeparationNode, "audio.melbandroformer-stem-separation.v1", ["audio"]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("node", "workflow", "input_names"), AUDIO_POC_NODES)
|
||||
def test_audio_poc_schemas_and_registration(node, workflow, input_names):
|
||||
schema = node.define_schema()
|
||||
registered = asyncio.run(nodes_comfy_cloud.ComfyCloudExtension().get_node_list())
|
||||
|
||||
assert schema.is_api_node
|
||||
assert schema.category == "partner/audio/Comfy Cloud"
|
||||
assert [input.id for input in schema.inputs] == input_names
|
||||
assert all(output.get_io_type() == "AUDIO" for output in schema.outputs)
|
||||
assert workflow in get_args(ComfyCloudWorkflow)
|
||||
assert node in registered
|
||||
|
||||
|
||||
def test_audio_poc_schema_defaults_ranges_and_enums():
|
||||
schemas = {workflow: {input.id: input for input in node.define_schema().inputs} for node, workflow, _ in AUDIO_POC_NODES}
|
||||
ace = schemas["audio.ace-step-1-5-xl-turbo.v1"]
|
||||
assert (ace["duration_seconds"].default, ace["duration_seconds"].min, ace["duration_seconds"].max, ace["duration_seconds"].step) == (120, 10, 300, 0.1)
|
||||
assert (ace["bpm"].default, ace["bpm"].min, ace["bpm"].max) == (120, 10, 300)
|
||||
assert ace["time_signature"].options == ["2", "3", "4", "6"]
|
||||
assert ace["language"].default == "en"
|
||||
assert ace["key"].default == "E minor"
|
||||
|
||||
stable = schemas["audio.stable-audio-3-medium.v1"]
|
||||
assert stable["category"].options == ["Music", "Instrument", "SFX", "One-shot"]
|
||||
assert stable["expand_prompt"].default is True
|
||||
for workflow in [
|
||||
"audio.chatterbox-multilingual-voice-clone.v1",
|
||||
"audio.chatterbox-dialogue.v1",
|
||||
"audio.chatterbox-voice-conversion.v1",
|
||||
]:
|
||||
assert schemas[workflow]["seed"].max == 0xFFFFFFFF
|
||||
|
||||
mel_schema = nodes_comfy_cloud.ComfyCloudMelBandRoFormerStemSeparationNode.define_schema()
|
||||
assert [output.id for output in mel_schema.outputs] == ["vocals", "instruments"]
|
||||
|
||||
|
||||
def test_audio_poc_request_mapping_and_named_result_decoding(monkeypatch):
|
||||
sync = AsyncMock(return_value=ComfyCloudGenerateResponse(task_id="task-audio", status="queued", polling_url="/tasks/task-audio", cancel_url="/tasks/task-audio/cancel"))
|
||||
poll = AsyncMock(return_value=ComfyCloudStatusResponse(task_id="task-audio", status="completed", output_urls={"vocals": "/vocals.mp3", "instruments": "/instruments.mp3"}))
|
||||
upload = AsyncMock(return_value="/uploads/song.m4a")
|
||||
download = AsyncMock(side_effect=["vocals-audio", "instruments-audio"])
|
||||
monkeypatch.setattr(nodes_comfy_cloud, "sync_op", sync)
|
||||
monkeypatch.setattr(nodes_comfy_cloud, "poll_op", poll)
|
||||
monkeypatch.setattr(nodes_comfy_cloud, "upload_audio_to_comfyapi", upload)
|
||||
monkeypatch.setattr(nodes_comfy_cloud, "download_url_to_audio_input", download)
|
||||
audio = {"waveform": torch.zeros(1, 2, 48000), "sample_rate": 48000}
|
||||
|
||||
output = asyncio.run(nodes_comfy_cloud.ComfyCloudMelBandRoFormerStemSeparationNode.execute(audio))
|
||||
|
||||
request = sync.call_args.kwargs["data"]
|
||||
assert request.workflow == "audio.melbandroformer-stem-separation.v1"
|
||||
assert request.inputs.model_dump(exclude_none=True) == {"assets": {"audio": {"type": "AUDIO", "url": "/uploads/song.m4a"}}}
|
||||
assert [call.args[0] for call in download.await_args_list] == ["/vocals.mp3", "/instruments.mp3"]
|
||||
assert poll.call_args.kwargs["cancel_endpoint"].path == "/tasks/task-audio/cancel"
|
||||
assert tuple(output) == ("vocals-audio", "instruments-audio")
|
||||
|
||||
|
||||
def test_chatterbox_audio_inputs_use_named_staged_assets(monkeypatch):
|
||||
run = AsyncMock(return_value=("audio-output",))
|
||||
upload = AsyncMock(side_effect=["/uploads/source.m4a", "/uploads/target.m4a"])
|
||||
monkeypatch.setattr(nodes_comfy_cloud, "_run_audio_workflow", run)
|
||||
monkeypatch.setattr(nodes_comfy_cloud, "upload_audio_to_comfyapi", upload)
|
||||
source = {"waveform": torch.zeros(1, 1, 48000), "sample_rate": 48000}
|
||||
target = {"waveform": torch.zeros(1, 1, 96000), "sample_rate": 48000}
|
||||
|
||||
asyncio.run(nodes_comfy_cloud.ComfyCloudChatterboxVoiceConversionNode.execute(source, target, 7))
|
||||
|
||||
inputs = run.call_args.args[2]
|
||||
assert inputs.model_dump(exclude_none=True) == {
|
||||
"assets": {
|
||||
"source_audio": {"type": "AUDIO", "url": "/uploads/source.m4a"},
|
||||
"target_voice_reference": {"type": "AUDIO", "url": "/uploads/target.m4a"},
|
||||
},
|
||||
"seed": 7,
|
||||
}
|
||||
assert "audio_url" not in inputs.model_dump(exclude_none=True)
|
||||
|
||||
|
||||
def test_chatterbox_dialogue_rejects_invalid_speaker_labels(monkeypatch):
|
||||
upload = AsyncMock()
|
||||
monkeypatch.setattr(nodes_comfy_cloud, "upload_audio_to_comfyapi", upload)
|
||||
audio = {"waveform": torch.zeros(1, 1, 48000), "sample_rate": 48000}
|
||||
|
||||
with pytest.raises(ValueError, match="Every nonblank utterance"):
|
||||
asyncio.run(nodes_comfy_cloud.ComfyCloudChatterboxDialogueNode.execute("NARRATOR: Hello", audio, audio, 0.5, 0.5, 0.8, 0))
|
||||
upload.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("file_format", "expected_format"), [(".GLB", "glb"), ("SPZ", "spz")])
|
||||
def test_download_cloud_3d_url_to_file_3d(monkeypatch, file_format, expected_format):
|
||||
node = nodes_comfy_cloud.ComfyCloudTextToImageNode
|
||||
|
||||
Reference in New Issue
Block a user