Skip to content

Commit c4c0f14

Browse files
committed
feat(api nodes): added 4K resolution for Veo models; added Veo 3 Lite model
Signed-off-by: bigcat88 <bigcat88@icloud.com>
1 parent b615af1 commit c4c0f14

1 file changed

Lines changed: 127 additions & 42 deletions

File tree

comfy_api_nodes/nodes_veo2.py

Lines changed: 127 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
AVERAGE_DURATION_VIDEO_GEN = 32
2525
MODELS_MAP = {
2626
"veo-2.0-generate-001": "veo-2.0-generate-001",
27-
"veo-3.1-generate": "veo-3.1-generate-preview",
28-
"veo-3.1-fast-generate": "veo-3.1-fast-generate-preview",
27+
"veo-3.1-generate": "veo-3.1-generate-001",
28+
"veo-3.1-fast-generate": "veo-3.1-fast-generate-001",
29+
"veo-3.1-lite": "veo-3.1-lite-generate-001",
2930
"veo-3.0-generate-001": "veo-3.0-generate-001",
3031
"veo-3.0-fast-generate-001": "veo-3.0-fast-generate-001",
3132
}
@@ -247,17 +248,8 @@ def status_extractor(response):
247248
raise Exception("Video generation completed but no video was returned")
248249

249250

250-
class Veo3VideoGenerationNode(VeoVideoGenerationNode):
251-
"""
252-
Generates videos from text prompts using Google's Veo 3 API.
253-
254-
Supported models:
255-
- veo-3.0-generate-001
256-
- veo-3.0-fast-generate-001
257-
258-
This node extends the base Veo node with Veo 3 specific features including
259-
audio generation and fixed 8-second duration.
260-
"""
251+
class Veo3VideoGenerationNode(IO.ComfyNode):
252+
"""Generates videos from text prompts using Google's Veo 3 API."""
261253

262254
@classmethod
263255
def define_schema(cls):
@@ -279,6 +271,13 @@ def define_schema(cls):
279271
default="16:9",
280272
tooltip="Aspect ratio of the output video",
281273
),
274+
IO.Combo.Input(
275+
"resolution",
276+
options=["720p", "1080p", "4k"],
277+
default="720p",
278+
tooltip="Output video resolution. 4K is not available for veo-3.1-lite and veo-3.0 models.",
279+
optional=True,
280+
),
282281
IO.String.Input(
283282
"negative_prompt",
284283
multiline=True,
@@ -289,11 +288,11 @@ def define_schema(cls):
289288
IO.Int.Input(
290289
"duration_seconds",
291290
default=8,
292-
min=8,
291+
min=4,
293292
max=8,
294-
step=1,
293+
step=2,
295294
display_mode=IO.NumberDisplay.number,
296-
tooltip="Duration of the output video in seconds (Veo 3 only supports 8 seconds)",
295+
tooltip="Duration of the output video in seconds",
297296
optional=True,
298297
),
299298
IO.Boolean.Input(
@@ -332,10 +331,10 @@ def define_schema(cls):
332331
options=[
333332
"veo-3.1-generate",
334333
"veo-3.1-fast-generate",
334+
"veo-3.1-lite",
335335
"veo-3.0-generate-001",
336336
"veo-3.0-fast-generate-001",
337337
],
338-
default="veo-3.0-generate-001",
339338
tooltip="Veo 3 model to use for video generation",
340339
optional=True,
341340
),
@@ -356,21 +355,111 @@ def define_schema(cls):
356355
],
357356
is_api_node=True,
358357
price_badge=IO.PriceBadge(
359-
depends_on=IO.PriceBadgeDepends(widgets=["model", "generate_audio"]),
358+
depends_on=IO.PriceBadgeDepends(widgets=["model", "generate_audio", "resolution", "duration_seconds"]),
360359
expr="""
361360
(
362361
$m := widgets.model;
362+
$r := widgets.resolution;
363363
$a := widgets.generate_audio;
364-
($contains($m,"veo-3.0-fast-generate-001") or $contains($m,"veo-3.1-fast-generate"))
365-
? {"type":"usd","usd": ($a ? 1.2 : 0.8)}
366-
: ($contains($m,"veo-3.0-generate-001") or $contains($m,"veo-3.1-generate"))
367-
? {"type":"usd","usd": ($a ? 3.2 : 1.6)}
368-
: {"type":"range_usd","min_usd":0.8,"max_usd":3.2}
364+
$seconds := widgets.duration_seconds;
365+
$pps :=
366+
$contains($m, "lite")
367+
? ($r = "1080p" ? ($a ? 0.08 : 0.05) : ($a ? 0.05 : 0.03))
368+
: $contains($m, "3.1-fast")
369+
? ($r = "4k" ? ($a ? 0.30 : 0.25) : $r = "1080p" ? ($a ? 0.12 : 0.10) : ($a ? 0.10 : 0.08))
370+
: $contains($m, "3.1-generate")
371+
? ($r = "4k" ? ($a ? 0.60 : 0.40) : ($a ? 0.40 : 0.20))
372+
: $contains($m, "3.0-fast")
373+
? ($a ? 0.15 : 0.10)
374+
: ($a ? 0.40 : 0.20);
375+
{"type":"usd","usd": $pps * $seconds}
369376
)
370377
""",
371378
),
372379
)
373380

381+
@classmethod
382+
async def execute(
383+
cls,
384+
prompt,
385+
aspect_ratio="16:9",
386+
resolution="720p",
387+
negative_prompt="",
388+
duration_seconds=8,
389+
enhance_prompt=True,
390+
person_generation="ALLOW",
391+
seed=0,
392+
image=None,
393+
model="veo-3.0-generate-001",
394+
generate_audio=False,
395+
):
396+
if "lite" in model and resolution == "4k":
397+
raise Exception("4K resolution is not supported by the veo-3.1-lite model.")
398+
399+
model = MODELS_MAP[model]
400+
401+
instances = [{"prompt": prompt}]
402+
if image is not None:
403+
image_base64 = tensor_to_base64_string(image)
404+
if image_base64:
405+
instances[0]["image"] = {"bytesBase64Encoded": image_base64, "mimeType": "image/png"}
406+
407+
parameters = {
408+
"aspectRatio": aspect_ratio,
409+
"personGeneration": person_generation,
410+
"durationSeconds": duration_seconds,
411+
"enhancePrompt": True,
412+
"generateAudio": generate_audio,
413+
}
414+
if negative_prompt:
415+
parameters["negativePrompt"] = negative_prompt
416+
if seed > 0:
417+
parameters["seed"] = seed
418+
if "veo-3.1" in model:
419+
parameters["resolution"] = resolution
420+
421+
initial_response = await sync_op(
422+
cls,
423+
ApiEndpoint(path=f"/proxy/veo/{model}/generate", method="POST"),
424+
response_model=VeoGenVidResponse,
425+
data=VeoGenVidRequest(
426+
instances=instances,
427+
parameters=parameters,
428+
),
429+
)
430+
431+
poll_response = await poll_op(
432+
cls,
433+
ApiEndpoint(path=f"/proxy/veo/{model}/poll", method="POST"),
434+
response_model=VeoGenVidPollResponse,
435+
status_extractor=lambda r: "completed" if r.done else "pending",
436+
data=VeoGenVidPollRequest(operationName=initial_response.name),
437+
poll_interval=5.0,
438+
estimated_duration=AVERAGE_DURATION_VIDEO_GEN,
439+
)
440+
441+
if poll_response.error:
442+
raise Exception(f"Veo API error: {poll_response.error.message} (code: {poll_response.error.code})")
443+
444+
response = poll_response.response
445+
filtered_count = response.raiMediaFilteredCount
446+
if filtered_count:
447+
reasons = response.raiMediaFilteredReasons or []
448+
reason_part = f": {reasons[0]}" if reasons else ""
449+
raise Exception(
450+
f"Content blocked by Google's Responsible AI filters{reason_part} "
451+
f"({filtered_count} video{'s' if filtered_count != 1 else ''} filtered)."
452+
)
453+
454+
if response.videos:
455+
video = response.videos[0]
456+
if video.bytesBase64Encoded:
457+
return IO.NodeOutput(InputImpl.VideoFromFile(BytesIO(base64.b64decode(video.bytesBase64Encoded))))
458+
if video.gcsUri:
459+
return IO.NodeOutput(await download_url_to_video_output(video.gcsUri))
460+
raise Exception("Video returned but no data or URL was provided")
461+
raise Exception("Video generation completed but no video was returned")
462+
374463

375464
class Veo3FirstLastFrameNode(IO.ComfyNode):
376465

@@ -394,7 +483,7 @@ def define_schema(cls):
394483
default="",
395484
tooltip="Negative text prompt to guide what to avoid in the video",
396485
),
397-
IO.Combo.Input("resolution", options=["720p", "1080p"]),
486+
IO.Combo.Input("resolution", options=["720p", "1080p", "4k"]),
398487
IO.Combo.Input(
399488
"aspect_ratio",
400489
options=["16:9", "9:16"],
@@ -424,8 +513,7 @@ def define_schema(cls):
424513
IO.Image.Input("last_frame", tooltip="End frame"),
425514
IO.Combo.Input(
426515
"model",
427-
options=["veo-3.1-generate", "veo-3.1-fast-generate"],
428-
default="veo-3.1-fast-generate",
516+
options=["veo-3.1-generate", "veo-3.1-fast-generate", "veo-3.1-lite"],
429517
),
430518
IO.Boolean.Input(
431519
"generate_audio",
@@ -443,26 +531,20 @@ def define_schema(cls):
443531
],
444532
is_api_node=True,
445533
price_badge=IO.PriceBadge(
446-
depends_on=IO.PriceBadgeDepends(widgets=["model", "generate_audio", "duration"]),
534+
depends_on=IO.PriceBadgeDepends(widgets=["model", "generate_audio", "duration", "resolution"]),
447535
expr="""
448536
(
449-
$prices := {
450-
"veo-3.1-fast-generate": { "audio": 0.15, "no_audio": 0.10 },
451-
"veo-3.1-generate": { "audio": 0.40, "no_audio": 0.20 }
452-
};
453537
$m := widgets.model;
454-
$ga := (widgets.generate_audio = "true");
538+
$r := widgets.resolution;
539+
$ga := widgets.generate_audio;
455540
$seconds := widgets.duration;
456-
$modelKey :=
457-
$contains($m, "veo-3.1-fast-generate") ? "veo-3.1-fast-generate" :
458-
$contains($m, "veo-3.1-generate") ? "veo-3.1-generate" :
459-
"";
460-
$audioKey := $ga ? "audio" : "no_audio";
461-
$modelPrices := $lookup($prices, $modelKey);
462-
$pps := $lookup($modelPrices, $audioKey);
463-
($pps != null)
464-
? {"type":"usd","usd": $pps * $seconds}
465-
: {"type":"range_usd","min_usd": 0.4, "max_usd": 3.2}
541+
$pps :=
542+
$contains($m, "lite")
543+
? ($r = "1080p" ? ($ga ? 0.08 : 0.05) : ($ga ? 0.05 : 0.03))
544+
: $contains($m, "fast")
545+
? ($r = "4k" ? ($ga ? 0.30 : 0.25) : $r = "1080p" ? ($ga ? 0.12 : 0.10) : ($ga ? 0.10 : 0.08))
546+
: ($r = "4k" ? ($ga ? 0.60 : 0.40) : ($ga ? 0.40 : 0.20));
547+
{"type":"usd","usd": $pps * $seconds}
466548
)
467549
""",
468550
),
@@ -482,6 +564,9 @@ async def execute(
482564
model: str,
483565
generate_audio: bool,
484566
):
567+
if "lite" in model and resolution == "4k":
568+
raise Exception("4K resolution is not supported by the veo-3.1-lite model.")
569+
485570
model = MODELS_MAP[model]
486571
initial_response = await sync_op(
487572
cls,

0 commit comments

Comments
 (0)