mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-14 02:17:13 +08:00
Compare commits
8 Commits
d11bc6e7c6
...
5e368bd667
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e368bd667 | ||
|
|
b08debceca | ||
|
|
000c6b784e | ||
|
|
985fb9d6ad | ||
|
|
7f287b705e | ||
|
|
b7ba504e06 | ||
|
|
6c62ca0b6b | ||
|
|
6a24746d14 |
@ -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"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
48
comfy_api_nodes/apis/cambai.py
Normal file
48
comfy_api_nodes/apis/cambai.py
Normal 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")
|
||||
467
comfy_api_nodes/nodes_cambai.py
Normal file
467
comfy_api_nodes/nodes_cambai.py
Normal 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()
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user