1- from typing import Optional
2-
31import torch
42from typing_extensions import override
53
6- from comfy_api .latest import IO , ComfyExtension
4+ from comfy_api .latest import IO , ComfyExtension , Input
75from comfy_api_nodes .apis .luma import (
6+ Luma2Generation ,
7+ Luma2GenerationRequest ,
8+ Luma2ImageRef ,
89 LumaAspectRatio ,
910 LumaCharacterRef ,
1011 LumaConceptChain ,
3031 download_url_to_video_output ,
3132 poll_op ,
3233 sync_op ,
34+ upload_image_to_comfyapi ,
3335 upload_images_to_comfyapi ,
3436 validate_string ,
3537)
@@ -212,9 +214,9 @@ async def execute(
212214 aspect_ratio : str ,
213215 seed ,
214216 style_image_weight : float ,
215- image_luma_ref : Optional [ LumaReferenceChain ] = None ,
216- style_image : Optional [ torch .Tensor ] = None ,
217- character_image : Optional [ torch .Tensor ] = None ,
217+ image_luma_ref : LumaReferenceChain | None = None ,
218+ style_image : torch .Tensor | None = None ,
219+ character_image : torch .Tensor | None = None ,
218220 ) -> IO .NodeOutput :
219221 validate_string (prompt , strip_whitespace = True , min_length = 3 )
220222 # handle image_luma_ref
@@ -434,7 +436,7 @@ async def execute(
434436 duration : str ,
435437 loop : bool ,
436438 seed ,
437- luma_concepts : Optional [ LumaConceptChain ] = None ,
439+ luma_concepts : LumaConceptChain | None = None ,
438440 ) -> IO .NodeOutput :
439441 validate_string (prompt , strip_whitespace = False , min_length = 3 )
440442 duration = duration if model != LumaVideoModel .ray_1_6 else None
@@ -533,7 +535,6 @@ def define_schema(cls) -> IO.Schema:
533535 ],
534536 is_api_node = True ,
535537 price_badge = PRICE_BADGE_VIDEO ,
536-
537538 )
538539
539540 @classmethod
@@ -644,6 +645,242 @@ async def _convert_to_keyframes(
644645)
645646
646647
648+ def _luma2_uni1_common_inputs (max_image_refs : int ) -> list :
649+ return [
650+ IO .Combo .Input (
651+ "style" ,
652+ options = ["auto" , "manga" ],
653+ default = "auto" ,
654+ tooltip = "Style preset. 'auto' picks based on the prompt; "
655+ "'manga' applies a manga/anime aesthetic and requires a portrait "
656+ "aspect ratio (2:3, 9:16, 1:2, 1:3)." ,
657+ ),
658+ IO .Boolean .Input (
659+ "web_search" ,
660+ default = False ,
661+ tooltip = "Search the web for visual references before generating." ,
662+ ),
663+ IO .Autogrow .Input (
664+ "image_ref" ,
665+ template = IO .Autogrow .TemplateNames (
666+ IO .Image .Input ("image" ),
667+ names = [f"image_{ i } " for i in range (1 , max_image_refs + 1 )],
668+ min = 0 ,
669+ ),
670+ optional = True ,
671+ tooltip = f"Up to { max_image_refs } reference images for style/content guidance." ,
672+ ),
673+ ]
674+
675+
676+ async def _luma2_upload_image_refs (
677+ cls : type [IO .ComfyNode ],
678+ refs : dict | None ,
679+ max_count : int ,
680+ ) -> list [Luma2ImageRef ] | None :
681+ if not refs :
682+ return None
683+ out : list [Luma2ImageRef ] = []
684+ for key in refs :
685+ url = await upload_image_to_comfyapi (cls , refs [key ])
686+ out .append (Luma2ImageRef (url = url ))
687+ if len (out ) > max_count :
688+ raise ValueError (f"Maximum { max_count } reference images are allowed." )
689+ return out or None
690+
691+
692+ async def _luma2_submit_and_poll (
693+ cls : type [IO .ComfyNode ],
694+ request : Luma2GenerationRequest ,
695+ ) -> Input .Image :
696+ initial = await sync_op (
697+ cls ,
698+ ApiEndpoint (path = "/proxy/luma_2/generations" , method = "POST" ),
699+ response_model = Luma2Generation ,
700+ data = request ,
701+ )
702+ if not initial .id :
703+ raise RuntimeError ("Luma 2 API did not return a generation id." )
704+ final = await poll_op (
705+ cls ,
706+ ApiEndpoint (path = f"/proxy/luma_2/generations/{ initial .id } " , method = "GET" ),
707+ response_model = Luma2Generation ,
708+ status_extractor = lambda r : r .state ,
709+ progress_extractor = lambda r : None ,
710+ )
711+ if not final .output :
712+ msg = final .failure_reason or "no output returned"
713+ raise RuntimeError (f"Luma 2 generation failed: { msg } " )
714+ url = final .output [0 ].url
715+ if not url :
716+ raise RuntimeError ("Luma 2 generation completed without an output URL." )
717+ return await download_url_to_image_tensor (url )
718+
719+
720+ class LumaImageNode (IO .ComfyNode ):
721+
722+ @classmethod
723+ def define_schema (cls ) -> IO .Schema :
724+ return IO .Schema (
725+ node_id = "LumaImageNode2" ,
726+ display_name = "Luma UNI-1 Image" ,
727+ category = "api node/image/Luma" ,
728+ description = "Generate images from text using the Luma UNI-1 model." ,
729+ inputs = [
730+ IO .String .Input (
731+ "prompt" ,
732+ multiline = True ,
733+ default = "" ,
734+ tooltip = "Text description of the desired image. 1–6000 characters." ,
735+ ),
736+ IO .DynamicCombo .Input (
737+ "model" ,
738+ options = [
739+ IO .DynamicCombo .Option (
740+ "uni-1" ,
741+ [
742+ IO .Combo .Input (
743+ "aspect_ratio" ,
744+ options = [
745+ "auto" ,
746+ "3:1" ,
747+ "2:1" ,
748+ "16:9" ,
749+ "3:2" ,
750+ "1:1" ,
751+ "2:3" ,
752+ "9:16" ,
753+ "1:2" ,
754+ "1:3" ,
755+ ],
756+ default = "auto" ,
757+ tooltip = "Output image aspect ratio. 'auto' lets "
758+ "the model pick based on the prompt." ,
759+ ),
760+ * _luma2_uni1_common_inputs (max_image_refs = 9 ),
761+ ],
762+ ),
763+ ],
764+ tooltip = "Model to use for generation." ,
765+ ),
766+ IO .Int .Input (
767+ "seed" ,
768+ default = 0 ,
769+ min = 0 ,
770+ max = 2147483647 ,
771+ control_after_generate = True ,
772+ tooltip = "Seed controls whether the node should re-run; "
773+ "results are non-deterministic regardless of seed." ,
774+ ),
775+ ],
776+ outputs = [IO .Image .Output ()],
777+ hidden = [
778+ IO .Hidden .auth_token_comfy_org ,
779+ IO .Hidden .api_key_comfy_org ,
780+ IO .Hidden .unique_id ,
781+ ],
782+ is_api_node = True ,
783+ )
784+
785+ @classmethod
786+ async def execute (
787+ cls ,
788+ prompt : str ,
789+ model : dict ,
790+ seed : int ,
791+ ) -> IO .NodeOutput :
792+ validate_string (prompt , min_length = 1 , max_length = 6000 )
793+ aspect_ratio = model ["aspect_ratio" ]
794+ style = model ["style" ]
795+ if style == "manga" and aspect_ratio != "auto" and aspect_ratio not in {"2:3" , "9:16" , "1:2" , "1:3" }:
796+ raise ValueError (
797+ f"'manga' style requires a portrait aspect ratio "
798+ f"({ ', ' .join (sorted ({"2:3" , "9:16" , "1:2" , "1:3" }))} ) or 'auto'; got '{ aspect_ratio } '."
799+ )
800+ request = Luma2GenerationRequest (
801+ prompt = prompt ,
802+ model = model ["model" ],
803+ type = "image" ,
804+ aspect_ratio = aspect_ratio if aspect_ratio != "auto" else None ,
805+ style = style if style != "auto" else None ,
806+ output_format = "png" ,
807+ web_search = model ["web_search" ],
808+ image_ref = await _luma2_upload_image_refs (cls , model .get ("image_ref" ), max_count = 9 ),
809+ )
810+ return IO .NodeOutput (await _luma2_submit_and_poll (cls , request ))
811+
812+
813+ class LumaImageEditNode (IO .ComfyNode ):
814+
815+ @classmethod
816+ def define_schema (cls ) -> IO .Schema :
817+ return IO .Schema (
818+ node_id = "LumaImageEditNode2" ,
819+ display_name = "Luma UNI-1 Image Edit" ,
820+ category = "api node/image/Luma" ,
821+ description = "Edit an existing image with a text prompt using the Luma UNI-1 model." ,
822+ inputs = [
823+ IO .Image .Input (
824+ "source" ,
825+ tooltip = "Source image to edit." ,
826+ ),
827+ IO .String .Input (
828+ "prompt" ,
829+ multiline = True ,
830+ default = "" ,
831+ tooltip = "Description of the desired edit. 1–6000 characters." ,
832+ ),
833+ IO .DynamicCombo .Input (
834+ "model" ,
835+ options = [
836+ IO .DynamicCombo .Option (
837+ "uni-1" ,
838+ _luma2_uni1_common_inputs (max_image_refs = 8 ),
839+ ),
840+ ],
841+ tooltip = "Model to use for editing." ,
842+ ),
843+ IO .Int .Input (
844+ "seed" ,
845+ default = 0 ,
846+ min = 0 ,
847+ max = 2147483647 ,
848+ control_after_generate = True ,
849+ tooltip = "Seed controls whether the node should re-run; "
850+ "results are non-deterministic regardless of seed." ,
851+ ),
852+ ],
853+ outputs = [IO .Image .Output ()],
854+ hidden = [
855+ IO .Hidden .auth_token_comfy_org ,
856+ IO .Hidden .api_key_comfy_org ,
857+ IO .Hidden .unique_id ,
858+ ],
859+ is_api_node = True ,
860+ )
861+
862+ @classmethod
863+ async def execute (
864+ cls ,
865+ source : Input .Image ,
866+ prompt : str ,
867+ model : dict ,
868+ seed : int ,
869+ ) -> IO .NodeOutput :
870+ validate_string (prompt , min_length = 1 , max_length = 6000 )
871+ request = Luma2GenerationRequest (
872+ prompt = prompt ,
873+ model = model ["model" ],
874+ type = "image_edit" ,
875+ source = Luma2ImageRef (url = await upload_image_to_comfyapi (cls , source )),
876+ style = model ["style" ] if model ["style" ] != "auto" else None ,
877+ output_format = "png" ,
878+ web_search = model ["web_search" ],
879+ image_ref = await _luma2_upload_image_refs (cls , model .get ("image_ref" ), max_count = 8 ),
880+ )
881+ return IO .NodeOutput (await _luma2_submit_and_poll (cls , request ))
882+
883+
647884class LumaExtension (ComfyExtension ):
648885 @override
649886 async def get_node_list (self ) -> list [type [IO .ComfyNode ]]:
@@ -654,6 +891,8 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]:
654891 LumaImageToVideoGenerationNode ,
655892 LumaReferenceNode ,
656893 LumaConceptsNode ,
894+ LumaImageNode ,
895+ LumaImageEditNode ,
657896 ]
658897
659898
0 commit comments