Skip to content

Commit 4fd375f

Browse files
committed
impl lip sync, advanced lip sync, and face detection nodes
1 parent 68e35b7 commit 4fd375f

8 files changed

Lines changed: 1284 additions & 2 deletions

File tree

griptape_nodes_library.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,39 @@
847847
"group": "create"
848848
}
849849
},
850+
{
851+
"class_name": "KlingIdentifyFace",
852+
"file_path": "griptape_nodes_library/video/kling_identify_face.py",
853+
"metadata": {
854+
"category": "video",
855+
"description": "Identify candidate faces in a video for downstream Kling advanced lip sync via Griptape Cloud model proxy.",
856+
"display_name": "Kling Identify Face",
857+
"icon": "ScanFace",
858+
"group": "describe"
859+
}
860+
},
861+
{
862+
"class_name": "KlingAdvancedLipSync",
863+
"file_path": "griptape_nodes_library/video/kling_advanced_lip_sync.py",
864+
"metadata": {
865+
"category": "video",
866+
"description": "Create an advanced lip-sync animation from a selected Kling face-identification result via Griptape Cloud model proxy.",
867+
"display_name": "Kling Advanced Lip Sync",
868+
"icon": "Sparkles",
869+
"group": "create"
870+
}
871+
},
872+
{
873+
"class_name": "KlingLipSync",
874+
"file_path": "griptape_nodes_library/video/kling_lip_sync.py",
875+
"metadata": {
876+
"category": "video",
877+
"description": "Create a simple one-step Kling lip-sync video via Griptape Cloud model proxy.",
878+
"display_name": "Kling Lip Sync",
879+
"icon": "Sparkles",
880+
"group": "create"
881+
}
882+
},
850883
{
851884
"class_name": "LTXTextToVideoGeneration",
852885
"file_path": "griptape_nodes_library/video/ltx_text_to_video_generation.py",

griptape_nodes_library/video/kling_advanced_lip_sync.py

Lines changed: 371 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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

Comments
 (0)