|
50 | 50 | ) |
51 | 51 | from comfy_api_nodes.apis.kling import ( |
52 | 52 | ImageToVideoWithAudioRequest, |
| 53 | + KlingAvatarRequest, |
53 | 54 | MotionControlRequest, |
54 | 55 | MultiPromptEntry, |
55 | 56 | OmniImageParamImage, |
|
74 | 75 | upload_image_to_comfyapi, |
75 | 76 | upload_images_to_comfyapi, |
76 | 77 | upload_video_to_comfyapi, |
| 78 | + validate_audio_duration, |
77 | 79 | validate_image_aspect_ratio, |
78 | 80 | validate_image_dimensions, |
79 | 81 | validate_string, |
@@ -3139,6 +3141,103 @@ async def execute( |
3139 | 3141 | return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url)) |
3140 | 3142 |
|
3141 | 3143 |
|
| 3144 | +class KlingAvatarNode(IO.ComfyNode): |
| 3145 | + |
| 3146 | + @classmethod |
| 3147 | + def define_schema(cls) -> IO.Schema: |
| 3148 | + return IO.Schema( |
| 3149 | + node_id="KlingAvatarNode", |
| 3150 | + display_name="Kling Avatar 2.0", |
| 3151 | + category="api node/video/Kling", |
| 3152 | + description="Generate broadcast-style digital human videos from a single photo and an audio file.", |
| 3153 | + inputs=[ |
| 3154 | + IO.Image.Input( |
| 3155 | + "image", |
| 3156 | + tooltip="Avatar reference image. " |
| 3157 | + "Width and height must be at least 300px. Aspect ratio must be between 1:2.5 and 2.5:1.", |
| 3158 | + ), |
| 3159 | + IO.Audio.Input( |
| 3160 | + "sound_file", |
| 3161 | + tooltip="Audio input. Must be between 2 and 300 seconds in duration.", |
| 3162 | + ), |
| 3163 | + IO.Combo.Input("mode", options=["std", "pro"]), |
| 3164 | + IO.String.Input( |
| 3165 | + "prompt", |
| 3166 | + multiline=True, |
| 3167 | + default="", |
| 3168 | + optional=True, |
| 3169 | + tooltip="Optional prompt to define avatar actions, emotions, and camera movements.", |
| 3170 | + ), |
| 3171 | + IO.Int.Input( |
| 3172 | + "seed", |
| 3173 | + default=0, |
| 3174 | + min=0, |
| 3175 | + max=2147483647, |
| 3176 | + display_mode=IO.NumberDisplay.number, |
| 3177 | + control_after_generate=True, |
| 3178 | + tooltip="Seed controls whether the node should re-run; " |
| 3179 | + "results are non-deterministic regardless of seed.", |
| 3180 | + ), |
| 3181 | + ], |
| 3182 | + outputs=[ |
| 3183 | + IO.Video.Output(), |
| 3184 | + ], |
| 3185 | + hidden=[ |
| 3186 | + IO.Hidden.auth_token_comfy_org, |
| 3187 | + IO.Hidden.api_key_comfy_org, |
| 3188 | + IO.Hidden.unique_id, |
| 3189 | + ], |
| 3190 | + is_api_node=True, |
| 3191 | + price_badge=IO.PriceBadge( |
| 3192 | + depends_on=IO.PriceBadgeDepends(widgets=["mode"]), |
| 3193 | + expr=""" |
| 3194 | + ( |
| 3195 | + $prices := {"std": 0.056, "pro": 0.112}; |
| 3196 | + {"type":"usd","usd": $lookup($prices, widgets.mode), "format":{"suffix":"/second"}} |
| 3197 | + ) |
| 3198 | + """, |
| 3199 | + ), |
| 3200 | + ) |
| 3201 | + |
| 3202 | + @classmethod |
| 3203 | + async def execute( |
| 3204 | + cls, |
| 3205 | + image: Input.Image, |
| 3206 | + sound_file: Input.Audio, |
| 3207 | + mode: str, |
| 3208 | + seed: int, |
| 3209 | + prompt: str = "", |
| 3210 | + ) -> IO.NodeOutput: |
| 3211 | + validate_image_dimensions(image, min_width=300, min_height=300) |
| 3212 | + validate_image_aspect_ratio(image, (1, 2.5), (2.5, 1)) |
| 3213 | + validate_audio_duration(sound_file, min_duration=2, max_duration=300) |
| 3214 | + response = await sync_op( |
| 3215 | + cls, |
| 3216 | + ApiEndpoint(path="/proxy/kling/v1/videos/avatar/image2video", method="POST"), |
| 3217 | + response_model=TaskStatusResponse, |
| 3218 | + data=KlingAvatarRequest( |
| 3219 | + image=await upload_image_to_comfyapi(cls, image), |
| 3220 | + sound_file=await upload_audio_to_comfyapi( |
| 3221 | + cls, sound_file, container_format="mp3", codec_name="libmp3lame", mime_type="audio/mpeg" |
| 3222 | + ), |
| 3223 | + prompt=prompt or None, |
| 3224 | + mode=mode, |
| 3225 | + ), |
| 3226 | + ) |
| 3227 | + if response.code: |
| 3228 | + raise RuntimeError( |
| 3229 | + f"Kling request failed. Code: {response.code}, Message: {response.message}, Data: {response.data}" |
| 3230 | + ) |
| 3231 | + final_response = await poll_op( |
| 3232 | + cls, |
| 3233 | + ApiEndpoint(path=f"/proxy/kling/v1/videos/avatar/image2video/{response.data.task_id}"), |
| 3234 | + response_model=TaskStatusResponse, |
| 3235 | + status_extractor=lambda r: (r.data.task_status if r.data else None), |
| 3236 | + max_poll_attempts=800, |
| 3237 | + ) |
| 3238 | + return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url)) |
| 3239 | + |
| 3240 | + |
3142 | 3241 | class KlingExtension(ComfyExtension): |
3143 | 3242 | @override |
3144 | 3243 | async def get_node_list(self) -> list[type[IO.ComfyNode]]: |
@@ -3167,6 +3266,7 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]: |
3167 | 3266 | MotionControl, |
3168 | 3267 | KlingVideoNode, |
3169 | 3268 | KlingFirstLastFrameNode, |
| 3269 | + KlingAvatarNode, |
3170 | 3270 | ] |
3171 | 3271 |
|
3172 | 3272 |
|
|
0 commit comments