Skip to content

Commit 65c5dba

Browse files
authored
Merge branch 'master' into codex/history-query-validation
2 parents 2274a5d + 1de83f9 commit 65c5dba

File tree

8 files changed

+298
-10
lines changed

8 files changed

+298
-10
lines changed

comfy/ldm/ernie/model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def rope(pos: torch.Tensor, dim: int, theta: int) -> torch.Tensor:
1515

1616
scale = torch.arange(0, dim, 2, dtype=torch.float64, device=device) / dim
1717
omega = 1.0 / (theta**scale)
18-
out = torch.einsum("...n,d->...nd", pos, omega)
18+
out = torch.einsum("...n,d->...nd", pos.to(device), omega)
1919
out = torch.stack([torch.cos(out), torch.sin(out)], dim=0)
2020
return out.to(dtype=torch.float32, device=pos.device)
2121

@@ -279,7 +279,7 @@ def forward(self, x, timesteps, context, **kwargs):
279279
rotary_pos_emb = self.pos_embed(torch.cat([image_ids, text_ids], dim=1)).to(x.dtype)
280280
del image_ids, text_ids
281281

282-
sample = self.time_proj(timesteps.to(dtype)).to(self.time_embedding.linear_1.weight.dtype)
282+
sample = self.time_proj(timesteps).to(dtype)
283283
c = self.time_embedding(sample)
284284

285285
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = [

comfy/ops.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1151,7 +1151,7 @@ def _apply(self, fn, recurse=True): # This is to get torch.compile + moving wei
11511151
if param is None:
11521152
continue
11531153
p = fn(param)
1154-
if p.is_inference():
1154+
if (not torch.is_inference_mode_enabled()) and p.is_inference():
11551155
p = p.clone()
11561156
self.register_parameter(key, torch.nn.Parameter(p, requires_grad=False))
11571157
for key, buf in self._buffers.items():

comfy/text_encoders/llama.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class Ministral3_3BConfig:
8282
rope_scale = None
8383
final_norm: bool = True
8484
lm_head: bool = False
85+
stop_tokens = [2]
8586

8687
@dataclass
8788
class Qwen25_3BConfig:
@@ -969,7 +970,7 @@ def __init__(self, config_dict, dtype, device, operations):
969970
self.model = Llama2_(config, device=device, dtype=dtype, ops=operations)
970971
self.dtype = dtype
971972

972-
class Ministral3_3B(BaseLlama, torch.nn.Module):
973+
class Ministral3_3B(BaseLlama, BaseQwen3, BaseGenerate, torch.nn.Module):
973974
def __init__(self, config_dict, dtype, device, operations):
974975
super().__init__()
975976
config = Ministral3_3BConfig(**config_dict)

comfy_api_nodes/nodes_sonilo.py

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import base64
2+
import json
3+
import logging
4+
import time
5+
from urllib.parse import urljoin
6+
7+
import aiohttp
8+
from typing_extensions import override
9+
10+
from comfy_api.latest import IO, ComfyExtension, Input
11+
from comfy_api_nodes.util import (
12+
ApiEndpoint,
13+
audio_bytes_to_audio_input,
14+
upload_video_to_comfyapi,
15+
validate_string,
16+
)
17+
from comfy_api_nodes.util._helpers import (
18+
default_base_url,
19+
get_auth_header,
20+
get_node_id,
21+
is_processing_interrupted,
22+
)
23+
from comfy_api_nodes.util.common_exceptions import ProcessingInterrupted
24+
from server import PromptServer
25+
26+
logger = logging.getLogger(__name__)
27+
28+
29+
class SoniloVideoToMusic(IO.ComfyNode):
30+
"""Generate music from video using Sonilo's AI model."""
31+
32+
@classmethod
33+
def define_schema(cls) -> IO.Schema:
34+
return IO.Schema(
35+
node_id="SoniloVideoToMusic",
36+
display_name="Sonilo Video to Music",
37+
category="api node/audio/Sonilo",
38+
description="Generate music from video content using Sonilo's AI model. "
39+
"Analyzes the video and creates matching music.",
40+
inputs=[
41+
IO.Video.Input(
42+
"video",
43+
tooltip="Input video to generate music from. Maximum duration: 6 minutes.",
44+
),
45+
IO.String.Input(
46+
"prompt",
47+
default="",
48+
multiline=True,
49+
tooltip="Optional text prompt to guide music generation. "
50+
"Leave empty for best quality - the model will fully analyze the video content.",
51+
),
52+
IO.Int.Input(
53+
"seed",
54+
default=0,
55+
min=0,
56+
max=0xFFFFFFFFFFFFFFFF,
57+
control_after_generate=True,
58+
tooltip="Seed for reproducibility. Currently ignored by the Sonilo "
59+
"service but kept for graph consistency.",
60+
),
61+
],
62+
outputs=[IO.Audio.Output()],
63+
hidden=[
64+
IO.Hidden.auth_token_comfy_org,
65+
IO.Hidden.api_key_comfy_org,
66+
IO.Hidden.unique_id,
67+
],
68+
is_api_node=True,
69+
price_badge=IO.PriceBadge(
70+
expr='{"type":"usd","usd":0.009,"format":{"suffix":"/second"}}',
71+
),
72+
)
73+
74+
@classmethod
75+
async def execute(
76+
cls,
77+
video: Input.Video,
78+
prompt: str = "",
79+
seed: int = 0,
80+
) -> IO.NodeOutput:
81+
video_url = await upload_video_to_comfyapi(cls, video, max_duration=360)
82+
form = aiohttp.FormData()
83+
form.add_field("video_url", video_url)
84+
if prompt.strip():
85+
form.add_field("prompt", prompt.strip())
86+
audio_bytes = await _stream_sonilo_music(
87+
cls,
88+
ApiEndpoint(path="/proxy/sonilo/v2m/generate", method="POST"),
89+
form,
90+
)
91+
return IO.NodeOutput(audio_bytes_to_audio_input(audio_bytes))
92+
93+
94+
class SoniloTextToMusic(IO.ComfyNode):
95+
"""Generate music from a text prompt using Sonilo's AI model."""
96+
97+
@classmethod
98+
def define_schema(cls) -> IO.Schema:
99+
return IO.Schema(
100+
node_id="SoniloTextToMusic",
101+
display_name="Sonilo Text to Music",
102+
category="api node/audio/Sonilo",
103+
description="Generate music from a text prompt using Sonilo's AI model. "
104+
"Leave duration at 0 to let the model infer it from the prompt.",
105+
inputs=[
106+
IO.String.Input(
107+
"prompt",
108+
default="",
109+
multiline=True,
110+
tooltip="Text prompt describing the music to generate.",
111+
),
112+
IO.Int.Input(
113+
"duration",
114+
default=0,
115+
min=0,
116+
max=360,
117+
tooltip="Target duration in seconds. Set to 0 to let the model "
118+
"infer the duration from the prompt. Maximum: 6 minutes.",
119+
),
120+
IO.Int.Input(
121+
"seed",
122+
default=0,
123+
min=0,
124+
max=0xFFFFFFFFFFFFFFFF,
125+
control_after_generate=True,
126+
tooltip="Seed for reproducibility. Currently ignored by the Sonilo "
127+
"service but kept for graph consistency.",
128+
),
129+
],
130+
outputs=[IO.Audio.Output()],
131+
hidden=[
132+
IO.Hidden.auth_token_comfy_org,
133+
IO.Hidden.api_key_comfy_org,
134+
IO.Hidden.unique_id,
135+
],
136+
is_api_node=True,
137+
price_badge=IO.PriceBadge(
138+
depends_on=IO.PriceBadgeDepends(widgets=["duration"]),
139+
expr="""
140+
(
141+
widgets.duration > 0
142+
? {"type":"usd","usd": 0.005 * widgets.duration}
143+
: {"type":"usd","usd": 0.005, "format":{"suffix":"/second"}}
144+
)
145+
""",
146+
),
147+
)
148+
149+
@classmethod
150+
async def execute(
151+
cls,
152+
prompt: str,
153+
duration: int = 0,
154+
seed: int = 0,
155+
) -> IO.NodeOutput:
156+
validate_string(prompt, strip_whitespace=True, min_length=1)
157+
form = aiohttp.FormData()
158+
form.add_field("prompt", prompt)
159+
if duration > 0:
160+
form.add_field("duration", str(duration))
161+
audio_bytes = await _stream_sonilo_music(
162+
cls,
163+
ApiEndpoint(path="/proxy/sonilo/t2m/generate", method="POST"),
164+
form,
165+
)
166+
return IO.NodeOutput(audio_bytes_to_audio_input(audio_bytes))
167+
168+
169+
async def _stream_sonilo_music(
170+
cls: type[IO.ComfyNode],
171+
endpoint: ApiEndpoint,
172+
form: aiohttp.FormData,
173+
) -> bytes:
174+
"""POST ``form`` to Sonilo, read the NDJSON stream, and return the first stream's audio bytes."""
175+
url = urljoin(default_base_url().rstrip("/") + "/", endpoint.path.lstrip("/"))
176+
177+
headers: dict[str, str] = {}
178+
headers.update(get_auth_header(cls))
179+
headers.update(endpoint.headers)
180+
181+
node_id = get_node_id(cls)
182+
start_ts = time.monotonic()
183+
last_chunk_status_ts = 0.0
184+
audio_streams: dict[int, list[bytes]] = {}
185+
title: str | None = None
186+
187+
timeout = aiohttp.ClientTimeout(total=1200.0, sock_read=300.0)
188+
async with aiohttp.ClientSession(timeout=timeout) as session:
189+
PromptServer.instance.send_progress_text("Status: Queued", node_id)
190+
async with session.post(url, data=form, headers=headers) as resp:
191+
if resp.status >= 400:
192+
msg = await _extract_error_message(resp)
193+
raise Exception(f"Sonilo API error ({resp.status}): {msg}")
194+
195+
while True:
196+
if is_processing_interrupted():
197+
raise ProcessingInterrupted("Task cancelled")
198+
199+
raw_line = await resp.content.readline()
200+
if not raw_line:
201+
break
202+
203+
line = raw_line.decode("utf-8").strip()
204+
if not line:
205+
continue
206+
207+
try:
208+
evt = json.loads(line)
209+
except json.JSONDecodeError:
210+
logger.warning("Sonilo: skipping malformed NDJSON line")
211+
continue
212+
213+
evt_type = evt.get("type")
214+
if evt_type == "error":
215+
code = evt.get("code", "UNKNOWN")
216+
message = evt.get("message", "Unknown error")
217+
raise Exception(f"Sonilo generation error ({code}): {message}")
218+
if evt_type == "duration":
219+
duration_sec = evt.get("duration_sec")
220+
if duration_sec is not None:
221+
PromptServer.instance.send_progress_text(
222+
f"Status: Generating\nVideo duration: {duration_sec:.1f}s",
223+
node_id,
224+
)
225+
elif evt_type in ("titles", "title"):
226+
# v2m sends a "titles" list, t2m sends a scalar "title"
227+
if evt_type == "titles":
228+
titles = evt.get("titles", [])
229+
if titles:
230+
title = titles[0]
231+
else:
232+
title = evt.get("title") or title
233+
if title:
234+
PromptServer.instance.send_progress_text(
235+
f"Status: Generating\nTitle: {title}",
236+
node_id,
237+
)
238+
elif evt_type == "audio_chunk":
239+
stream_idx = evt.get("stream_index", 0)
240+
chunk_data = base64.b64decode(evt["data"])
241+
242+
if stream_idx not in audio_streams:
243+
audio_streams[stream_idx] = []
244+
audio_streams[stream_idx].append(chunk_data)
245+
246+
now = time.monotonic()
247+
if now - last_chunk_status_ts >= 1.0:
248+
total_chunks = sum(len(chunks) for chunks in audio_streams.values())
249+
elapsed = int(now - start_ts)
250+
status_lines = ["Status: Receiving audio"]
251+
if title:
252+
status_lines.append(f"Title: {title}")
253+
status_lines.append(f"Chunks received: {total_chunks}")
254+
status_lines.append(f"Time elapsed: {elapsed}s")
255+
PromptServer.instance.send_progress_text("\n".join(status_lines), node_id)
256+
last_chunk_status_ts = now
257+
elif evt_type == "complete":
258+
break
259+
260+
if not audio_streams:
261+
raise Exception("Sonilo API returned no audio data.")
262+
263+
PromptServer.instance.send_progress_text("Status: Completed", node_id)
264+
selected_stream = 0 if 0 in audio_streams else min(audio_streams)
265+
return b"".join(audio_streams[selected_stream])
266+
267+
268+
async def _extract_error_message(resp: aiohttp.ClientResponse) -> str:
269+
"""Extract a human-readable error message from an HTTP error response."""
270+
try:
271+
error_body = await resp.json()
272+
detail = error_body.get("detail", {})
273+
if isinstance(detail, dict):
274+
return detail.get("message", str(detail))
275+
return str(detail)
276+
except Exception:
277+
return await resp.text()
278+
279+
280+
class SoniloExtension(ComfyExtension):
281+
@override
282+
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
283+
return [SoniloVideoToMusic, SoniloTextToMusic]
284+
285+
286+
async def comfy_entrypoint() -> SoniloExtension:
287+
return SoniloExtension()

comfy_extras/nodes_preview_any.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def INPUT_TYPES(cls):
1111
"required": {"source": (IO.ANY, {})},
1212
}
1313

14-
RETURN_TYPES = ()
14+
RETURN_TYPES = (IO.STRING,)
1515
FUNCTION = "main"
1616
OUTPUT_NODE = True
1717

@@ -33,7 +33,7 @@ def main(self, source=None):
3333
except Exception:
3434
value = 'source exists, but could not be serialized.'
3535

36-
return {"ui": {"text": (value,)}}
36+
return {"ui": {"text": (value,)}, "result": (value,)}
3737

3838
NODE_CLASS_MAPPINGS = {
3939
"PreviewAny": PreviewAny,

comfyui_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# This file is automatically generated by the build process when version is
22
# updated in pyproject.toml.
3-
__version__ = "0.19.0"
3+
__version__ = "0.19.1"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "ComfyUI"
3-
version = "0.19.0"
3+
version = "0.19.1"
44
readme = "README.md"
55
license = { file = "LICENSE" }
66
requires-python = ">=3.10"

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
comfyui-frontend-package==1.42.10
2-
comfyui-workflow-templates==0.9.47
1+
comfyui-frontend-package==1.42.11
2+
comfyui-workflow-templates==0.9.54
33
comfyui-embedded-docs==0.4.3
44
torch
55
torchsde

0 commit comments

Comments
 (0)