diff --git a/nodes/gen_video_from_text_node.py b/nodes/gen_video_from_text_node.py index 3734cbb..70e2f05 100644 --- a/nodes/gen_video_from_text_node.py +++ b/nodes/gen_video_from_text_node.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict, Iterable -from comfy_api.latest import InputImpl, _io, io +from comfy_api.latest import InputImpl, io from ..nodes.catalog_utils import video_model_specs from ..nodes.utils import encode_tensor_for_vision, ensure_prompt_length @@ -16,9 +16,20 @@ LOG = logging.getLogger(__name__) class GenerateVideoFromText(io.ComfyNode): - _DEFAULT_DURATIONS = ["4s", "5s", "6s", "8s", "10s", "12s", "14s", "15s", "16s", "18s", "20s"] - _DEFAULT_ASPECT_RATIOS = ["16:9", "9:16", "1:1"] - _DEFAULT_RESOLUTIONS = ["1080p", "720p", "480p"] + + @staticmethod + def _option_input_id(model_id: str, field: str) -> str: + sanitized = "".join(ch if ch.isalnum() or ch in {"_", "-"} else "_" for ch in model_id) + return f"{sanitized}__{field}" + + @staticmethod + def _constraint_values(value: Iterable | None) -> list[str]: + if not value: + return [] + if isinstance(value, (str, bytes)): + normalized = str(value).strip() + return [normalized] if normalized else [] + return [str(item) for item in value if item] @classmethod def _video_specs(cls) -> Dict[str, Dict[str, Any]]: @@ -29,11 +40,6 @@ class GenerateVideoFromText(io.ComfyNode): ) return specs - @staticmethod - def _option_input_id(model_id: str, field: str) -> str: - sanitized = "".join(ch if ch.isalnum() or ch in {"_", "-"} else "_" for ch in model_id) - return f"{sanitized}__{field}" - @classmethod def _get_option_value(cls, model_payload: Dict[str, Any], model_id: str, field: str) -> Any: candidates = ( @@ -48,20 +54,31 @@ class GenerateVideoFromText(io.ComfyNode): return None @classmethod - def _first_or_default(cls, values: Iterable[str] | None, fallback: list[str]) -> list[str]: - items = list(values) if values else [] - return items or list(fallback) - - @classmethod - def _build_model_options(cls) -> list[_io.DynamicCombo.Option]: + def _build_model_options(cls) -> list[io.DynamicCombo.Option]: specs = cls._video_specs() - options: list[_io.DynamicCombo.Option] = [] + options: list[io.DynamicCombo.Option] = [] - for model_id, spec in sorted(specs.items()): + def _sorted_models_by_group(group: str) -> list[tuple[str, Dict[str, Any]]]: + return sorted( + ( + (model_id, spec) + for model_id, spec in specs.items() + if spec.get("constraints", {}).get("model_type") == group + ), + key=lambda item: item[0], + ) + + ordered_specs = [ + # didnt know about this, is same as [] + [] + *_sorted_models_by_group("text-to-video"), + *_sorted_models_by_group("image-to-video"), + ] + + for model_id, spec in ordered_specs: constraints = spec.get("constraints") or {} - aspect_ratios = cls._first_or_default(constraints.get("aspect_ratios"), cls._DEFAULT_ASPECT_RATIOS) - resolutions = cls._first_or_default(constraints.get("resolutions"), cls._DEFAULT_RESOLUTIONS) - durations = cls._first_or_default(constraints.get("durations"), cls._DEFAULT_DURATIONS) + aspect_ratios = cls._constraint_values(constraints.get("aspect_ratios")) + resolutions = cls._constraint_values(constraints.get("resolutions")) + durations = cls._constraint_values(constraints.get("durations")) audio_default = bool(constraints.get("audio")) if constraints.get("audio") is not None else False option_inputs: list[io.Input] = [] @@ -74,31 +91,36 @@ class GenerateVideoFromText(io.ComfyNode): ) ) - option_inputs.extend( - [ + if durations: + option_inputs.append( io.Combo.Input( id=cls._option_input_id(model_id, "duration"), - display_name="durationaa", + display_name="duration", options=durations, default=durations[0], tooltip="Duration allowed by the selected model", - ), + ) + ) + if aspect_ratios: + option_inputs.append( io.Combo.Input( cls._option_input_id(model_id, "aspect_ratio"), display_name="aspect_ratio", options=aspect_ratios, default=aspect_ratios[0], tooltip="Aspect ratios allowed by the selected model", - ), + ) + ) + if resolutions: + option_inputs.append( io.Combo.Input( cls._option_input_id(model_id, "resolution"), display_name="resolution", options=resolutions, default=resolutions[0], tooltip="Resolutions allowed by the selected model", - ), - ] - ) + ) + ) if constraints.get("audio_configurable"): option_inputs.append( @@ -110,7 +132,7 @@ class GenerateVideoFromText(io.ComfyNode): ) ) - options.append(_io.DynamicCombo.Option(model_id, option_inputs)) + options.append(io.DynamicCombo.Option(model_id, option_inputs)) return options @@ -127,7 +149,7 @@ class GenerateVideoFromText(io.ComfyNode): display_name="Generate Video from Text (Venice)", category="venice.ai", inputs=[ - _io.DynamicCombo.Input( + io.DynamicCombo.Input( "model", options=model_options, tooltip="Select a Venice video model to auto-populate valid parameters", @@ -185,20 +207,30 @@ class GenerateVideoFromText(io.ComfyNode): raise ValueError("Selected model is missing from the Venice catalog; refresh the catalog and try again.") constraints = spec.get("constraints") or {} - durations = cls._first_or_default(constraints.get("durations"), cls._DEFAULT_DURATIONS) - aspect_ratios = cls._first_or_default(constraints.get("aspect_ratios"), cls._DEFAULT_ASPECT_RATIOS) - resolutions = cls._first_or_default(constraints.get("resolutions"), cls._DEFAULT_RESOLUTIONS) + durations = cls._constraint_values(constraints.get("durations")) + aspect_ratios = cls._constraint_values(constraints.get("aspect_ratios")) + resolutions = cls._constraint_values(constraints.get("resolutions")) - duration = cls._get_option_value(model, model_id, "duration") or durations[0] - aspect_ratio = cls._get_option_value(model, model_id, "aspect_ratio") or aspect_ratios[0] - resolution = cls._get_option_value(model, model_id, "resolution") or resolutions[0] + duration = cls._get_option_value(model, model_id, "duration") + if durations: + if duration is None: + raise ValueError(f"Model {model_id} requires a duration selection") + if duration not in durations: + raise ValueError(f"Duration '{duration}' is not supported by model {model_id}") - if duration not in durations: - raise ValueError(f"Duration '{duration}' is not supported by model {model_id}") - if aspect_ratio not in aspect_ratios: - raise ValueError(f"Aspect ratio '{aspect_ratio}' is not supported by model {model_id}") - if resolution not in resolutions: - raise ValueError(f"Resolution '{resolution}' is not supported by model {model_id}") + aspect_ratio = cls._get_option_value(model, model_id, "aspect_ratio") + if aspect_ratios: + if aspect_ratio is None: + raise ValueError(f"Model {model_id} requires an aspect ratio selection") + if aspect_ratio not in aspect_ratios: + raise ValueError(f"Aspect ratio '{aspect_ratio}' is not supported by model {model_id}") + + resolution = cls._get_option_value(model, model_id, "resolution") + if resolutions: + if resolution is None: + raise ValueError(f"Model {model_id} requires a resolution selection") + if resolution not in resolutions: + raise ValueError(f"Resolution '{resolution}' is not supported by model {model_id}") audio_configurable = bool(constraints.get("audio_configurable")) audio_default = bool(constraints.get("audio")) if constraints.get("audio") is not None else False @@ -209,11 +241,15 @@ class GenerateVideoFromText(io.ComfyNode): "model": model_id, "prompt": prompt, "negative_prompt": negative_prompt, - "duration": duration, - "aspect_ratio": aspect_ratio, - "resolution": resolution, - "audio": audio, } + if durations: + payload["duration"] = duration + if aspect_ratios: + payload["aspect_ratio"] = aspect_ratio + if resolutions: + payload["resolution"] = resolution + if audio is not None: + payload["audio"] = audio if constraints.get("model_type") == "image-to-video": image = cls._get_option_value(model, model_id, "image")