|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import logging |
| 4 | +from typing import Any |
| 5 | + |
| 6 | +from griptape_nodes.exe_types.core_types import Parameter, ParameterMode |
| 7 | +from griptape_nodes.exe_types.param_components.artifact_url.public_artifact_url_parameter import ( |
| 8 | + PublicArtifactUrlParameter, |
| 9 | +) |
| 10 | +from griptape_nodes.exe_types.param_types.parameter_dict import ParameterDict |
| 11 | +from griptape_nodes.exe_types.param_types.parameter_int import ParameterInt |
| 12 | +from griptape_nodes.exe_types.param_types.parameter_string import ParameterString |
| 13 | +from griptape_nodes.exe_types.param_types.parameter_video import ParameterVideo |
| 14 | +from griptape_nodes.traits.options import Options |
| 15 | + |
| 16 | +from griptape_nodes_library.griptape_proxy_node import GriptapeProxyNode |
| 17 | + |
| 18 | +logger = logging.getLogger("griptape_nodes") |
| 19 | + |
| 20 | +__all__ = ["KlingIdentifyFace"] |
| 21 | + |
| 22 | + |
| 23 | +class KlingIdentifyFace(GriptapeProxyNode): |
| 24 | + """Identify lip-sync candidate faces in a video using Kling AI via Griptape Cloud model proxy.""" |
| 25 | + |
| 26 | + def __init__(self, **kwargs: Any) -> None: |
| 27 | + super().__init__(**kwargs) |
| 28 | + |
| 29 | + self.add_parameter( |
| 30 | + ParameterString( |
| 31 | + name="video_input_type", |
| 32 | + default_value="video_url", |
| 33 | + tooltip="Choose whether to identify faces from a prior Kling video id or from a video URL/file.", |
| 34 | + allowed_modes={ParameterMode.INPUT, ParameterMode.PROPERTY}, |
| 35 | + traits={Options(choices=["video_url", "video_id"])}, |
| 36 | + ui_options={"display_name": "video input type"}, |
| 37 | + ) |
| 38 | + ) |
| 39 | + self.add_parameter( |
| 40 | + ParameterString( |
| 41 | + name="video_id", |
| 42 | + tooltip="Kling video id to inspect for lip-sync faces.", |
| 43 | + allowed_modes={ParameterMode.INPUT, ParameterMode.PROPERTY}, |
| 44 | + placeholder_text="The Kling AI video id", |
| 45 | + hide=True, |
| 46 | + ) |
| 47 | + ) |
| 48 | + |
| 49 | + self._public_video_url_parameter = PublicArtifactUrlParameter( |
| 50 | + node=self, |
| 51 | + artifact_url_parameter=ParameterVideo( |
| 52 | + name="video_url", |
| 53 | + tooltip="Video URL or uploaded video file for face identification.", |
| 54 | + allowed_modes={ParameterMode.INPUT, ParameterMode.PROPERTY}, |
| 55 | + ui_options={"display_name": "video"}, |
| 56 | + ), |
| 57 | + disclaimer_message="The Kling lip-sync service utilizes this URL to access the video for face identification.", |
| 58 | + ) |
| 59 | + self._public_video_url_parameter.add_input_parameters() |
| 60 | + |
| 61 | + self.add_parameter( |
| 62 | + ParameterInt( |
| 63 | + name="selected_face_index", |
| 64 | + default_value=0, |
| 65 | + tooltip="Zero-based index of the face to expose for downstream advanced lip-sync nodes.", |
| 66 | + allowed_modes={ParameterMode.INPUT, ParameterMode.PROPERTY}, |
| 67 | + ui_options={"display_name": "selected face index"}, |
| 68 | + ) |
| 69 | + ) |
| 70 | + |
| 71 | + self.add_parameter( |
| 72 | + ParameterString( |
| 73 | + name="generation_id", |
| 74 | + tooltip="Griptape Cloud generation id", |
| 75 | + allowed_modes={ParameterMode.OUTPUT}, |
| 76 | + hide=True, |
| 77 | + ) |
| 78 | + ) |
| 79 | + self.add_parameter( |
| 80 | + ParameterDict( |
| 81 | + name="provider_response", |
| 82 | + tooltip="Verbatim response from API", |
| 83 | + allowed_modes={ParameterMode.OUTPUT}, |
| 84 | + hide_property=True, |
| 85 | + hide=True, |
| 86 | + ) |
| 87 | + ) |
| 88 | + self.add_parameter( |
| 89 | + ParameterString( |
| 90 | + name="session_id", |
| 91 | + tooltip="Kling face-identification session id", |
| 92 | + allowed_modes={ParameterMode.OUTPUT}, |
| 93 | + ) |
| 94 | + ) |
| 95 | + self.add_parameter( |
| 96 | + Parameter( |
| 97 | + name="face_data", |
| 98 | + type="list[dict]", |
| 99 | + output_type="list[dict]", |
| 100 | + default_value=[], |
| 101 | + tooltip="Detected face metadata list returned by Kling.", |
| 102 | + allowed_modes={ParameterMode.OUTPUT}, |
| 103 | + hide_property=True, |
| 104 | + ) |
| 105 | + ) |
| 106 | + self.add_parameter( |
| 107 | + ParameterDict( |
| 108 | + name="selected_face", |
| 109 | + tooltip="Selected face metadata for wiring into Kling Advanced Lip Sync.", |
| 110 | + allowed_modes={ParameterMode.OUTPUT}, |
| 111 | + ) |
| 112 | + ) |
| 113 | + self.add_parameter( |
| 114 | + ParameterString( |
| 115 | + name="selected_face_id", |
| 116 | + tooltip="Selected face id", |
| 117 | + allowed_modes={ParameterMode.OUTPUT}, |
| 118 | + ) |
| 119 | + ) |
| 120 | + |
| 121 | + self._create_status_parameters( |
| 122 | + result_details_tooltip="Details about the face-identification result or any errors", |
| 123 | + result_details_placeholder="Face-identification status and details will appear here.", |
| 124 | + parameter_group_initially_collapsed=True, |
| 125 | + ) |
| 126 | + self._update_video_input_visibility(self.get_parameter_value("video_input_type")) |
| 127 | + |
| 128 | + async def aprocess(self) -> None: |
| 129 | + try: |
| 130 | + await super().aprocess() |
| 131 | + finally: |
| 132 | + self._public_video_url_parameter.delete_uploaded_artifact() |
| 133 | + |
| 134 | + def after_value_set(self, parameter: Parameter, value: Any) -> None: |
| 135 | + super().after_value_set(parameter, value) |
| 136 | + |
| 137 | + if parameter.name == "video_input_type": |
| 138 | + self._update_video_input_visibility(value) |
| 139 | + if parameter.name == "selected_face_index": |
| 140 | + self._update_selected_face_output() |
| 141 | + |
| 142 | + def _update_video_input_visibility(self, value: Any) -> None: |
| 143 | + if value == "video_id": |
| 144 | + self.show_parameter_by_name("video_id") |
| 145 | + self.hide_parameter_by_name("video_url") |
| 146 | + self.hide_message_by_name("artifact_url_parameter_message_video_url") |
| 147 | + return |
| 148 | + |
| 149 | + self.hide_parameter_by_name("video_id") |
| 150 | + self.show_parameter_by_name("video_url") |
| 151 | + self.show_message_by_name("artifact_url_parameter_message_video_url") |
| 152 | + |
| 153 | + def _get_api_model_id(self) -> str: |
| 154 | + return "kling:identify-face" |
| 155 | + |
| 156 | + async def _build_payload(self) -> dict[str, Any]: |
| 157 | + video_input_type = self.get_parameter_value("video_input_type") or "video_url" |
| 158 | + |
| 159 | + if video_input_type == "video_id": |
| 160 | + video_id = (self.get_parameter_value("video_id") or "").strip() |
| 161 | + return {"video_id": video_id} |
| 162 | + |
| 163 | + video_url = self._public_video_url_parameter.get_public_url_for_parameter() |
| 164 | + return {"video_url": (video_url or "").strip()} |
| 165 | + |
| 166 | + async def _parse_result(self, result_json: dict[str, Any], generation_id: str) -> None: |
| 167 | + data = result_json.get("data", {}) |
| 168 | + session_id = data.get("session_id") |
| 169 | + face_data = data.get("face_data") |
| 170 | + |
| 171 | + if not session_id: |
| 172 | + self._set_safe_defaults() |
| 173 | + self._set_status_results( |
| 174 | + was_successful=False, |
| 175 | + result_details=f"{self.name} completed but did not return a session_id.", |
| 176 | + ) |
| 177 | + return |
| 178 | + |
| 179 | + if not isinstance(face_data, list): |
| 180 | + self._set_safe_defaults() |
| 181 | + self._set_status_results( |
| 182 | + was_successful=False, |
| 183 | + result_details=f"{self.name} completed but did not return face_data.", |
| 184 | + ) |
| 185 | + return |
| 186 | + |
| 187 | + self.parameter_output_values["session_id"] = session_id |
| 188 | + self.parameter_output_values["face_data"] = face_data |
| 189 | + self._update_selected_face_output() |
| 190 | + |
| 191 | + if not face_data: |
| 192 | + self._set_status_results( |
| 193 | + was_successful=False, |
| 194 | + result_details="Face identification completed successfully, but Kling did not detect any usable faces.", |
| 195 | + ) |
| 196 | + return |
| 197 | + |
| 198 | + selected_face_id = self.parameter_output_values.get("selected_face_id") or "" |
| 199 | + self._set_status_results( |
| 200 | + was_successful=True, |
| 201 | + result_details=( |
| 202 | + f"Face identification completed successfully for generation {generation_id}. " |
| 203 | + f"Detected {len(face_data)} face(s); selected face id: {selected_face_id}." |
| 204 | + ), |
| 205 | + ) |
| 206 | + |
| 207 | + def _update_selected_face_output(self) -> None: |
| 208 | + face_data = self.parameter_output_values.get("face_data") or [] |
| 209 | + if not isinstance(face_data, list) or not face_data: |
| 210 | + self.parameter_output_values["selected_face"] = {} |
| 211 | + self.parameter_output_values["selected_face_id"] = "" |
| 212 | + return |
| 213 | + |
| 214 | + raw_index = self.get_parameter_value("selected_face_index") |
| 215 | + try: |
| 216 | + selected_index = int(raw_index) |
| 217 | + except (TypeError, ValueError): |
| 218 | + selected_index = 0 |
| 219 | + |
| 220 | + if selected_index < 0 or selected_index >= len(face_data): |
| 221 | + self.parameter_output_values["selected_face"] = {} |
| 222 | + self.parameter_output_values["selected_face_id"] = "" |
| 223 | + return |
| 224 | + |
| 225 | + selected_face = face_data[selected_index] |
| 226 | + if not isinstance(selected_face, dict): |
| 227 | + self.parameter_output_values["selected_face"] = {} |
| 228 | + self.parameter_output_values["selected_face_id"] = "" |
| 229 | + return |
| 230 | + |
| 231 | + self.parameter_output_values["selected_face"] = selected_face |
| 232 | + self.parameter_output_values["selected_face_id"] = str( |
| 233 | + selected_face.get("face_id") or "" |
| 234 | + ) |
| 235 | + |
| 236 | + def _set_safe_defaults(self) -> None: |
| 237 | + self.parameter_output_values["session_id"] = "" |
| 238 | + self.parameter_output_values["face_data"] = [] |
| 239 | + self.parameter_output_values["selected_face"] = {} |
| 240 | + self.parameter_output_values["selected_face_id"] = "" |
| 241 | + |
| 242 | + def validate_before_node_run(self) -> list[Exception] | None: |
| 243 | + exceptions = super().validate_before_node_run() or [] |
| 244 | + |
| 245 | + video_input_type = self.get_parameter_value("video_input_type") or "video_url" |
| 246 | + selected_face_index = self.get_parameter_value("selected_face_index") or 0 |
| 247 | + |
| 248 | + if video_input_type == "video_id": |
| 249 | + video_id = (self.get_parameter_value("video_id") or "").strip() |
| 250 | + if not video_id: |
| 251 | + exceptions.append(ValueError(f"{self.name} requires video_id when video input type is video_id.")) |
| 252 | + else: |
| 253 | + video_url = self.get_parameter_value("video_url") |
| 254 | + if not video_url: |
| 255 | + exceptions.append(ValueError(f"{self.name} requires a video file or URL when video input type is video_url.")) |
| 256 | + |
| 257 | + try: |
| 258 | + if int(selected_face_index) < 0: |
| 259 | + exceptions.append(ValueError(f"{self.name} selected_face_index must be 0 or greater.")) |
| 260 | + except (TypeError, ValueError): |
| 261 | + exceptions.append(ValueError(f"{self.name} selected_face_index must be an integer.")) |
| 262 | + |
| 263 | + return exceptions if exceptions else None |
0 commit comments