Skip to content

Commit c646d21

Browse files
authored
feat(api-nodes): add Quiver SVG nodes (#13047)
1 parent 589228e commit c646d21

2 files changed

Lines changed: 334 additions & 0 deletions

File tree

comfy_api_nodes/apis/quiver.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from pydantic import BaseModel, Field
2+
3+
4+
class QuiverImageObject(BaseModel):
5+
url: str = Field(...)
6+
7+
8+
class QuiverTextToSVGRequest(BaseModel):
9+
model: str = Field(default="arrow-preview")
10+
prompt: str = Field(...)
11+
instructions: str | None = Field(default=None)
12+
references: list[QuiverImageObject] | None = Field(default=None, max_length=4)
13+
temperature: float | None = Field(default=None, ge=0, le=2)
14+
top_p: float | None = Field(default=None, ge=0, le=1)
15+
presence_penalty: float | None = Field(default=None, ge=-2, le=2)
16+
17+
18+
class QuiverImageToSVGRequest(BaseModel):
19+
model: str = Field(default="arrow-preview")
20+
image: QuiverImageObject = Field(...)
21+
auto_crop: bool | None = Field(default=None)
22+
target_size: int | None = Field(default=None, ge=128, le=4096)
23+
temperature: float | None = Field(default=None, ge=0, le=2)
24+
top_p: float | None = Field(default=None, ge=0, le=1)
25+
presence_penalty: float | None = Field(default=None, ge=-2, le=2)
26+
27+
28+
class QuiverSVGResponseItem(BaseModel):
29+
svg: str = Field(...)
30+
mime_type: str | None = Field(default="image/svg+xml")
31+
32+
33+
class QuiverSVGUsage(BaseModel):
34+
total_tokens: int | None = Field(default=None)
35+
input_tokens: int | None = Field(default=None)
36+
output_tokens: int | None = Field(default=None)
37+
38+
39+
class QuiverSVGResponse(BaseModel):
40+
id: str | None = Field(default=None)
41+
created: int | None = Field(default=None)
42+
data: list[QuiverSVGResponseItem] = Field(...)
43+
usage: QuiverSVGUsage | None = Field(default=None)

comfy_api_nodes/nodes_quiver.py

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
from io import BytesIO
2+
3+
from typing_extensions import override
4+
5+
from comfy_api.latest import IO, ComfyExtension
6+
from comfy_api_nodes.apis.quiver import (
7+
QuiverImageObject,
8+
QuiverImageToSVGRequest,
9+
QuiverSVGResponse,
10+
QuiverTextToSVGRequest,
11+
)
12+
from comfy_api_nodes.util import (
13+
ApiEndpoint,
14+
sync_op,
15+
upload_image_to_comfyapi,
16+
validate_string,
17+
)
18+
from comfy_extras.nodes_images import SVG
19+
20+
21+
class QuiverTextToSVGNode(IO.ComfyNode):
22+
@classmethod
23+
def define_schema(cls):
24+
return IO.Schema(
25+
node_id="QuiverTextToSVGNode",
26+
display_name="Quiver Text to SVG",
27+
category="api node/image/Quiver",
28+
description="Generate an SVG from a text prompt using Quiver AI.",
29+
inputs=[
30+
IO.String.Input(
31+
"prompt",
32+
multiline=True,
33+
default="",
34+
tooltip="Text description of the desired SVG output.",
35+
),
36+
IO.String.Input(
37+
"instructions",
38+
multiline=True,
39+
default="",
40+
tooltip="Additional style or formatting guidance.",
41+
optional=True,
42+
),
43+
IO.Autogrow.Input(
44+
"reference_images",
45+
template=IO.Autogrow.TemplatePrefix(
46+
IO.Image.Input("image"),
47+
prefix="ref_",
48+
min=0,
49+
max=4,
50+
),
51+
tooltip="Up to 4 reference images to guide the generation.",
52+
optional=True,
53+
),
54+
IO.DynamicCombo.Input(
55+
"model",
56+
options=[
57+
IO.DynamicCombo.Option(
58+
"arrow-preview",
59+
[
60+
IO.Float.Input(
61+
"temperature",
62+
default=1.0,
63+
min=0.0,
64+
max=2.0,
65+
step=0.1,
66+
display_mode=IO.NumberDisplay.slider,
67+
tooltip="Randomness control. Higher values increase randomness.",
68+
advanced=True,
69+
),
70+
IO.Float.Input(
71+
"top_p",
72+
default=1.0,
73+
min=0.05,
74+
max=1.0,
75+
step=0.05,
76+
display_mode=IO.NumberDisplay.slider,
77+
tooltip="Nucleus sampling parameter.",
78+
advanced=True,
79+
),
80+
IO.Float.Input(
81+
"presence_penalty",
82+
default=0.0,
83+
min=-2.0,
84+
max=2.0,
85+
step=0.1,
86+
display_mode=IO.NumberDisplay.slider,
87+
tooltip="Token presence penalty.",
88+
advanced=True,
89+
),
90+
],
91+
),
92+
],
93+
tooltip="Model to use for SVG generation.",
94+
),
95+
IO.Int.Input(
96+
"seed",
97+
default=0,
98+
min=0,
99+
max=2147483647,
100+
control_after_generate=True,
101+
tooltip="Seed to determine if node should re-run; "
102+
"actual results are nondeterministic regardless of seed.",
103+
),
104+
],
105+
outputs=[
106+
IO.SVG.Output(),
107+
],
108+
hidden=[
109+
IO.Hidden.auth_token_comfy_org,
110+
IO.Hidden.api_key_comfy_org,
111+
IO.Hidden.unique_id,
112+
],
113+
is_api_node=True,
114+
price_badge=IO.PriceBadge(
115+
expr="""{"type":"usd","usd":0.429}""",
116+
),
117+
)
118+
119+
@classmethod
120+
async def execute(
121+
cls,
122+
prompt: str,
123+
model: dict,
124+
seed: int,
125+
instructions: str = None,
126+
reference_images: IO.Autogrow.Type = None,
127+
) -> IO.NodeOutput:
128+
validate_string(prompt, strip_whitespace=False, min_length=1)
129+
130+
references = None
131+
if reference_images:
132+
references = []
133+
for key in reference_images:
134+
url = await upload_image_to_comfyapi(cls, reference_images[key])
135+
references.append(QuiverImageObject(url=url))
136+
if len(references) > 4:
137+
raise ValueError("Maximum 4 reference images are allowed.")
138+
139+
instructions_val = instructions.strip() if instructions else None
140+
if instructions_val == "":
141+
instructions_val = None
142+
143+
response = await sync_op(
144+
cls,
145+
ApiEndpoint(path="/proxy/quiver/v1/svgs/generations", method="POST"),
146+
response_model=QuiverSVGResponse,
147+
data=QuiverTextToSVGRequest(
148+
model=model["model"],
149+
prompt=prompt,
150+
instructions=instructions_val,
151+
references=references,
152+
temperature=model.get("temperature"),
153+
top_p=model.get("top_p"),
154+
presence_penalty=model.get("presence_penalty"),
155+
),
156+
)
157+
158+
svg_data = [BytesIO(item.svg.encode("utf-8")) for item in response.data]
159+
return IO.NodeOutput(SVG(svg_data))
160+
161+
162+
class QuiverImageToSVGNode(IO.ComfyNode):
163+
@classmethod
164+
def define_schema(cls):
165+
return IO.Schema(
166+
node_id="QuiverImageToSVGNode",
167+
display_name="Quiver Image to SVG",
168+
category="api node/image/Quiver",
169+
description="Vectorize a raster image into SVG using Quiver AI.",
170+
inputs=[
171+
IO.Image.Input(
172+
"image",
173+
tooltip="Input image to vectorize.",
174+
),
175+
IO.Boolean.Input(
176+
"auto_crop",
177+
default=False,
178+
tooltip="Automatically crop to the dominant subject.",
179+
),
180+
IO.DynamicCombo.Input(
181+
"model",
182+
options=[
183+
IO.DynamicCombo.Option(
184+
"arrow-preview",
185+
[
186+
IO.Int.Input(
187+
"target_size",
188+
default=1024,
189+
min=128,
190+
max=4096,
191+
tooltip="Square resize target in pixels.",
192+
),
193+
IO.Float.Input(
194+
"temperature",
195+
default=1.0,
196+
min=0.0,
197+
max=2.0,
198+
step=0.1,
199+
display_mode=IO.NumberDisplay.slider,
200+
tooltip="Randomness control. Higher values increase randomness.",
201+
advanced=True,
202+
),
203+
IO.Float.Input(
204+
"top_p",
205+
default=1.0,
206+
min=0.05,
207+
max=1.0,
208+
step=0.05,
209+
display_mode=IO.NumberDisplay.slider,
210+
tooltip="Nucleus sampling parameter.",
211+
advanced=True,
212+
),
213+
IO.Float.Input(
214+
"presence_penalty",
215+
default=0.0,
216+
min=-2.0,
217+
max=2.0,
218+
step=0.1,
219+
display_mode=IO.NumberDisplay.slider,
220+
tooltip="Token presence penalty.",
221+
advanced=True,
222+
),
223+
],
224+
),
225+
],
226+
tooltip="Model to use for SVG vectorization.",
227+
),
228+
IO.Int.Input(
229+
"seed",
230+
default=0,
231+
min=0,
232+
max=2147483647,
233+
control_after_generate=True,
234+
tooltip="Seed to determine if node should re-run; "
235+
"actual results are nondeterministic regardless of seed.",
236+
),
237+
],
238+
outputs=[
239+
IO.SVG.Output(),
240+
],
241+
hidden=[
242+
IO.Hidden.auth_token_comfy_org,
243+
IO.Hidden.api_key_comfy_org,
244+
IO.Hidden.unique_id,
245+
],
246+
is_api_node=True,
247+
price_badge=IO.PriceBadge(
248+
expr="""{"type":"usd","usd":0.429}""",
249+
),
250+
)
251+
252+
@classmethod
253+
async def execute(
254+
cls,
255+
image,
256+
auto_crop: bool,
257+
model: dict,
258+
seed: int,
259+
) -> IO.NodeOutput:
260+
image_url = await upload_image_to_comfyapi(cls, image)
261+
262+
response = await sync_op(
263+
cls,
264+
ApiEndpoint(path="/proxy/quiver/v1/svgs/vectorizations", method="POST"),
265+
response_model=QuiverSVGResponse,
266+
data=QuiverImageToSVGRequest(
267+
model=model["model"],
268+
image=QuiverImageObject(url=image_url),
269+
auto_crop=auto_crop if auto_crop else None,
270+
target_size=model.get("target_size"),
271+
temperature=model.get("temperature"),
272+
top_p=model.get("top_p"),
273+
presence_penalty=model.get("presence_penalty"),
274+
),
275+
)
276+
277+
svg_data = [BytesIO(item.svg.encode("utf-8")) for item in response.data]
278+
return IO.NodeOutput(SVG(svg_data))
279+
280+
281+
class QuiverExtension(ComfyExtension):
282+
@override
283+
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
284+
return [
285+
QuiverTextToSVGNode,
286+
QuiverImageToSVGNode,
287+
]
288+
289+
290+
async def comfy_entrypoint() -> QuiverExtension:
291+
return QuiverExtension()

0 commit comments

Comments
 (0)