mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-12-21 20:10:48 +08:00
Merge branch 'comfyanonymous:master' into feature/custom-node-paths-cli-args
This commit is contained in:
commit
7396bff86a
2
.github/PULL_REQUEST_TEMPLATE/api-node.md
vendored
2
.github/PULL_REQUEST_TEMPLATE/api-node.md
vendored
@ -18,4 +18,4 @@ If **Need pricing update**:
|
|||||||
- [ ] **QA not required**
|
- [ ] **QA not required**
|
||||||
|
|
||||||
### Comms
|
### Comms
|
||||||
- [ ] Informed **@Kosinkadink**
|
- [ ] Informed **Kosinkadink**
|
||||||
|
|||||||
2
.github/workflows/api-node-template.yml
vendored
2
.github/workflows/api-node-template.yml
vendored
@ -2,7 +2,7 @@ name: Append API Node PR template
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, reopened, synchronize, edited, ready_for_review]
|
types: [opened, reopened, synchronize, ready_for_review]
|
||||||
paths:
|
paths:
|
||||||
- 'comfy_api_nodes/**' # only run if these files changed
|
- 'comfy_api_nodes/**' # only run if these files changed
|
||||||
|
|
||||||
|
|||||||
17
.github/workflows/release-stable-all.yml
vendored
17
.github/workflows/release-stable-all.yml
vendored
@ -43,6 +43,23 @@ jobs:
|
|||||||
test_release: true
|
test_release: true
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
release_nvidia_cu126:
|
||||||
|
permissions:
|
||||||
|
contents: "write"
|
||||||
|
packages: "write"
|
||||||
|
pull-requests: "read"
|
||||||
|
name: "Release NVIDIA cu126"
|
||||||
|
uses: ./.github/workflows/stable-release.yml
|
||||||
|
with:
|
||||||
|
git_tag: ${{ inputs.git_tag }}
|
||||||
|
cache_tag: "cu126"
|
||||||
|
python_minor: "12"
|
||||||
|
python_patch: "10"
|
||||||
|
rel_name: "nvidia"
|
||||||
|
rel_extra_name: "_cu126"
|
||||||
|
test_release: true
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
release_amd_rocm:
|
release_amd_rocm:
|
||||||
permissions:
|
permissions:
|
||||||
contents: "write"
|
contents: "write"
|
||||||
|
|||||||
@ -183,7 +183,9 @@ Update your Nvidia drivers if it doesn't start.
|
|||||||
|
|
||||||
[Experimental portable for AMD GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_amd.7z)
|
[Experimental portable for AMD GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_amd.7z)
|
||||||
|
|
||||||
[Portable with pytorch cuda 12.8 and python 3.12](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu128.7z) (Supports Nvidia 10 series and older GPUs).
|
[Portable with pytorch cuda 12.8 and python 3.12](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu128.7z).
|
||||||
|
|
||||||
|
[Portable with pytorch cuda 12.6 and python 3.12](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu126.7z) (Supports Nvidia 10 series and older GPUs).
|
||||||
|
|
||||||
#### How do I share models between another UI and ComfyUI?
|
#### How do I share models between another UI and ComfyUI?
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,8 @@ import comfy.model_management
|
|||||||
|
|
||||||
|
|
||||||
def attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor, mask=None, transformer_options={}) -> Tensor:
|
def attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor, mask=None, transformer_options={}) -> Tensor:
|
||||||
q, k = apply_rope(q, k, pe)
|
if pe is not None:
|
||||||
|
q, k = apply_rope(q, k, pe)
|
||||||
heads = q.shape[1]
|
heads = q.shape[1]
|
||||||
x = optimized_attention(q, k, v, heads, skip_reshape=True, mask=mask, transformer_options=transformer_options)
|
x = optimized_attention(q, k, v, heads, skip_reshape=True, mask=mask, transformer_options=transformer_options)
|
||||||
return x
|
return x
|
||||||
|
|||||||
@ -32,6 +32,7 @@ class Llama2Config:
|
|||||||
q_norm = None
|
q_norm = None
|
||||||
k_norm = None
|
k_norm = None
|
||||||
rope_scale = None
|
rope_scale = None
|
||||||
|
final_norm: bool = True
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Qwen25_3BConfig:
|
class Qwen25_3BConfig:
|
||||||
@ -53,6 +54,7 @@ class Qwen25_3BConfig:
|
|||||||
q_norm = None
|
q_norm = None
|
||||||
k_norm = None
|
k_norm = None
|
||||||
rope_scale = None
|
rope_scale = None
|
||||||
|
final_norm: bool = True
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Qwen25_7BVLI_Config:
|
class Qwen25_7BVLI_Config:
|
||||||
@ -74,6 +76,7 @@ class Qwen25_7BVLI_Config:
|
|||||||
q_norm = None
|
q_norm = None
|
||||||
k_norm = None
|
k_norm = None
|
||||||
rope_scale = None
|
rope_scale = None
|
||||||
|
final_norm: bool = True
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Gemma2_2B_Config:
|
class Gemma2_2B_Config:
|
||||||
@ -96,6 +99,7 @@ class Gemma2_2B_Config:
|
|||||||
k_norm = None
|
k_norm = None
|
||||||
sliding_attention = None
|
sliding_attention = None
|
||||||
rope_scale = None
|
rope_scale = None
|
||||||
|
final_norm: bool = True
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Gemma3_4B_Config:
|
class Gemma3_4B_Config:
|
||||||
@ -118,6 +122,7 @@ class Gemma3_4B_Config:
|
|||||||
k_norm = "gemma3"
|
k_norm = "gemma3"
|
||||||
sliding_attention = [False, False, False, False, False, 1024]
|
sliding_attention = [False, False, False, False, False, 1024]
|
||||||
rope_scale = [1.0, 8.0]
|
rope_scale = [1.0, 8.0]
|
||||||
|
final_norm: bool = True
|
||||||
|
|
||||||
class RMSNorm(nn.Module):
|
class RMSNorm(nn.Module):
|
||||||
def __init__(self, dim: int, eps: float = 1e-5, add=False, device=None, dtype=None):
|
def __init__(self, dim: int, eps: float = 1e-5, add=False, device=None, dtype=None):
|
||||||
@ -366,7 +371,12 @@ class Llama2_(nn.Module):
|
|||||||
transformer(config, index=i, device=device, dtype=dtype, ops=ops)
|
transformer(config, index=i, device=device, dtype=dtype, ops=ops)
|
||||||
for i in range(config.num_hidden_layers)
|
for i in range(config.num_hidden_layers)
|
||||||
])
|
])
|
||||||
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, add=config.rms_norm_add, device=device, dtype=dtype)
|
|
||||||
|
if config.final_norm:
|
||||||
|
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, add=config.rms_norm_add, device=device, dtype=dtype)
|
||||||
|
else:
|
||||||
|
self.norm = None
|
||||||
|
|
||||||
# self.lm_head = ops.Linear(config.hidden_size, config.vocab_size, bias=False, device=device, dtype=dtype)
|
# self.lm_head = ops.Linear(config.hidden_size, config.vocab_size, bias=False, device=device, dtype=dtype)
|
||||||
|
|
||||||
def forward(self, x, attention_mask=None, embeds=None, num_tokens=None, intermediate_output=None, final_layer_norm_intermediate=True, dtype=None, position_ids=None, embeds_info=[]):
|
def forward(self, x, attention_mask=None, embeds=None, num_tokens=None, intermediate_output=None, final_layer_norm_intermediate=True, dtype=None, position_ids=None, embeds_info=[]):
|
||||||
@ -421,14 +431,16 @@ class Llama2_(nn.Module):
|
|||||||
if i == intermediate_output:
|
if i == intermediate_output:
|
||||||
intermediate = x.clone()
|
intermediate = x.clone()
|
||||||
|
|
||||||
x = self.norm(x)
|
if self.norm is not None:
|
||||||
|
x = self.norm(x)
|
||||||
|
|
||||||
if all_intermediate is not None:
|
if all_intermediate is not None:
|
||||||
all_intermediate.append(x.unsqueeze(1).clone())
|
all_intermediate.append(x.unsqueeze(1).clone())
|
||||||
|
|
||||||
if all_intermediate is not None:
|
if all_intermediate is not None:
|
||||||
intermediate = torch.cat(all_intermediate, dim=1)
|
intermediate = torch.cat(all_intermediate, dim=1)
|
||||||
|
|
||||||
if intermediate is not None and final_layer_norm_intermediate:
|
if intermediate is not None and final_layer_norm_intermediate and self.norm is not None:
|
||||||
intermediate = self.norm(intermediate)
|
intermediate = self.norm(intermediate)
|
||||||
|
|
||||||
return x, intermediate
|
return x, intermediate
|
||||||
|
|||||||
@ -1,22 +1,229 @@
|
|||||||
from typing import Optional
|
from datetime import date
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from comfy_api_nodes.apis import GeminiGenerationConfig, GeminiContent, GeminiSafetySetting, GeminiSystemInstructionContent, GeminiTool, GeminiVideoMetadata
|
from pydantic import BaseModel, Field
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
class GeminiSafetyCategory(str, Enum):
|
||||||
|
HARM_CATEGORY_SEXUALLY_EXPLICIT = "HARM_CATEGORY_SEXUALLY_EXPLICIT"
|
||||||
|
HARM_CATEGORY_HATE_SPEECH = "HARM_CATEGORY_HATE_SPEECH"
|
||||||
|
HARM_CATEGORY_HARASSMENT = "HARM_CATEGORY_HARASSMENT"
|
||||||
|
HARM_CATEGORY_DANGEROUS_CONTENT = "HARM_CATEGORY_DANGEROUS_CONTENT"
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiSafetyThreshold(str, Enum):
|
||||||
|
OFF = "OFF"
|
||||||
|
BLOCK_NONE = "BLOCK_NONE"
|
||||||
|
BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE"
|
||||||
|
BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE"
|
||||||
|
BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH"
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiSafetySetting(BaseModel):
|
||||||
|
category: GeminiSafetyCategory
|
||||||
|
threshold: GeminiSafetyThreshold
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiRole(str, Enum):
|
||||||
|
user = "user"
|
||||||
|
model = "model"
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiMimeType(str, Enum):
|
||||||
|
application_pdf = "application/pdf"
|
||||||
|
audio_mpeg = "audio/mpeg"
|
||||||
|
audio_mp3 = "audio/mp3"
|
||||||
|
audio_wav = "audio/wav"
|
||||||
|
image_png = "image/png"
|
||||||
|
image_jpeg = "image/jpeg"
|
||||||
|
image_webp = "image/webp"
|
||||||
|
text_plain = "text/plain"
|
||||||
|
video_mov = "video/mov"
|
||||||
|
video_mpeg = "video/mpeg"
|
||||||
|
video_mp4 = "video/mp4"
|
||||||
|
video_mpg = "video/mpg"
|
||||||
|
video_avi = "video/avi"
|
||||||
|
video_wmv = "video/wmv"
|
||||||
|
video_mpegps = "video/mpegps"
|
||||||
|
video_flv = "video/flv"
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiInlineData(BaseModel):
|
||||||
|
data: str | None = Field(
|
||||||
|
None,
|
||||||
|
description="The base64 encoding of the image, PDF, or video to include inline in the prompt. "
|
||||||
|
"When including media inline, you must also specify the media type (mimeType) of the data. Size limit: 20MB",
|
||||||
|
)
|
||||||
|
mimeType: GeminiMimeType | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiPart(BaseModel):
|
||||||
|
inlineData: GeminiInlineData | None = Field(None)
|
||||||
|
text: str | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiTextPart(BaseModel):
|
||||||
|
text: str | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiContent(BaseModel):
|
||||||
|
parts: list[GeminiPart] = Field(...)
|
||||||
|
role: GeminiRole = Field(..., examples=["user"])
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiSystemInstructionContent(BaseModel):
|
||||||
|
parts: list[GeminiTextPart] = Field(
|
||||||
|
...,
|
||||||
|
description="A list of ordered parts that make up a single message. "
|
||||||
|
"Different parts may have different IANA MIME types.",
|
||||||
|
)
|
||||||
|
role: GeminiRole = Field(
|
||||||
|
...,
|
||||||
|
description="The identity of the entity that creates the message. "
|
||||||
|
"The following values are supported: "
|
||||||
|
"user: This indicates that the message is sent by a real person, typically a user-generated message. "
|
||||||
|
"model: This indicates that the message is generated by the model. "
|
||||||
|
"The model value is used to insert messages from model into the conversation during multi-turn conversations. "
|
||||||
|
"For non-multi-turn conversations, this field can be left blank or unset.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiFunctionDeclaration(BaseModel):
|
||||||
|
description: str | None = Field(None)
|
||||||
|
name: str = Field(...)
|
||||||
|
parameters: dict[str, Any] = Field(..., description="JSON schema for the function parameters")
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiTool(BaseModel):
|
||||||
|
functionDeclarations: list[GeminiFunctionDeclaration] | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiOffset(BaseModel):
|
||||||
|
nanos: int | None = Field(None, ge=0, le=999999999)
|
||||||
|
seconds: int | None = Field(None, ge=-315576000000, le=315576000000)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiVideoMetadata(BaseModel):
|
||||||
|
endOffset: GeminiOffset | None = Field(None)
|
||||||
|
startOffset: GeminiOffset | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiGenerationConfig(BaseModel):
|
||||||
|
maxOutputTokens: int | None = Field(None, ge=16, le=8192)
|
||||||
|
seed: int | None = Field(None)
|
||||||
|
stopSequences: list[str] | None = Field(None)
|
||||||
|
temperature: float | None = Field(1, ge=0.0, le=2.0)
|
||||||
|
topK: int | None = Field(40, ge=1)
|
||||||
|
topP: float | None = Field(0.95, ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
class GeminiImageConfig(BaseModel):
|
class GeminiImageConfig(BaseModel):
|
||||||
aspectRatio: Optional[str] = None
|
aspectRatio: str | None = Field(None)
|
||||||
|
resolution: str | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
class GeminiImageGenerationConfig(GeminiGenerationConfig):
|
class GeminiImageGenerationConfig(GeminiGenerationConfig):
|
||||||
responseModalities: Optional[list[str]] = None
|
responseModalities: list[str] | None = Field(None)
|
||||||
imageConfig: Optional[GeminiImageConfig] = None
|
imageConfig: GeminiImageConfig | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
class GeminiImageGenerateContentRequest(BaseModel):
|
class GeminiImageGenerateContentRequest(BaseModel):
|
||||||
contents: list[GeminiContent]
|
contents: list[GeminiContent] = Field(...)
|
||||||
generationConfig: Optional[GeminiImageGenerationConfig] = None
|
generationConfig: GeminiImageGenerationConfig | None = Field(None)
|
||||||
safetySettings: Optional[list[GeminiSafetySetting]] = None
|
safetySettings: list[GeminiSafetySetting] | None = Field(None)
|
||||||
systemInstruction: Optional[GeminiSystemInstructionContent] = None
|
systemInstruction: GeminiSystemInstructionContent | None = Field(None)
|
||||||
tools: Optional[list[GeminiTool]] = None
|
tools: list[GeminiTool] | None = Field(None)
|
||||||
videoMetadata: Optional[GeminiVideoMetadata] = None
|
videoMetadata: GeminiVideoMetadata | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiGenerateContentRequest(BaseModel):
|
||||||
|
contents: list[GeminiContent] = Field(...)
|
||||||
|
generationConfig: GeminiGenerationConfig | None = Field(None)
|
||||||
|
safetySettings: list[GeminiSafetySetting] | None = Field(None)
|
||||||
|
systemInstruction: GeminiSystemInstructionContent | None = Field(None)
|
||||||
|
tools: list[GeminiTool] | None = Field(None)
|
||||||
|
videoMetadata: GeminiVideoMetadata | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class Modality(str, Enum):
|
||||||
|
MODALITY_UNSPECIFIED = "MODALITY_UNSPECIFIED"
|
||||||
|
TEXT = "TEXT"
|
||||||
|
IMAGE = "IMAGE"
|
||||||
|
VIDEO = "VIDEO"
|
||||||
|
AUDIO = "AUDIO"
|
||||||
|
DOCUMENT = "DOCUMENT"
|
||||||
|
|
||||||
|
|
||||||
|
class ModalityTokenCount(BaseModel):
|
||||||
|
modality: Modality | None = None
|
||||||
|
tokenCount: int | None = Field(None, description="Number of tokens for the given modality.")
|
||||||
|
|
||||||
|
|
||||||
|
class Probability(str, Enum):
|
||||||
|
NEGLIGIBLE = "NEGLIGIBLE"
|
||||||
|
LOW = "LOW"
|
||||||
|
MEDIUM = "MEDIUM"
|
||||||
|
HIGH = "HIGH"
|
||||||
|
UNKNOWN = "UNKNOWN"
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiSafetyRating(BaseModel):
|
||||||
|
category: GeminiSafetyCategory | None = None
|
||||||
|
probability: Probability | None = Field(
|
||||||
|
None,
|
||||||
|
description="The probability that the content violates the specified safety category",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiCitation(BaseModel):
|
||||||
|
authors: list[str] | None = None
|
||||||
|
endIndex: int | None = None
|
||||||
|
license: str | None = None
|
||||||
|
publicationDate: date | None = None
|
||||||
|
startIndex: int | None = None
|
||||||
|
title: str | None = None
|
||||||
|
uri: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiCitationMetadata(BaseModel):
|
||||||
|
citations: list[GeminiCitation] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiCandidate(BaseModel):
|
||||||
|
citationMetadata: GeminiCitationMetadata | None = None
|
||||||
|
content: GeminiContent | None = None
|
||||||
|
finishReason: str | None = None
|
||||||
|
safetyRatings: list[GeminiSafetyRating] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiPromptFeedback(BaseModel):
|
||||||
|
blockReason: str | None = None
|
||||||
|
blockReasonMessage: str | None = None
|
||||||
|
safetyRatings: list[GeminiSafetyRating] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiUsageMetadata(BaseModel):
|
||||||
|
cachedContentTokenCount: int | None = Field(
|
||||||
|
None,
|
||||||
|
description="Output only. Number of tokens in the cached part in the input (the cached content).",
|
||||||
|
)
|
||||||
|
candidatesTokenCount: int | None = Field(None, description="Number of tokens in the response(s).")
|
||||||
|
candidatesTokensDetails: list[ModalityTokenCount] | None = Field(
|
||||||
|
None, description="Breakdown of candidate tokens by modality."
|
||||||
|
)
|
||||||
|
promptTokenCount: int | None = Field(
|
||||||
|
None,
|
||||||
|
description="Number of tokens in the request. When cachedContent is set, this is still the total effective prompt size meaning this includes the number of tokens in the cached content.",
|
||||||
|
)
|
||||||
|
promptTokensDetails: list[ModalityTokenCount] | None = Field(
|
||||||
|
None, description="Breakdown of prompt tokens by modality."
|
||||||
|
)
|
||||||
|
thoughtsTokenCount: int | None = Field(None, description="Number of tokens present in thoughts output.")
|
||||||
|
toolUsePromptTokenCount: int | None = Field(None, description="Number of tokens present in tool-use prompt(s).")
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiGenerateContentResponse(BaseModel):
|
||||||
|
candidates: list[GeminiCandidate] | None = Field(None)
|
||||||
|
promptFeedback: GeminiPromptFeedback | None = Field(None)
|
||||||
|
usageMetadata: GeminiUsageMetadata | None = Field(None)
|
||||||
|
|||||||
@ -3,8 +3,6 @@ API Nodes for Gemini Multimodal LLM Usage via Remote API
|
|||||||
See: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference
|
See: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@ -12,7 +10,7 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Literal, Optional
|
from typing import Literal
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
@ -20,18 +18,17 @@ from typing_extensions import override
|
|||||||
import folder_paths
|
import folder_paths
|
||||||
from comfy_api.latest import IO, ComfyExtension, Input
|
from comfy_api.latest import IO, ComfyExtension, Input
|
||||||
from comfy_api.util import VideoCodec, VideoContainer
|
from comfy_api.util import VideoCodec, VideoContainer
|
||||||
from comfy_api_nodes.apis import (
|
from comfy_api_nodes.apis.gemini_api import (
|
||||||
GeminiContent,
|
GeminiContent,
|
||||||
GeminiGenerateContentRequest,
|
GeminiGenerateContentRequest,
|
||||||
GeminiGenerateContentResponse,
|
GeminiGenerateContentResponse,
|
||||||
GeminiInlineData,
|
|
||||||
GeminiMimeType,
|
|
||||||
GeminiPart,
|
|
||||||
)
|
|
||||||
from comfy_api_nodes.apis.gemini_api import (
|
|
||||||
GeminiImageConfig,
|
GeminiImageConfig,
|
||||||
GeminiImageGenerateContentRequest,
|
GeminiImageGenerateContentRequest,
|
||||||
GeminiImageGenerationConfig,
|
GeminiImageGenerationConfig,
|
||||||
|
GeminiInlineData,
|
||||||
|
GeminiMimeType,
|
||||||
|
GeminiPart,
|
||||||
|
GeminiRole,
|
||||||
)
|
)
|
||||||
from comfy_api_nodes.util import (
|
from comfy_api_nodes.util import (
|
||||||
ApiEndpoint,
|
ApiEndpoint,
|
||||||
@ -57,6 +54,7 @@ class GeminiModel(str, Enum):
|
|||||||
gemini_2_5_flash_preview_04_17 = "gemini-2.5-flash-preview-04-17"
|
gemini_2_5_flash_preview_04_17 = "gemini-2.5-flash-preview-04-17"
|
||||||
gemini_2_5_pro = "gemini-2.5-pro"
|
gemini_2_5_pro = "gemini-2.5-pro"
|
||||||
gemini_2_5_flash = "gemini-2.5-flash"
|
gemini_2_5_flash = "gemini-2.5-flash"
|
||||||
|
gemini_3_0_pro = "gemini-3-pro-preview"
|
||||||
|
|
||||||
|
|
||||||
class GeminiImageModel(str, Enum):
|
class GeminiImageModel(str, Enum):
|
||||||
@ -103,6 +101,16 @@ def get_parts_by_type(response: GeminiGenerateContentResponse, part_type: Litera
|
|||||||
Returns:
|
Returns:
|
||||||
List of response parts matching the requested type.
|
List of response parts matching the requested type.
|
||||||
"""
|
"""
|
||||||
|
if response.candidates is None:
|
||||||
|
if response.promptFeedback.blockReason:
|
||||||
|
feedback = response.promptFeedback
|
||||||
|
raise ValueError(
|
||||||
|
f"Gemini API blocked the request. Reason: {feedback.blockReason} ({feedback.blockReasonMessage})"
|
||||||
|
)
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Gemini returned no response candidates. "
|
||||||
|
"Please report to ComfyUI repository with the example of workflow to reproduce this."
|
||||||
|
)
|
||||||
parts = []
|
parts = []
|
||||||
for part in response.candidates[0].content.parts:
|
for part in response.candidates[0].content.parts:
|
||||||
if part_type == "text" and hasattr(part, "text") and part.text:
|
if part_type == "text" and hasattr(part, "text") and part.text:
|
||||||
@ -272,10 +280,10 @@ class GeminiNode(IO.ComfyNode):
|
|||||||
prompt: str,
|
prompt: str,
|
||||||
model: str,
|
model: str,
|
||||||
seed: int,
|
seed: int,
|
||||||
images: Optional[torch.Tensor] = None,
|
images: torch.Tensor | None = None,
|
||||||
audio: Optional[Input.Audio] = None,
|
audio: Input.Audio | None = None,
|
||||||
video: Optional[Input.Video] = None,
|
video: Input.Video | None = None,
|
||||||
files: Optional[list[GeminiPart]] = None,
|
files: list[GeminiPart] | None = None,
|
||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
validate_string(prompt, strip_whitespace=False)
|
validate_string(prompt, strip_whitespace=False)
|
||||||
|
|
||||||
@ -300,7 +308,7 @@ class GeminiNode(IO.ComfyNode):
|
|||||||
data=GeminiGenerateContentRequest(
|
data=GeminiGenerateContentRequest(
|
||||||
contents=[
|
contents=[
|
||||||
GeminiContent(
|
GeminiContent(
|
||||||
role="user",
|
role=GeminiRole.user,
|
||||||
parts=parts,
|
parts=parts,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@ -308,7 +316,6 @@ class GeminiNode(IO.ComfyNode):
|
|||||||
response_model=GeminiGenerateContentResponse,
|
response_model=GeminiGenerateContentResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get result output
|
|
||||||
output_text = get_text_from_response(response)
|
output_text = get_text_from_response(response)
|
||||||
if output_text:
|
if output_text:
|
||||||
# Not a true chat history like the OpenAI Chat node. It is emulated so the frontend can show a copy button.
|
# Not a true chat history like the OpenAI Chat node. It is emulated so the frontend can show a copy button.
|
||||||
@ -406,7 +413,7 @@ class GeminiInputFiles(IO.ComfyNode):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, file: str, GEMINI_INPUT_FILES: Optional[list[GeminiPart]] = None) -> IO.NodeOutput:
|
def execute(cls, file: str, GEMINI_INPUT_FILES: list[GeminiPart] | None = None) -> IO.NodeOutput:
|
||||||
"""Loads and formats input files for Gemini API."""
|
"""Loads and formats input files for Gemini API."""
|
||||||
if GEMINI_INPUT_FILES is None:
|
if GEMINI_INPUT_FILES is None:
|
||||||
GEMINI_INPUT_FILES = []
|
GEMINI_INPUT_FILES = []
|
||||||
@ -421,7 +428,7 @@ class GeminiImage(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="GeminiImageNode",
|
node_id="GeminiImageNode",
|
||||||
display_name="Google Gemini Image",
|
display_name="Nano Banana (Google Gemini Image)",
|
||||||
category="api node/image/Gemini",
|
category="api node/image/Gemini",
|
||||||
description="Edit images synchronously via Google API.",
|
description="Edit images synchronously via Google API.",
|
||||||
inputs=[
|
inputs=[
|
||||||
@ -488,8 +495,8 @@ class GeminiImage(IO.ComfyNode):
|
|||||||
prompt: str,
|
prompt: str,
|
||||||
model: str,
|
model: str,
|
||||||
seed: int,
|
seed: int,
|
||||||
images: Optional[torch.Tensor] = None,
|
images: torch.Tensor | None = None,
|
||||||
files: Optional[list[GeminiPart]] = None,
|
files: list[GeminiPart] | None = None,
|
||||||
aspect_ratio: str = "auto",
|
aspect_ratio: str = "auto",
|
||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
validate_string(prompt, strip_whitespace=True, min_length=1)
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
||||||
@ -510,7 +517,7 @@ class GeminiImage(IO.ComfyNode):
|
|||||||
endpoint=ApiEndpoint(path=f"{GEMINI_BASE_ENDPOINT}/{model}", method="POST"),
|
endpoint=ApiEndpoint(path=f"{GEMINI_BASE_ENDPOINT}/{model}", method="POST"),
|
||||||
data=GeminiImageGenerateContentRequest(
|
data=GeminiImageGenerateContentRequest(
|
||||||
contents=[
|
contents=[
|
||||||
GeminiContent(role="user", parts=parts),
|
GeminiContent(role=GeminiRole.user, parts=parts),
|
||||||
],
|
],
|
||||||
generationConfig=GeminiImageGenerationConfig(
|
generationConfig=GeminiImageGenerationConfig(
|
||||||
responseModalities=["TEXT", "IMAGE"],
|
responseModalities=["TEXT", "IMAGE"],
|
||||||
|
|||||||
@ -11,13 +11,13 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
def easycache_forward_wrapper(executor, *args, **kwargs):
|
def easycache_forward_wrapper(executor, *args, **kwargs):
|
||||||
# get values from args
|
# get values from args
|
||||||
x: torch.Tensor = args[0]
|
|
||||||
transformer_options: dict[str] = args[-1]
|
transformer_options: dict[str] = args[-1]
|
||||||
if not isinstance(transformer_options, dict):
|
if not isinstance(transformer_options, dict):
|
||||||
transformer_options = kwargs.get("transformer_options")
|
transformer_options = kwargs.get("transformer_options")
|
||||||
if not transformer_options:
|
if not transformer_options:
|
||||||
transformer_options = args[-2]
|
transformer_options = args[-2]
|
||||||
easycache: EasyCacheHolder = transformer_options["easycache"]
|
easycache: EasyCacheHolder = transformer_options["easycache"]
|
||||||
|
x: torch.Tensor = args[0][:, :easycache.output_channels]
|
||||||
sigmas = transformer_options["sigmas"]
|
sigmas = transformer_options["sigmas"]
|
||||||
uuids = transformer_options["uuids"]
|
uuids = transformer_options["uuids"]
|
||||||
if sigmas is not None and easycache.is_past_end_timestep(sigmas):
|
if sigmas is not None and easycache.is_past_end_timestep(sigmas):
|
||||||
@ -82,13 +82,13 @@ def easycache_forward_wrapper(executor, *args, **kwargs):
|
|||||||
|
|
||||||
def lazycache_predict_noise_wrapper(executor, *args, **kwargs):
|
def lazycache_predict_noise_wrapper(executor, *args, **kwargs):
|
||||||
# get values from args
|
# get values from args
|
||||||
x: torch.Tensor = args[0]
|
|
||||||
timestep: float = args[1]
|
timestep: float = args[1]
|
||||||
model_options: dict[str] = args[2]
|
model_options: dict[str] = args[2]
|
||||||
easycache: LazyCacheHolder = model_options["transformer_options"]["easycache"]
|
easycache: LazyCacheHolder = model_options["transformer_options"]["easycache"]
|
||||||
if easycache.is_past_end_timestep(timestep):
|
if easycache.is_past_end_timestep(timestep):
|
||||||
return executor(*args, **kwargs)
|
return executor(*args, **kwargs)
|
||||||
# prepare next x_prev
|
# prepare next x_prev
|
||||||
|
x: torch.Tensor = args[0][:, :easycache.output_channels]
|
||||||
next_x_prev = x
|
next_x_prev = x
|
||||||
input_change = None
|
input_change = None
|
||||||
do_easycache = easycache.should_do_easycache(timestep)
|
do_easycache = easycache.should_do_easycache(timestep)
|
||||||
@ -173,7 +173,7 @@ def easycache_sample_wrapper(executor, *args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
class EasyCacheHolder:
|
class EasyCacheHolder:
|
||||||
def __init__(self, reuse_threshold: float, start_percent: float, end_percent: float, subsample_factor: int, offload_cache_diff: bool, verbose: bool=False):
|
def __init__(self, reuse_threshold: float, start_percent: float, end_percent: float, subsample_factor: int, offload_cache_diff: bool, verbose: bool=False, output_channels: int=None):
|
||||||
self.name = "EasyCache"
|
self.name = "EasyCache"
|
||||||
self.reuse_threshold = reuse_threshold
|
self.reuse_threshold = reuse_threshold
|
||||||
self.start_percent = start_percent
|
self.start_percent = start_percent
|
||||||
@ -202,6 +202,7 @@ class EasyCacheHolder:
|
|||||||
self.allow_mismatch = True
|
self.allow_mismatch = True
|
||||||
self.cut_from_start = True
|
self.cut_from_start = True
|
||||||
self.state_metadata = None
|
self.state_metadata = None
|
||||||
|
self.output_channels = output_channels
|
||||||
|
|
||||||
def is_past_end_timestep(self, timestep: float) -> bool:
|
def is_past_end_timestep(self, timestep: float) -> bool:
|
||||||
return not (timestep[0] > self.end_t).item()
|
return not (timestep[0] > self.end_t).item()
|
||||||
@ -264,7 +265,7 @@ class EasyCacheHolder:
|
|||||||
else:
|
else:
|
||||||
slicing.append(slice(None))
|
slicing.append(slice(None))
|
||||||
batch_slice = batch_slice + slicing
|
batch_slice = batch_slice + slicing
|
||||||
x[batch_slice] += self.uuid_cache_diffs[uuid].to(x.device)
|
x[tuple(batch_slice)] += self.uuid_cache_diffs[uuid].to(x.device)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
def update_cache_diff(self, output: torch.Tensor, x: torch.Tensor, uuids: list[UUID]):
|
def update_cache_diff(self, output: torch.Tensor, x: torch.Tensor, uuids: list[UUID]):
|
||||||
@ -283,7 +284,7 @@ class EasyCacheHolder:
|
|||||||
else:
|
else:
|
||||||
slicing.append(slice(None))
|
slicing.append(slice(None))
|
||||||
skip_dim = False
|
skip_dim = False
|
||||||
x = x[slicing]
|
x = x[tuple(slicing)]
|
||||||
diff = output - x
|
diff = output - x
|
||||||
batch_offset = diff.shape[0] // len(uuids)
|
batch_offset = diff.shape[0] // len(uuids)
|
||||||
for i, uuid in enumerate(uuids):
|
for i, uuid in enumerate(uuids):
|
||||||
@ -323,7 +324,7 @@ class EasyCacheHolder:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def clone(self):
|
def clone(self):
|
||||||
return EasyCacheHolder(self.reuse_threshold, self.start_percent, self.end_percent, self.subsample_factor, self.offload_cache_diff, self.verbose)
|
return EasyCacheHolder(self.reuse_threshold, self.start_percent, self.end_percent, self.subsample_factor, self.offload_cache_diff, self.verbose, output_channels=self.output_channels)
|
||||||
|
|
||||||
|
|
||||||
class EasyCacheNode(io.ComfyNode):
|
class EasyCacheNode(io.ComfyNode):
|
||||||
@ -350,7 +351,7 @@ class EasyCacheNode(io.ComfyNode):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, model: io.Model.Type, reuse_threshold: float, start_percent: float, end_percent: float, verbose: bool) -> io.NodeOutput:
|
def execute(cls, model: io.Model.Type, reuse_threshold: float, start_percent: float, end_percent: float, verbose: bool) -> io.NodeOutput:
|
||||||
model = model.clone()
|
model = model.clone()
|
||||||
model.model_options["transformer_options"]["easycache"] = EasyCacheHolder(reuse_threshold, start_percent, end_percent, subsample_factor=8, offload_cache_diff=False, verbose=verbose)
|
model.model_options["transformer_options"]["easycache"] = EasyCacheHolder(reuse_threshold, start_percent, end_percent, subsample_factor=8, offload_cache_diff=False, verbose=verbose, output_channels=model.model.latent_format.latent_channels)
|
||||||
model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.OUTER_SAMPLE, "easycache", easycache_sample_wrapper)
|
model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.OUTER_SAMPLE, "easycache", easycache_sample_wrapper)
|
||||||
model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.CALC_COND_BATCH, "easycache", easycache_calc_cond_batch_wrapper)
|
model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.CALC_COND_BATCH, "easycache", easycache_calc_cond_batch_wrapper)
|
||||||
model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, "easycache", easycache_forward_wrapper)
|
model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, "easycache", easycache_forward_wrapper)
|
||||||
@ -358,7 +359,7 @@ class EasyCacheNode(io.ComfyNode):
|
|||||||
|
|
||||||
|
|
||||||
class LazyCacheHolder:
|
class LazyCacheHolder:
|
||||||
def __init__(self, reuse_threshold: float, start_percent: float, end_percent: float, subsample_factor: int, offload_cache_diff: bool, verbose: bool=False):
|
def __init__(self, reuse_threshold: float, start_percent: float, end_percent: float, subsample_factor: int, offload_cache_diff: bool, verbose: bool=False, output_channels: int=None):
|
||||||
self.name = "LazyCache"
|
self.name = "LazyCache"
|
||||||
self.reuse_threshold = reuse_threshold
|
self.reuse_threshold = reuse_threshold
|
||||||
self.start_percent = start_percent
|
self.start_percent = start_percent
|
||||||
@ -382,6 +383,7 @@ class LazyCacheHolder:
|
|||||||
self.approx_output_change_rates = []
|
self.approx_output_change_rates = []
|
||||||
self.total_steps_skipped = 0
|
self.total_steps_skipped = 0
|
||||||
self.state_metadata = None
|
self.state_metadata = None
|
||||||
|
self.output_channels = output_channels
|
||||||
|
|
||||||
def has_cache_diff(self) -> bool:
|
def has_cache_diff(self) -> bool:
|
||||||
return self.cache_diff is not None
|
return self.cache_diff is not None
|
||||||
@ -456,7 +458,7 @@ class LazyCacheHolder:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def clone(self):
|
def clone(self):
|
||||||
return LazyCacheHolder(self.reuse_threshold, self.start_percent, self.end_percent, self.subsample_factor, self.offload_cache_diff, self.verbose)
|
return LazyCacheHolder(self.reuse_threshold, self.start_percent, self.end_percent, self.subsample_factor, self.offload_cache_diff, self.verbose, output_channels=self.output_channels)
|
||||||
|
|
||||||
class LazyCacheNode(io.ComfyNode):
|
class LazyCacheNode(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -482,7 +484,7 @@ class LazyCacheNode(io.ComfyNode):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, model: io.Model.Type, reuse_threshold: float, start_percent: float, end_percent: float, verbose: bool) -> io.NodeOutput:
|
def execute(cls, model: io.Model.Type, reuse_threshold: float, start_percent: float, end_percent: float, verbose: bool) -> io.NodeOutput:
|
||||||
model = model.clone()
|
model = model.clone()
|
||||||
model.model_options["transformer_options"]["easycache"] = LazyCacheHolder(reuse_threshold, start_percent, end_percent, subsample_factor=8, offload_cache_diff=False, verbose=verbose)
|
model.model_options["transformer_options"]["easycache"] = LazyCacheHolder(reuse_threshold, start_percent, end_percent, subsample_factor=8, offload_cache_diff=False, verbose=verbose, output_channels=model.model.latent_format.latent_channels)
|
||||||
model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.OUTER_SAMPLE, "lazycache", easycache_sample_wrapper)
|
model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.OUTER_SAMPLE, "lazycache", easycache_sample_wrapper)
|
||||||
model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.PREDICT_NOISE, "lazycache", lazycache_predict_noise_wrapper)
|
model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.PREDICT_NOISE, "lazycache", lazycache_predict_noise_wrapper)
|
||||||
return io.NodeOutput(model)
|
return io.NodeOutput(model)
|
||||||
|
|||||||
39
comfy_extras/nodes_nop.py
Normal file
39
comfy_extras/nodes_nop.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
from typing_extensions import override
|
||||||
|
# If you write a node that is so useless that it breaks ComfyUI it will be featured in this exclusive list
|
||||||
|
|
||||||
|
# "native" block swap nodes are placebo at best and break the ComfyUI memory management system.
|
||||||
|
# They are also considered harmful because instead of users reporting issues with the built in
|
||||||
|
# memory management they install these stupid nodes and complain even harder. Now it completely
|
||||||
|
# breaks with some of the new ComfyUI memory optimizations so I have made the decision to NOP it
|
||||||
|
# out of all workflows.
|
||||||
|
class wanBlockSwap(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="wanBlockSwap",
|
||||||
|
category="",
|
||||||
|
description="NOP",
|
||||||
|
inputs=[
|
||||||
|
io.Model.Input("model"),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Model.Output(),
|
||||||
|
],
|
||||||
|
is_deprecated=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, model) -> io.NodeOutput:
|
||||||
|
return io.NodeOutput(model)
|
||||||
|
|
||||||
|
|
||||||
|
class NopExtension(ComfyExtension):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
wanBlockSwap
|
||||||
|
]
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> NopExtension:
|
||||||
|
return NopExtension()
|
||||||
@ -39,5 +39,5 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"PreviewAny": "Preview Any",
|
"PreviewAny": "Preview as Text",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
# This file is automatically generated by the build process when version is
|
# This file is automatically generated by the build process when version is
|
||||||
# updated in pyproject.toml.
|
# updated in pyproject.toml.
|
||||||
__version__ = "0.3.68"
|
__version__ = "0.3.70"
|
||||||
|
|||||||
1
nodes.py
1
nodes.py
@ -2330,6 +2330,7 @@ async def init_builtin_extra_nodes():
|
|||||||
"nodes_easycache.py",
|
"nodes_easycache.py",
|
||||||
"nodes_audio_encoder.py",
|
"nodes_audio_encoder.py",
|
||||||
"nodes_rope.py",
|
"nodes_rope.py",
|
||||||
|
"nodes_nop.py",
|
||||||
]
|
]
|
||||||
|
|
||||||
import_failed = []
|
import_failed = []
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "ComfyUI"
|
name = "ComfyUI"
|
||||||
version = "0.3.68"
|
version = "0.3.70"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
@ -24,7 +24,7 @@ lint.select = [
|
|||||||
exclude = ["*.ipynb", "**/generated/*.pyi"]
|
exclude = ["*.ipynb", "**/generated/*.pyi"]
|
||||||
|
|
||||||
[tool.pylint]
|
[tool.pylint]
|
||||||
master.py-version = "3.9"
|
master.py-version = "3.10"
|
||||||
master.extension-pkg-allow-list = [
|
master.extension-pkg-allow-list = [
|
||||||
"pydantic",
|
"pydantic",
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user