Merge branch 'master' into 20260503a_temporal_downscale_ratio

This commit is contained in:
Jukka Seppänen 2026-05-05 19:12:44 +03:00 committed by GitHub
commit 0a96b3c4cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 390 additions and 60 deletions

View File

@ -1,15 +1,12 @@
from __future__ import annotations from __future__ import annotations
import torch
from enum import Enum from enum import Enum
from typing import Optional, Union from typing import Optional, Union
import torch
from pydantic import BaseModel, Field, confloat from pydantic import BaseModel, Field, confloat
class LumaIO: class LumaIO:
LUMA_REF = "LUMA_REF" LUMA_REF = "LUMA_REF"
LUMA_CONCEPTS = "LUMA_CONCEPTS" LUMA_CONCEPTS = "LUMA_CONCEPTS"
@ -183,13 +180,13 @@ class LumaAssets(BaseModel):
class LumaImageRef(BaseModel): class LumaImageRef(BaseModel):
'''Used for image gen''' """Used for image gen"""
url: str = Field(..., description='The URL of the image reference') url: str = Field(..., description='The URL of the image reference')
weight: confloat(ge=0.0, le=1.0) = Field(..., description='The weight of the image reference') weight: confloat(ge=0.0, le=1.0) = Field(..., description='The weight of the image reference')
class LumaImageReference(BaseModel): class LumaImageReference(BaseModel):
'''Used for video gen''' """Used for video gen"""
type: Optional[str] = Field('image', description='Input type, defaults to image') type: Optional[str] = Field('image', description='Input type, defaults to image')
url: str = Field(..., description='The URL of the image') url: str = Field(..., description='The URL of the image')
@ -251,3 +248,32 @@ class LumaGeneration(BaseModel):
assets: Optional[LumaAssets] = Field(None, description='The assets of the generation') assets: Optional[LumaAssets] = Field(None, description='The assets of the generation')
model: str = Field(..., description='The model used for the generation') model: str = Field(..., description='The model used for the generation')
request: Union[LumaGenerationRequest, LumaImageGenerationRequest] = Field(..., description="The request used for the generation") request: Union[LumaGenerationRequest, LumaImageGenerationRequest] = Field(..., description="The request used for the generation")
class Luma2ImageRef(BaseModel):
url: str | None = None
data: str | None = None
media_type: str | None = None
class Luma2GenerationRequest(BaseModel):
prompt: str = Field(..., min_length=1, max_length=6000)
model: str | None = None
type: str | None = None
aspect_ratio: str | None = None
style: str | None = None
output_format: str | None = None
web_search: bool | None = None
image_ref: list[Luma2ImageRef] | None = None
source: Luma2ImageRef | None = None
class Luma2Generation(BaseModel):
id: str | None = None
type: str | None = None
state: str | None = None
model: str | None = None
created_at: str | None = None
output: list[LumaImageReference] | None = None
failure_reason: str | None = None
failure_code: str | None = None

View File

@ -56,14 +56,14 @@ class ModelResponseProperties(BaseModel):
instructions: str | None = Field(None) instructions: str | None = Field(None)
max_output_tokens: int | None = Field(None) max_output_tokens: int | None = Field(None)
model: str | None = Field(None) model: str | None = Field(None)
temperature: float | None = Field(1, description="Controls randomness in the response", ge=0.0, le=2.0) temperature: float | None = Field(None, description="Controls randomness in the response", ge=0.0, le=2.0)
top_p: float | None = Field( top_p: float | None = Field(
1, None,
description="Controls diversity of the response via nucleus sampling", description="Controls diversity of the response via nucleus sampling",
ge=0.0, ge=0.0,
le=1.0, le=1.0,
) )
truncation: str | None = Field("disabled", description="Allowed values: 'auto' or 'disabled'") truncation: str | None = Field(None, description="Allowed values: 'auto' or 'disabled'")
class ResponseProperties(BaseModel): class ResponseProperties(BaseModel):

View File

@ -1,10 +1,11 @@
from typing import Optional
import torch import torch
from typing_extensions import override from typing_extensions import override
from comfy_api.latest import IO, ComfyExtension from comfy_api.latest import IO, ComfyExtension, Input
from comfy_api_nodes.apis.luma import ( from comfy_api_nodes.apis.luma import (
Luma2Generation,
Luma2GenerationRequest,
Luma2ImageRef,
LumaAspectRatio, LumaAspectRatio,
LumaCharacterRef, LumaCharacterRef,
LumaConceptChain, LumaConceptChain,
@ -30,6 +31,7 @@ from comfy_api_nodes.util import (
download_url_to_video_output, download_url_to_video_output,
poll_op, poll_op,
sync_op, sync_op,
upload_image_to_comfyapi,
upload_images_to_comfyapi, upload_images_to_comfyapi,
validate_string, validate_string,
) )
@ -212,9 +214,9 @@ class LumaImageGenerationNode(IO.ComfyNode):
aspect_ratio: str, aspect_ratio: str,
seed, seed,
style_image_weight: float, style_image_weight: float,
image_luma_ref: Optional[LumaReferenceChain] = None, image_luma_ref: LumaReferenceChain | None = None,
style_image: Optional[torch.Tensor] = None, style_image: torch.Tensor | None = None,
character_image: Optional[torch.Tensor] = None, character_image: torch.Tensor | None = None,
) -> IO.NodeOutput: ) -> IO.NodeOutput:
validate_string(prompt, strip_whitespace=True, min_length=3) validate_string(prompt, strip_whitespace=True, min_length=3)
# handle image_luma_ref # handle image_luma_ref
@ -434,7 +436,7 @@ class LumaTextToVideoGenerationNode(IO.ComfyNode):
duration: str, duration: str,
loop: bool, loop: bool,
seed, seed,
luma_concepts: Optional[LumaConceptChain] = None, luma_concepts: LumaConceptChain | None = None,
) -> IO.NodeOutput: ) -> IO.NodeOutput:
validate_string(prompt, strip_whitespace=False, min_length=3) validate_string(prompt, strip_whitespace=False, min_length=3)
duration = duration if model != LumaVideoModel.ray_1_6 else None duration = duration if model != LumaVideoModel.ray_1_6 else None
@ -533,7 +535,6 @@ class LumaImageToVideoGenerationNode(IO.ComfyNode):
], ],
is_api_node=True, is_api_node=True,
price_badge=PRICE_BADGE_VIDEO, price_badge=PRICE_BADGE_VIDEO,
) )
@classmethod @classmethod
@ -644,6 +645,293 @@ PRICE_BADGE_VIDEO = IO.PriceBadge(
) )
def _luma2_uni1_common_inputs(max_image_refs: int) -> list:
return [
IO.Combo.Input(
"style",
options=["auto", "manga"],
default="auto",
tooltip="Style preset. 'auto' picks based on the prompt; "
"'manga' applies a manga/anime aesthetic and requires a portrait "
"aspect ratio (2:3, 9:16, 1:2, 1:3).",
),
IO.Boolean.Input(
"web_search",
default=False,
tooltip="Search the web for visual references before generating.",
),
IO.Autogrow.Input(
"image_ref",
template=IO.Autogrow.TemplateNames(
IO.Image.Input("image"),
names=[f"image_{i}" for i in range(1, max_image_refs + 1)],
min=0,
),
optional=True,
tooltip=f"Up to {max_image_refs} reference images for style/content guidance.",
),
]
async def _luma2_upload_image_refs(
cls: type[IO.ComfyNode],
refs: dict | None,
max_count: int,
) -> list[Luma2ImageRef] | None:
if not refs:
return None
out: list[Luma2ImageRef] = []
for key in refs:
url = await upload_image_to_comfyapi(cls, refs[key])
out.append(Luma2ImageRef(url=url))
if len(out) > max_count:
raise ValueError(f"Maximum {max_count} reference images are allowed.")
return out or None
async def _luma2_submit_and_poll(
cls: type[IO.ComfyNode],
request: Luma2GenerationRequest,
) -> Input.Image:
initial = await sync_op(
cls,
ApiEndpoint(path="/proxy/luma_2/generations", method="POST"),
response_model=Luma2Generation,
data=request,
)
if not initial.id:
raise RuntimeError("Luma 2 API did not return a generation id.")
final = await poll_op(
cls,
ApiEndpoint(path=f"/proxy/luma_2/generations/{initial.id}", method="GET"),
response_model=Luma2Generation,
status_extractor=lambda r: r.state,
progress_extractor=lambda r: None,
)
if not final.output:
msg = final.failure_reason or "no output returned"
raise RuntimeError(f"Luma 2 generation failed: {msg}")
url = final.output[0].url
if not url:
raise RuntimeError("Luma 2 generation completed without an output URL.")
return await download_url_to_image_tensor(url)
class LumaImageNode(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="LumaImageNode2",
display_name="Luma UNI-1 Image",
category="api node/image/Luma",
description="Generate images from text using the Luma UNI-1 model.",
inputs=[
IO.String.Input(
"prompt",
multiline=True,
default="",
tooltip="Text description of the desired image. 16000 characters.",
),
IO.DynamicCombo.Input(
"model",
options=[
IO.DynamicCombo.Option(
"uni-1",
[
IO.Combo.Input(
"aspect_ratio",
options=[
"auto",
"3:1",
"2:1",
"16:9",
"3:2",
"1:1",
"2:3",
"9:16",
"1:2",
"1:3",
],
default="auto",
tooltip="Output image aspect ratio. 'auto' lets "
"the model pick based on the prompt.",
),
*_luma2_uni1_common_inputs(max_image_refs=9),
],
),
IO.DynamicCombo.Option(
"uni-1-max",
[
IO.Combo.Input(
"aspect_ratio",
options=[
"auto",
"3:1",
"2:1",
"16:9",
"3:2",
"1:1",
"2:3",
"9:16",
"1:2",
"1:3",
],
default="auto",
tooltip="Output image aspect ratio. 'auto' lets "
"the model pick based on the prompt.",
),
*_luma2_uni1_common_inputs(max_image_refs=9),
],
),
],
tooltip="Model to use for generation.",
),
IO.Int.Input(
"seed",
default=0,
min=0,
max=2147483647,
control_after_generate=True,
tooltip="Seed controls whether the node should re-run; "
"results are non-deterministic regardless of seed.",
),
],
outputs=[IO.Image.Output()],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
price_badge=IO.PriceBadge(
depends_on=IO.PriceBadgeDepends(widgets=["model"], input_groups=["model.image_ref"]),
expr="""
(
$m := widgets.model;
$refs := $lookup(inputGroups, "model.image_ref");
$base := $m = "uni-1-max" ? 0.1 : 0.0404;
{"type":"usd","usd": $round($base + 0.003 * $refs, 4)}
)
""",
),
)
@classmethod
async def execute(
cls,
prompt: str,
model: dict,
seed: int,
) -> IO.NodeOutput:
validate_string(prompt, min_length=1, max_length=6000)
aspect_ratio = model["aspect_ratio"]
style = model["style"]
allowed_manga_ratios = {"2:3", "9:16", "1:2", "1:3"}
if style == "manga" and aspect_ratio != "auto" and aspect_ratio not in allowed_manga_ratios:
raise ValueError(
f"'manga' style requires a portrait aspect ratio "
f"({', '.join(sorted(allowed_manga_ratios))}) or 'auto'; got '{aspect_ratio}'."
)
request = Luma2GenerationRequest(
prompt=prompt,
model=model["model"],
type="image",
aspect_ratio=aspect_ratio if aspect_ratio != "auto" else None,
style=style if style != "auto" else None,
output_format="png",
web_search=model["web_search"],
image_ref=await _luma2_upload_image_refs(cls, model.get("image_ref"), max_count=9),
)
return IO.NodeOutput(await _luma2_submit_and_poll(cls, request))
class LumaImageEditNode(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="LumaImageEditNode2",
display_name="Luma UNI-1 Image Edit",
category="api node/image/Luma",
description="Edit an existing image with a text prompt using the Luma UNI-1 model.",
inputs=[
IO.Image.Input(
"source",
tooltip="Source image to edit.",
),
IO.String.Input(
"prompt",
multiline=True,
default="",
tooltip="Description of the desired edit. 16000 characters.",
),
IO.DynamicCombo.Input(
"model",
options=[
IO.DynamicCombo.Option(
"uni-1",
_luma2_uni1_common_inputs(max_image_refs=8),
),
IO.DynamicCombo.Option(
"uni-1-max",
_luma2_uni1_common_inputs(max_image_refs=8),
),
],
tooltip="Model to use for editing.",
),
IO.Int.Input(
"seed",
default=0,
min=0,
max=2147483647,
control_after_generate=True,
tooltip="Seed controls whether the node should re-run; "
"results are non-deterministic regardless of seed.",
),
],
outputs=[IO.Image.Output()],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
price_badge=IO.PriceBadge(
depends_on=IO.PriceBadgeDepends(widgets=["model"], input_groups=["model.image_ref"]),
expr="""
(
$m := widgets.model;
$refs := $lookup(inputGroups, "model.image_ref");
$base := $m = "uni-1-max" ? 0.103 : 0.0434;
{"type":"usd","usd": $round($base + 0.003 * $refs, 4)}
)
""",
),
)
@classmethod
async def execute(
cls,
source: Input.Image,
prompt: str,
model: dict,
seed: int,
) -> IO.NodeOutput:
validate_string(prompt, min_length=1, max_length=6000)
request = Luma2GenerationRequest(
prompt=prompt,
model=model["model"],
type="image_edit",
source=Luma2ImageRef(url=await upload_image_to_comfyapi(cls, source)),
style=model["style"] if model["style"] != "auto" else None,
output_format="png",
web_search=model["web_search"],
image_ref=await _luma2_upload_image_refs(cls, model.get("image_ref"), max_count=8),
)
return IO.NodeOutput(await _luma2_submit_and_poll(cls, request))
class LumaExtension(ComfyExtension): class LumaExtension(ComfyExtension):
@override @override
async def get_node_list(self) -> list[type[IO.ComfyNode]]: async def get_node_list(self) -> list[type[IO.ComfyNode]]:
@ -654,6 +942,8 @@ class LumaExtension(ComfyExtension):
LumaImageToVideoGenerationNode, LumaImageToVideoGenerationNode,
LumaReferenceNode, LumaReferenceNode,
LumaConceptsNode, LumaConceptsNode,
LumaImageNode,
LumaImageEditNode,
] ]

View File

@ -39,16 +39,18 @@ STARTING_POINT_ID_PATTERN = r"<starting_point_id:(.*)>"
class SupportedOpenAIModel(str, Enum): class SupportedOpenAIModel(str, Enum):
o4_mini = "o4-mini" gpt_5_5_pro = "gpt-5.5-pro"
o1 = "o1" gpt_5_5 = "gpt-5.5"
o3 = "o3"
o1_pro = "o1-pro"
gpt_4_1 = "gpt-4.1"
gpt_4_1_mini = "gpt-4.1-mini"
gpt_4_1_nano = "gpt-4.1-nano"
gpt_5 = "gpt-5" gpt_5 = "gpt-5"
gpt_5_mini = "gpt-5-mini" gpt_5_mini = "gpt-5-mini"
gpt_5_nano = "gpt-5-nano" gpt_5_nano = "gpt-5-nano"
gpt_4_1 = "gpt-4.1"
gpt_4_1_mini = "gpt-4.1-mini"
gpt_4_1_nano = "gpt-4.1-nano"
o4_mini = "o4-mini"
o3 = "o3"
o1_pro = "o1-pro"
o1 = "o1"
async def validate_and_cast_response(response, timeout: int = None) -> torch.Tensor: async def validate_and_cast_response(response, timeout: int = None) -> torch.Tensor:
@ -739,6 +741,16 @@ class OpenAIChatNode(IO.ComfyNode):
"usd": [0.002, 0.008], "usd": [0.002, 0.008],
"format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" } "format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" }
} }
: $contains($m, "gpt-5.5-pro") ? {
"type": "list_usd",
"usd": [0.03, 0.18],
"format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" }
}
: $contains($m, "gpt-5.5") ? {
"type": "list_usd",
"usd": [0.005, 0.03],
"format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" }
}
: $contains($m, "gpt-5-nano") ? { : $contains($m, "gpt-5-nano") ? {
"type": "list_usd", "type": "list_usd",
"usd": [0.00005, 0.0004], "usd": [0.00005, 0.0004],

View File

@ -9,7 +9,8 @@ class String(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="PrimitiveString", node_id="PrimitiveString",
display_name="String", search_aliases=["text", "string", "text box", "prompt"],
display_name="Text String",
category="utils/primitive", category="utils/primitive",
inputs=[ inputs=[
io.String.Input("value"), io.String.Input("value"),
@ -27,7 +28,8 @@ class StringMultiline(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="PrimitiveStringMultiline", node_id="PrimitiveStringMultiline",
display_name="String (Multiline)", search_aliases=["text", "string", "text multiline", "string multiline", "text box", "prompt"],
display_name="Text String (Multiline)",
category="utils/primitive", category="utils/primitive",
essentials_category="Basics", essentials_category="Basics",
inputs=[ inputs=[

View File

@ -10,9 +10,9 @@ class StringConcatenate(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="StringConcatenate", node_id="StringConcatenate",
display_name="Text Concatenate", search_aliases=["concatenate", "text concat", "join text", "merge text", "combine strings", "string concat", "append text", "combine text"],
category="utils/string", display_name="Concatenate Text",
search_aliases=["Concatenate", "text concat", "join text", "merge text", "combine strings", "concat", "concatenate", "append text", "combine text", "string"], category="text",
inputs=[ inputs=[
io.String.Input("string_a", multiline=True), io.String.Input("string_a", multiline=True),
io.String.Input("string_b", multiline=True), io.String.Input("string_b", multiline=True),
@ -33,9 +33,9 @@ class StringSubstring(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="StringSubstring", node_id="StringSubstring",
search_aliases=["Substring", "extract text", "text portion"], search_aliases=["substring", "extract text", "text portion"],
display_name="Text Substring", display_name="Substring",
category="utils/string", category="text",
inputs=[ inputs=[
io.String.Input("string", multiline=True), io.String.Input("string", multiline=True),
io.Int.Input("start"), io.Int.Input("start"),
@ -58,7 +58,7 @@ class StringLength(io.ComfyNode):
node_id="StringLength", node_id="StringLength",
search_aliases=["character count", "text size", "string length"], search_aliases=["character count", "text size", "string length"],
display_name="Text Length", display_name="Text Length",
category="utils/string", category="text",
inputs=[ inputs=[
io.String.Input("string", multiline=True), io.String.Input("string", multiline=True),
], ],
@ -77,9 +77,9 @@ class CaseConverter(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="CaseConverter", node_id="CaseConverter",
search_aliases=["Case Converter", "text case", "uppercase", "lowercase", "capitalize"], search_aliases=["case converter", "text case", "uppercase", "lowercase", "capitalize"],
display_name="Text Case Converter", display_name="Convert Text Case",
category="utils/string", category="text",
inputs=[ inputs=[
io.String.Input("string", multiline=True), io.String.Input("string", multiline=True),
io.Combo.Input("mode", options=["UPPERCASE", "lowercase", "Capitalize", "Title Case"]), io.Combo.Input("mode", options=["UPPERCASE", "lowercase", "Capitalize", "Title Case"]),
@ -110,9 +110,9 @@ class StringTrim(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="StringTrim", node_id="StringTrim",
search_aliases=["Trim", "clean whitespace", "remove whitespace", "strip"], search_aliases=["trim", "clean whitespace", "remove whitespace", "remove spaces","strip"],
display_name="Text Trim", display_name="Trim Text",
category="utils/string", category="text",
inputs=[ inputs=[
io.String.Input("string", multiline=True), io.String.Input("string", multiline=True),
io.Combo.Input("mode", options=["Both", "Left", "Right"]), io.Combo.Input("mode", options=["Both", "Left", "Right"]),
@ -141,9 +141,9 @@ class StringReplace(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="StringReplace", node_id="StringReplace",
search_aliases=["Replace", "find and replace", "substitute", "swap text"], search_aliases=["replace", "find and replace", "substitute", "swap text"],
display_name="Text Replace", display_name="Replace Text",
category="utils/string", category="text",
inputs=[ inputs=[
io.String.Input("string", multiline=True), io.String.Input("string", multiline=True),
io.String.Input("find", multiline=True), io.String.Input("find", multiline=True),
@ -164,9 +164,9 @@ class StringContains(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="StringContains", node_id="StringContains",
search_aliases=["Contains", "text includes", "string includes"], search_aliases=["contains", "text includes", "string includes"],
display_name="Text Contains", display_name="Contains Text",
category="utils/string", category="text",
inputs=[ inputs=[
io.String.Input("string", multiline=True), io.String.Input("string", multiline=True),
io.String.Input("substring", multiline=True), io.String.Input("substring", multiline=True),
@ -192,9 +192,9 @@ class StringCompare(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="StringCompare", node_id="StringCompare",
search_aliases=["Compare", "text match", "string equals", "starts with", "ends with"], search_aliases=["compare", "text match", "string equals", "starts with", "ends with"],
display_name="Text Compare", display_name="Compare Text",
category="utils/string", category="text",
inputs=[ inputs=[
io.String.Input("string_a", multiline=True), io.String.Input("string_a", multiline=True),
io.String.Input("string_b", multiline=True), io.String.Input("string_b", multiline=True),
@ -228,9 +228,9 @@ class RegexMatch(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="RegexMatch", node_id="RegexMatch",
search_aliases=["Regex Match", "regex", "pattern match", "text contains", "string match"], search_aliases=["regex match", "regex", "pattern match", "text contains", "string match"],
display_name="Text Match", display_name="Match Text",
category="utils/string", category="text",
inputs=[ inputs=[
io.String.Input("string", multiline=True), io.String.Input("string", multiline=True),
io.String.Input("regex_pattern", multiline=True), io.String.Input("regex_pattern", multiline=True),
@ -269,9 +269,9 @@ class RegexExtract(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="RegexExtract", node_id="RegexExtract",
search_aliases=["Regex Extract", "regex", "pattern extract", "text parser", "parse text"], search_aliases=["regex extract", "regex", "pattern extract", "text parser", "parse text"],
display_name="Text Extract Substring", display_name="Extract Text",
category="utils/string", category="text",
inputs=[ inputs=[
io.String.Input("string", multiline=True), io.String.Input("string", multiline=True),
io.String.Input("regex_pattern", multiline=True), io.String.Input("regex_pattern", multiline=True),
@ -344,9 +344,9 @@ class RegexReplace(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="RegexReplace", node_id="RegexReplace",
search_aliases=["Regex Replace", "regex", "pattern replace", "regex replace", "substitution"], search_aliases=["regex replace", "regex", "pattern replace", "substitution"],
display_name="Text Replace (Regex)", display_name="Replace Text (Regex)",
category="utils/string", category="text",
description="Find and replace text using regex patterns.", description="Find and replace text using regex patterns.",
inputs=[ inputs=[
io.String.Input("string", multiline=True), io.String.Input("string", multiline=True),
@ -381,8 +381,8 @@ class JsonExtractString(io.ComfyNode):
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
node_id="JsonExtractString", node_id="JsonExtractString",
display_name="Extract String from JSON", display_name="Extract Text from JSON",
category="utils/string", category="text",
search_aliases=["json", "extract json", "parse json", "json value", "read json"], search_aliases=["json", "extract json", "parse json", "json value", "read json"],
inputs=[ inputs=[
io.String.Input("json_string", multiline=True), io.String.Input("json_string", multiline=True),

View File

@ -1,5 +1,5 @@
comfyui-frontend-package==1.42.15 comfyui-frontend-package==1.42.15
comfyui-workflow-templates==0.9.68 comfyui-workflow-templates==0.9.69
comfyui-embedded-docs==0.4.4 comfyui-embedded-docs==0.4.4
torch torch
torchsde torchsde