Compare commits

...

8 Commits

Author SHA1 Message Date
neil from camb.ai
5e368bd667
Merge 6a24746d14 into b08debceca 2026-07-06 17:34:08 +08:00
Daxiong (Lin)
b08debceca
chore: update embedded docs to v0.5.7 (#14783)
Some checks failed
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
Build package / Build Test (3.10) (push) Has been cancelled
Build package / Build Test (3.11) (push) Has been cancelled
Build package / Build Test (3.12) (push) Has been cancelled
Build package / Build Test (3.13) (push) Has been cancelled
Build package / Build Test (3.14) (push) Has been cancelled
2026-07-06 09:56:09 +08:00
comfyanonymous
000c6b784e
Small speedup for text model sampling. (#14773) 2026-07-05 18:39:24 -07:00
Alexander Piskun
985fb9d6ad
[Partner Nodes] fix(logs-auth): mask authorization headers in logs (#14774)
Some checks failed
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
Generate Pydantic Stubs from api.comfy.org / generate-models (push) Has been cancelled
Signed-off-by: bigcat88 <bigcat88@icloud.com>
2026-07-05 13:55:29 +03:00
Alexis Rolland
7f287b705e
fix: Bug when setting transparency in color picker (#14764)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-07-04 19:13:38 -04:00
comfyanonymous
b7ba504e06
Try to make coderabbit enforce AGENTS.md (#14759)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-07-04 14:25:24 -04:00
Silver
6c62ca0b6b
fix: error when embedding is loaded with models using llama_template (#14744)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-07-04 17:06:09 +08:00
Neil Ruaro
6a24746d14 [API Nodes] add CAMB AI nodes 2026-03-18 16:56:39 +08:00
8 changed files with 592 additions and 27 deletions

View File

@ -4,12 +4,12 @@ early_access: false
tone_instructions: "Only comment on issues introduced by this PR's changes. Do not flag pre-existing problems in moved, re-indented, or reformatted code."
reviews:
profile: "chill"
request_changes_workflow: false
profile: "assertive"
request_changes_workflow: true
high_level_summary: false
poem: false
review_status: false
review_details: false
review_details: true
commit_status: true
collapse_walkthrough: true
changed_files_summary: false
@ -39,6 +39,14 @@ reviews:
- path: "**"
instructions: |
IMPORTANT: Only comment on issues directly introduced by this PR's code changes.
Treat AGENTS.md as mandatory repository policy, not optional style guidance.
Flag PR changes that violate AGENTS.md even when the code is otherwise functional.
In particular, enforce architecture boundaries, dtype/device/memory rules,
interface contracts, import style, no unnecessary try/except blocks, no inline
imports, no outbound internet paths in core ComfyUI, and narrow scoped fixes.
Prefer direct findings over suggestions when a rule is violated. Only ignore
AGENTS.md when it clearly conflicts with a newer explicit maintainer instruction
in the PR.
Do NOT flag pre-existing issues in code that was merely moved, re-indented,
de-indented, or reformatted without logic changes. If code appears in the diff
only due to whitespace or structural reformatting (e.g., removing a `with:` block),
@ -123,5 +131,10 @@ chat:
knowledge_base:
opt_out: false
code_guidelines:
enabled: true
filePatterns:
- files: "AGENTS.md"
applyTo: "**"
learnings:
scope: "auto"

View File

@ -543,18 +543,24 @@ class SDTokenizer:
def _try_get_embedding(self, embedding_name:str):
'''
Takes a potential embedding name and tries to retrieve it.
Returns a Tuple consisting of the embedding and any leftover string, embedding can be None.
Returns a Tuple consisting of the embedding, the cleaned embedding name, and any leftover string, embedding can be None.
'''
split_embed = embedding_name.split()
embedding_name = split_embed[0]
leftover = ' '.join(split_embed[1:])
match = re.search(r'[<\[]', embedding_name)
if match is not None:
leftover = embedding_name[match.start():] + (" " + leftover if leftover else "")
embedding_name = embedding_name[:match.start()]
embed = load_embed(embedding_name, self.embedding_directory, self.embedding_size, self.embedding_key)
if embed is None:
stripped = embedding_name.strip(',')
if len(stripped) < len(embedding_name):
embed = load_embed(stripped, self.embedding_directory, self.embedding_size, self.embedding_key)
return (embed, "{} {}".format(embedding_name[len(stripped):], leftover))
return (embed, leftover)
return (embed, embedding_name, "{} {}".format(embedding_name[len(stripped):], leftover))
return (embed, embedding_name, leftover)
def pad_tokens(self, tokens, amount):
if self.pad_left:
@ -585,7 +591,7 @@ class SDTokenizer:
tokens = []
for weighted_segment, weight in parsed_weights:
to_tokenize = unescape_important(weighted_segment)
split = re.split(' {0}|\n{0}'.format(self.embedding_identifier), to_tokenize)
split = re.split(r'(?<=\s){}'.format(re.escape(self.embedding_identifier)), to_tokenize)
to_tokenize = [split[0]]
for i in range(1, len(split)):
to_tokenize.append("{}{}".format(self.embedding_identifier, split[i]))
@ -595,7 +601,7 @@ class SDTokenizer:
# if we find an embedding, deal with the embedding
if word.startswith(self.embedding_identifier) and self.embedding_directory is not None:
embedding_name = word[len(self.embedding_identifier):].strip('\n')
embed, leftover = self._try_get_embedding(embedding_name)
embed, embedding_name, leftover = self._try_get_embedding(embedding_name)
if embed is None:
logging.warning(f"warning, embedding:{embedding_name} does not exist, ignoring")
else:

View File

@ -937,22 +937,41 @@ class BaseGenerate:
return torch.argmax(logits, dim=-1, keepdim=True)
# Sampling mode
if repetition_penalty != 1.0:
for i in range(logits.shape[0]):
for token_id in set(token_history):
logits[i, token_id] *= repetition_penalty if logits[i, token_id] < 0 else 1/repetition_penalty
if presence_penalty is not None and presence_penalty != 0.0:
for i in range(logits.shape[0]):
for token_id in set(token_history):
logits[i, token_id] -= presence_penalty
if len(token_history) > 0 and (repetition_penalty != 1.0 or (presence_penalty is not None and presence_penalty != 0.0)):
token_ids = torch.tensor(list(set(token_history)), device=logits.device)
token_logits = logits[:, token_ids]
if repetition_penalty != 1.0:
token_logits = torch.where(token_logits < 0, token_logits * repetition_penalty, token_logits / repetition_penalty)
if presence_penalty is not None and presence_penalty != 0.0:
token_logits = token_logits - presence_penalty
logits[:, token_ids] = token_logits
if temperature != 1.0:
logits = logits / temperature
if top_k > 0:
indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
logits[indices_to_remove] = torch.finfo(logits.dtype).min
top_k = min(top_k, logits.shape[-1])
logits, top_indices = torch.topk(logits, top_k)
if min_p > 0.0:
probs_before_filter = torch.nn.functional.softmax(logits, dim=-1)
top_probs, _ = probs_before_filter.max(dim=-1, keepdim=True)
min_threshold = min_p * top_probs
indices_to_remove = probs_before_filter < min_threshold
logits[indices_to_remove] = torch.finfo(logits.dtype).min
if top_p < 1.0:
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
cumulative_probs = torch.cumsum(torch.nn.functional.softmax(sorted_logits, dim=-1), dim=-1)
sorted_indices_to_remove = cumulative_probs > top_p
sorted_indices_to_remove[..., 0] = False
indices_to_remove = torch.zeros_like(logits, dtype=torch.bool)
indices_to_remove.scatter_(1, sorted_indices, sorted_indices_to_remove)
logits[indices_to_remove] = torch.finfo(logits.dtype).min
probs = torch.nn.functional.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, num_samples=1, generator=generator)
return top_indices.gather(1, next_token)
if min_p > 0.0:
probs_before_filter = torch.nn.functional.softmax(logits, dim=-1)

View File

@ -0,0 +1,48 @@
from pydantic import BaseModel, Field
class CambAITTSRequest(BaseModel):
text: str = Field(..., description="Text to convert to speech")
voice_id: int = Field(..., description="Voice ID for TTS")
language: str = Field(..., description="Language code (e.g., 'en-us')")
speech_model: str = Field(..., description="TTS model to use")
output_configuration: dict = Field(
default_factory=lambda: {"format": "wav"},
description="Output format configuration",
)
class CambAITranslateRequest(BaseModel):
source_language: int = Field(..., description="Source language ID")
target_language: int = Field(..., description="Target language ID")
texts: list[str] = Field(..., description="Texts to translate")
class CambAITaskResponse(BaseModel):
task_id: str = Field(..., description="Async task ID")
class CambAIPollResult(BaseModel):
status: str = Field(..., description="Task status")
run_id: int | None = Field(None, description="Run ID for fetching results")
class CambAITranslateResult(BaseModel):
texts: list[str] = Field(default_factory=list, description="Translated texts")
class CambAIDialogueItem(BaseModel):
start: float = Field(..., description="Start time in seconds")
end: float = Field(..., description="End time in seconds")
text: str = Field(..., description="Dialogue text")
speaker: str = Field(..., description="Speaker identifier")
class CambAIVoiceCloneResponse(BaseModel):
voice_id: int = Field(..., description="Cloned voice ID")
class CambAITextToSoundRequest(BaseModel):
prompt: str = Field(..., description="Text prompt for sound generation")
audio_type: str = Field(..., description="Type of audio: 'sound' or 'music'")
duration: float = Field(..., description="Duration in seconds")

View File

@ -0,0 +1,467 @@
import os
from typing_extensions import override
from comfy_api.latest import IO, ComfyExtension, Input
from comfy_api_nodes.apis.cambai import (
CambAIDialogueItem,
CambAIPollResult,
CambAITaskResponse,
CambAITextToSoundRequest,
CambAITranslateRequest,
CambAITranslateResult,
CambAITTSRequest,
CambAIVoiceCloneResponse,
)
from comfy_api_nodes.util import (
ApiEndpoint,
audio_bytes_to_audio_input,
audio_ndarray_to_bytesio,
audio_tensor_to_contiguous_ndarray,
poll_op,
sync_op,
sync_op_raw,
validate_string,
)
CAMBAI_API_BASE = "https://client.camb.ai/apis"
CAMBAI_VOICE = "CAMBAI_VOICE"
CAMBAI_GENDER_MAP = {"male": 0, "female": 1, "other": 2, "prefer not to say": 9}
def _cambai_endpoint(route: str, method: str = "GET") -> ApiEndpoint:
api_key = os.environ.get("CAMBAI_API_KEY", "")
return ApiEndpoint(
path=f"{CAMBAI_API_BASE}/{route}",
method=method,
headers={"x-api-key": api_key},
)
CAMBAI_LANGUAGES_TTS = [
"en-us", "es-es", "fr-fr", "de-de", "it-it", "pt-br",
"zh-cn", "ja-jp", "ko-kr", "ar-sa", "hi-in", "ru-ru",
"nl-nl", "pl-pl", "tr-tr", "sv-se",
]
CAMBAI_LANGUAGE_MAP = {
"English": 1, "Spanish": 54, "French": 76, "German": 31,
"Italian": 83, "Portuguese": 112, "Chinese": 139, "Japanese": 88,
"Korean": 93, "Arabic": 4, "Hindi": 73, "Russian": 116,
"Dutch": 103, "Polish": 110, "Turkish": 133, "Swedish": 125,
}
CAMBAI_TRANSCRIPTION_LANGUAGE_MAP = {
"English": 1, "Spanish": 54, "French": 76, "German": 31,
"Italian": 83, "Portuguese": 112, "Chinese": 139, "Japanese": 88,
"Korean": 93, "Arabic": 4, "Hindi": 73, "Russian": 116,
}
class CambAIVoiceSelector(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="CambAIVoiceSelector",
display_name="CAMB AI Voice Selector",
category="api node/audio/CAMB AI",
description="Select a CAMB AI voice by ID for text-to-speech generation.",
inputs=[
IO.Int.Input(
"voice_id",
default=147320,
min=1,
max=999999999,
tooltip="Voice ID to use for CAMB AI TTS.",
),
],
outputs=[
IO.Custom(CAMBAI_VOICE).Output(display_name="voice"),
],
is_api_node=False,
)
@classmethod
def execute(cls, voice_id: int) -> IO.NodeOutput:
return IO.NodeOutput(voice_id)
class CambAIVoiceClone(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="CambAIVoiceClone",
display_name="CAMB AI Voice Clone",
category="api node/audio/CAMB AI",
description="Create a custom cloned voice from an audio sample.",
inputs=[
IO.Audio.Input(
"audio",
tooltip="Audio sample of the voice to clone.",
),
IO.String.Input(
"voice_name",
default="My Custom Voice",
tooltip="Name for the cloned voice.",
),
IO.Combo.Input(
"gender",
options=["male", "female", "other", "prefer not to say"],
default="male",
tooltip="Gender of the voice to clone.",
),
],
outputs=[
IO.Custom(CAMBAI_VOICE).Output(display_name="voice"),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
)
@classmethod
async def execute(
cls,
audio: Input.Audio,
voice_name: str,
gender: str,
) -> IO.NodeOutput:
audio_data_np = audio_tensor_to_contiguous_ndarray(audio["waveform"])
audio_bytes_io = audio_ndarray_to_bytesio(audio_data_np, audio["sample_rate"], "wav", "pcm_s16le")
response = await sync_op(
cls,
_cambai_endpoint("create-custom-voice", "POST"),
response_model=CambAIVoiceCloneResponse,
data=None,
files={
"voice_name": (None, voice_name),
"gender": (None, str(CAMBAI_GENDER_MAP[gender])),
"file": ("voice.wav", audio_bytes_io.getvalue(), "audio/wav"),
},
content_type="multipart/form-data",
)
return IO.NodeOutput(response.voice_id)
class CambAITextToSpeech(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="CambAITextToSpeech",
display_name="CAMB AI Text to Speech",
category="api node/audio/CAMB AI",
description="Convert text to speech using CAMB AI TTS models.",
inputs=[
IO.Custom(CAMBAI_VOICE).Input(
"voice",
tooltip="Voice to use for speech synthesis. Connect from Voice Selector or Voice Clone.",
),
IO.String.Input(
"text",
multiline=True,
default="",
tooltip="The text to convert to speech.",
),
IO.Combo.Input(
"language",
options=CAMBAI_LANGUAGES_TTS,
default="en-us",
tooltip="Language for speech synthesis.",
),
IO.Combo.Input(
"model",
options=["mars-flash", "mars-pro", "mars-instruct"],
default="mars-flash",
tooltip="TTS model to use.",
),
],
outputs=[
IO.Audio.Output(),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
)
@classmethod
async def execute(
cls,
voice: int,
text: str,
language: str,
model: str,
) -> IO.NodeOutput:
validate_string(text, min_length=1)
request = CambAITTSRequest(
text=text,
voice_id=voice,
language=language,
speech_model=model,
)
response = await sync_op_raw(
cls,
_cambai_endpoint("tts-stream", "POST"),
data=request,
as_binary=True,
)
return IO.NodeOutput(audio_bytes_to_audio_input(response))
class CambAITranslation(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="CambAITranslation",
display_name="CAMB AI Translation",
category="api node/text/CAMB AI",
description="Translate text between languages using CAMB AI.",
inputs=[
IO.String.Input(
"text",
multiline=True,
default="",
tooltip="Text to translate.",
),
IO.Combo.Input(
"source_language",
options=list(CAMBAI_LANGUAGE_MAP.keys()),
default="English",
tooltip="Source language.",
),
IO.Combo.Input(
"target_language",
options=list(CAMBAI_LANGUAGE_MAP.keys()),
default="Spanish",
tooltip="Target language.",
),
],
outputs=[
IO.String.Output(display_name="text"),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
)
@classmethod
async def execute(
cls,
text: str,
source_language: str,
target_language: str,
) -> IO.NodeOutput:
validate_string(text, min_length=1)
src_id = CAMBAI_LANGUAGE_MAP[source_language]
tgt_id = CAMBAI_LANGUAGE_MAP[target_language]
request = CambAITranslateRequest(
source_language=src_id,
target_language=tgt_id,
texts=[text],
)
response = await sync_op(
cls,
_cambai_endpoint("translate", "POST"),
response_model=CambAITaskResponse,
data=request,
)
poll_result = await poll_op(
cls,
_cambai_endpoint(f"translate/{response.task_id}"),
response_model=CambAIPollResult,
status_extractor=lambda x: x.status,
)
if not poll_result.run_id:
raise ValueError("No run_id returned from CAMB AI translation task.")
result = await sync_op(
cls,
_cambai_endpoint(f"translation-result/{poll_result.run_id}"),
response_model=CambAITranslateResult,
)
if result.texts and len(result.texts) > 0:
return IO.NodeOutput(result.texts[0])
return IO.NodeOutput("")
class CambAITranscription(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="CambAITranscription",
display_name="CAMB AI Transcription",
category="api node/audio/CAMB AI",
description="Transcribe audio to text using CAMB AI.",
inputs=[
IO.Audio.Input(
"audio",
tooltip="Audio to transcribe.",
),
IO.Combo.Input(
"language",
options=list(CAMBAI_TRANSCRIPTION_LANGUAGE_MAP.keys()),
default="English",
tooltip="Language of the audio.",
),
],
outputs=[
IO.String.Output(display_name="text"),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
)
@classmethod
async def execute(
cls,
audio: Input.Audio,
language: str,
) -> IO.NodeOutput:
lang_id = CAMBAI_TRANSCRIPTION_LANGUAGE_MAP[language]
audio_data_np = audio_tensor_to_contiguous_ndarray(audio["waveform"])
audio_bytes_io = audio_ndarray_to_bytesio(audio_data_np, audio["sample_rate"], "wav", "pcm_s16le")
response = await sync_op(
cls,
_cambai_endpoint("transcribe", "POST"),
response_model=CambAITaskResponse,
data=None,
files={
"language": (None, str(lang_id)),
"media_file": ("audio.wav", audio_bytes_io.getvalue(), "audio/wav"),
},
content_type="multipart/form-data",
)
poll_result = await poll_op(
cls,
_cambai_endpoint(f"transcribe/{response.task_id}"),
response_model=CambAIPollResult,
status_extractor=lambda x: x.status,
)
if not poll_result.run_id:
raise ValueError("No run_id returned from CAMB AI transcription task.")
result_raw = await sync_op_raw(
cls,
_cambai_endpoint(f"transcription-result/{poll_result.run_id}"),
)
transcript = result_raw.get("transcript", [])
dialogues = [CambAIDialogueItem(**item) for item in transcript]
text = " ".join(item.text for item in dialogues)
return IO.NodeOutput(text)
class CambAITextToSound(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="CambAITextToSound",
display_name="CAMB AI Text to Sound",
category="api node/audio/CAMB AI",
description="Generate sound effects or music from a text description using CAMB AI.",
inputs=[
IO.String.Input(
"prompt",
multiline=True,
default="",
tooltip="Text description of the sound to generate.",
),
IO.Combo.Input(
"audio_type",
options=["sound", "music"],
default="sound",
tooltip="Type of audio to generate.",
),
IO.Float.Input(
"duration",
default=5.0,
min=0.5,
max=30.0,
step=0.5,
display_mode=IO.NumberDisplay.slider,
tooltip="Duration of generated audio in seconds.",
),
],
outputs=[
IO.Audio.Output(),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
)
@classmethod
async def execute(
cls,
prompt: str,
audio_type: str,
duration: float,
) -> IO.NodeOutput:
validate_string(prompt, min_length=1)
request = CambAITextToSoundRequest(
prompt=prompt,
audio_type=audio_type,
duration=duration,
)
response = await sync_op(
cls,
_cambai_endpoint("text-to-sound", "POST"),
response_model=CambAITaskResponse,
data=request,
)
poll_result = await poll_op(
cls,
_cambai_endpoint(f"text-to-sound/{response.task_id}"),
response_model=CambAIPollResult,
status_extractor=lambda x: x.status,
)
if not poll_result.run_id:
raise ValueError("No run_id returned from CAMB AI text-to-sound task.")
audio_bytes = await sync_op_raw(
cls,
_cambai_endpoint(f"text-to-sound-result/{poll_result.run_id}"),
as_binary=True,
)
return IO.NodeOutput(audio_bytes_to_audio_input(audio_bytes))
class CambAIExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
return [
CambAIVoiceSelector,
CambAIVoiceClone,
CambAITextToSpeech,
CambAITranslation,
CambAITranscription,
CambAITextToSound,
]
async def comfy_entrypoint() -> CambAIExtension:
return CambAIExtension()

View File

@ -9,6 +9,7 @@ from typing import Any
import folder_paths
logger = logging.getLogger(__name__)
_SENSITIVE_HEADERS = {"authorization", "x-api-key"}
def get_log_directory():
@ -73,6 +74,10 @@ def _format_data_for_logging(data: Any) -> str:
return str(data)
def _redact_headers(headers: dict) -> dict:
return {k: ("***" if k.lower() in _SENSITIVE_HEADERS else v) for k, v in headers.items()}
def log_request_response(
operation_id: str,
request_method: str,
@ -101,7 +106,7 @@ def log_request_response(
log_content.append(f"Method: {request_method}")
log_content.append(f"URL: {request_url}")
if request_headers:
log_content.append(f"Headers:\n{_format_data_for_logging(request_headers)}")
log_content.append(f"Headers:\n{_format_data_for_logging(_redact_headers(request_headers))}")
if request_params:
log_content.append(f"Params:\n{_format_data_for_logging(request_params)}")
if request_data is not None:

View File

@ -16,23 +16,30 @@ class ColorToRGBInt(io.ComfyNode):
],
outputs=[
io.Int.Output(display_name="rgb_int"),
io.Color.Output(display_name="hex")
io.Color.Output(display_name="hex"),
io.Float.Output(display_name="alpha"),
],
)
@classmethod
def execute(cls, color: str) -> io.NodeOutput:
# expect format #RRGGBB
if len(color) != 7 or color[0] != "#":
raise ValueError("Color must be in format #RRGGBB")
# expect format #RRGGBB or #RRGGBBAA
if len(color) not in (7, 9) or color[0] != "#":
raise ValueError("Color must be in format #RRGGBB or #RRGGBBAA")
try:
int(color[1:], 16)
except ValueError:
raise ValueError("Color must be in format #RRGGBB") from None
raise ValueError("Color must be in format #RRGGBB or #RRGGBBAA") from None
alpha = 1.0
if len(color) == 9:
alpha = int(color[7:9], 16) / 255.0
color = color[:7]
r, g, b = hex_to_rgb(color)
rgb_int = r * 256 * 256 + g * 256 + b
return io.NodeOutput(rgb_int, color)
return io.NodeOutput(rgb_int, color, alpha)
class ColorExtension(ComfyExtension):

View File

@ -1,6 +1,6 @@
comfyui-frontend-package==1.45.20
comfyui-workflow-templates==0.11.2
comfyui-embedded-docs==0.5.6
comfyui-embedded-docs==0.5.7
torch
torchsde
torchvision