Skip to content

[Partner Nodes] add Sonilo Audio nodes#13391

Merged
Kosinkadink merged 3 commits intomasterfrom
feat/api-nodes/sonilo-nodes
Apr 14, 2026
Merged

[Partner Nodes] add Sonilo Audio nodes#13391
Kosinkadink merged 3 commits intomasterfrom
feat/api-nodes/sonilo-nodes

Conversation

@bigcat88
Copy link
Copy Markdown
Contributor

@bigcat88 bigcat88 commented Apr 13, 2026

API Node PR Checklist

Scope

  • Is API Node Change

Pricing & Billing

  • Need pricing update
  • No pricing update

If Need pricing update:

  • Metronome rate cards updated
  • Auto‑billing tests updated and passing

QA

  • QA done
  • QA not required

Comms

  • Informed Kosinkadink

@bigcat88 bigcat88 changed the title [Parner Nodes] add Sonilo Audio nodes [Partner Nodes] add Sonilo Audio nodes Apr 13, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

Adds a new ComfyUI extension module comfy_api_nodes/nodes_sonilo.py that introduces two API nodes: SoniloVideoToMusic (accepts a video upload, enforces a 360s cap, proxies video-to-music requests) and SoniloTextToMusic (accepts a non-empty prompt and optional duration, proxies text-to-music requests). Both use a shared _stream_sonilo_music helper that POSTs multipart data, reads NDJSON stream responses, handles events (error, duration, title(s), audio_chunk, complete), decodes and accumulates base64 audio chunks per stream index, and returns the concatenated audio bytes.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding Sonilo Audio nodes as partner nodes, which directly aligns with the new module introducing SoniloVideoToMusic and SoniloTextToMusic nodes.
Description check ✅ Passed The PR description includes a completed API Node PR Checklist that directly relates to the changeset, which introduces new Sonilo audio nodes with pricing and QA considerations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@comfy_api_nodes/nodes_sonilo.py`:
- Around line 259-260: The return currently uses
next(iter(audio_streams.values())) which picks whichever stream yields first;
change it to deterministically pick stream index 0 when present (or a stable
fallback like the lowest numeric key) before joining bytes so outputs don't vary
with network timing—locate the code in nodes_sonilo.py where
PromptServer.instance.send_progress_text is called and replace the
non-deterministic selection with a lookup for audio_streams[0] if it exists,
otherwise sort the keys and pick the first key consistently, then b"".join(...)
that chosen stream.
- Around line 186-200: Wrap the Sonilo API request/response block (the
aiohttp.ClientSession post call and the resp.content.readline loop inside the
async with) in a try/except that catches asyncio.TimeoutError and
aiohttp.ClientError, and re-raises a clear, consistent Exception (or a
domain-specific exception if used elsewhere) with a user-friendly message that
includes the original exception text; ensure you still call
PromptServer.instance.send_progress_text("Status: Queued", node_id) before the
request, preserve the existing handling for HTTP errors using
_extract_error_message, and keep the
is_processing_interrupted()/ProcessingInterrupted flow intact so cancellation
still works.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8cc53e19-3cae-4dd4-a3b4-43eb89beacd4

📥 Commits

Reviewing files that changed from the base of the PR and between acd7185 and fe47dd8.

📒 Files selected for processing (1)
  • comfy_api_nodes/nodes_sonilo.py

Comment thread comfy_api_nodes/nodes_sonilo.py
Comment thread comfy_api_nodes/nodes_sonilo.py Outdated
@bigcat88 bigcat88 force-pushed the feat/api-nodes/sonilo-nodes branch from fe47dd8 to 06c108c Compare April 13, 2026 18:09
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
comfy_api_nodes/nodes_sonilo.py (1)

186-200: ⚠️ Potential issue | 🟠 Major

Wrap network/timeout errors in explicit error handling.

The aiohttp request can raise asyncio.TimeoutError or aiohttp.ClientError that would bubble up as low-level errors rather than user-friendly messages. Consider catching these and raising a clear exception.

Proposed fix
+import asyncio
 import base64
     timeout = aiohttp.ClientTimeout(total=1200.0, sock_read=300.0)
     async with aiohttp.ClientSession(timeout=timeout) as session:
         PromptServer.instance.send_progress_text("Status: Queued", node_id)
-        async with session.post(url, data=form, headers=headers) as resp:
-            if resp.status >= 400:
-                msg = await _extract_error_message(resp)
-                raise Exception(f"Sonilo API error ({resp.status}): {msg}")
-
-            while True:
+        try:
+            async with session.post(url, data=form, headers=headers) as resp:
+                if resp.status >= 400:
+                    msg = await _extract_error_message(resp)
+                    raise Exception(f"Sonilo API error ({resp.status}): {msg}")
+
+                while True:
                 # ... existing loop code indented one level ...
+        except asyncio.TimeoutError:
+            raise Exception("Sonilo API request timed out. Please try again.") from None
+        except aiohttp.ClientError as e:
+            raise Exception(f"Sonilo API network error: {e}") from e
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy_api_nodes/nodes_sonilo.py` around lines 186 - 200, Wrap the network
call and streaming read in explicit exception handling: around the
aiohttp.ClientSession() / session.post(...) and the resp.content.readline() loop
in the block using ClientSession, catch asyncio.TimeoutError and
aiohttp.ClientError (and optionally Exception for unexpected errors) and raise a
clear, user-friendly Exception (or a custom error) that includes context like
"Sonilo API request failed" and the original error message; preserve existing
behavior for HTTP error responses (the _extract_error_message and raise
Exception for resp.status >= 400) and re-raise ProcessingInterrupted unchanged
when is_processing_interrupted() is true so cancellation still works.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@comfy_api_nodes/nodes_sonilo.py`:
- Around line 156-158: The prompt is validated using
validate_string(strip_whitespace=True) but the original unstripped prompt is
added to the FormData; update the code after validation to assign the stripped
value (e.g., prompt = prompt.strip()) before calling form.add_field("prompt",
prompt) so it matches the behavior used in SoniloVideoToMusic and ensures
consistent whitespace handling between validate_string and form.add_field.
- Around line 237-243: In the audio_chunk handling branch (the block that reads
evt_type == "audio_chunk" and uses evt, stream_idx, audio_streams), validate the
presence and type of evt["data"] before calling base64.b64decode: use data =
evt.get("data") and if data is missing or not a str, raise or log a clear error
(e.g., ValueError with context including evt and stream_idx) and skip
processing; also wrap base64.b64decode(...) in a try/except to catch decoding
errors and surface a helpful message rather than letting a bare
KeyError/decoding exception bubble up.

---

Duplicate comments:
In `@comfy_api_nodes/nodes_sonilo.py`:
- Around line 186-200: Wrap the network call and streaming read in explicit
exception handling: around the aiohttp.ClientSession() / session.post(...) and
the resp.content.readline() loop in the block using ClientSession, catch
asyncio.TimeoutError and aiohttp.ClientError (and optionally Exception for
unexpected errors) and raise a clear, user-friendly Exception (or a custom
error) that includes context like "Sonilo API request failed" and the original
error message; preserve existing behavior for HTTP error responses (the
_extract_error_message and raise Exception for resp.status >= 400) and re-raise
ProcessingInterrupted unchanged when is_processing_interrupted() is true so
cancellation still works.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 705610dd-8555-4ead-89e5-21c92956ce4e

📥 Commits

Reviewing files that changed from the base of the PR and between fe47dd8 and 06c108c.

📒 Files selected for processing (1)
  • comfy_api_nodes/nodes_sonilo.py

Comment thread comfy_api_nodes/nodes_sonilo.py
Comment thread comfy_api_nodes/nodes_sonilo.py
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@comfy_api_nodes/nodes_sonilo.py`:
- Around line 195-201: The loop that reads resp.content.readline() in
nodes_sonilo.py (using is_processing_interrupted() and raising
ProcessingInterrupted) currently treats an unexpected EOF as a successful
completion; change the logic so that if raw_line is empty and you have not yet
seen the terminal "complete" event you raise an appropriate exception (e.g., an
API/StreamError) instead of breaking and returning "Completed"; update both the
occurrence around the readline loop and the similar block at lines 257-265 to
track whether a "complete" event was received (a boolean flag) and if EOF is
reached without that flag set, log/raise a descriptive error so truncated audio
is not reported as success.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 29d3efd8-0a05-410a-976b-a2e08d4dbf22

📥 Commits

Reviewing files that changed from the base of the PR and between 06c108c and 2070c99.

📒 Files selected for processing (1)
  • comfy_api_nodes/nodes_sonilo.py

Comment thread comfy_api_nodes/nodes_sonilo.py
Signed-off-by: bigcat88 <bigcat88@icloud.com>
Signed-off-by: bigcat88 <bigcat88@icloud.com>
Signed-off-by: bigcat88 <bigcat88@icloud.com>
@bigcat88 bigcat88 force-pushed the feat/api-nodes/sonilo-nodes branch from 2070c99 to adda132 Compare April 14, 2026 05:07
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
comfy_api_nodes/nodes_sonilo.py (4)

156-158: ⚠️ Potential issue | 🟡 Minor

Strip the validated prompt before sending it.

Line 156 validates the stripped value, but Line 158 still posts the original whitespace-padded prompt. That makes SoniloTextToMusic behave differently from SoniloVideoToMusic and can change generation for prompts with leading/trailing spaces.

Suggested fix
         validate_string(prompt, strip_whitespace=True, min_length=1)
+        prompt = prompt.strip()
         form = aiohttp.FormData()
         form.add_field("prompt", prompt)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy_api_nodes/nodes_sonilo.py` around lines 156 - 158, The code validates a
stripped prompt via validate_string(prompt, strip_whitespace=True) but then
sends the original unstripped prompt in form.add_field("prompt", prompt),
causing inconsistent behavior; update the flow so you use the stripped value
when posting (e.g., assign the validated/stripped result back to prompt or to a
new variable) before calling form.add_field in the SoniloTextToMusic-related
code path in comfy_api_nodes/nodes_sonilo.py.

238-240: ⚠️ Potential issue | 🟡 Minor

Validate audio_chunk payloads before decoding.

Line 240 assumes every audio_chunk includes a string data field with valid base64. A malformed event currently turns into a bare KeyError or decode exception instead of a clear Sonilo stream error. Guard evt.get("data") and the decode step so bad chunks fail with a readable Sonilo-specific message.

As per coding guidelines, comfy_api_nodes/** integrations should have proper error handling for API failures (timeouts, rate limits, auth errors).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy_api_nodes/nodes_sonilo.py` around lines 238 - 240, The audio_chunk
handling assumes evt["data"] exists and is valid base64; update the block that
checks evt_type == "audio_chunk" to validate evt.get("data") is a non-empty
string before calling base64.b64decode, handle base64.binascii.Error/TypeError
from decoding, and on any validation/decoding failure raise or emit a clear
Sonilo-specific error (include stream_index and the invalid payload summary)
instead of letting a KeyError/raw decode exception propagate; reference the
existing variables evt, stream_idx, and chunk_data when adding the guard and
error handling.

199-201: ⚠️ Potential issue | 🟠 Major

Don't report a dropped stream as completed.

If the backend disconnects after sending some chunks, Line 200 breaks the loop, Line 263 marks the node completed, and Line 265 returns truncated audio. Track whether a "complete" event was seen and fail on EOF before that.

Suggested fix
     last_chunk_status_ts = 0.0
     audio_streams: dict[int, list[bytes]] = {}
     title: str | None = None
+    saw_complete = False
@@
                 raw_line = await resp.content.readline()
                 if not raw_line:
-                    break
+                    if saw_complete:
+                        break
+                    raise Exception("Sonilo stream ended before completion.")
@@
                 elif evt_type == "complete":
+                    saw_complete = True
                     break

As per coding guidelines, comfy_api_nodes/** integrations should have proper error handling for API failures (timeouts, rate limits, auth errors).

Also applies to: 257-265

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy_api_nodes/nodes_sonilo.py` around lines 199 - 201, The code currently
breaks the read loop on EOF (raw_line = await resp.content.readline(); if not
raw_line: break) and later marks the node as completed and returns audio even if
no "complete" event was received; add a boolean (e.g., saw_complete = False)
that is set true when the handler sees the "complete" event and only consider
the stream successful if saw_complete is true, otherwise raise/propagate an
error on EOF and avoid marking the node completed or returning truncated audio.
Update the loop that reads resp.content.readline() and the event handling logic
that sets completion (the code paths that set completed/return audio) to consult
saw_complete, and also add proper error handling for backend failures (timeouts,
rate limits, auth) around the resp I/O to surface meaningful exceptions instead
of silently returning partial data.

187-190: ⚠️ Potential issue | 🟠 Major

Handle Sonilo transport failures explicitly.

Line 190 and Line 199 can raise aiohttp.ClientError or timeout exceptions that currently bubble up as raw library errors. Wrap the POST + stream-read block and re-raise a consistent Sonilo request failure instead of leaking low-level transport details.

As per coding guidelines, comfy_api_nodes/** integrations should have proper error handling for API failures (timeouts, rate limits, auth errors).

Also applies to: 199-200

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy_api_nodes/nodes_sonilo.py` around lines 187 - 190, Wrap the POST +
stream-read block around session.post(...) and the streaming read that follows
in a try/except that catches aiohttp.ClientError and asyncio.TimeoutError (and
any aiohttp.ClientResponseError), then re-raise a module-specific exception
(e.g., SoniloTransportError or SoniloRequestError) instead of letting low-level
errors bubble up; include the original exception as context (raise ... from e),
and update the error path to call
PromptServer.instance.send_progress_text("Status: Failed: <brief reason>",
node_id) so the node surface gets a consistent failure message; add the new
exception class to comfy_api_nodes/nodes_sonilo.py if it doesn't exist and use
it where the POST/stream code currently lives to standardize Sonilo transport
failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@comfy_api_nodes/nodes_sonilo.py`:
- Around line 156-158: The code validates a stripped prompt via
validate_string(prompt, strip_whitespace=True) but then sends the original
unstripped prompt in form.add_field("prompt", prompt), causing inconsistent
behavior; update the flow so you use the stripped value when posting (e.g.,
assign the validated/stripped result back to prompt or to a new variable) before
calling form.add_field in the SoniloTextToMusic-related code path in
comfy_api_nodes/nodes_sonilo.py.
- Around line 238-240: The audio_chunk handling assumes evt["data"] exists and
is valid base64; update the block that checks evt_type == "audio_chunk" to
validate evt.get("data") is a non-empty string before calling base64.b64decode,
handle base64.binascii.Error/TypeError from decoding, and on any
validation/decoding failure raise or emit a clear Sonilo-specific error (include
stream_index and the invalid payload summary) instead of letting a KeyError/raw
decode exception propagate; reference the existing variables evt, stream_idx,
and chunk_data when adding the guard and error handling.
- Around line 199-201: The code currently breaks the read loop on EOF (raw_line
= await resp.content.readline(); if not raw_line: break) and later marks the
node as completed and returns audio even if no "complete" event was received;
add a boolean (e.g., saw_complete = False) that is set true when the handler
sees the "complete" event and only consider the stream successful if
saw_complete is true, otherwise raise/propagate an error on EOF and avoid
marking the node completed or returning truncated audio. Update the loop that
reads resp.content.readline() and the event handling logic that sets completion
(the code paths that set completed/return audio) to consult saw_complete, and
also add proper error handling for backend failures (timeouts, rate limits,
auth) around the resp I/O to surface meaningful exceptions instead of silently
returning partial data.
- Around line 187-190: Wrap the POST + stream-read block around
session.post(...) and the streaming read that follows in a try/except that
catches aiohttp.ClientError and asyncio.TimeoutError (and any
aiohttp.ClientResponseError), then re-raise a module-specific exception (e.g.,
SoniloTransportError or SoniloRequestError) instead of letting low-level errors
bubble up; include the original exception as context (raise ... from e), and
update the error path to call PromptServer.instance.send_progress_text("Status:
Failed: <brief reason>", node_id) so the node surface gets a consistent failure
message; add the new exception class to comfy_api_nodes/nodes_sonilo.py if it
doesn't exist and use it where the POST/stream code currently lives to
standardize Sonilo transport failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 980176dc-9c6b-45d1-966d-0fb6fb3ba4df

📥 Commits

Reviewing files that changed from the base of the PR and between 2070c99 and adda132.

📒 Files selected for processing (1)
  • comfy_api_nodes/nodes_sonilo.py

@Kosinkadink Kosinkadink merged commit 35dfcbb into master Apr 14, 2026
17 checks passed
@bigcat88 bigcat88 deleted the feat/api-nodes/sonilo-nodes branch April 14, 2026 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants